Skip to content

test(context-gateway): pin Settings sync route + soft-abort contract (RFC-761 PR-2)#770

Merged
memtomem merged 1 commit intomainfrom
test/rfc-761-settings-http-layer-tests
May 4, 2026
Merged

test(context-gateway): pin Settings sync route + soft-abort contract (RFC-761 PR-2)#770
memtomem merged 1 commit intomainfrom
test/rfc-761-settings-http-layer-tests

Conversation

@memtomem
Copy link
Copy Markdown
Owner

@memtomem memtomem commented May 4, 2026

Summary

  • Add 2 HTTP-layer tests to packages/memtomem/tests/test_context_settings.py
    pinning the FastAPI route contract for the Settings Hooks sync surface
    (Phase D), satisfying ADR-0001 §5 c2 (round-trip) and §5 c4 (conflict
    path).

Why

PR-2 of RFC #761 — gap-fill against the §5 readiness contract that
landed in #769. The existing test_context_settings.py has 21 tests
covering merge / mtime / atomic-write at the Python helper layer, but
zero HTTP-layer tests against /api/context/settings/*. Production
UI depends on those routes, so a future refactor of settings_sync.py
could silently regress in-sync detection or the soft-abort response
shape without these tests catching it.

Tests added

test_sync_route_round_trip_unidirectional (§5 c2, unidirectional shape)

  1. Write canonical .memtomem/settings.json with one
    (PostToolUse, "Write|Edit") hook.
  2. Pre-populate target ~/.claude/settings.json with a user-authored
    (PreToolUse, "Bash") hook + non-hook keys (permissions, env).
  3. POST /api/context/settings/sync {"allow_host_writes": true}
    assert HTTP 200 + claude_settings.status == "ok".
  4. GET /api/context/settings → assert status == "in_sync",
    canonical hook surfaces under hooks.synced.
  5. Merge invariant: re-read target file from disk; assert user's
    (PreToolUse, "Bash") hook + permissions + env keys all
    survive verbatim (additive merge does not clobber).

Settings has no reverse-import API by design (additive merge cannot
distinguish canonical-authored from user-authored entries), so the
round-trip is shaped per §5 c2's unidirectional clause: write
canonical → apply via sync route → re-read via diff route + assert
target-side merge invariants.

test_resolve_route_returns_soft_abort_on_stale_mtime (§5 c4, soft-abort)

  1. Setup canonical + target with conflicting (PostToolUse, "Write")
    rules.
  2. Monkeypatch _safe_load_json so reading the target as a side
    effect bumps its mtime — simulates a concurrent writer landing
    between the route's mtime capture (line 239) and recheck
    (line 262).
  3. POST /api/context/settings/resolve → assert HTTP 200 + body
    {"status": "aborted", "reason": <... modified by another process ...>}.

Mirrors the helper-level pattern from
TestClaudeSettingsMergeConcurrent.test_aborts_on_mtime_change
(line 243), but pins the route response contract — the helper test
covers the merge function's behavior, this test covers the FastAPI
surface that wires it up.

Scope notes

  • No i18n JSON changes. Inventory confirmed existing
    settings.hooks.* keys (10 keys, en + ko both, lines 338–347 in
    web/static/locales/*.json) already cover the Settings overview
    card and section. PR-3's gate flip will use them as-is. The
    original RFC-761 plan's "add settings.ctx.settings_title" step
    was based on a wrong assumption — verified by re-reading
    context-gateway.js:88-95 (the card label uses
    t('settings.hooks.title'), not a separate ctx_settings.* key).
    §5 c3 (i18n parity, auto-discovered by test_i18n.py) is already
    satisfied.
  • Tests use mode="dev" (currently required — settings_sync lives
    in _DEV_ONLY_ROUTERS). They remain correct after PR-3 moves the
    router to _PROD_ROUTERS (the router is mounted in either mode).

Out of scope

  • PR-3 (the flip): router move + 2× STATE.uiMode === 'dev'
    gate removal in context-gateway.js + CHANGELOG entry. Ships
    after this PR lands.
  • Phases B / C 409 conflict-path backfill: ADR-0001 §5 is
    prospective; gaps in already-shipped phases are tracked as hygiene
    follow-ups, not regressions.

Test plan

  • uv run pytest packages/memtomem/tests/test_context_settings.py -v
    37 tests pass (21 existing + 2 new + 14 unaffected).
  • uv run pytest packages/memtomem/tests/test_i18n.py -v
    16 tests pass; no JSON drift.
  • uv run ruff check packages/memtomem/tests/test_context_settings.py
    clean.
  • uv run ruff format --check packages/memtomem/tests/test_context_settings.py
    clean.

Refs: #761
Follows: #769

🤖 Generated with Claude Code

…(RFC-761 PR-2)

Add HTTP-layer tests for the Settings Hooks sync surface (Phase D),
satisfying ADR-0001 §5 c2 (round-trip) and §5 c4 (conflict path) at
the FastAPI-route layer:

- test_sync_route_round_trip_unidirectional — write canonical →
  POST /api/context/settings/sync → GET /api/context/settings asserts
  in_sync, AND target file on disk preserves user-authored hooks +
  non-hook keys (permissions, env). The unidirectional shape per the
  §5 c2 wording: Settings has no reverse-import API by design because
  additive merge cannot distinguish canonical-authored from
  user-authored entries.

- test_resolve_route_returns_soft_abort_on_stale_mtime — pins the
  POST /api/context/settings/resolve response contract: HTTP 200 +
  {"status": "aborted", "reason": <... modified by another process ...>}
  when target mtime changes between the route's capture and
  recheck. Mirrors the helper-level pattern from
  TestClaudeSettingsMergeConcurrent.test_aborts_on_mtime_change but
  exercises the route handler.

Helper-level merge / concurrent / atomic-write behavior is already
covered by the 21 existing tests in this file; these two new tests
specifically pin the FastAPI surface that production UI depends on,
so a future refactor of the route layer can't silently regress the
in-sync detection or soft-abort response shape.

No i18n JSON changes — existing settings.hooks.* keys (en/ko both)
already cover the prod overview card; the original plan's i18n step
was based on a wrong assumption (verified via inventory).

PR-3 (settings_sync router move from _DEV_ONLY_ROUTERS to
_PROD_ROUTERS + STATE.uiMode === 'dev' gate removal in
context-gateway.js) ships separately. These tests use mode="dev" so
they pass against current main and remain correct after PR-3 lands
(the router is mounted in either mode).

Refs: #761

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit fc5f638 into main May 4, 2026
11 checks passed
@memtomem memtomem deleted the test/rfc-761-settings-http-layer-tests branch May 4, 2026 14:39
@github-actions github-actions Bot locked and limited conversation to collaborators May 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants