Skip to content

Testing plan: one state root per configuration, and health surfaces that report on it

Test matrix

#TypeApplies?Scope / what it provesWhere it runs
T1Unityesresolve_state_root / apply_state_root over the resolution table (absent, relative, nested .the-loop, absolute, ~, non-string); rival_roots; the health body's status computation; the spawn environment each of the three sites buildsuv run pytest cli/tests/test_cli_config.py cli/tests/test_state.py cli/tests/test_core_daemons.py cli/tests/test_client.py
T2Integration (scenario)yesthe two behaviours only a scenario states: one config file read from two working directories resolves to one root, and /api/v1/health is degraded with an enabled poller absent; plus ingress.hosted_failed reaching the event log, and status naming its config, root and a rivaluv run pytest cli/tests/test_state_root_integration.py cli/tests/test_api_health_integration.py
T3Contract (OpenAPI)yesthe health response's new fields are in docs/api-specs/openapi/ and the served surface matches ituv run pytest cli/tests/test_api_contract_parity.py
T4End-to-endn/a — an E2E would need a real GitHub source and a 17-hour clock; T2 reproduces the divergence deterministically with two cwds and a TestClient.
T5UI / visualn/a — no UI surface changes. The dashboard reads /api/v1/health but renders no new field in this change.
T6Snapshotn/a — the one rendered surface (status's text) is asserted line-by-line in T1/T2, which says what changed; a snapshot would only say that it changed.
T7Performance / loadn/a — the added work is one Path.resolve() per config load and, per /health call, three flock probes already performed by the-loop status.
T8Security / abuse caseyesAC1–AC5 of bugfix.md §Security considerations, one negative test eachuv run pytest cli/tests/test_api_health_integration.py cli/tests/test_state_root_integration.py -k abuse or security
T9Accessibilityn/a — no UI.
T10Migration / upgradeyesa config carrying an explicit state.root: .the-loop resolves to the same directory it resolved to before, when read from the base directory — the "no silent state move" property D1 is built onuv run pytest cli/tests/test_state_root_integration.py -k migration
T11Manual exploratoryyesthe reporter's repro, end to end: start, status from another directory, curl /health with and without the pollerrecorded under evidence/manual.md
T12Docs parityyesdocs/cli/state.md and GENERATED_PATHS still agree; the two schema copies are byte-identical; every new doc page is linkeduv run pytest cli/tests/test_docs_parity.py cli/tests/test_config_schema_parity.py cli/tests/test_state_portability.py
T13Regression (whole suite)yesno existing behaviour moved — in particular nothing that pins a relative state.root through a loaded configmake check

Scenarios & requirement trace

RowRequirement(s)Scenario / case
T1R1.1, R1.2, R1.3the resolution table: absent → base dir's .the-loop; .the-loop → the same; var/state<base>/var/state; /srv/x → verbatim; ~/x → expanded; a list → warned, default
T1R1.5each spawn site's child environment carries THE_LOOP_CLI_CONFIG = the path this process resolved
T1R2.1, R2.2the health status is degraded iff some enabled ingress's lock is unheld
T1R4.2rival_roots names a candidate holding a heartbeat, skips the resolved root, skips an unreadable directory
T2R1.4Scenario: one config file, two working directories, one state root
T2R1.5Scenario: a spawned service reads the config the CLI resolved, not the one its cwd suggests
T2R2.1, R2.3Scenario: health reports degraded, and names the files it is using, when an enabled poller is absent
T2R2.2Scenario: health reports ok when every enabled ingress holds its lock
T2R3.1, R3.3Scenario: an enabled ingress that cannot start writes an error event, a disabled one writes none
T2R3.3Scenario: read.mode poll is a configuration, not a failure
T2R2.6, R3.1Scenario: the poller thread dies during startup
T2R4.1, R4.2, R4.3Scenario: status names its config, its root and a rival root without changing its exit code
T3R2.1, R2.3the served /api/v1/health schema is the published one
T8AC1THE_LOOP_CLI_CONFIG in a spawned child is the parent's resolved path and nothing caller-supplied reaches it
T8AC3the health body carries no pid, token, secret or environment value
T8AC4a degraded health is HTTP 200, so client.healthy() still returns True and ensure_service does not respawn
T8AC5an ingress.hosted_failed reason names a missing env variable, never its value
T10R1.2Scenario: an explicit state.root resolves to the directory it always resolved to
T12R5.1, R5.2the supervision page exists, is linked, and the state page no longer claims a cwd-relative root

Verification environment

  • Repositories: this repo only.
  • Services / containers: none. The API rows use fastapi.testclient.TestClient against an app built in-process; the lock rows use real flock on tmp_path, as test_core_daemons.py does today.
  • Fixtures & data: tmp_path-built config trees (<tmp>/repo/.the-loop/cli-config.yaml, <tmp>/home/.the-loop/cli-config.yaml). No network, no GitHub, no gh.
  • Credentials: none. No row reads a token; the AC5 row asserts on a variable name (SLACK_APP_TOKEN) and never sets a value.
  • Bring-up: uv sync · Tear-down: none (tmp_path is pytest's).
  • If bring-up fails: record it under Verification results, leave the dependent activities unticked, and escalate.

Evidence plan

RowEvidencePath under evidence/
T1, T13make check output: lint, typecheck, unit countsverification.md
T2, T3, T10, T12the scenario table and the run outputverification.md
T8one section per abuse case: the test, and what it pinssecurity-review.md
T11the reporter's repro re-run against the fix — status from two directories, curl /health with the poller absent and presentmanual.md

Verification activities

  • [x] T1 — uv run pytest cli/tests/test_cli_config.py cli/tests/test_state.py cli/tests/test_core_daemons.py cli/tests/test_client.py
  • [x] T2 — uv run pytest cli/tests/test_state_root_integration.py cli/tests/test_api_health_integration.py
  • [x] T3 — uv run pytest cli/tests/test_api_contract_parity.py
  • [x] T8 — uv run pytest cli/tests/test_api_health_integration.py cli/tests/test_state_root_integration.py
  • [x] T10 — uv run pytest cli/tests/test_state_root_integration.py -k migration
  • [x] T11 — manual: the reporter's repro, recorded in evidence/manual.md
  • [x] T12 — uv run pytest cli/tests/test_docs_parity.py cli/tests/test_config_schema_parity.py cli/tests/test_state_portability.py
  • [x] T13 — make check

Verification results

Executed 2026-09-11 on claude/github-issue-339-2rj3oh, base 48d1e8f (13.11.0). Every activity ran; none was skipped.

ActivityCommand / procedureOutcomeEvidence
T1uv run pytest cli/tests/test_cli_config.py cli/tests/test_state.py cli/tests/test_core_daemons.py cli/tests/test_client.py40 passedverification.md
T2uv run pytest cli/tests/test_state_root_integration.py cli/tests/test_api_health_integration.py19 passed; both files were red against 48d1e8fverification.md
T3uv run pytest cli/tests/test_api_contract_parity.py2 passedverification.md
T8the five abuse cases, one negative test each5 of 5 closedsecurity-review.md
T10uv run pytest cli/tests/test_state_root_integration.py -k migrationpassed — an explicit state.root: .the-loop resolves to the directory it always didverification.md
T11the reporter's repro against a real service on this boxreproduced and fixed: start exits 1 with poller failed, health is degraded, status from an unrelated directory reports the live poller correctlyverification.md
T12uv run pytest cli/tests/test_docs_parity.py cli/tests/test_config_schema_parity.py cli/tests/test_state_portability.py · npx markdownlint-cli215 passed; 0 markdown errorsverification.md
T13make check (ruff, ruff format, pyright, validate_config, pytest)lint clean, 0 type errors, 7 configs valid, 3366 passed, 1 skippedverification.md

Released under the MIT License.