Imported from civitas-cerebrum/achilles (
skills/test-composer/SKILL.md). Install upstream withnpx skills add civitas-cerebrum/achilles --skill test-composer. Copyright stays with the author.
Activation banner: The first user-facing reply after this skill loads MUST begin with the line: Protocol Achilles activated. Once per session — skip if already declared in this conversation. Subagents (which return structured data, not user-facing text) are exempt.
Test Composer — Stage 5 Atom: One Journey's Full Test Portfolio
Skill names: see
../achilles-protocol/references/skill-registry.md. Copy skill names from the registry verbatim. Never reconstruct a skill name from memory or recase it.
Stage 5 of the achilles-protocol workflow as the atomic unit of coverage. Given one mapped user journey, compose its complete test portfolio, stabilize, API-compliance-review, verify coverage is exhaustive for that journey, and return.
Scope: exactly one journey per invocation. The iterative loop over all journeys in an app lives in the coverage-expansion skill.
Coverage ownership: this skill is responsible for achieving exhaustive test coverage of its assigned journey. Every step, every branch, and every applicable state variation in the journey's map block must have a corresponding test before this skill returns. The orchestrator (typically coverage-expansion) trusts this contract and does not re-check per-journey coverage itself.
Role under dual-stage. When coverage-expansion runs in depth mode, this skill is Stage A of a per-journey-per-pass dual-stage pipeline. After this skill returns, a fresh staff-level-QA reviewer (Stage B, see skills/coverage-expansion/references/reviewer-subagent-contract.md) inspects the output and either greenlights or returns improvements-needed with must-fix findings. If improvements are needed, coverage-expansion re-dispatches this skill in cycle 2 with the findings appended to the brief — up to 7 A↔B cycles per journey per pass. Nothing about this skill's contract changes; you compose, stabilize, API-review, verify coverage, and return as before.
Pre-empting reviewer must-fix items. Skim §"Must-fix calibration" in reviewer-subagent-contract.md before composing — the reviewer will demand: (a) every Test expectations: item has a covering test, (b) tests use the Steps API correctly with page-repo selectors (no inline selectors), (c) file-level serial mode on tenant-mutating specs, (d) mobile variant on P0/P1 journeys, (e) test assertions match what the live DOM exposes. Meeting that bar in cycle 1 is the difference between a 1-cycle journey and a 4-cycle journey. The reviewer is not antagonistic — it is consistent, and you can know in advance what it will check.
When to Use
Activate this skill when:
- A caller (user or
coverage-expansionskill) asks to compose tests for one specific journey. - The caller supplies a
journey=<id>reference to an entry in a sentinel-bearingjourney-map.md.
Do NOT use this for:
- Iterating across many journeys or expanding coverage across an entire app →
coverage-expansion. - A single ad-hoc scenario with no journey context → Stages 1–4 of the main workflow.
Mandatory stages per invocation
Every invocation performs these stages in order, inside this subagent's own context. Do not return to the caller until all five complete cleanly.
- Compose (Steps 2–3 below) — write the full variant set for the journey, adding selectors to
page-repository.jsonas needed. - Stabilize (Step 4) — run, fix, re-run until 100% of new tests pass.
- Test Optimization (Step 6a) — load
../achilles-protocol/references/test-optimization.mdand run its 7-check protocol on the freshly-written tests. Apply auto-fixes; re-stabilize if any auto-fix regresses a test. - API compliance review (Step 6b) — run the Stage 4b API review protocol on the freshly-written tests. Fix any non-compliance and re-stabilize if needed.
- Composition judge (Step 6c) — dispatch the independent
composition-judge-subagent per../achilles-protocol/references/test-composition-standards.md§4 (skipped when this invocation runs undercoverage-expansiondual-stage — see Step 6c). - Coverage verification + whole-suite gate (Step 7) — check every step, branch, and applicable state variation from the journey's map block against the composed tests. Loop back to Compose for any missing coverage. After coverage is exhaustive, run the whole-suite re-run gate (see Step 7); only return to the caller when the gate passes.
The multi-journey iterative cycle (inventory, cross-app gap analysis, multi-pass decide) is documented in coverage-expansion. This skill owns the per-journey work items only.
Step 1: Load journey context
The caller (user or coverage-expansion) passes journey=<id> referencing an entry in tests/e2e/docs/journey-map.md. Before composing anything:
-
Verify
journey-map.mdexists and line 1 is<!-- journey-mapping:generated -->. If the sentinel is missing or the file is absent, stop and return an error pointing the caller at thejourney-mappingskill. -
Locate the
### j-<id>: <name>block in the map. Load only that block plus anysj-<slug>blocks it references underSub-journey refs:. -
Note the journey's
Priority,Pages touched:, andTest expectations:. These determine which variants to compose:- P0 → happy-path + error-states + edge-cases + mobile + negative flows + any data-lifecycle variants in the expectations list.
- P1 → happy-path + error-states + edge-cases + mobile.
- P2 → happy-path + one error-state + one data-verification check.
- P3 → smoke test (loads, key elements present).
Depth per variant follows the smoke-vs-e2e doctrine — the journey's UI walk is the subject of exactly one e2e test; derivatives shortcut prerequisites via API/state injection and assert only their own surface (
../achilles-protocol/references/test-composition-standards.md§5). -
List existing tests that already cover any step of this journey (from
npx playwright test --list). These are the starting point — add variants, do not duplicate.
Do NOT read other journey blocks. Do NOT hold the whole map in context. Do NOT compute cross-app priority or gap analysis — that is the caller's job.
Step 2: Discover
For each uncovered page or feature, use @playwright/cli (see ../achilles-protocol/references/playwright-cli-protocol.md) to inspect the live DOM.
Discovery protocol:
- Open your dedicated session:
npx playwright-cli -s=tc-<journey-slug> open --browser=chromium <URL> - Capture a snapshot:
npx playwright-cli -s=tc-<journey-slug> snapshot - Note all interactive elements: buttons, inputs, links, tabs, dialogs, dropdowns (each appears as
[ref=eN]in the ARIA snapshot) - Note the page's text content (headings, labels) for selector creation
- Click interactive elements to discover hidden UI (dropdowns, modals, menus):
npx playwright-cli -s=tc-<journey-slug> click eN - When done, close the session:
npx playwright-cli -s=tc-<journey-slug> close
If @playwright/cli is unavailable: Ask the user to provide screenshots or describe the page structure. Do not guess selectors.
Record discoveries in a structured format AND save to app-context.md (see Rule 8):
### PageName (/url/path)
- heading "Title" [h1]
- button "Action" → opens dialog with fields X, Y
- tab "Tab1" / "Tab2" / "Tab3"
- empty state: "No items found"
CRITICAL: Every page you visit and every component you discover MUST be saved to tests/e2e/docs/app-context.md per Rule 8. This is not optional — it is the primary way knowledge is preserved between sessions. If you discover a page and don't save it, the next session will re-discover it from scratch.
Step 3: Implement
A spec file contains exactly the tests its journey expectations and partition analysis demand — never pad toward a count, never stop at one. Split files by area when they exceed ~200 lines.
Implementation rules:
-
Every test must use the Steps API from
./fixtures/base -
Every element selector goes in
page-repository.json— no inline selectors in test code (kernel mirror — canon:../achilles-protocol/SKILL.md§"Hard rules — kernel-resident"; scope + exception:../achilles-protocol/references/test-composition-standards.md§3.1) -
Use
test.describe.configure({ timeout: 60_000 })on every describe block -
File-level serial mode is mandatory for tenant-mutating specs — and carries a
// serial-deliberate: <reason>comment. If the spec issues any POST / PUT / PATCH / DELETE to a mutable endpoint, the file must open withtest.describe.configure({ mode: 'serial' })at the top of the file — before anytest.describe(...)ortest(...)block — with a// serial-deliberate: <reason>comment on the line above it stating why serial is required. Stage 4a's §6 review treats that annotation as satisfying review (nostage4a:serial-mode-reviewflag — see../achilles-protocol/references/test-optimization.md§6; resolution record:test-composition-standards.md§3.4). Rationale: parallel Playwright workers sharing a credential against a single tenant produce random CSRF-token invalidations when concurrent mutating requests race against the session-bound token. Serial mode at the file level eliminates the race without capping global parallelism. Follow-up (not landed in this PR): add a lint rule or pre-commit check that rejects any spec with a mutating request that lacks the serial directive.What counts as a mutable endpoint. Any request whose server response represents a persistence change against tenant or user data — entity create / update / delete, state transitions (publish, archive, submit), role or permission mutations, file uploads that persist, password or MFA changes. Read-only methods (GET / HEAD / OPTIONS) do NOT trigger the rule, even when they tunnel through a POST for query-payload reasons, provided the handler is idempotent and server-side writes are limited to audit-log entries. When in doubt, apply the rule: the cost is one line of configuration per file; the cost of missing it is non-deterministic CI failures that surface later as "flaky auth".
-
Tests that depend on data from other tests must handle both states (e.g., job status could be "draft" or "published")
-
Tests that need specific data should use
test.skip()when that data isn't found, not fail — skip by name per the premise/app-state/infra taxonomy in../test-data-conventions/SKILL.mdRule 3 (only a premise is skippable; a broken rendering or transport failure still fails) -
If any variant emits
steps.api*calls, invoke thecontract-testingskill and apply its minimum obligations (status + error-envelope assertion) to each endpoint touched. This is what makes an L2 oracle real (see §"Oracle strength ladder" below) — an unassertedapiGetis not an oracle.
Prioritize by test type:
- Functional tests — verify things work when clicked/submitted (highest value)
- Data verification — verify displayed values match expected data
- Navigation tests — verify routing between pages
- Presence tests — verify elements exist (lowest value, but fast to write)
- Negative tests — verify error states and validation
- Responsive tests — verify layout at different viewports
- Security tests — XSS, injection, session handling
Implementation order within this journey
Compose variants in this order so selectors build up cleanly and each variant inherits from the previous one:
- Happy path end-to-end. Walk every step of the journey, introducing selectors for every page the journey touches. Every later variant inherits these selectors.
- Error-state variants. Validation errors, network failures, session expiry, invalid input at each step.
- Edge-case variants. Boundary inputs, unusual timing, empty or overflow data. Load
references/input-domain-analysis.mdand derive the edge-case variant set from the partition table, not ad hoc. - Mobile variant (P0/P1 only). The happy path at mobile viewport (375x812).
- Negative flows. Permission-denied, unauthorized access, out-of-order step execution.
- Data-lifecycle variants (where
Test expectations:lists them): create → read → update → delete across sessions, draft persistence, bulk operations. - Visual-regression variants — when the surface is design-locked. For pages or components whose visual layout the team treats as a contract (marketing landing pages, settled design-system components, dashboard layouts in a stable product), add one
verifyVisualMatchtest. Reference dynamic regions (clocks, generated ids, live counters, "updated N minutes ago" badges, user avatars, charts that re-render) by{ elementName, pageName }in themaskoption so the pixel diff stays stable across runs. Skip this variant for surfaces still under active design churn — visual regression on a moving target is pure noise, and the right call there is to come back to it after the design settles. Seeachilles-protocol/SKILL.md§16 (visual regression —verifyVisualMatchwith masks, not animation-freezing hacks) for the full policy.
Each variant is its own test(...) inside one describe block for the journey — or split into a small cluster of describe blocks if the file grows beyond ~200 lines.
Oracle strength ladder
Every test proves its claim through an oracle — the assertion that would fail if the app regressed. Oracles vary in strength, and a variant's required strength is set by the journey's priority and whether the step mutates state. This subsection is the canonical ladder definition; achilles-protocol/SKILL.md's kernel rule and the reviewer calibration in ../coverage-expansion/references/reviewer-subagent-contract.md mirror it in one line each.
The ladder is orthogonal to test-optimization.md §3b's round-trip / delta / shape oracles: L0–L3 picks which layer confirms the effect; §3b picks the assertion form that stays stable against volatile values within that layer (relationship recorded in ../achilles-protocol/references/test-composition-standards.md §3.6).
| Level | Oracle | What it proves |
|---|---|---|
| L0 | Visibility (toast, heading, success banner) | The UI said it worked. |
| L1 | UI round-trip — reload or re-navigate, then re-verify the persisted state via extraction | The app renders the mutation after a fresh load. |
| L2 | API oracle — steps.apiGet of the mutated resource + status/shape assertions per contract-testing's minimum obligations |
The backend serves the mutation. |
| L3 | DB oracle — steps.sql* via the database-testing skill (gated on the framework version shipping steps.sql*; see that skill's preflight) |
The mutation persisted to the database. |
Required strength:
| Journey priority | Mutating steps | Non-mutating steps |
|---|---|---|
| P0 | ≥ L2 (L3 when a DB is configured AND steps.sql* has shipped) |
≥ L1 |
| P1 | ≥ L1 | L0 acceptable |
| P2/P3 | L0 acceptable | L0 acceptable |
Reciprocal rule: any variant whose strongest oracle is L0 on a P0 mutating step is a coverage gap — loop back in Step 7.
UI bite-check (analogue of contract-testing Rule 8): for each journey, mutate the expected value of one L1+ assertion, confirm the test fails with a useful diff, then revert. A round-trip assertion that cannot fail is L0 wearing an L1 costume.
Tenant cleanup hooks are non-negotiable for add-* journeys
Any journey whose happy path creates a persistent tenant entity (typically j-*-add-<entity-type> journeys — users, records, teams, resources, admins, etc.) must include an explicit test.afterAll teardown attempt in the spec. Accumulated test records across many passes pollute shared tenants and eventually obscure real behaviour.
Two cases, both mandatory:
- UI exposes a Delete affordance. The spec's
test.afterAlluses the Steps API to delete every entity the suite created. If the teardown step itself fails, the spec must surface that failure in the subagent's structured return rather than swallowing it. - UI lacks a Delete affordance. The spec calls
cleanupViaApiBackdoor(<entity-type>, <id>)from the local stubtests/e2e/utils/cleanup-backdoor.ts— see contract below. While the real helper is unshipped (or unavailable in the current project, e.g., per-tenant API credentials not configured), the subagent does not silently skip cleanup. It records thecleanup-blockedannotation and returnscleanup: blockedin its structured summary so the orchestrator can log the tenant-pollution risk explicitly instead of having it hide in the spec.
Rationalizations to reject:
| Excuse | Reality |
|---|---|
| "Cleanup hook errored but the main tests passed, move on" | A swallowed cleanup failure is silent tenant pollution. Surface it in the subagent return; the orchestrator decides. |
| "I don't have API credentials so I'll log in as the shared admin and call the UI delete" | That bypasses the reason the backdoor exists (UI has no Delete). If the UI has no Delete path, an admin-UI Delete doesn't exist either — you are inventing a workflow the app does not expose. Return cleanup: blocked. |
| "One record per test doesn't matter, the tenant is big" | Per pass × per journey × per variant × 5 compositional passes × 2 adversarial passes = hundreds of records per run. Pollution compounds across runs. |
| "I'll skip cleanup and add a TODO" | A TODO in a committed spec is a silent commitment to do the work later. It rarely gets done. Return cleanup: blocked — the orchestrator's log of blocked cleanups IS the follow-up ledger. |
| "The backdoor helper isn't implemented yet so I'll skip" | Correct response: create/import the local stub (tests/e2e/utils/cleanup-backdoor.ts, contract below), call it as documented, catch the CleanupBackdoorUnavailableError, annotate, and return cleanup: blocked. Do NOT inline ad-hoc cleanup that circumvents the contract. |
cleanupViaApiBackdoor contract (local stub until the framework ships the real helper)
⚠ Not-yet-shipped helper. The framework does not expose
cleanupViaApiBackdooryet. Specs do NOT call a phantom framework export and let it crash — they create (or import, if a sibling spec already created it) a local stub attests/e2e/utils/cleanup-backdoor.tsthat throws a named error, so thetest.afterAllcatch path is typed, deterministic, and leaves a per-run signal in the test report. Do NOT substitute an inline ad-hoc cleanup to make the call succeed; that would mask the pollution risk the return value is meant to surface. When the framework ships the real helper, pin the dependency to that named framework version and replace the stub's body with a re-export — the call sites do not change.
The stub:
// tests/e2e/utils/cleanup-backdoor.ts
export class CleanupBackdoorUnavailableError extends Error {
constructor(entityType: string, id: string) {
super(`cleanupViaApiBackdoor unavailable — cannot clean up ${entityType}:${id}`);
this.name = 'CleanupBackdoorUnavailableError';
}
}
export async function cleanupViaApiBackdoor(entityType: string, id: string): Promise<void> {
throw new CleanupBackdoorUnavailableError(entityType, id);
}
The call site — the spec's test.afterAll catches the error and records a cleanup-blocked annotation so every future run carries the per-run signal:
test.afterAll(async ({}, testInfo) => {
try {
await cleanupViaApiBackdoor('user', createdId);
} catch (e) {
testInfo.annotations.push({ type: 'cleanup-blocked', description: `user:${createdId}` });
}
});
- Intent. Delete a tenant entity created during a test when the UI exposes no Delete path. Invoked from
test.afterAllafter the suite's happy-path variant has finished. - Signature.
entityTypeis an entity slug (e.g.,'user','record','team','resource').idis the server-assigned identifier captured during the create flow. - Credentials. Per-tenant API credentials live in env (
<TENANT>_API_TOKENor equivalent). The real helper will read them; specs never handle raw credentials. - Subagent return. The subagent still returns
cleanup: blockedin its structured summary.cleanupis a typed enum (done | blocked | not-needed) documented inschemas/subagent-returns/composer.schema.json. - Status. Stub-backed contract. The real helper, the env-credential convention, and any per-entity endpoint mapping ship as a framework follow-up; pin the named framework version here once known.
Cross-journey ordering (which journey to tackle first among many) is the caller's concern, not this skill's.
Step 4: Stabilize
Run the new tests. Fix every failure. Run again. Repeat until 0 failures.
If tests fail: invoke the failure-diagnosis protocol to run the full diagnostic pipeline. It will collect evidence (screenshot, DOM, error context), group failures by root cause, classify (test issue vs app bug), and fix test issues autonomously with stability validation (3 consecutive green for a new/edited test; 5 consecutive for a heal of a previously-flaky test). App bugs are reported with full evidence.
After fixing, re-run the full suite (not just the fixed test) to catch regressions.
Step 5: Document
After stabilization, write every scenario in plain English. No code. No selectors. Write what a human tester would do and verify.
Format:
### Test Name
**Area:** Dashboard
**Steps:**
1. Open the dashboard page
2. Look for the welcome message
3. Check that the company name appears
**Expected Result:** "Welkom terug" heading and "spriteCloud" are visible
Save to docs/e2e-test-scenarios.md (or a path the user specifies).
Why this matters: The plain English document serves as the single source of truth for what's tested. It's reviewable by non-technical stakeholders, it reveals gaps that code-level review misses, and it's the input for the next review step.
Step 6: Post-stabilization review (split into 6a + 6b + 6c)
Step 6a runs first, Step 6b second, Step 6c third. All run automatically after Step 4 (Stabilize) reports all new tests passing, before Step 7 (Coverage verification).
Step 6a: Test Optimization
Load ../achilles-protocol/references/test-optimization.md and run the 7-check protocol against the freshly-written tests for this journey. Apply auto-fixes per the protocol; re-stabilize (Step 4) if any auto-fix causes a regression (follow Rule 7 — failure-diagnosis).
Emit the structured return per ../achilles-protocol/references/test-optimization.md §8 as part of this skill's per-journey return block (under a new top-level stage_4a key — see Step 8's Canonical return schema for the addition).
Step 6b: API Compliance Review
Run the Stage 4b API review protocol on the freshly-written tests for this journey. The full protocol is documented in ../achilles-protocol/SKILL.md under "Stage 4b: API Compliance Review". Scope the review to the tests composed in this invocation, not the whole suite.
If any non-compliance is found (wrong argument order, deprecated APIs, missing options, incorrect types, direct selector usage instead of the Steps API, inline selectors outside page-repository.json, fixture misuse), fix it and re-run Step 4 (Stabilize). Do not proceed to Step 7 until the tests are both green and API-compliant.
A lightweight self-review checklist for this journey only:
- Every test uses the Steps API from
./fixtures/base(no rawpage.locator(...)in test files). - Every element selector lives in
page-repository.json— no inline selectors in spec files (citation — canon:../achilles-protocol/references/test-composition-standards.md§3.1). - Verification methods use correct option shapes (
{ exactly, greaterThan, lessThan }forverifyCount; bareverifyText()for "not empty"). - No use of deprecated methods or option shapes flagged in the API reference.
- Every test ends with a verification that proves the action's effect — not a tautology.
test.describe.configure({ timeout: 60_000 })on every describe block composed for this journey.
Step 6c: Composition Judge
Once 6a + 6b are clean, dispatch the independent composition-judge- subagent per the canonical charter in ../achilles-protocol/references/test-composition-standards.md §4 — four dimensions (scenario-intent coverage, oracle strength, API-compliance spot-check, test-data feasibility), reviewer-inloop return shape, fresh judge per cycle, 3 consecutive NOT SATISFIED → escalate to the caller/operator. Fix must-fix findings, re-run 6a/6b if code changed, re-judge.
Not double-imposed under dual-stage. When this invocation runs under coverage-expansion's dual-stage pipeline, the Stage-B reviewer cycle (../coverage-expansion/references/reviewer-subagent-contract.md) satisfies Stage 4c provided its brief includes the test-data feasibility dimension — skip Step 6c and let the caller's reviewer own the verdict. Standalone invocations (user-direct, onboarding Phase 3, whole-rewrite heals from test-repair/self-repair) run Step 6c themselves.
Step 7: Coverage verification + whole-suite gate
Before returning, verify the journey is exhaustively covered. This is the coverage-ownership contract:
- Re-read the assigned journey block's
Steps:,Branches:, andState variations:lists. - Build a coverage matrix: each listed item × the tests that exercise it, plus a partitions covered column mapping each input's equivalence classes and boundary pairs (from the spec file's partition table — see
references/input-domain-analysis.md) to the tests that exercise them. - If any step, branch, or applicable state variation has zero tests, loop back to Step 3 (Implement) to add missing coverage, then re-stabilize (Step 4) and re-review (Step 6).
- Only exit the loop when every item is covered or each remaining gap has an explicit justification (e.g., "branch X requires a seeded database row that cannot be created in tests — documented as external-setup gap").
This skill owns the coverage outcome for its assigned journey. The orchestrator will not re-check.
Whole-suite re-run gate (Step 7 exit)
After coverage verification confirms the journey is covered-exhaustively, run the whole-suite re-run gate documented in ../achilles-protocol/references/test-optimization.md §7.
Procedure: identical to coverage-expansion's per-pass gate (see ../coverage-expansion/SKILL.md). On refusal, this skill returns { status: 'whole-suite-gate-failed', journey: <id>, failures: [...], skips: [...] } to its caller and does NOT mark the journey as complete.
Why it runs here: test-composer is the atomic unit of per-journey work. Other journeys may share fixtures, helpers, or backend state with this journey; an integration regression introduced by this journey's new tests must surface before this skill returns.
Step 8: Return
Exit gate — the compliance sweep is not optional. This mode writes test code, so it runs the Stage-4b compliance sweep over every spec it touched before it returns, and announces it with the documented API Compliance Review block. That sweep is where API misuse, tautological assertions, missing test IDs and untagged intentional reds get caught. Harness-enforced at stop time by hooks/compliance-sweep-exit-gate.sh; the rule and the per-mode table live in stages-protocol.md §"Stage 4b is every mode's exit gate".
Step 6b's sweep is that gate for this skill: a return that reports composed tests without it is incomplete, and the stop gate will say so.
Emit a structured report to the caller. Do not paste test source, DOM snapshots, or playwright-cli transcripts into the return — the caller will not read them.
Canonical return schema
Every finding reported in the return block (coverage gaps, app-bug flags, new-discovery anomalies) MUST follow the canonical subagent finding-return schema documented in ../achilles-protocol/references/subagent-return-schema.md:
- **<FINDING-ID>** [<severity>] — <one-line title>
- scope: <what was probed>
- expected: <what should happen>
- observed: <what happened>
- coverage: <existing test or none>
FINDING-IDuses<journey-slug>-<pass>-<nn>(when invoked bycoverage-expansionwith a pass number) or<journey-slug>-<nn>(standalone).severityper../achilles-protocol/references/subagent-return-schema.md§1.- Do not invent alternative ID schemes or severities.
Return states — covered-exhaustively vs rationalisation
If this invocation produced zero new tests, pick one of the states defined in schemas/subagent-returns/composer.schema.json:
status: covered-exhaustively— only valid when the subagent inspected the journey. Required evidence: a per-expectation mapping table (one row per item in the journey'sTest expectations:list, each mapped to a spec file + test name). Every row must name concrete coverage — nocoverage: nonerows are tolerated under this status.status: no-new-tests-by-rationalisation— not a valid return from any compositional pass. If the only justification is "tests would be redundant" without an inspection, perform the inspection. Orchestrators will reject this return and re-dispatch with a stricter brief.
When invoked by coverage-expansion as a re-pass subagent (Pass 2 or 3), the mapping table MUST also include an explicit check against every re-pass trigger:
- trigger 1 (map delta since Pass 1): <none|<delta description>>
- trigger 2 (Pass-1 coverage gaps or deferred stabilization): <none|<gap>>
- trigger 3 (sibling-bug regression required here): <none|<sibling finding ID>>
- trigger 4 (unresolved review findings carried forward from prior pass): <none|<finding-ID list>>
The four-trigger format is non-negotiable — the orchestrator's rejection check (§"Re-pass mode for compositional passes 2–3" in coverage-expansion/SKILL.md) greps for the literals "trigger 1" through "trigger 4" and re-dispatches any return missing one of them.
Return shape (composer)
Full schema: schemas/subagent-returns/composer.schema.json.
Every composer return MUST open with a handover envelope as its first key. The envelope has exactly four required fields:
| Field | Rule |
|---|---|
role |
Kebab-case slug, e.g. composer-j-login-flow. |
cycle |
Integer ≥ 1. The cycle number within this journey's dispatch loop. |
status |
One of new-tests-landed, covered-exhaustively, blocked, skipped. |
next-action |
One-line directive for the orchestrator. |
phase and summary are top-level fields — they MUST NOT appear inside handover.
JSON is preferred over YAML. YAML's compact-mapping form silently breaks when a value contains :, causing schema validation to fail.
Worked example — new-tests-landed:
{
"handover": {
"role": "composer-j-login-flow",
"cycle": 1,
"status": "new-tests-landed",
"next-action": "reviewer-inloop to review pass 1 cycle 1 for login-flow"
},
"journey": "j-login-flow",
"pass": 1,
"tests-added": 4,
"run-time": "2m15s",
"summary": "Added happy-path, mobile viewport, error-state, and data-lifecycle tests for login flow."
}
For status: covered-exhaustively, the per-expectation mapping table moves to disk per the spillover contract below; the inline return inlines only index-level fields (see §"Spillover contract" below).
The orchestrator uses the table to audit that the "no new tests" claim is supported by inspection, not rationalised.
Spillover contract (covered-exhaustively)
When the verdict is covered-exhaustively, the per-expectation mapping table moves to disk. The return body inlines only the index-level fields — handover envelope + journey, pass, cycle, spill, tests-added: 0, and summary. Example (JSON):
{
"handover": {
"role": "composer-j-login-flow",
"cycle": 1,
"status": "covered-exhaustively",
"next-action": "reviewer-inloop to verify exhaustive coverage for login-flow pass 1"
},
"journey": "j-login-flow",
"pass": 1,
"tests-added": 0,
"spill": "tests/e2e/docs/.subagent-returns/composer-login-flow-1-c1.md",
"summary": "All expectations already covered; per-expectation mapping table in spill file."
}
The spill file starts with the sentinel <!-- subagent-returns:composer:<slug>:pass-<N>:cycle-<C> -->. The full | Expectation | Covering spec | Test name | table (with one row per Test expectations: entry) goes in the spill body, NOT inline in the return.
The SubagentStop rewrite-gate that previously enforced this contract was retired in 0.3.6; the rule still applies and reviewer dispatches enforce it — keep the verbose mapping table in the spill file so it never reaches the parent's transcript. The live hooks/subagent-return-schema-guard.sh (PostToolUse:Agent) WARNs on returns that fail composer.schema.json. Other composer statuses (new-tests-landed, blocked, skipped) are exempt from spillover (small bodies — counts + reasons, no large block). See harness-hooks.md.
AI-Assisted Test Patterns
When the application under test includes an AI chatbot or conversational interface, use a local or remote LLM to simulate user input.
Architecture:
App's AI asks question → Test reads question from DOM
→ Test sends question to LLM (Ollama/Gemini)
→ LLM returns structured answer
→ Test types answer into chat input
→ Repeat until conversation completes
LLM utility pattern (utils/ollama.ts or similar):
- Support multiple backends (Ollama for local, Gemini for CI) via env vars
- Use structured output (JSON schema in
formatfield for Ollama,responseSchemafor Gemini) - Define a response interface with the answer text and completion signal
- Include conversation history in each prompt for coherent multi-turn responses
- Handle backend-specific quirks (e.g., some models put output in
thinkingfield instead ofresponse) - Support bearer token auth for remote instances
System prompt pattern:
You are a [role] at [company] creating a [thing].
Answer concisely in 1-2 sentences.
Reply in the same language as the question.
Do not ask questions back.
Exit conditions:
- UI state changes (chat input disappears, preview appears, URL changes)
- Maximum turn count reached
- The LLM signals completion via structured output
Parallelization
Cross-journey parallelization (dispatching subagents for multiple journeys at once) is coverage-expansion's responsibility, not this skill's. A single test-composer invocation stays focused on one journey.
Within a journey, variants (happy path, error states, edge cases, mobile, negative flows, data lifecycle) are composed sequentially so each variant inherits from the selectors added by the previous one.
Commit-message conventions
Every test this skill commits MUST use the compositional-pass template:
test(<j-slug>): <variant>
<j-slug>is the journey ID (thej-<slug>fromjourney-map.md, without angle brackets).<variant>names the variant just committed:happy-path,error-states,edge-cases,mobile,negative-flows,data-lifecycle, or a specific sub-variant (e.g.happy-path-returning-user).- One journey per commit, one variant per commit. Do not batch multiple variants into a single commit; do not batch multiple journeys into a single commit.
Examples:
test(j-<slug>): happy-pathtest(j-<slug>): error-statestest(j-add-<entity>): data-lifecycle
Do NOT use test(pass<N>): …, feat(e2e): …, or test(<j1>, <j2>): … — see the Commit-message conventions table in coverage-expansion/SKILL.md for the full list of anti-patterns across all passes.
Anti-Patterns
Presence-only coverage: Writing 100 tests that all just verify elements exist gives a false sense of security. Prioritize functional tests that click, type, submit, and verify outcomes.
Hardcoded test data: Tests that depend on specific database IDs or job titles break when the environment changes. Use selectors and patterns that work regardless of data state.
Ignoring flakes: A test that fails 1 in 10 runs is a bug, not a "flake to ignore." Fix the root cause (timing, state, selector specificity) before moving on.
Over-mocking: E2E tests should exercise the real application. Don't mock APIs, don't intercept network requests, don't stub components. If a feature needs external data, use test.skip() instead of faking it.
Giant spec files: Keep spec files under 200 lines. Split by area, not by "I kept adding tests to the same file."
Count padding: tests-added is a report field, not a score. Three tests that exhaust distinct equivalence classes beat fifteen same-class duplicates that dedup will delete next pass.
Invocation options
test-composer accepts a single required parameter:
| Parameter | Meaning |
|---|---|
journey=<j-slug> |
The ID of a journey in tests/e2e/docs/journey-map.md. This is the only journey composed during this invocation. |
Example: args: "journey=j-book-demo".
Backward compatibility
The legacy passScope: priority=<Pn> depth=<tokens> form is deprecated. If a caller still passes it, emit a one-line deprecation warning directing them at coverage-expansion mode: breadth (which is the proper home for priority/depth sweeps) and then compose against the highest-priority journey with uncovered steps matching the listed depths. Remove this fallback in a future major release.