Testing
Overview
The AI-DLC test suite is entirely TypeScript — every test is a
t*.test.ts file run under bun, with zero shell (.sh) test files. This is
the platform-invariance guarantee by construction: the same files run
identically on macOS, Linux, and native Windows.
The suite is organized into four levels — smoke, unit, integration,
e2e — one directory each under tests/. The four levels map onto the
classic three-layer test pyramid that balances speed vs. thoroughness:
/\
/ \ ACCEPTANCE — full workflows, artifact + experience verification
/ L3 \ Level: e2e · When: before releases (--release / --all)
/------\
/ \
/ L2 \ STAGE — individual stages with stub input, verify artifacts
/------------\ Level: integration · When: CI push (--ci, every PR)
/ \
/ L1 \ PROTOCOL — contracts, structure, cross-references
/------------------\ Levels: smoke + unit · When: every local change
The --ci profile and the no-flag default both run smoke + unit +
integration (so the integration level rides along on every local
bun tests/run-tests.ts); --release / --all adds e2e. The pyramid above
shows where each level sits conceptually — the profile flags below are how you
actually select them.
Filename convention. A test's filename is t<NN>[-description].test.ts —
just the level directory it lives in and an optional human description. There
is no mechanism segment in the name: a test's mechanism (whether it spawns
a CLI, drives the SDK, or renders a live TUI) is a derived set computed from
the drivers its body actually calls, not declared in the filename. The
machine-checked index of what each test covers lives in
tests/.coverage-registry.json (generated by tests/gen-coverage-registry.ts
from the covers: headers on disk), not in a hand-maintained table here — see
Test Registry below.
Layer 1: Protocol (every change, no LLM, seconds)
Verifies the orchestrator's structural correctness without invoking the LLM. If these pass, the protocol is internally consistent — stages reference valid files, inputs/outputs chain correctly, routing tables match stage files.
Levels: smoke, unit, integration
What it tests:
- File existence, permissions, naming conventions (smoke)
- Hook scripts (11 TypeScript via bun), stage frontmatter, knowledge inventory (unit)
- Scope-stage mapping, graph consistency, stage I/O contract chains, protocol compliance (integration)
- Stage output-to-step validation: all declared outputs referenced in instruction steps (integration, deterministic via the aidlc-validate.ts CLI tool)
Run: bun tests/run-tests.ts (default, no flags needed). bash tests/run-tests.sh is a compatibility wrapper for existing POSIX commands.
Layer 2: Stage (CI push, LLM, minutes)
Runs individual stages in isolation with known workspace + state fixtures. Verifies each stage produces correct artifacts when given deterministic input.
Levels: integration
What it tests: - Preflight health gate: Claude CLI on PATH, AWS credentials valid, Claude responds (exit 0), response non-empty (preflight) - CLI tool utility handlers: intent-birth, --doctor, --status, --stage, --phase (integration) - Individual stages with greenfield/brownfield stubs, artifact verification (integration)
Run: bun tests/run-tests.ts --ci
Layer 3: Acceptance (release, LLM, hours)
Runs full workflows and verifies the experience: beyond state transitions, it checks artifact content, cross-stage coherence, and domain correctness.
Levels: e2e
What it tests: - Full bugfix lifecycle with brownfield stub + artifact assertions - Full POC lifecycle with greenfield stub + artifact assertions - State progression, scope routing, audit completeness, jump mechanics - LLM semantic review of stage instruction quality (clarity, logical flow, ambiguity detection)
Run: bun tests/run-tests.ts --release
Cross-Platform Coverage
The test suite runs on macOS, Linux, and Windows through the native Bun runner:
bun tests/run-tests.ts [--ci | --all --debug -P 8]
bash tests/run-tests.sh ... remains as a POSIX compatibility wrapper and delegates to the same TypeScript runner. At runtime this implementation's hooks, CLI tools, and test runner require bun; Bash is no longer the primary runner substrate.
Portability constraints baked into the suite:
- Paths:
createTestProjectintests/harness/fixtures.tsnormalizes temporary project paths so they round-trip cleanly through JSON and nativebun. - In-place edits: Prefer TypeScript file writes in tests. If a shell helper is unavoidable, avoid BSD/GNU-specific
sed -iforms. grep -qiF: Git Bash has a known bug combining-iand-F. Use-ialone if your pattern has no regex metacharacters. Tests hit this in t16 before it was fixed.tararchives: macOStarinjects._*AppleDouble sidecar files by default. When bundling source for cross-platform test runs, useCOPYFILE_DISABLE=1 tar …orgit archive.- LLM timing on Windows: Bedrock calls from Windows EC2 can be meaningfully slower than from macOS (first-call cold start, MSYS process fork overhead). SDK/tui tests should assert on driver result surfaces and let the runner's preflight/per-file Claude gate separate absent substrate from real failures.
Running the suite on Windows manually:
- Install
bun, Node.js, and the Claude Code CLI. - Install Git for Windows if you are running the full suite or the POSIX wrapper compatibility smoke; the native runner path itself does not require Bash.
- For e2e TUI tests, install the dev dependencies with npm so node can resolve
node-ptyand@xterm/headless. - Set
AIDLC_NODE_BINto the concretenode.exepath and setAIDLC_TUI_LIVE=1for a full acceptance run. - Run
bun tests/run-tests.ts --all --debug -P 8.
No WSL or Docker is required; the supported validation substrate is native Windows.
Repeatable MR10 Windows EC2 runbook:
- Stand up a disposable Windows Server 2022 host with SSM access:
aws cloudformation deploy \
--stack-name aidlc-windows-test \
--template-file tests/harness/windows/windows-test.cfn.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides VpcId=vpc-... SubnetId=subnet-...
- Sync the committed git tree under test:
bun tests/harness/windows/sync.ts --stack-name aidlc-windows-test HEAD
- Install repo dev dependencies on the box:
bun tests/harness/windows/ssm-run.ts --stack-name aidlc-windows-test -- \
powershell -ExecutionPolicy Bypass -File C:\aidlc\tests\harness\windows\setup.ps1 -ProjectDir C:\aidlc
- Run the Windows
--allgate with live TUI enabled:
bun tests/harness/windows/ssm-run.ts --stack-name aidlc-windows-test -- \
powershell -ExecutionPolicy Bypass -File C:\aidlc\tests\harness\windows\run-all.ps1 -ProjectDir C:\aidlc -Parallel 8
- Tear down the host:
aws cloudformation delete-stack --stack-name aidlc-windows-test
run-all.ps1 exports AIDLC_NODE_BIN and AIDLC_TUI_LIVE=1 before invoking bun tests/run-tests.ts --all --debug -P <N>, so a green result cannot come from silently skipping the live TUI journeys. It probes the claude binary across C:\Users\Administrator\.local\bin and the systemprofile home, since the native installer drops claude.exe under whichever user ran the CloudFormation UserData bootstrap (Administrator under EC2Launch v2).
The stack defaults to c5.4xlarge — the proven size for the full --all -P 8 live run. The e2e tier carries per-test bun:test timeouts (the Bolt-worktree lifecycle test lands at ~5.5s of its 5s budget on c5.4xlarge), so a smaller box (e.g. t3.large) tips deterministic Bolt/runtime tests into spurious timeouts under parallel load. Shrink the InstanceType parameter only when running a lighter tier selection.
Preflight Validation
Before running unfiltered live-capable levels (integration or e2e), the runner executes tests/integration/t19.test.ts as a gate. It drives a tiny real turn through the Claude Agent SDK (the same live path the integration tier uses) and asserts only on deterministic surfaces. If the preflight fails, deterministic files still run and Claude-dependent files are skipped with per-file SKIP entries.
| Assertion | Surface | On fail |
|---|---|---|
| AWS credentials valid | aws sts get-caller-identity exits 0 (PASS-by-skip when the aws CLI is absent) |
bail — Bedrock needs IAM auth |
| Live turn reaches a terminal result | the SDK run produces a non-undefined resultEvent (the binary the tier needs is present and reachable) |
bail — substrate/API unreachable |
| Turn completes without error | resultEvent.is_error === false (the deterministic equal of claude -p exit 0; a 124/137 hang leaves it undefined) |
bail — API unresponsive |
| Response is non-empty | the run captured some output — a tool_result or assistant text (presence, never content) |
bail — API produced nothing |
A red here is a real environment finding (missing claude, expired creds), never a flake to soften — exactly the gate's job of bailing the downstream LLM tiers fast.
Test Registry
The suite is discovered, not registered: bun tests/run-tests.ts walks the
four level directories (tests/{smoke,unit,integration,e2e}/) and runs every
t*.test.ts it finds. There is no hand-maintained per-test table to keep in
sync — adding a test file is all it takes for the runner to pick it up.
What each test covers is tracked mechanically in
tests/.coverage-registry.json, generated by
bun tests/gen-coverage-registry.ts from the covers: header in a test file's
leading comment block (typically line 1; a few files legitimately declare none
and simply contribute no coverage claim). The generator enumerates the framework's units across seven
classes (function, audit, scope, stage, hook, subcommand,
render-surface), maps each covers: claim onto an enumerated unit, and emits
the coverage counts plus a ratchet floor. Regenerate and verify drift with:
bun tests/gen-coverage-registry.ts # rewrite the registry from disk
bun tests/gen-coverage-registry.ts --check # fail if the committed registry is stale
tests/.coverage-registry.json is the authoritative, machine-checked index —
consult it (or grep the covers: headers directly) to find which test exercises
a given function, audit event, scope, stage, hook, subcommand, or render
surface. The --check mode is wired into the suite so a registry that drifts
from disk reds the gate.
Note: t19 appears in both unit (
tests/unit/t19.test.ts, the jump CLI tool) and integration (tests/integration/t19.test.ts, the live preflight gate) — a level/file path, not a bare ID, disambiguates such collisions.
Trigger Points
| Trigger | Layer | Command | Where |
|---|---|---|---|
git commit |
L1 | bun tests/run-tests.ts |
Local (pre-commit hook) |
| CI pipeline | L2 | bun tests/run-tests.ts --ci |
CI/CD pipeline |
| Release / merge to main | L3 | bun tests/run-tests.ts --release |
CI/CD pipeline |
L1 can be enforced via a git pre-commit hook: bun tests/run-tests.ts || exit 1.
Stubs
Greenfield Stub: tests/fixtures/greenfield-todo/
A project description with no source code. Workspace-detection classifies as greenfield. Gives the LLM deterministic intent context for ideation stages.
Contents: Just README.md describing a React Todo App with TypeScript and Vite.
Brownfield Stub: tests/fixtures/brownfield-todo/
Minimal React+TypeScript+Vite source (~10 files, ~200 LOC). Workspace-detection classifies as brownfield. RE, requirements, and design stages have concrete code to analyze.
Contents:
- package.json — react, react-dom, typescript, vite, vitest
- tsconfig.json, vite.config.ts, index.html
- src/main.tsx, src/App.tsx
- src/types/todo.ts — Todo interface (id, title, completed)
- src/components/TodoList.tsx — list + add form (~40 lines)
- src/components/TodoItem.tsx — checkbox + title + delete button
- src/hooks/useTodos.ts — addTodo, toggleTodo, deleteTodo
RE Artifacts Fixture: tests/fixtures/re-artifacts/
Pre-seeded reverse-engineering output for downstream stage tests. Copied into the test project's space-level repository store at $PROJ/aidlc/spaces/default/codekb/<repo>/ during setup.
Contents: 4 minimal .md files (architecture-overview, technology-stack, codebase-analysis, integration-points) describing the brownfield-todo app.
Inception Artifacts Fixture: tests/fixtures/inception-artifacts/
Pre-seeded inception phase output for tests that jump into construction. Copied into $PROJ/aidlc/spaces/default/intents/<record>/inception/{requirements-analysis,application-design,units-generation}/ during setup.
Contents: 7 minimal .md files (requirements, components, component-methods, services, component-dependency, unit-of-work, unit-of-work-story-map) describing the Todo app. Unit name: todo-core.
Construction Artifacts Fixture: tests/fixtures/construction-artifacts/
Pre-seeded construction phase output for tests that jump to mid-construction stages (e.g., code-generation). Copied into $PROJ/aidlc/spaces/default/intents/<record>/construction/todo-core/functional-design/ during setup.
Contents: 1 minimal .md file (functional-design) describing the todo-core unit's component specs and state management.
State Fixtures
| Fixture | Project Type | Scope | State | Used By |
|---|---|---|---|---|
state-pre-workspace-detection.md |
-- | feature | Welcome+scaffold done, workspace-detection next | t70, t71 |
state-initialization-done.md |
Greenfield | feature | Init done, intent-capture next | t73 |
state-brownfield-init-done.md |
Brownfield | bugfix | Init done, RE next | t72 |
state-mid-inception.md |
Brownfield | bugfix | RE done, requirements-analysis next | t74 |
state-mid-ideation.md |
Greenfield | feature | Intent+market done, feasibility next | t08, t10, t11, t12, t20, t22, t24, t25, t37 |
state-construction.md |
-- | -- | Construction phase | t07, t10, t11, t26, t57 |
state-operation.md |
-- | -- | Operation phase | t07, t10, t11 |
state-completed.md |
-- | -- | All stages done | t08, t11 |
state-jumped.md |
Brownfield | bugfix | Mid-workflow with jump history | t11, t37, t42 |
state-corrupted.md |
-- | -- | Invalid/corrupted state | t08, t10 |
How to Add a Stage Test
- Choose the stage to test and identify what state fixture it needs (the state must show that stage as the current/next stage)
- Create or reuse a state fixture in
tests/fixtures/ - Create
tests/integration/tNN-stage-SLUG.test.tsand use the shared TypeScript harness helpers (tests/harness/fixtures.ts,tests/harness/sdk-drive.ts, ortests/harness/tui-drive.ts) rather than shell TAP helpers. - Run with
bun tests/run-tests.ts --integrationor directly:bun test tests/integration/tNN-stage-SLUG.test.ts
How to Add Acceptance Assertions
To add artifact assertions to an existing e2e workflow test under tests/e2e/:
- Read the current test and understand what it already checks
- Add
expect(...)assertions inside the existingtest(...)block (bun:test counts assertions from the calls themselves — there is noplanline to keep in sync) - Use flexible patterns: match
/[Tt]odo/againstreadFileSynccontent, not exact strings - Use
test.skipIf(...)/ an early return for assertions that depend on non-deterministic LLM output format - Use
expect(statSync(path).size).toBeGreaterThan(minBytes)for size-bound checks
Assertion Design Principles
- Keyword classes — Use case-insensitive regex:
[Tt]odo,[Rr]eact,[Bb]rownfield - Flexible discovery — Use
find+wc -lto count files rather than checking exact names - Size bounds — Use
statSync(path).sizewithtoBeGreaterThan()for minimum content - Graceful degradation — Use
skipwhen an assertion depends on non-deterministic LLM output - Structure over content — Check for markdown headings (
^#), file existence, directory creation before checking content
Environment Variables
| Variable | Default | Description |
|---|---|---|
AIDLC_TEST_TIMEOUT |
1800 |
Per-claude -p call timeout in seconds. Set to 0 to disable. |
AIDLC_TUI_SETTING_SOURCES |
project |
Setting sources injected into live claude TUI launches. Use default or an empty value only for focused calibration that intentionally includes user/local Claude settings. |
AIDLC_TUI_TRACE_POLL_MS |
10000 |
Minimum interval between answer_gate_poll snapshots in TUI NDJSON traces while a long journey is waiting for the next menu or disk terminator. |
CLI Reference
# Entrypoints
bun tests/run-tests.ts # Native cross-platform runner
bash tests/run-tests.sh # POSIX compatibility wrapper
# Level flags (combinable)
--smoke # Structural validation
--unit # Single-component isolation
--integration # Cross-component contracts and stage/CLI utilities
--e2e # Full lifecycle, worktree, and rendered terminal journeys
# Profile flags (shortcuts)
(default) # smoke + unit + integration
--ci # smoke + unit + integration
--release # smoke + unit + integration + e2e
--all # Same as --release
# Output modifiers
--verbose # Write per-test logs to tests/logs/
--debug # Implies --verbose; streams per-test output and writes SDK/TUI
# driver traces to tests/logs/
--filter PAT # Only run tests whose filename matches extended regex PAT
--parallel N # Run up to N test files concurrently within a tier (alias: -P N).
# Default: 1 (serial). Smoke and unit tiers are always serial.
Live SDK and TUI harness drivers default to project-only Claude setting sources.
That means they load the copied test .claude/ project settings and hooks while
excluding developer user-level hooks/settings. This mirrors the installed
framework surface and prevents local interactive preferences from changing test
behavior; explicit driver options or AIDLC_TUI_SETTING_SOURCES remain the
escape hatch for calibration.
--all --debug (and --release --debug) defaults AIDLC_TUI_LIVE=1 unless the
environment already set it. This makes the "everything with traces" profile run
the live, token-spending TUI journeys by default; set AIDLC_TUI_LIVE=0
explicitly to keep those files on their in-test SKIP path.
Parallel Execution
--parallel N (or -P N) runs up to N test files concurrently within a tier. Default is serial (1).
When it helps. Integration and e2e levels each spend most of their wall-clock on claude -p subprocess startup and LLM turns. These tests are already filesystem-isolated — setup_integration_project scaffolds a fresh $PROJ per test — so they can run side-by-side without interfering.
Spike results (2026-05-06, Opus 4.7 via Bedrock):
| Scenario | Serial | --parallel 4 |
--parallel 8 |
|---|---|---|---|
4 × /aidlc --help |
56s | 16s (3.5x) | — |
8 × /aidlc --help |
— | — | 31s |
All 8 parallel calls observed cache_read=73789 — Bedrock prompt caching stays warm across concurrent workers. No throttling or corruption observed at 8-way.
What stays serial. Smoke and unit tiers ignore --parallel and run serially regardless. They already complete in seconds and their interleaved output would hurt debuggability for no wall-clock gain. The preflight gate (tests/integration/t19.test.ts) also runs serially because the LLM tiers depend on its exit status.
Output under parallelism. START markers stream live (several can appear back-to-back before the first DONE — that's the visible signal workers are concurrent). In normal/verbose mode, each worker's TAP body is buffered and flushed to stdout as one contiguous block under a directory-mutex (mkdir $LOG_DIR/.stdout.lock, atomic on POSIX — works on macOS bash 3.2 without flock). So ok/not ok lines from different files never interleave; stdout reads top-to-bottom like a serial run, just with the file completion order determined by how long each test took rather than dispatch order. In --debug mode, Bun stdout/stderr streams live while still being written to each per-test log; parallel debug output is prefixed by file basename so overlapping live workers remain attributable. SDK/TUI/Kiro-ACP driver traces are written beside the logs as $LOG_DIR/sdk-drive-*.ndjson, $LOG_DIR/tui-drive-*.ndjson, and $LOG_DIR/kiro-acp-drive-*.ndjson; their exact filenames depend on process IDs and TUI session names, so the runner prints the glob at startup and at each test start. The Kiro-ACP trace records the live kiro-cli acp turn event-by-event (spawn, prompt start, each tool_call/tool_call_update with its verbatim output preview, permission answers, the spawned process's stderr, and the terminal result/timeout/end), so a session/prompt timeout can be diagnosed after the fact — distinguishing a turn that was progressing (real tool calls firing) from one that stalled.
Worker coordination. The parent backgrounds run_bun_test_file with & and holds a slot gate via jobs -rp | wc -l. Each worker writes an atomic .meta sidecar to $LOG_DIR/_results/; the parent reads them after wait to populate the summary tables. macOS ships bash 3.2.57 (no wait -n), so the gate polls every 200ms — negligible next to minute-long LLM calls.
Guidance. Start with --parallel 4. Raise to 8 if Bedrock capacity and your bill tolerate it. Drop back to serial for debugging a single failing test — or use --filter to isolate it.