Skip to content

Testing plan: per-work-item model and effort choice, and spawning after the gate

Derived from requirements.md and design.md, before tasks.md. Authored at test-planning, completed at verification.

This file is executable content — it names commands an agent will run. No credential appears in it, by value or by reference: this work item reads and writes none.

What has to be proved

Three things carry the risk, and the matrix is shaped around them:

  1. No comment text reaches an argv. Everything a reply can do is pick a key into the operator's declared list, and the argv is built from config or the adapter. This is the work item's one new trust boundary (T8).
  2. Nothing runs on a model the harness refuses. The probe measures, the checklist withholds, resolution falls back, and the re-probe bounds the loop (T1, T2).
  3. R8 cannot strand a work item. Deferral applies only at a human-gate start node; every other path spawns and respawns exactly as it does today (T2, T10).

Test matrix

#TypeApplies?Scope / what it provesWhere it runs
T1Unityesmodelchoice resolution and merge order; modelprobe verdicts, digest invalidation and the withhold-not-introduce rule; adapter effort_args/with_args; selection.py rendering and per-section parsing; registry round-tripmake test (uv run --project cli python -m pytest -q cli)
T2Integration (scenario)yesthe four end-to-end behaviours below, Gherkin-documented, against a fake registry and a stub tmuxuv run --project cli python -m pytest -q cli/tests/test_*_integration.py
T3Contract (OpenAPI)yesthat the surface did not need a contract change: /api/v1/sessions types its response as untyped objects, so the three new record fields flow through and the contract-parity assertion still holdsmake validate + test_api_contract_parity.py
T4End-to-endn/a — an end-to-end run needs a real harness, a real tmux server and a real GitHub repository. The verification environment section says why that is out of scope here and what stands in for it (T2 with stubs at the same seams).
T5UI / visualn/a — no product UI. The three human surfaces are text (checklist body, sessions list table, models check matrix) and are asserted as strings in T1/T2.
T6Snapshotn/a — the one rendered artifact that would justify a snapshot is the checklist body, and T1 asserts its rows individually, which fails more usefully than a whole-body diff.
T7Performance / loadn/a — the feature adds no work to the delivery path: resolution reads a record the dispatcher already reads, and probing is off-path. T1 asserts the no-new-I/O property directly (no probe is invoked during a delivery).
T8Security / abuse caseyesone negative test per abuse case A1–A7 of requirements.md § Security considerationsuv run --project cli python -m pytest -q cli -k abuse
T9Accessibilityn/a — no UI surface.
T10Migration / upgradeyesa config with no new sections behaves exactly as today; a session record written before this change still parses; routing.harnessArgs still works behind the deprecation shimmake test
T11Manual exploratoryn/a — every surface is deterministic and asserted; there is no interactive flow a human would find something in that T1/T2 would not.

Scenarios & requirement trace

RowRequirement(s)Scenario / case
T1R2.5–R2.9, R3.1–R3.2model name → [model_flag, name]; effort level → adapter.effort_args; merge order is base → model → effort; a harness with no model_flag offers no model section
T1R2.2–R2.4a bare name is a candidate everywhere; harnesses: narrows; an undeclared harness in harnesses: contributes nothing
T1R7.2, R7.6verdict caching, args_digest invalidation, unknown stays offerable
T1R1.4–R1.5per-section parse: one tick, no tick, two ticks, unknown token, ambiguous model beside a valid effort
T1R5.1–R5.4the three session fields round-trip; a record without them parses and renders -
T2R1.3, R4.1–R4.2Scenario: a work item that chose a model and an effort is respawned on both
T2R7.3–R7.4Scenario: a model the harness refuses is never spawned
T2R8.1–R8.2Scenario: an armed work item gets no session until its gate is answered
T2R8.3Scenario: a mid-graph work item still respawns
T8A1an unauthorized reply freezes nothing
T8A2a reply naming a flag, a path or a metacharacter resolves to no choice
T8A3the argv contains only what the config and the adapter produced
T8A4a hand-edited frozen record naming an undeclared choice is ignored
T8A5a label named after a model selects nothing
T8A6an unreadable checklist keeps the operator's arguments
T8A7a forged availability verdict can withhold a choice but never introduce one
T10R6.1–R6.3, R2.12an untouched config is byte-identical in behaviour; the routing.harnessArgs shim warns and works

Verification environment

  • Repositories: this one only.
  • Services / containers: none. Every external seam is stubbed — the harness binary (modelprobe takes an adapter, so the probe is a fake in tests), tmux (the existing runner stub), and the GitHub integration (the existing resolve("github", …) seam the selection-hook tests already patch).
  • Fixtures & data: temporary directories for the registry, the portable state and the verdict cache; the shipped process graphs, unmodified.
  • Credentials: none. This work item reads and writes no secret, token or environment variable, and the probe inherits the daemon's environment exactly as a critic run does.
  • Bring-up: uv sync · Tear-down: none (pytest temporary directories).
  • If bring-up fails: record it under Verification results, leave the dependent activities unticked, and escalate.

Why there is no T4. An end-to-end run would need a real harness CLI with real model access, a real tmux server and a real repository — none of which this repository's CI has, and a probe against a live vendor would make the suite depend on an account. T2 stands in for it at the same seams, which is where the logic being proved actually lives.

Evidence plan

RowEvidencePath under evidence/
T1pytest summary (counts, duration)unit.md
T2scenario table (the-loop scenarios --format markdown) + run outputintegration.md
T3make validate output and the changed contract excerptcontract.md
T8the seven negative tests and their assertionssecurity-review.md
T10the before/after sessions list table and the shim's warningmigration.md

Nothing captured here can contain a secret — no credential is read, and the only rendered output is a checklist body, a verdict matrix and a session table over fixture data. The redaction rule still applies to anything unexpected in captured output.

Verification activities

  • [x] T1 — make test
  • [x] T2 — uv run --project cli python -m pytest -q cli/tests/test_spawn_gate_integration.py cli/tests/test_dispatcher_choice.py
  • [x] T3 — make validate (+ the two parity tests inside T1)
  • [x] T8 — uv run --project cli python -m pytest -q cli -k abuse
  • [x] T10 — uv run --project cli python -m pytest -q cli/tests/test_dispatcher_choice.py -k "before_this_feature or declared_nothing or deprecated"
  • [x] lint/format/typecheck parity — ruff check, ruff format --check, pyright, markdownlint

Verification results

Executed on the work item's branch at 3c14f33. Full record: evidence/verification.md.

ActivityCommand / procedureOutcomeEvidence
T1 unitmake testpass — 3603 passed, 1 skipped (3539 before this work item, so 64 new tests and none broken)verification.md § T1
T2 integrationpytest cli/tests/test_spawn_gate_integration.py cli/tests/test_dispatcher_choice.pypass — 19 passed; the four Gherkin scenarios named in the matrixverification.md § T2
T3 contractmake validate + test_api_contract_parity.py, test_config_schema_parity.pypass, and the row changed meaning: there was no OpenAPI edit to makeverification.md § T3
T8 securitypytest cli -k abusepass — 38 passed; the seven abuse cases, one new finding fixedsecurity-review.md
T10 migrationpytest cli/tests/test_dispatcher_choice.py -k "before_this_feature or declared_nothing or deprecated"pass — 3 passed, plus the record-level half in test_routing.pyverification.md § T10
tooling parityruff check, ruff format --check, pyright, markdownlintpass — the same commands CI runsverification.md § Tooling parity

Not executed: T4 (end-to-end) stayed n/a as planned — no real harness, tmux server or repository in CI, with T2 standing in at the same seams. A live models check against a vendor was also deliberately not run: the probe spends real tokens and a suite depending on an account fails for reasons unrelated to the change, so test_modelprobe.py decides each outcome with a stub adapter instead.

Review comments

Appended by the-loop's record-feedback hook when a human gate approves with comments.

Released under the MIT License.