Skip to content

test(dynamic): live-gated user-authored ruleset round-trip pin - #59

Merged
2000game merged 2 commits into
mainfrom
test/dynamic-roundtrip-36
Jul 9, 2026
Merged

2000game merged 2 commits into
mainfrom
test/dynamic-roundtrip-36

Conversation

@2000game

@2000game 2000game commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Part of #36.

normalizeRuleset (src/engine/dynamic.ts) only strips the two read-only
timestamp keys and normalizes the query subtree. A no-op plan requires
deepEqual(desired.dynamic, actual.dynamic), which silently assumes every
other RuleSet-level field (description, shorty, importance,
personIdFieldName, process) round-trips byte-for-byte through ChurchTools.
The existing live round-trip test (tests/dynamic.integration.test.ts) writes
back CT's OWN GET output, so it is drift-free by construction and cannot
observe CT rewriting one of those fields on PUT — which, if it happens, makes
an affected dynamic group update on every apply forever.

This PR adds a new, doubly-gated live test that:

  • PUTs a ruleset authored fresh in the test (custom description,
    shorty, importance, …) to a designated dev dynamic group — never a
    value copied from a prior GET, so a server-side rewrite is actually
    observable.
  • GETs it back and asserts normalized deep-equality field by field; on
    failure, the message names exactly which RuleSet-level field(s) CT
    rewrote, so it tells you directly what to add to normalizeRuleset.
  • Builds a plan from that same user-authored desired ruleset and asserts the
    dynamic field diffs to no-op — the actual property bug: dynamic ruleset round-trip assumption unverified — CT-recomputed fields would cause perpetual diffs #36 protects, not
    just raw-object equality.
  • Captures the group's prior ruleset before writing and restores it in a
    finally, so the dev instance isn't left mutated.

Docs: tests/fixtures/dynamic/README.md and docs/dynamic-groups.md now
document how to run the gated round-trip and what pinned assumption it
protects.

Not run against any live instance in this PR — no credentials were
available or sought in this session. normalizeRuleset is unchanged; it
will only be extended once this test has actually run against eqrm-dev and
reported which field(s), if any, CT rewrites.

How to run it (on eqrm-dev only)

CT_LIVE=1 CT_LIVE_WRITE=1 \
CT_LIVE_WRITE_HOST=<eqrm-dev host> \
CT_DYNAMIC_FIXTURE_GID=<disposable dev dynamic group id> \
npx vitest run tests/dynamic.integration.test.ts

CT_LIVE_WRITE_HOST must exactly match the authenticated host (non-production
confirmation guard, same pattern as tests/permission.integration.test.ts).
CT_DYNAMIC_FIXTURE_GID must point at a disposable dynamic group on a
dev instance — its ruleset is overwritten and then restored, but a killed
run would leave it holding the test-authored ruleset.

If it fails, extend normalizeRuleset to drop/canonicalize the named
field(s) — that is the completion criterion for #36, not this PR (which only
authors the pin; #36 closes after a live run confirms or refutes the
assumption).

Test plan

  • npm test — full suite green, new test shows as skipped (3 skipped in
    tests/dynamic.integration.test.ts, 5 skipped total across the repo)
  • npm run typecheck clean
  • npm run lint clean
  • Live run against eqrm-dev (requires the user's credentials — not done
    in this PR)

2000game added 2 commits July 9, 2026 14:26
The existing live round-trip test writes back CT's own GET output, so it
is drift-free by construction and cannot catch CT silently rewriting a
RuleSet-level field (description/shorty/importance/personIdFieldName/
process) on PUT — which would make an affected dynamic group update on
every apply forever, since a no-op plan requires
deepEqual(desired.dynamic, actual.dynamic).

Add a doubly-gated (CT_LIVE=1 + CT_LIVE_WRITE=1 + CT_LIVE_WRITE_HOST)
test that PUTs a ruleset authored fresh in the test (not copied from a
GET), reads it back, and asserts normalized field-by-field equality with
a failure message naming exactly which fields CT rewrote. It also
builds a plan from the same desired ruleset and asserts the `dynamic`
diff is empty post-PUT, and restores the group's prior ruleset in a
finally. Skipped by default; not run against any live instance here.

Document how to run it and what to do on failure in
tests/fixtures/dynamic/README.md and docs/dynamic-groups.md.
…op assertion

The plan-no-op assertion in the #36 ruleset round-trip pin built its
synthetic desired object with a hardcoded `dynamic: { status: "manual",
ruleset: authored }`, but the test never sets or reads the fixture group's
real status — only the ruleset is PUT. Since diffFields compares the whole
`dynamic` field as one unit, this false-failed whenever the live group's
status wasn't exactly "manual" (the committed fixture shows realistic
status is "active"). Fetch the group's actual live status via GET
/dynamicgroups/{id}/status and use that in the desired object instead, so
the no-op assertion isolates exactly the #36 property (ruleset field
rewriting) from status drift. Test-only change; still fully skipped by
default (CT_LIVE/CT_LIVE_WRITE gated).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant