Skip to content

fix: build update bodies from fetched actuals, desired-side hierarchy gate, plan/apply robustness - #38

Merged
2000game merged 3 commits into
mainfrom
fix/apply-correctness-27-28-33
Jul 9, 2026
Merged

2000game merged 3 commits into
mainfrom
fix/apply-correctness-27-28-33

Conversation

@2000game

@2000game 2000game commented Jul 9, 2026

Copy link
Copy Markdown
Member

Fixes three verified bugs from the 2026-07-08 codebase review.

#27 — apply reverts drifted fields

Update bodies were built from the state-file snapshot, so a field edited in the CT UI got silently clobbered by any unrelated update — with a write the plan never showed. Plan items now carry the fetched actual: PATCH resources send only the changed fields; PUT resources send actual ∪ changes; post-apply state records what was actually written.

#28 — first apply on fresh state silently drops all hierarchy edges

parentsField.fold gated on managed groups in the pre-apply state, so a fresh state (the exact #23 bootstrap path) created all groups but no edges, exit 0. The gate is now desired-side (d.type === "group" && d.parents !== undefined), which also stops fetching /groups/hierarchies when nothing opts in — resolving #17 item 5.

#33 items 1, 2, 4 (item 3 ships with the #29 fix)

  1. Version gate enforced: CtClient.assertMinVersion() (one cached /info GET) called from authedSession — plan/apply/destroy now hard-fail below the documented minimum instead of half-applying.
  2. Transient fetch errors no longer render as "recreate": fetch-failed keys thread into computePlan and render as ? <key> — fetch failed (<status>), excluded from create/stale classification.
  3. Guarded 2xx body parsing: empty 2xx → undefined; non-JSON → CtApiError naming method + path, never a raw SyntaxError.

Verification: 224 passed / 4 skipped, typecheck + lint clean.

Closes #27. Closes #28.
Addresses #33 items 1, 2, 4 (item 3 tracked with #29).

2000game added 3 commits July 9, 2026 07:28
#28)

The parents fold keyed on managed groups existing in the pre-apply state
file, so a fresh/empty state returned early: create items carried no
parents change and the first apply produced a flat hierarchy (exit 0).
Gate on 'some desired group opted into parents' instead — the first apply
now writes edges, and the /groups/hierarchies fetch is still skipped when
nobody opts in (#17 item 5).
…27)

computePlan diffed desired vs actual, but executePlan built the write body
from the state-file snapshot, so any update reverted a sibling field that
had drifted in the CT UI — a write the plan never showed. Thread the
fetched actual onto each update PlanItem; PATCH resources now send only the
changed fields, PUT resources send actual ∪ changes. Post-apply state
records actual ∪ changes regardless of verb.

(Also adds the fetch-failed PlanNote + detail scaffolding to types.ts/plan.ts
that #33 item 2 wires up.)
…ard 2xx JSON (#33)

Item 1: assert the minimum CT version in authedSession via one cached /info
GET — plan/apply/destroy now hard-fail below the minimum instead of only
warning in auth login.

Item 2: thread fetch-failed keys (non-404 errors) from buildPlan into
computePlan so they render as '? <key> — fetch failed (<status>)' and are
excluded from create/recreate/stale classification — a transient 500 no
longer reads as 'recreate — missing in ChurchTools'.

Item 4: guard res.json() on non-204 2xx bodies — empty body returns
undefined, non-JSON body throws a CtApiError naming method+path instead of
a raw SyntaxError.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant