@post-print/agent-test 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -173
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -86
- package/dist/cli.js.map +1 -1
- package/dist/debug-bundle.d.ts +0 -1
- package/dist/debug-bundle.d.ts.map +1 -1
- package/dist/debug-bundle.js +0 -7
- package/dist/debug-bundle.js.map +1 -1
- package/dist/doctor.js +4 -4
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/live-isolation.d.ts +0 -1
- package/dist/live-isolation.d.ts.map +1 -1
- package/dist/live-isolation.js +1 -4
- package/dist/live-isolation.js.map +1 -1
- package/dist/load-suite.d.ts.map +1 -1
- package/dist/load-suite.js +12 -0
- package/dist/load-suite.js.map +1 -1
- package/dist/preflight.d.ts +2 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +4 -4
- package/dist/preflight.js.map +1 -1
- package/dist/record-trace.d.ts +3 -14
- package/dist/record-trace.d.ts.map +1 -1
- package/dist/record-trace.js +7 -43
- package/dist/record-trace.js.map +1 -1
- package/dist/run-suite.d.ts +28 -10
- package/dist/run-suite.d.ts.map +1 -1
- package/dist/run-suite.js +157 -135
- package/dist/run-suite.js.map +1 -1
- package/dist/types.d.ts +11 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-suite.d.ts.map +1 -1
- package/dist/validate-suite.js +11 -12
- package/dist/validate-suite.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,210 +1,100 @@
|
|
|
1
1
|
# @post-print/agent-test
|
|
2
2
|
|
|
3
|
-
**Source of truth for** agent
|
|
3
|
+
**Source of truth for** the direct-agent test package.
|
|
4
4
|
|
|
5
|
-
<!-- doc-meta: owner=eng | last-reviewed=2026-
|
|
5
|
+
<!-- doc-meta: owner=eng | last-reviewed=2026-09-02 -->
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Scenario runner for real Cursor and Claude agents, built on `@post-print/agent-harness`.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
> **Deprecated and removed:** stored-trace replay is not an agent test. `host: "replay"`, `replayTrace`, `--record-fixtures`, and the old `--live` mode flag are rejected with migration guidance. Every scenario execution now launches Cursor or Claude and can incur provider usage.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Direct TypeScript API
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
`runAgentTest` is the execution core. JSON suites load into this same function.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { runAgentTest } from "@post-print/agent-test";
|
|
17
|
+
|
|
18
|
+
const result = await runAgentTest({
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
scenario: {
|
|
21
|
+
name: "uses the project instructions",
|
|
22
|
+
prompt: "Review the current change.",
|
|
23
|
+
rubric: { mustReadPath: ["AGENTS.md"] },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
16
26
|
```
|
|
17
27
|
|
|
18
|
-
|
|
28
|
+
The default host is Cursor. Set `scenario.host: "claude"`, `defaults.host`, or the top-level `host` option for Claude. The scenario host wins over the top-level override, which wins over suite defaults. The default suite name is `direct`; worktree isolation, judging, timeout enforcement, and announce-stop retry are enabled by default.
|
|
29
|
+
|
|
30
|
+
## JSON suites and CLI
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
JSON is an authoring adapter, not a stored answer. Consumer repositories can keep suites under `agent-suites/<suite>/scenarios.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"name": "routing",
|
|
37
|
+
"defaults": { "host": "cursor", "profile": "cursor" },
|
|
38
|
+
"scenarios": [
|
|
39
|
+
{
|
|
40
|
+
"name": "reads instructions",
|
|
41
|
+
"prompt": "Review the current change.",
|
|
42
|
+
"rubric": { "mustReadPath": ["AGENTS.md"] }
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
21
47
|
|
|
22
|
-
|
|
48
|
+
Run Cursor by default or select Claude explicitly:
|
|
23
49
|
|
|
24
50
|
```bash
|
|
25
51
|
npx agent-test --suites-dir agent-suites
|
|
26
|
-
npx agent-test --suites-dir agent-suites --suite
|
|
27
|
-
npx agent-test --suites-dir agent-suites --
|
|
28
|
-
npx agent-test --suites-dir agent-suites --live --host claude --suite ambient-routing # CLAUDE_AUTH_MODE + claude CLI
|
|
29
|
-
npx agent-test --live --compare-pairs skeleton-clean:skeleton-messy --out-dir "$TMPDIR/compare"
|
|
30
|
-
npx agent-test compare --a clean.suite-report.json --b messy.suite-report.json --out-dir "$TMPDIR/compare"
|
|
31
|
-
npx agent-test --suites-dir agent-suites --report-out "$TMPDIR/agent-test-report" # dir: html + suite JSON
|
|
32
|
-
npx agent-test --suites-dir agent-suites --report-out "$TMPDIR/reports/run.html" # file: html only
|
|
52
|
+
npx agent-test --suites-dir agent-suites --suite routing
|
|
53
|
+
npx agent-test --suites-dir agent-suites --host claude
|
|
33
54
|
npx agent-test --doctor
|
|
34
55
|
```
|
|
35
56
|
|
|
36
|
-
|
|
57
|
+
Direct runs require an exported `CURSOR_API_KEY` for Cursor or `ANTHROPIC_API_KEY` plus the Claude Code CLI for Claude. Judge criteria use `CURSOR_API_KEY` unless `--no-judge` is set. The CLI does not load `.env`.
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
The old `--live` flag is removed because direct execution is now the only execution mode. Direct runs capture transient traces automatically; use `--keep-recordings` or `--debug` to retain diagnostics.
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| ends with `.html` | the HTML report is written to exactly that file (parent dirs created) |
|
|
43
|
-
| any other path | treated as a directory: `report.html` plus all other report content — per-suite `<suite>.suite-report.json`, and the compare JSON / markdown / HTML when `--compare-pairs` is used |
|
|
61
|
+
## Validation, rubrics, and comparison
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Bun is fine for local package development (`bun install` / `bun run build` in this monorepo), but consumers do not need Bun to run suites.
|
|
48
|
-
|
|
49
|
-
Default suites root: `agent-suites/` (must exist, or pass `--suites-dir`). Absolute `--suites-dir` is supported. Optional `--rubrics-dir` loads harness-only answer keys from `<rubricsDir>/<suite>/rubrics.json` (preferred over a sibling file when present).
|
|
50
|
-
|
|
51
|
-
### Harness-only rubrics
|
|
52
|
-
|
|
53
|
-
`scenarios.json` may omit `rubric` (or use `{}`) and keep only prompts / `seedPatch` / `compareId`. Put `must` / `mustNot` / `judge` / tool matchers in:
|
|
54
|
-
|
|
55
|
-
- sibling `rubrics.json` or `scenarios.rubric.json`: `{ "scenarios": { "<scenario name>": { "must": […] } } }`
|
|
56
|
-
- or `--rubrics-dir <path>` → `<path>/<suiteName>/rubrics.json`
|
|
57
|
-
|
|
58
|
-
External entries **replace** the inline rubric for that scenario name. Unknown names in the rubrics file are an error. Keep rubrics off agent-visible roots (absolute `--suites-dir` / `--rubrics-dir` under `$TMPDIR`, or outside the IDE-open workspace).
|
|
59
|
-
|
|
60
|
-
Live output always uses ANSI color (including under Cursor agent shells that set `NO_COLOR`).
|
|
61
|
-
|
|
62
|
-
## Environment variables
|
|
63
|
-
|
|
64
|
-
See repo-root `.env.example`. Common knobs:
|
|
65
|
-
|
|
66
|
-
| Variable | Purpose |
|
|
67
|
-
| ------------------------------------------- | ------------------------------------------------------------- |
|
|
68
|
-
| `CURSOR_API_KEY` | Required for `--live` Cursor and judge classifiers |
|
|
69
|
-
| `CLAUDE_AUTH_MODE` | Required for `--live --host claude`: `api-key` or `subscription` (no default) |
|
|
70
|
-
| `ANTHROPIC_API_KEY` | Required when `CLAUDE_AUTH_MODE=api-key` |
|
|
71
|
-
| `CLAUDE_CODE_BIN` | Optional path to Claude Code CLI binary |
|
|
72
|
-
| `CLAUDE_AGENT_MODEL` | Optional Claude model override |
|
|
73
|
-
| `CLAUDE_CODE_ALLOWED_TOOLS` | Optional `--allowedTools` list for Claude live runs |
|
|
74
|
-
| `AGENT_TEST_DEBUG` | Same as `--debug` when `1`/`true` |
|
|
75
|
-
| `AGENT_TEST_VERBOSE` | Extra tips (e.g. OOM isolation) when `1` |
|
|
76
|
-
| `AGENT_TEST_VERBOSE_PATHS` | Print full paths when `1` |
|
|
77
|
-
| `AGENT_TEST_QUIET` | Suppress progress when `1` |
|
|
78
|
-
| `AGENT_TEST_TIMEOUT_MS` | Live hard timeout (default 600000; `0` disables) |
|
|
79
|
-
| `AGENT_TEST_LIVE_RETRIES` | Judge infra retry attempts (default 3) |
|
|
80
|
-
| `AGENT_TEST_SCENARIO_RETRIES` | Live announce-stop scenario retries (default 1; `0` disables) |
|
|
81
|
-
| `AGENT_TEST_ALLOW_IN_PLACE` | Allow `--no-worktree` live runs when `1` |
|
|
82
|
-
| `AGENT_TEST_NO_WORKTREE` | Disable worktree isolation when `1`/`true` |
|
|
83
|
-
| `AGENT_TEST_NO_ISOLATE` | Disable isolated subprocesses when `1` |
|
|
84
|
-
| `AGENT_TEST_SCENARIO_SETTLE_MS` | Settle delay between live scenarios |
|
|
85
|
-
| `CURSOR_AGENT_MODEL` / `CURSOR_JUDGE_MODEL` | Optional model overrides |
|
|
86
|
-
| `CURSOR_JUDGE_TEMPERATURE` | Optional judge temperature |
|
|
87
|
-
|
|
88
|
-
## Debug mode
|
|
63
|
+
These commands inspect configuration or existing reports; they do not claim to execute an agent:
|
|
89
64
|
|
|
90
65
|
```bash
|
|
91
|
-
npx agent-test --suites-dir agent-suites
|
|
92
|
-
npx agent-test --suites-dir agent-suites
|
|
93
|
-
npx agent-test --
|
|
66
|
+
npx agent-test --validate-only --validate-paths --suites-dir agent-suites
|
|
67
|
+
npx agent-test --validate-seeds --suites-dir agent-suites
|
|
68
|
+
npx agent-test --compare-pairs clean:changed --out-dir "$TMPDIR/compare"
|
|
69
|
+
npx agent-test compare --a clean.suite-report.json --b changed.suite-report.json --out-dir "$TMPDIR/compare"
|
|
94
70
|
```
|
|
95
71
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
sessions/<id>/<suite>/<scenario>.debug/
|
|
100
|
-
summary.md # verdict + Why (category hint + evidence + trace stats)
|
|
101
|
-
transcript.md # Why, prompt, rubric, interleaved messages/tools (incl. results), failures
|
|
102
|
-
scenario.json # prompt + rubric + seed metadata
|
|
103
|
-
result.json # pass/fail, duration, failures, usage, skillsInvoked, routing, counts
|
|
104
|
-
trace.json
|
|
105
|
-
failures.json # includes category + evidence
|
|
106
|
-
judge-debug.json # when judge criteria ran (SDK status/error, sizes, attempt)
|
|
107
|
-
environment.json # versions/models/timeout/isolation; API keys only as booleans
|
|
108
|
-
rerun.sh # shell-quoted exact re-run command (export API keys yourself)
|
|
109
|
-
```
|
|
72
|
+
`scenarios.json` may omit inline rubric keys when they are supplied by sibling `rubrics.json` / `scenarios.rubric.json`, or by `--rubrics-dir <path>` at `<path>/<suite>/rubrics.json`. External rubric entries replace the inline rubric for the same scenario.
|
|
110
73
|
|
|
111
|
-
|
|
74
|
+
Reports pair scenarios by `compareId` when present, otherwise by normalized scenario name. JSON, Markdown, and HTML reports include outcomes, token usage, duration, tools, and grounding signals.
|
|
112
75
|
|
|
113
|
-
|
|
76
|
+
## Isolation and diagnostics
|
|
114
77
|
|
|
115
|
-
|
|
78
|
+
Each scenario uses a detached git worktree by default. This isolates edits, but it does not prevent a local host from reading the IDE-open caller checkout. Keep answer keys outside agent-visible roots when that distinction matters.
|
|
116
79
|
|
|
117
|
-
|
|
118
|
-
| ----------------- | -------------------------------------------------------------------------------------- |
|
|
119
|
-
| `rubric_miss` | Assertion/judge criterion miss |
|
|
120
|
-
| `judge_infra` | Judge SDK/API failure (not a criterion miss) |
|
|
121
|
-
| `agent_runtime` | Agent session error, timeout, AskQuestion, subprocess exit |
|
|
122
|
-
| `worktree_leak` | Live agent mutated the caller working tree (harness `--debug-dir` staging is excluded) |
|
|
123
|
-
| `recording_error` | Failed to persist a staging/fixture trace |
|
|
80
|
+
`--debug` retains an evidence bundle under `$TMPDIR/agent-spec/sessions/<id>/` by default. It includes the transcript, scenario, result, trace, failures, environment metadata, judge details, and an exact direct-run rerun command. Use `--debug-dir` to override the parent directory.
|
|
124
81
|
|
|
125
|
-
|
|
82
|
+
`Ctrl+C` cancels active Cursor/Claude work and cleans worktrees. `--no-worktree` requires `AGENT_TEST_ALLOW_IN_PLACE=1` because agent edits will persist in the caller checkout.
|
|
126
83
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Live runs need `CURSOR_API_KEY` and a suites directory that exists. Preflight fails when the resolved suites directory is missing (default `agent-suites/` if `--suites-dir` is omitted). `--suites-dir` may be relative to the repo cwd or an absolute path (for example a scrubbed suite tree under `$TMPDIR`).
|
|
130
|
-
|
|
131
|
-
Passing live runs write staging traces under `$TMPDIR/agent-spec/sessions/<pid>-<timestamp>/` (removed on exit unless `--keep-recordings`). Use `--record-fixtures` to overwrite each scenario's committed `replayTrace` path. `--no-worktree` requires `AGENT_TEST_ALLOW_IN_PLACE=1`.
|
|
132
|
-
|
|
133
|
-
### Isolation model
|
|
134
|
-
|
|
135
|
-
Live runs use a **detached git worktree** for agent file edits. That is not full filesystem isolation:
|
|
136
|
-
|
|
137
|
-
- **Worktree does:** keep seed/apply edits and agent Write/Edit tools off the caller's working tree (leak checks catch escapes).
|
|
138
|
-
- **Worktree does not:** stop context (skills/rules) from loading from the caller checkout by design, or stop Cursor **local** agents from Shell/Read against the IDE-open workspace instead of only `local.cwd`.
|
|
139
|
-
|
|
140
|
-
Therefore: do **not** put answer keys, golden replays, or judge-bearing scenario text where a null-arm agent can forage them on the caller/IDE root. Prefer:
|
|
141
|
-
|
|
142
|
-
- opaque prompts + `compareId`
|
|
143
|
-
- seeds that only mutate fixtures (not skill bodies)
|
|
144
|
-
- harness-only rubrics (sibling `rubrics.json` / `scenarios.rubric.json`, or `--rubrics-dir` outside the open workspace)
|
|
145
|
-
- consumer orchestrators that park answer keys off the open workspace (toolbox pattern)
|
|
146
|
-
- cloud runtime when true FS isolation is required
|
|
147
|
-
|
|
148
|
-
Live agent runs have a **hard timeout** (default **10 minutes**, override with `--timeout-ms` or `AGENT_TEST_TIMEOUT_MS`; disable with `--no-timeout` or `AGENT_TEST_TIMEOUT_MS=0`). If the agent invokes `AskQuestion` or similar user-input tools, the harness fails fast with a clear error — live mode is single-shot and cannot supply follow-up turns. Use `--allow-user-input` only for intentional multi-turn dogfood (the run may still hang waiting for stdin).
|
|
149
|
-
|
|
150
|
-
Announce-stop flakes (agent exits after Routing with no tools) are retried once by default on live runs (`AGENT_TEST_SCENARIO_RETRIES=1` or `--scenario-retries 1`; set `0` to disable). This is separate from `AGENT_TEST_LIVE_RETRIES` (judge infra only).
|
|
151
|
-
|
|
152
|
-
### Dialogue skills in live runs
|
|
153
|
-
|
|
154
|
-
Skills that expect multi-turn Socratic dialogue (for example `crystallize`) will hang or fail in `--live` unless the scenario is written for one-shot completion:
|
|
155
|
-
|
|
156
|
-
- **Replay-only** — commit a golden trace where the agent finishes without asking questions; use `host: "replay"` or `skip: true` with live skipped at suite level.
|
|
157
|
-
- **One-shot live prompt** — instruct the agent to mirror intent and emit the final artifact in a single turn (`no AskQuestion; produce Crystallized idea now`).
|
|
158
|
-
- **Ambient routing** — fuzzy-intent scenarios that only require mirroring + one assumption are naturally one-shot; full crystallize dialogue is not.
|
|
159
|
-
|
|
160
|
-
Do not weaken dialogue-first product skills for CI; reshape the suite contract instead.
|
|
84
|
+
Dialogue-first skills must be tested with a one-shot prompt or an intentional `--allow-user-input` run. Do not weaken the production skill to make a headless test pass.
|
|
161
85
|
|
|
162
86
|
## MCP servers
|
|
163
87
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
```json
|
|
167
|
-
{
|
|
168
|
-
"defaults": {
|
|
169
|
-
"mcpServers": {
|
|
170
|
-
"docs": {
|
|
171
|
-
"type": "http",
|
|
172
|
-
"url": "https://example.com/mcp",
|
|
173
|
-
"headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
"scenarios": [
|
|
178
|
-
{
|
|
179
|
-
"name": "use echo",
|
|
180
|
-
"prompt": "Call the echo tool with text hello.",
|
|
181
|
-
"mcpServers": {
|
|
182
|
-
"echo": {
|
|
183
|
-
"type": "stdio",
|
|
184
|
-
"command": "node",
|
|
185
|
-
"args": ["packages/test/fixtures/mcp-echo/server.mjs"]
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
"rubric": {
|
|
189
|
-
"mustCallTool": ["echo:hello"],
|
|
190
|
-
"mustNotCallTool": ["shell"]
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
]
|
|
194
|
-
}
|
|
195
|
-
```
|
|
88
|
+
Cursor and Claude scenarios can attach inline stdio or HTTP/SSE MCP servers through suite defaults or scenario overrides. Scenario server names replace matching defaults. `${ENV_VAR}` placeholders resolve at run time. Ambient project/user MCP configuration is not loaded.
|
|
196
89
|
|
|
197
|
-
-
|
|
198
|
-
- `${ENV_VAR}` placeholders expand in `command`, `args`, `env`, `url`, `headers`, and OAuth fields at run time.
|
|
199
|
-
- Rubric `mustCallTool` / `mustNotCallTool` match tool **name** substrings (works with MCP name prefixes). Use `name:argFragment` to also require a substring in JSON args.
|
|
200
|
-
- Rubric `mustReadPath` / `mustNotReadPath` match substrings on **Read** tool JSON args (registry-first / avoid inventing paths). Keep hallucination scoring in live `judge` questions — no heavy factuality engine in v1.
|
|
201
|
-
- Suite defaults may set `profile: "skeleton"` and/or `contextSources` (additive paths / `.skeleton/customize/` basenames). Shared/cursor/claude defaults stay backwards-compatible.
|
|
202
|
-
- Live runs surface provider `usage` on traces/results; suite summary + HTML report include token sum / p50 / p95 when present.
|
|
203
|
-
- `--compare-pairs A:B` (or `agent-test compare --a/--b`) pairs scenarios by **`compareId`** when present, else band-neutral scenario name (`outcome:` / `transfer:` stripped), and writes `compare-report.json` / `.md` / `.html` with pass/fail, tokens, toolCallCount, durationMs, and skill/registry hop proxies. Suite HTML also embeds an A/B table when two reports are present.
|
|
204
|
-
- Replay hosts ignore `mcpServers` but still score recorded `toolCalls` against those matchers.
|
|
90
|
+
## In-repo package checks
|
|
205
91
|
|
|
206
|
-
|
|
92
|
+
The repository does not run paid agents in its default CI package-integrity checks:
|
|
207
93
|
|
|
208
|
-
```
|
|
209
|
-
|
|
94
|
+
```bash
|
|
95
|
+
bun run build
|
|
96
|
+
node packages/test/dist/cli.js --validate-only --suites-dir packages/test/fixtures --suite smoke
|
|
97
|
+
node packages/test/dist/cli.js --doctor
|
|
210
98
|
```
|
|
99
|
+
|
|
100
|
+
Credentialed acceptance requires one TypeScript `runAgentTest` call and one JSON-suite CLI run against a real host.
|
package/dist/cli.d.ts
CHANGED
|
@@ -10,9 +10,6 @@ export interface ParsedCliArgs {
|
|
|
10
10
|
filter?: string;
|
|
11
11
|
scenarioFilter?: string;
|
|
12
12
|
stagingSessionId?: string;
|
|
13
|
-
record: boolean;
|
|
14
|
-
recordFixtures: boolean;
|
|
15
|
-
live: boolean;
|
|
16
13
|
judge?: boolean;
|
|
17
14
|
worktree?: boolean;
|
|
18
15
|
keepRecordings: boolean;
|
|
@@ -35,16 +32,13 @@ export interface ParsedCliArgs {
|
|
|
35
32
|
compareMode: boolean;
|
|
36
33
|
compareA?: string;
|
|
37
34
|
compareB?: string;
|
|
38
|
-
/**
|
|
35
|
+
/** Direct-run A:B suite dirs or report JSON paths. */
|
|
39
36
|
comparePairs?: string;
|
|
40
37
|
compareOutDir?: string;
|
|
41
38
|
}
|
|
42
39
|
/** Parse agent-test CLI argv (exported for unit tests). */
|
|
43
40
|
export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
|
|
44
|
-
/**
|
|
45
|
-
* Split `--report-out` into an HTML file path and, when a directory was given,
|
|
46
|
-
* the directory every other report artifact is written to.
|
|
47
|
-
*/
|
|
41
|
+
/** Resolve an explicit report target into the HTML path and optional artifact directory. */
|
|
48
42
|
export declare function resolveReportOutput(reportOut?: string): {
|
|
49
43
|
htmlPath?: string;
|
|
50
44
|
outDir?: string;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EACN,KAAK,SAAS,EAKd,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EACN,KAAK,SAAS,EAKd,MAAM,2BAA2B,CAAC;AAsBnC,OAAO,EACN,KAAK,UAAU,EAIf,MAAM,oBAAoB,CAAC;AAU5B,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAmL1D;AAED,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ9F"}
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@ import { compareSuiteReports, labelForCompareSide, loadSuiteRunReport, parseComp
|
|
|
8
8
|
import { discoverSuites } from "./discover-suites.js";
|
|
9
9
|
import { runDoctor } from "./doctor.js";
|
|
10
10
|
import { writeHtmlReport } from "./html-report.js";
|
|
11
|
-
import {
|
|
11
|
+
import { assertDirectAgentPreflight } from "./preflight.js";
|
|
12
12
|
import { logProgress } from "./progress.js";
|
|
13
|
-
import {
|
|
13
|
+
import { cleanupStagingSession, createLiveStagingSessionId, getLiveStagingSessionRoot, setLiveStagingRootOverride, } from "./record-trace.js";
|
|
14
14
|
import { registerLiveRunHandlers, runAllSuites, runSuite } from "./run-suite.js";
|
|
15
15
|
import { formatRunSummary, shouldFailScenario, summarizeReports, } from "./suite-summary.js";
|
|
16
16
|
import { configureCliColor, theme } from "./theme.js";
|
|
@@ -28,9 +28,6 @@ export function parseCliArgs(argv) {
|
|
|
28
28
|
let filter;
|
|
29
29
|
let scenarioFilter;
|
|
30
30
|
let stagingSessionId;
|
|
31
|
-
let record = false;
|
|
32
|
-
let recordFixtures = false;
|
|
33
|
-
let live = false;
|
|
34
31
|
let judge;
|
|
35
32
|
let worktree;
|
|
36
33
|
let keepRecordings = false;
|
|
@@ -59,7 +56,14 @@ export function parseCliArgs(argv) {
|
|
|
59
56
|
for (let i = startIndex; i < argv.length; i++) {
|
|
60
57
|
const token = argv[i];
|
|
61
58
|
if (token === "--host" && argv[i + 1]) {
|
|
62
|
-
|
|
59
|
+
const value = argv[++i];
|
|
60
|
+
if (value === "replay") {
|
|
61
|
+
throw new Error("Replay-based testing is deprecated and no longer supported; use --host cursor or --host claude.");
|
|
62
|
+
}
|
|
63
|
+
if (value !== "cursor" && value !== "claude") {
|
|
64
|
+
throw new Error("--host must be cursor|claude");
|
|
65
|
+
}
|
|
66
|
+
host = value;
|
|
63
67
|
}
|
|
64
68
|
else if (token === "--suites-dir" && argv[i + 1]) {
|
|
65
69
|
suitesDir = argv[++i];
|
|
@@ -76,15 +80,14 @@ export function parseCliArgs(argv) {
|
|
|
76
80
|
else if (token === "--staging-session-id" && argv[i + 1]) {
|
|
77
81
|
stagingSessionId = argv[++i];
|
|
78
82
|
}
|
|
83
|
+
else if (token === "--live") {
|
|
84
|
+
throw new Error("--live was removed because agent-test now always runs a real agent");
|
|
85
|
+
}
|
|
79
86
|
else if (token === "--record") {
|
|
80
|
-
record
|
|
87
|
+
throw new Error("--record was removed; direct runs capture transient traces automatically (use --keep-recordings to retain them)");
|
|
81
88
|
}
|
|
82
89
|
else if (token === "--record-fixtures") {
|
|
83
|
-
record
|
|
84
|
-
recordFixtures = true;
|
|
85
|
-
}
|
|
86
|
-
else if (token === "--live") {
|
|
87
|
-
live = true;
|
|
90
|
+
throw new Error("--record-fixtures was removed because replay-based testing is deprecated and no longer supported");
|
|
88
91
|
}
|
|
89
92
|
else if (token === "--keep-recordings") {
|
|
90
93
|
keepRecordings = true;
|
|
@@ -176,12 +179,8 @@ export function parseCliArgs(argv) {
|
|
|
176
179
|
throw new Error("compare requires --a <report.json> and --b <report.json>");
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
record = true;
|
|
182
|
-
judge = judge ?? true;
|
|
183
|
-
worktree = worktree ?? true;
|
|
184
|
-
}
|
|
182
|
+
judge = judge ?? true;
|
|
183
|
+
worktree = worktree ?? true;
|
|
185
184
|
if (debug) {
|
|
186
185
|
keepRecordings = true;
|
|
187
186
|
process.env.AGENT_TEST_DEBUG = "1";
|
|
@@ -196,9 +195,6 @@ export function parseCliArgs(argv) {
|
|
|
196
195
|
filter,
|
|
197
196
|
scenarioFilter,
|
|
198
197
|
stagingSessionId,
|
|
199
|
-
record,
|
|
200
|
-
recordFixtures,
|
|
201
|
-
live,
|
|
202
198
|
judge,
|
|
203
199
|
worktree,
|
|
204
200
|
keepRecordings,
|
|
@@ -222,6 +218,16 @@ export function parseCliArgs(argv) {
|
|
|
222
218
|
compareOutDir: compareOutDir ? resolve(cwd, compareOutDir) : undefined,
|
|
223
219
|
};
|
|
224
220
|
}
|
|
221
|
+
/** Resolve an explicit report target into the HTML path and optional artifact directory. */
|
|
222
|
+
export function resolveReportOutput(reportOut) {
|
|
223
|
+
if (!reportOut) {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
if (reportOut.toLowerCase().endsWith(".html")) {
|
|
227
|
+
return { htmlPath: reportOut };
|
|
228
|
+
}
|
|
229
|
+
return { htmlPath: join(reportOut, "report.html"), outDir: reportOut };
|
|
230
|
+
}
|
|
225
231
|
async function pathExists(path) {
|
|
226
232
|
try {
|
|
227
233
|
await access(path);
|
|
@@ -260,8 +266,6 @@ async function loadOrRunCompareSide(args, side, stagingSessionId) {
|
|
|
260
266
|
suitePath,
|
|
261
267
|
host: args.host,
|
|
262
268
|
scenarioFilter: args.scenarioFilter,
|
|
263
|
-
record: args.record,
|
|
264
|
-
recordFixtures: args.recordFixtures,
|
|
265
269
|
judge: args.judge,
|
|
266
270
|
worktree: args.worktree,
|
|
267
271
|
stagingSessionId,
|
|
@@ -275,26 +279,13 @@ async function loadOrRunCompareSide(args, side, stagingSessionId) {
|
|
|
275
279
|
scenarioRetries: args.scenarioRetries,
|
|
276
280
|
});
|
|
277
281
|
}
|
|
278
|
-
/**
|
|
279
|
-
* Split `--report-out` into an HTML file path and, when a directory was given,
|
|
280
|
-
* the directory every other report artifact is written to.
|
|
281
|
-
*/
|
|
282
|
-
export function resolveReportOutput(reportOut) {
|
|
283
|
-
if (!reportOut) {
|
|
284
|
-
return {};
|
|
285
|
-
}
|
|
286
|
-
if (reportOut.toLowerCase().endsWith(".html")) {
|
|
287
|
-
return { htmlPath: reportOut };
|
|
288
|
-
}
|
|
289
|
-
return { htmlPath: join(reportOut, "report.html"), outDir: reportOut };
|
|
290
|
-
}
|
|
291
282
|
async function writeSuiteReportDump(outDir, label, report) {
|
|
292
283
|
await mkdir(outDir, { recursive: true });
|
|
293
284
|
const path = join(outDir, `${label}.suite-report.json`);
|
|
294
285
|
await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
295
286
|
return path;
|
|
296
287
|
}
|
|
297
|
-
async function
|
|
288
|
+
async function cleanupRunArtifacts(stagingSessionRoot, keepRecordings) {
|
|
298
289
|
if (keepRecordings) {
|
|
299
290
|
return;
|
|
300
291
|
}
|
|
@@ -306,10 +297,6 @@ async function cleanupLiveRunArtifacts(cwd, stagingSessionRoot, keepRecordings)
|
|
|
306
297
|
// best-effort
|
|
307
298
|
}
|
|
308
299
|
}
|
|
309
|
-
const legacyRemoved = await cleanupLegacyRepoRecordings(cwd);
|
|
310
|
-
if (legacyRemoved.length > 0) {
|
|
311
|
-
console.log(`Removed legacy in-repo recording dir(s):\n ${legacyRemoved.join("\n ")}`);
|
|
312
|
-
}
|
|
313
300
|
}
|
|
314
301
|
async function main() {
|
|
315
302
|
let args;
|
|
@@ -332,9 +319,7 @@ async function main() {
|
|
|
332
319
|
const bPath = resolve(args.cwd, args.compareB);
|
|
333
320
|
const aReport = await loadSuiteRunReport(aPath);
|
|
334
321
|
const bReport = await loadSuiteRunReport(bPath);
|
|
335
|
-
const outDir = args.compareOutDir ??
|
|
336
|
-
resolveReportOutput(args.reportOut).outDir ??
|
|
337
|
-
resolve(args.cwd, "compare-out");
|
|
322
|
+
const outDir = args.compareOutDir ?? resolve(args.cwd, "compare-out");
|
|
338
323
|
const compare = compareSuiteReports({
|
|
339
324
|
aLabel: labelForCompareSide(aPath),
|
|
340
325
|
bLabel: labelForCompareSide(bPath),
|
|
@@ -382,47 +367,41 @@ async function main() {
|
|
|
382
367
|
}
|
|
383
368
|
const isChild = process.env.AGENT_TEST_CHILD === "1";
|
|
384
369
|
if (args.debugDir && isPathUnderRoot(args.debugDir, args.cwd) && !isChild) {
|
|
385
|
-
console.warn(theme.warn(`--debug-dir is inside the repo (${args.debugDir}). Default is $TMPDIR/agent-spec — prefer that for
|
|
370
|
+
console.warn(theme.warn(`--debug-dir is inside the repo (${args.debugDir}). Default is $TMPDIR/agent-spec — prefer that for direct runs so debug output stays out of git status.`));
|
|
386
371
|
}
|
|
387
372
|
const verbose = args.debug || process.env.AGENT_TEST_VERBOSE === "1" || process.env.AGENT_TEST_DEBUG === "1";
|
|
388
373
|
const stagingSessionId = args.stagingSessionId?.trim() ||
|
|
389
374
|
process.env.AGENT_TEST_STAGING_SESSION_ID?.trim() ||
|
|
390
|
-
(
|
|
391
|
-
? createLiveStagingSessionId()
|
|
392
|
-
: undefined);
|
|
375
|
+
createLiveStagingSessionId();
|
|
393
376
|
const stagingSessionRoot = stagingSessionId
|
|
394
377
|
? getLiveStagingSessionRoot(stagingSessionId)
|
|
395
378
|
: undefined;
|
|
396
|
-
const reportOutput = resolveReportOutput(args.reportOut);
|
|
397
379
|
try {
|
|
398
|
-
if (args.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (authMode === "api-key" && !process.env.ANTHROPIC_API_KEY?.trim()) {
|
|
404
|
-
console.error(`${CLAUDE_AUTH_MODE_ENV}=api-key requires ANTHROPIC_API_KEY for --live --host claude`);
|
|
405
|
-
return 1;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
console.error(error instanceof Error ? error.message : error);
|
|
380
|
+
if (args.host === "claude") {
|
|
381
|
+
try {
|
|
382
|
+
const authMode = parseClaudeAuthMode(process.env[CLAUDE_AUTH_MODE_ENV]);
|
|
383
|
+
if (authMode === "api-key" && !process.env.ANTHROPIC_API_KEY?.trim()) {
|
|
384
|
+
console.error(`${CLAUDE_AUTH_MODE_ENV}=api-key requires ANTHROPIC_API_KEY`);
|
|
410
385
|
return 1;
|
|
411
386
|
}
|
|
412
387
|
}
|
|
413
|
-
|
|
414
|
-
console.error(
|
|
415
|
-
return 1;
|
|
416
|
-
}
|
|
417
|
-
// Judge classifiers still use the Cursor SDK.
|
|
418
|
-
if (args.judge !== false && !process.env.CURSOR_API_KEY?.trim()) {
|
|
419
|
-
console.error("CURSOR_API_KEY required for live judge classifiers (use --no-judge to skip)");
|
|
388
|
+
catch (error) {
|
|
389
|
+
console.error(error instanceof Error ? error.message : error);
|
|
420
390
|
return 1;
|
|
421
391
|
}
|
|
422
392
|
}
|
|
423
|
-
if (args.
|
|
393
|
+
if (args.host === "cursor" && !process.env.CURSOR_API_KEY?.trim()) {
|
|
394
|
+
console.error("CURSOR_API_KEY required for Cursor agent runs");
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
// Judge classifiers still use the Cursor SDK.
|
|
398
|
+
if (args.judge !== false && !process.env.CURSOR_API_KEY?.trim()) {
|
|
399
|
+
console.error("CURSOR_API_KEY required for judge classifiers (use --no-judge to skip)");
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
{
|
|
424
403
|
try {
|
|
425
|
-
await
|
|
404
|
+
await assertDirectAgentPreflight(args.cwd, args.suitesDir);
|
|
426
405
|
}
|
|
427
406
|
catch (error) {
|
|
428
407
|
console.error(error instanceof Error ? error.message : error);
|
|
@@ -433,7 +412,7 @@ async function main() {
|
|
|
433
412
|
process.env.AGENT_TEST_NO_WORKTREE === "1" ||
|
|
434
413
|
process.env.AGENT_TEST_NO_WORKTREE === "true";
|
|
435
414
|
if (worktreeDisabled && !inPlaceAllowed) {
|
|
436
|
-
console.error("
|
|
415
|
+
console.error("Direct agent tests require git worktree isolation. Set AGENT_TEST_ALLOW_IN_PLACE=1 to run in repo cwd (--no-worktree leaks agent edits into your working tree).");
|
|
437
416
|
return 1;
|
|
438
417
|
}
|
|
439
418
|
registerLiveRunHandlers();
|
|
@@ -442,7 +421,7 @@ async function main() {
|
|
|
442
421
|
if (removed.length > 0) {
|
|
443
422
|
console.log(theme.warn(`Cleaned ${removed.length} stale agent-test worktree(s) from a prior crash`));
|
|
444
423
|
}
|
|
445
|
-
console.log(theme.banner(args.debug ? "
|
|
424
|
+
console.log(theme.banner(args.debug ? "direct debug" : "direct"));
|
|
446
425
|
if (stagingSessionRoot) {
|
|
447
426
|
console.log(theme.bannerSession(stagingSessionRoot));
|
|
448
427
|
}
|
|
@@ -464,10 +443,6 @@ async function main() {
|
|
|
464
443
|
}
|
|
465
444
|
}
|
|
466
445
|
}
|
|
467
|
-
else if (args.debug && !isChild && stagingSessionRoot) {
|
|
468
|
-
console.log(theme.banner("debug"));
|
|
469
|
-
console.log(theme.bannerSession(stagingSessionRoot));
|
|
470
|
-
}
|
|
471
446
|
let reports;
|
|
472
447
|
let comparePassRegressions = 0;
|
|
473
448
|
if (args.comparePairs) {
|
|
@@ -477,7 +452,6 @@ async function main() {
|
|
|
477
452
|
reports = [aReport, bReport];
|
|
478
453
|
if (!isChild) {
|
|
479
454
|
const outDir = args.compareOutDir ??
|
|
480
|
-
reportOutput.outDir ??
|
|
481
455
|
(stagingSessionRoot
|
|
482
456
|
? join(stagingSessionRoot, "compare")
|
|
483
457
|
: resolve(args.cwd, "compare-out"));
|
|
@@ -547,15 +521,8 @@ async function main() {
|
|
|
547
521
|
includeCompare: Boolean(args.comparePairs),
|
|
548
522
|
compareALabel: pair ? labelForCompareSide(pair.a) : undefined,
|
|
549
523
|
compareBLabel: pair ? labelForCompareSide(pair.b) : undefined,
|
|
550
|
-
}
|
|
524
|
+
});
|
|
551
525
|
console.log(`\n${theme.fileTip("HTML report", reportPath)}`);
|
|
552
|
-
// A directory collects everything, not just the HTML.
|
|
553
|
-
if (reportOutput.outDir && !args.comparePairs) {
|
|
554
|
-
for (const report of reports) {
|
|
555
|
-
const dumpPath = await writeSuiteReportDump(reportOutput.outDir, report.suite, report);
|
|
556
|
-
console.log(theme.tip(`suite JSON: ${dumpPath}`));
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
526
|
}
|
|
560
527
|
catch (error) {
|
|
561
528
|
console.warn(theme.warn(`HTML report failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
@@ -581,7 +548,7 @@ async function main() {
|
|
|
581
548
|
}
|
|
582
549
|
finally {
|
|
583
550
|
if (stagingSessionId && !isChild) {
|
|
584
|
-
await
|
|
551
|
+
await cleanupRunArtifacts(stagingSessionRoot, args.keepRecordings);
|
|
585
552
|
}
|
|
586
553
|
}
|
|
587
554
|
}
|