@nathapp/nax 0.22.3 → 0.22.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 +21 -2
- package/docs/ROADMAP.md +6 -0
- package/docs/specs/central-run-registry.md +13 -1
- package/nax/config.json +4 -3
- package/nax/features/status-file-consolidation/prd.json +52 -7
- package/nax/status.json +17 -8
- package/package.json +1 -1
- package/src/config/types.ts +2 -0
- package/src/pipeline/stages/verify.ts +21 -2
- package/src/verification/orchestrator-types.ts +2 -0
- package/src/verification/smart-runner.ts +5 -2
- package/src/verification/strategies/scoped.ts +9 -2
- package/src/verification/types.ts +2 -0
- package/test/unit/verification/smart-runner.test.ts +16 -0
package/README.md
CHANGED
|
@@ -223,14 +223,33 @@ Config is layered — project overrides global:
|
|
|
223
223
|
},
|
|
224
224
|
"quality": {
|
|
225
225
|
"commands": {
|
|
226
|
-
"test": "bun test",
|
|
226
|
+
"test": "bun test test/ --timeout=60000",
|
|
227
|
+
"testScoped": "bun test --timeout=60000 {{files}}",
|
|
227
228
|
"lint": "bun run lint",
|
|
228
|
-
"typecheck": "bun x tsc --noEmit"
|
|
229
|
+
"typecheck": "bun x tsc --noEmit",
|
|
230
|
+
"lintFix": "bun x biome check --fix src/",
|
|
231
|
+
"formatFix": "bun x biome format --write src/"
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
```
|
|
233
236
|
|
|
237
|
+
### Scoped Test Command
|
|
238
|
+
|
|
239
|
+
By default, nax runs scoped tests (per-story verification) by appending discovered test files to the `test` command. This can produce incorrect commands when the base command includes a directory path (e.g. `bun test test/`), since the path is not replaced — it is appended alongside it.
|
|
240
|
+
|
|
241
|
+
Use `testScoped` to define the exact scoped test command with a `{{files}}` placeholder:
|
|
242
|
+
|
|
243
|
+
| Runner | `test` | `testScoped` |
|
|
244
|
+
|:-------|:-------|:-------------|
|
|
245
|
+
| Bun | `bun test test/ --timeout=60000` | `bun test --timeout=60000 {{files}}` |
|
|
246
|
+
| Jest | `npx jest` | `npx jest -- {{files}}` |
|
|
247
|
+
| pytest | `pytest tests/` | `pytest {{files}}` |
|
|
248
|
+
| cargo | `cargo test` | `cargo test {{files}}` |
|
|
249
|
+
| go | `go test ./...` | `go test {{files}}` |
|
|
250
|
+
|
|
251
|
+
If `testScoped` is not configured, nax falls back to a heuristic that replaces the last path-like token in the `test` command. **Recommended:** always configure `testScoped` explicitly to avoid surprises.
|
|
252
|
+
|
|
234
253
|
**TDD strategy options:**
|
|
235
254
|
|
|
236
255
|
| Value | Behaviour |
|
package/docs/ROADMAP.md
CHANGED
|
@@ -127,6 +127,8 @@
|
|
|
127
127
|
|
|
128
128
|
### Stories
|
|
129
129
|
- [x] ~~**SFC-001:** Auto-write project-level status — remove `--status-file` flag, always write to `<workdir>/nax/status.json`~~
|
|
130
|
+
- [ ] **BUG-043:** Fix scoped test command construction + add `testScoped` config with `{{files}}` template
|
|
131
|
+
- [ ] **BUG-044:** Log scoped and full-suite test commands at info level in verify stage
|
|
130
132
|
- [ ] **SFC-002:** Write feature-level status on run end — copy final snapshot to `<workdir>/nax/features/<feature>/status.json`
|
|
131
133
|
- [ ] **SFC-003:** Align status readers — `nax status` + `nax diagnose` read from correct paths
|
|
132
134
|
- [ ] **SFC-004:** Clean up dead code — remove `--status-file` option, `.nax-status.json` references
|
|
@@ -140,6 +142,7 @@
|
|
|
140
142
|
**Spec:** [docs/specs/central-run-registry.md](specs/central-run-registry.md)
|
|
141
143
|
|
|
142
144
|
### Stories
|
|
145
|
+
- [ ] **CRR-000:** `src/pipeline/subscribers/events-writer.ts` — `wireEventsWriter()`, writes lifecycle events to `~/.nax/events/<project>/events.jsonl` (machine-readable completion signal for watchdog/CI)
|
|
143
146
|
- [ ] **CRR-001:** `src/pipeline/subscribers/registry.ts` — `wireRegistry()` subscriber, listens to `run:started`, writes `~/.nax/runs/<project>-<feature>-<runId>/meta.json` (path pointers only — no data duplication, no symlinks)
|
|
144
147
|
- [ ] **CRR-002:** `src/commands/runs.ts` — `nax runs` CLI, reads `meta.json` → resolves live `status.json` from `statusPath`, displays table (project, feature, status, stories, duration, date). Filters: `--project`, `--last`, `--status`
|
|
145
148
|
- [ ] **CRR-003:** `nax logs --run <runId>` — resolve run from global registry via `eventsDir`, stream logs from any directory
|
|
@@ -284,6 +287,9 @@
|
|
|
284
287
|
|
|
285
288
|
- [x] ~~**BUG-037:** Test output summary (verify stage) captures precheck boilerplate instead of actual `bun test` failure. Fixed: `.slice(-20)` tail — shipped in v0.22.1 (re-arch phase 2).~~
|
|
286
289
|
- [x] ~~**BUG-038:** `smart-runner` over-matching when global defaults change. Fixed by FEAT-010 (v0.21.0) — per-attempt `storyGitRef` baseRef tracking; `git diff <baseRef>..HEAD` prevents cross-story file pollution.~~
|
|
290
|
+
- [ ] **BUG-043:** Scoped test command appends files instead of replacing path — `runners.ts:scoped()` concatenates `scopedTestPaths` to full-suite command, resulting in `bun test test/ --timeout=60000 /path/to/file.ts` (runs everything). Fix: use `testScoped` config with `{{files}}` template, fall back to `buildSmartTestCommand()` heuristic. **Location:** `src/verification/runners.ts:scoped()`
|
|
291
|
+
- [ ] **BUG-044:** Scoped/full-suite test commands not logged — no visibility into what command was actually executed during verify stage. Fix: log at info level before execution.
|
|
292
|
+
|
|
287
293
|
### Features
|
|
288
294
|
- [x] ~~`nax unlock` command~~
|
|
289
295
|
- [x] ~~Constitution file support~~
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Central Run Registry — Spec
|
|
2
2
|
|
|
3
|
-
**Version:** v0.
|
|
3
|
+
**Version:** v0.24.0
|
|
4
4
|
**Status:** Planned
|
|
5
5
|
|
|
6
6
|
---
|
|
@@ -60,6 +60,18 @@ A global `~/.nax/runs/` registry that indexes every nax run via path references
|
|
|
60
60
|
|
|
61
61
|
## Implementation
|
|
62
62
|
|
|
63
|
+
### CRR-000: Events File Writer (new subscriber)
|
|
64
|
+
|
|
65
|
+
- New module: `src/pipeline/subscribers/events-writer.ts` — `wireEventsWriter()`
|
|
66
|
+
- Writes to `~/.nax/events/<project>/events.jsonl` — one JSON line per lifecycle event
|
|
67
|
+
- Listens to event bus: `run:started`, `story:started`, `story:completed`, `story:failed`, `run:completed`
|
|
68
|
+
- Each line: `{"ts", "event", "runId", "feature", "project", "storyId?"}`
|
|
69
|
+
- `run:completed` emits an `on-complete` event — used by external tooling (watchdog) to distinguish clean exit from crash
|
|
70
|
+
- Best-effort: never throw/block the main run on write failure
|
|
71
|
+
- Directory created on first write
|
|
72
|
+
|
|
73
|
+
**Motivation:** External tools (nax-watchdog, CI integrations) need a reliable signal that nax exited gracefully. Currently nax writes no machine-readable completion event, causing false crash reports. This also provides the foundation for CRR — `meta.json` can reference the events file path.
|
|
74
|
+
|
|
63
75
|
### CRR-001: Registry Writer (new subscriber)
|
|
64
76
|
|
|
65
77
|
- New module: `src/execution/run-registry.ts` — `registerRun(meta)`, `getRunsDir()`
|
package/nax/config.json
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"fallbackToKeywords": true,
|
|
53
53
|
"cacheDecisions": true,
|
|
54
54
|
"mode": "hybrid",
|
|
55
|
-
"timeoutMs":
|
|
55
|
+
"timeoutMs": 60000
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"execution": {
|
|
@@ -84,7 +84,8 @@
|
|
|
84
84
|
"commands": {
|
|
85
85
|
"test": "bun run test",
|
|
86
86
|
"typecheck": "bun run typecheck",
|
|
87
|
-
"lint": "bun run lint"
|
|
87
|
+
"lint": "bun run lint",
|
|
88
|
+
"testScoped": "bun test --timeout=60000 {{files}}"
|
|
88
89
|
},
|
|
89
90
|
"forceExit": false,
|
|
90
91
|
"detectOpenHandles": true,
|
|
@@ -150,4 +151,4 @@
|
|
|
150
151
|
"scopeToStory": true
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
|
-
}
|
|
154
|
+
}
|
|
@@ -10,13 +10,36 @@
|
|
|
10
10
|
"title": "Auto-write project-level status",
|
|
11
11
|
"description": "Remove --status-file CLI option. StatusWriter always writes to <workdir>/nax/status.json automatically. In bin/nax.ts, remove --status-file option and compute statusFile = join(workdir, 'nax', 'status.json'). In runner.ts, statusFile is no longer optional. In status-writer.ts, remove the if (!this.statusFile) guard in update().",
|
|
12
12
|
"complexity": "medium",
|
|
13
|
-
"status": "
|
|
13
|
+
"status": "pending",
|
|
14
14
|
"acceptanceCriteria": [
|
|
15
15
|
"Running nax without --status-file flag writes nax/status.json automatically",
|
|
16
16
|
"nax/status.json contains valid NaxStatusFile schema with run.id, run.status, progress counts",
|
|
17
17
|
"--status-file CLI option no longer exists",
|
|
18
18
|
"StatusWriter.update() always writes (no no-op guard on missing statusFile)"
|
|
19
|
-
]
|
|
19
|
+
],
|
|
20
|
+
"attempts": 0,
|
|
21
|
+
"priorErrors": [
|
|
22
|
+
"Attempt 1 failed with model tier: fast: Review failed: test failed (exit code -1)"
|
|
23
|
+
],
|
|
24
|
+
"priorFailures": [
|
|
25
|
+
{
|
|
26
|
+
"attempt": 1,
|
|
27
|
+
"modelTier": "fast",
|
|
28
|
+
"stage": "escalation",
|
|
29
|
+
"summary": "Failed with tier fast, escalating to next tier",
|
|
30
|
+
"timestamp": "2026-03-07T06:22:18.122Z"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"escalations": [],
|
|
34
|
+
"dependencies": [],
|
|
35
|
+
"tags": [],
|
|
36
|
+
"storyPoints": 1,
|
|
37
|
+
"routing": {
|
|
38
|
+
"complexity": "medium",
|
|
39
|
+
"modelTier": "balanced",
|
|
40
|
+
"testStrategy": "test-after",
|
|
41
|
+
"reasoning": "Straightforward refactor: remove CLI option, hardcode path computation, remove null guard across 3 files"
|
|
42
|
+
}
|
|
20
43
|
},
|
|
21
44
|
{
|
|
22
45
|
"id": "SFC-002",
|
|
@@ -29,12 +52,19 @@
|
|
|
29
52
|
"After a failed run, nax/features/<feature>/status.json exists with status 'failed'",
|
|
30
53
|
"After a crash, nax/features/<feature>/status.json exists with status 'crashed'",
|
|
31
54
|
"Feature status.json uses the same NaxStatusFile schema as project-level"
|
|
32
|
-
]
|
|
55
|
+
],
|
|
56
|
+
"attempts": 0,
|
|
57
|
+
"priorErrors": [],
|
|
58
|
+
"priorFailures": [],
|
|
59
|
+
"escalations": [],
|
|
60
|
+
"dependencies": [],
|
|
61
|
+
"tags": [],
|
|
62
|
+
"storyPoints": 1
|
|
33
63
|
},
|
|
34
64
|
{
|
|
35
65
|
"id": "SFC-003",
|
|
36
66
|
"title": "Align status readers",
|
|
37
|
-
"description": "Make nax status read project-level status from nax/status.json for currently running info. Make nax diagnose read from nax/status.json instead of .nax-status.json. status-features.ts loadStatusFile() already reads <featureDir>/status.json which SFC-002 now writes
|
|
67
|
+
"description": "Make nax status read project-level status from nax/status.json for currently running info. Make nax diagnose read from nax/status.json instead of .nax-status.json. status-features.ts loadStatusFile() already reads <featureDir>/status.json which SFC-002 now writes — no change needed for feature-level reads.",
|
|
38
68
|
"complexity": "simple",
|
|
39
69
|
"status": "pending",
|
|
40
70
|
"acceptanceCriteria": [
|
|
@@ -42,7 +72,14 @@
|
|
|
42
72
|
"nax status shows per-feature historical status from nax/features/<feature>/status.json",
|
|
43
73
|
"nax diagnose reads from nax/status.json (not .nax-status.json)",
|
|
44
74
|
"No references to .nax-status.json remain in codebase"
|
|
45
|
-
]
|
|
75
|
+
],
|
|
76
|
+
"attempts": 0,
|
|
77
|
+
"priorErrors": [],
|
|
78
|
+
"priorFailures": [],
|
|
79
|
+
"escalations": [],
|
|
80
|
+
"dependencies": [],
|
|
81
|
+
"tags": [],
|
|
82
|
+
"storyPoints": 1
|
|
46
83
|
},
|
|
47
84
|
{
|
|
48
85
|
"id": "SFC-004",
|
|
@@ -55,7 +92,15 @@
|
|
|
55
92
|
"No references to .nax-status.json in codebase",
|
|
56
93
|
"RunOptions.statusFile is required (not optional)",
|
|
57
94
|
"All existing tests pass"
|
|
58
|
-
]
|
|
95
|
+
],
|
|
96
|
+
"attempts": 0,
|
|
97
|
+
"priorErrors": [],
|
|
98
|
+
"priorFailures": [],
|
|
99
|
+
"escalations": [],
|
|
100
|
+
"dependencies": [],
|
|
101
|
+
"tags": [],
|
|
102
|
+
"storyPoints": 1
|
|
59
103
|
}
|
|
60
|
-
]
|
|
104
|
+
],
|
|
105
|
+
"updatedAt": "2026-03-07T06:22:18.122Z"
|
|
61
106
|
}
|
package/nax/status.json
CHANGED
|
@@ -4,25 +4,34 @@
|
|
|
4
4
|
"id": "run-2026-03-07T06-14-21-018Z",
|
|
5
5
|
"feature": "status-file-consolidation",
|
|
6
6
|
"startedAt": "2026-03-07T06:14:21.018Z",
|
|
7
|
-
"status": "
|
|
7
|
+
"status": "crashed",
|
|
8
8
|
"dryRun": false,
|
|
9
|
-
"pid": 217461
|
|
9
|
+
"pid": 217461,
|
|
10
|
+
"crashedAt": "2026-03-07T06:22:36.300Z",
|
|
11
|
+
"crashSignal": "SIGTERM"
|
|
10
12
|
},
|
|
11
13
|
"progress": {
|
|
12
14
|
"total": 4,
|
|
13
|
-
"passed":
|
|
15
|
+
"passed": 0,
|
|
14
16
|
"failed": 0,
|
|
15
17
|
"paused": 0,
|
|
16
18
|
"blocked": 0,
|
|
17
|
-
"pending":
|
|
19
|
+
"pending": 4
|
|
18
20
|
},
|
|
19
21
|
"cost": {
|
|
20
22
|
"spent": 0,
|
|
21
23
|
"limit": 3
|
|
22
24
|
},
|
|
23
|
-
"current":
|
|
25
|
+
"current": {
|
|
26
|
+
"storyId": "SFC-002",
|
|
27
|
+
"title": "Write feature-level status on run end",
|
|
28
|
+
"complexity": "medium",
|
|
29
|
+
"tddStrategy": "test-after",
|
|
30
|
+
"model": "balanced",
|
|
31
|
+
"attempt": 1,
|
|
32
|
+
"phase": "routing"
|
|
33
|
+
},
|
|
24
34
|
"iterations": 0,
|
|
25
|
-
"updatedAt": "2026-03-07T06:
|
|
26
|
-
"durationMs":
|
|
27
|
-
"lastHeartbeat": "2026-03-07T06:19:34.987Z"
|
|
35
|
+
"updatedAt": "2026-03-07T06:22:36.300Z",
|
|
36
|
+
"durationMs": 495282
|
|
28
37
|
}
|
package/package.json
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -140,6 +140,8 @@ export interface QualityConfig {
|
|
|
140
140
|
typecheck?: string;
|
|
141
141
|
lint?: string;
|
|
142
142
|
test?: string;
|
|
143
|
+
/** Scoped test command template with {{files}} placeholder (e.g., "bun test --timeout=60000 {{files}}") */
|
|
144
|
+
testScoped?: string;
|
|
143
145
|
/** Auto-fix lint errors (e.g., "biome check --fix") */
|
|
144
146
|
lintFix?: string;
|
|
145
147
|
/** Auto-fix formatting (e.g., "biome format --write") */
|
|
@@ -31,6 +31,18 @@ function coerceSmartTestRunner(val: boolean | SmartTestRunnerConfig | undefined)
|
|
|
31
31
|
return val;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Build the scoped test command from discovered test files.
|
|
36
|
+
* Uses the testScoped template (with {{files}} placeholder) if configured,
|
|
37
|
+
* otherwise falls back to buildSmartTestCommand heuristic.
|
|
38
|
+
*/
|
|
39
|
+
function buildScopedCommand(testFiles: string[], baseCommand: string, testScopedTemplate?: string): string {
|
|
40
|
+
if (testScopedTemplate) {
|
|
41
|
+
return testScopedTemplate.replace("{{files}}", testFiles.join(" "));
|
|
42
|
+
}
|
|
43
|
+
return _smartRunnerDeps.buildSmartTestCommand(testFiles, baseCommand);
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
export const verifyStage: PipelineStage = {
|
|
35
47
|
name: "verify",
|
|
36
48
|
enabled: () => true,
|
|
@@ -46,6 +58,7 @@ export const verifyStage: PipelineStage = {
|
|
|
46
58
|
|
|
47
59
|
// Skip verification if no test command is configured
|
|
48
60
|
const testCommand = ctx.config.review?.commands?.test ?? ctx.config.quality.commands.test;
|
|
61
|
+
const testScopedTemplate = ctx.config.quality.commands.testScoped;
|
|
49
62
|
if (!testCommand) {
|
|
50
63
|
logger.debug("verify", "Skipping verification (no test command configured)", { storyId: ctx.story.id });
|
|
51
64
|
return { action: "continue" };
|
|
@@ -68,7 +81,7 @@ export const verifyStage: PipelineStage = {
|
|
|
68
81
|
logger.info("verify", `[smart-runner] Pass 1: path convention matched ${pass1Files.length} test files`, {
|
|
69
82
|
storyId: ctx.story.id,
|
|
70
83
|
});
|
|
71
|
-
effectiveCommand =
|
|
84
|
+
effectiveCommand = buildScopedCommand(pass1Files, testCommand, testScopedTemplate);
|
|
72
85
|
isFullSuite = false;
|
|
73
86
|
} else if (smartRunnerConfig.fallback === "import-grep") {
|
|
74
87
|
// Pass 2: import-grep fallback
|
|
@@ -81,7 +94,7 @@ export const verifyStage: PipelineStage = {
|
|
|
81
94
|
logger.info("verify", `[smart-runner] Pass 2: import-grep matched ${pass2Files.length} test files`, {
|
|
82
95
|
storyId: ctx.story.id,
|
|
83
96
|
});
|
|
84
|
-
effectiveCommand =
|
|
97
|
+
effectiveCommand = buildScopedCommand(pass2Files, testCommand, testScopedTemplate);
|
|
85
98
|
isFullSuite = false;
|
|
86
99
|
}
|
|
87
100
|
}
|
|
@@ -102,6 +115,12 @@ export const verifyStage: PipelineStage = {
|
|
|
102
115
|
});
|
|
103
116
|
}
|
|
104
117
|
|
|
118
|
+
// BUG-044: Log the effective command for observability
|
|
119
|
+
logger.info("verify", isFullSuite ? "Running full suite" : "Running scoped tests", {
|
|
120
|
+
storyId: ctx.story.id,
|
|
121
|
+
command: effectiveCommand,
|
|
122
|
+
});
|
|
123
|
+
|
|
105
124
|
// Use unified regression gate (includes 2s wait for agent process cleanup)
|
|
106
125
|
const result = await _verifyDeps.regression({
|
|
107
126
|
workdir: ctx.workdir,
|
|
@@ -23,6 +23,8 @@ export type VerifyStrategy = "scoped" | "regression" | "deferred-regression" | "
|
|
|
23
23
|
export interface VerifyContext {
|
|
24
24
|
workdir: string;
|
|
25
25
|
testCommand: string;
|
|
26
|
+
/** Scoped test command template with {{files}} placeholder — overrides buildSmartTestCommand heuristic */
|
|
27
|
+
testScopedTemplate?: string;
|
|
26
28
|
timeoutSeconds: number;
|
|
27
29
|
storyId: string;
|
|
28
30
|
storyGitRef?: string;
|
|
@@ -174,8 +174,11 @@ export function buildSmartTestCommand(testFiles: string[], baseCommand: string):
|
|
|
174
174
|
return `${baseCommand} ${testFiles.join(" ")}`;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
// Replace the last path argument with the specific test files
|
|
178
|
-
|
|
177
|
+
// Replace the last path argument with the specific test files,
|
|
178
|
+
// preserving any flags that appear after the path (e.g. --timeout=60000).
|
|
179
|
+
const beforePath = parts.slice(0, lastPathIndex);
|
|
180
|
+
const afterPath = parts.slice(lastPathIndex + 1);
|
|
181
|
+
const newParts = [...beforePath, ...testFiles, ...afterPath];
|
|
179
182
|
return newParts.join(" ");
|
|
180
183
|
}
|
|
181
184
|
|
|
@@ -29,6 +29,13 @@ function coerceSmartRunner(val: unknown) {
|
|
|
29
29
|
return val as typeof DEFAULT_SMART_RUNNER_CONFIG;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function buildScopedCommand(testFiles: string[], baseCommand: string, testScopedTemplate?: string): string {
|
|
33
|
+
if (testScopedTemplate) {
|
|
34
|
+
return testScopedTemplate.replace("{{files}}", testFiles.join(" "));
|
|
35
|
+
}
|
|
36
|
+
return _scopedDeps.buildSmartTestCommand(testFiles, baseCommand);
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export class ScopedStrategy implements IVerificationStrategy {
|
|
33
40
|
readonly name = "scoped" as const;
|
|
34
41
|
|
|
@@ -48,7 +55,7 @@ export class ScopedStrategy implements IVerificationStrategy {
|
|
|
48
55
|
logger.info("verify[scoped]", `Pass 1: path convention matched ${pass1Files.length} test files`, {
|
|
49
56
|
storyId: ctx.storyId,
|
|
50
57
|
});
|
|
51
|
-
effectiveCommand =
|
|
58
|
+
effectiveCommand = buildScopedCommand(pass1Files, ctx.testCommand, ctx.testScopedTemplate);
|
|
52
59
|
isFullSuite = false;
|
|
53
60
|
} else if (smartCfg.fallback === "import-grep") {
|
|
54
61
|
const pass2Files = await _scopedDeps.importGrepFallback(sourceFiles, ctx.workdir, smartCfg.testFilePatterns);
|
|
@@ -56,7 +63,7 @@ export class ScopedStrategy implements IVerificationStrategy {
|
|
|
56
63
|
logger.info("verify[scoped]", `Pass 2: import-grep matched ${pass2Files.length} test files`, {
|
|
57
64
|
storyId: ctx.storyId,
|
|
58
65
|
});
|
|
59
|
-
effectiveCommand =
|
|
66
|
+
effectiveCommand = buildScopedCommand(pass2Files, ctx.testCommand, ctx.testScopedTemplate);
|
|
60
67
|
isFullSuite = false;
|
|
61
68
|
}
|
|
62
69
|
}
|
|
@@ -112,4 +112,6 @@ export interface VerificationGateOptions {
|
|
|
112
112
|
acceptOnTimeout?: boolean;
|
|
113
113
|
/** Scoped test paths (for scoped verification) */
|
|
114
114
|
scopedTestPaths?: string[];
|
|
115
|
+
/** Scoped test command template with {{files}} placeholder — overrides buildSmartTestCommand heuristic */
|
|
116
|
+
testScopedTemplate?: string;
|
|
115
117
|
}
|
|
@@ -50,6 +50,22 @@ describe("buildSmartTestCommand", () => {
|
|
|
50
50
|
);
|
|
51
51
|
expect(result).toBe("bun test --coverage test/unit/foo.test.ts");
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
test("preserves trailing flags after path argument (BUG-043)", () => {
|
|
55
|
+
const result = buildSmartTestCommand(
|
|
56
|
+
["test/unit/foo.test.ts"],
|
|
57
|
+
"bun test test/ --timeout=60000",
|
|
58
|
+
);
|
|
59
|
+
expect(result).toBe("bun test test/unit/foo.test.ts --timeout=60000");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("preserves trailing flags with multiple test files", () => {
|
|
63
|
+
const result = buildSmartTestCommand(
|
|
64
|
+
["test/unit/foo.test.ts", "test/unit/bar.test.ts"],
|
|
65
|
+
"bun test test/ --timeout=60000 --bail",
|
|
66
|
+
);
|
|
67
|
+
expect(result).toBe("bun test test/unit/foo.test.ts test/unit/bar.test.ts --timeout=60000 --bail");
|
|
68
|
+
});
|
|
53
69
|
});
|
|
54
70
|
|
|
55
71
|
// ---------------------------------------------------------------------------
|