@nathapp/nax 0.18.6 → 0.20.0

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.
Files changed (66) hide show
  1. package/docs/ROADMAP.md +2 -0
  2. package/nax/config.json +2 -2
  3. package/nax/features/nax-compliance/prd.json +52 -0
  4. package/nax/features/nax-compliance/progress.txt +1 -0
  5. package/nax/features/v0.19.0-hardening/plan.md +7 -0
  6. package/nax/features/v0.19.0-hardening/prd.json +84 -0
  7. package/nax/features/v0.19.0-hardening/progress.txt +7 -0
  8. package/nax/features/v0.19.0-hardening/spec.md +18 -0
  9. package/nax/features/v0.19.0-hardening/tasks.md +8 -0
  10. package/nax/features/verify-v2/prd.json +79 -0
  11. package/nax/features/verify-v2/progress.txt +3 -0
  12. package/nax/status.json +27 -0
  13. package/package.json +2 -2
  14. package/src/acceptance/fix-generator.ts +6 -2
  15. package/src/acceptance/generator.ts +3 -1
  16. package/src/acceptance/types.ts +3 -1
  17. package/src/agents/claude-plan.ts +6 -5
  18. package/src/cli/analyze.ts +1 -0
  19. package/src/cli/init.ts +7 -6
  20. package/src/config/defaults.ts +3 -1
  21. package/src/config/schemas.ts +2 -0
  22. package/src/config/types.ts +6 -0
  23. package/src/context/injector.ts +18 -18
  24. package/src/execution/crash-recovery.ts +7 -10
  25. package/src/execution/lifecycle/acceptance-loop.ts +1 -0
  26. package/src/execution/lifecycle/index.ts +1 -1
  27. package/src/execution/lifecycle/precheck-runner.ts +1 -1
  28. package/src/execution/lifecycle/run-completion.ts +29 -0
  29. package/src/execution/lifecycle/run-regression.ts +301 -0
  30. package/src/execution/lifecycle/run-setup.ts +14 -14
  31. package/src/execution/parallel.ts +1 -1
  32. package/src/execution/pipeline-result-handler.ts +0 -1
  33. package/src/execution/post-verify.ts +31 -194
  34. package/src/execution/runner.ts +2 -19
  35. package/src/execution/sequential-executor.ts +1 -1
  36. package/src/hooks/runner.ts +2 -2
  37. package/src/interaction/plugins/auto.ts +2 -2
  38. package/src/logger/logger.ts +3 -5
  39. package/src/pipeline/stages/verify.ts +26 -22
  40. package/src/plugins/loader.ts +36 -9
  41. package/src/routing/batch-route.ts +32 -0
  42. package/src/routing/index.ts +1 -0
  43. package/src/routing/loader.ts +7 -0
  44. package/src/utils/path-security.ts +56 -0
  45. package/src/verification/executor.ts +6 -13
  46. package/src/verification/smart-runner.ts +52 -0
  47. package/test/integration/plugins/config-resolution.test.ts +3 -3
  48. package/test/integration/plugins/loader.test.ts +3 -1
  49. package/test/integration/precheck-integration.test.ts +18 -11
  50. package/test/integration/rectification-flow.test.ts +3 -3
  51. package/test/integration/review-config-commands.test.ts +1 -1
  52. package/test/integration/security-loader.test.ts +83 -0
  53. package/test/integration/verify-stage.test.ts +9 -0
  54. package/test/unit/config/defaults.test.ts +69 -0
  55. package/test/unit/config/regression-gate-schema.test.ts +159 -0
  56. package/test/unit/execution/lifecycle/run-completion.test.ts +239 -0
  57. package/test/unit/execution/lifecycle/run-regression.test.ts +418 -0
  58. package/test/unit/execution/post-verify-regression.test.ts +31 -84
  59. package/test/unit/execution/post-verify.test.ts +28 -48
  60. package/test/unit/formatters.test.ts +2 -3
  61. package/test/unit/hooks/shell-security.test.ts +40 -0
  62. package/test/unit/pipeline/stages/verify.test.ts +266 -0
  63. package/test/unit/pipeline/verify-smart-runner.test.ts +1 -0
  64. package/test/unit/utils/path-security.test.ts +47 -0
  65. package/src/execution/lifecycle/run-lifecycle.ts +0 -312
  66. package/test/unit/run-lifecycle.test.ts +0 -140
@@ -1,18 +1,18 @@
1
1
  /**
2
- * BUG-026: Regression gate timeout accepts scoped pass instead of escalating
2
+ * BUG-026: Regression gate timeout acceptance
3
3
  *
4
- * Tests that runRegressionGate (via runPostAgentVerification):
4
+ * Tests that runPostAgentVerification:
5
5
  * - Returns passed when regression gate TIMES OUT and acceptOnTimeout=true (default)
6
6
  * - Returns failed when regression gate TIMES OUT and acceptOnTimeout=false
7
- * - Returns failed when regression gate returns TEST_FAILURE (existing behavior unchanged)
7
+ * - Returns failed when regression gate returns TEST_FAILURE
8
8
  * - Defaults acceptOnTimeout to true when not set in config
9
9
  *
10
- * These are behavioral tests that call the actual function with mocked dependencies.
11
- * They complement the type-level tests already in post-verify.test.ts.
10
+ * With the removal of scoped verification, post-verify now ONLY runs the full-suite regression gate.
11
+ * These behavioral tests call the actual function with mocked dependencies.
12
12
  */
13
13
 
14
14
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
15
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
15
+ import { mkdtempSync, rmSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { tmpdir } from "node:os";
18
18
  import type { NaxConfig } from "../../../src/config";
@@ -37,7 +37,7 @@ const mockRunVerification = mock(async (): Promise<VerResult> => {
37
37
  return resp;
38
38
  });
39
39
 
40
- const mockRevertStoriesOnFailure = mock(async ({ prd }: { prd: PRD; [k: string]: unknown }) => prd);
40
+ const mockRevertStoriesOnFailure = mock(async (opts: any) => opts.prd);
41
41
  const mockRunRectificationLoop = mock(async () => false);
42
42
 
43
43
  // ---------------------------------------------------------------------------
@@ -53,47 +53,10 @@ const _origPostVerifyDeps = { ..._postVerifyDeps };
53
53
  // Fixtures
54
54
  // ---------------------------------------------------------------------------
55
55
 
56
- /** Run a git command in a directory using Bun-native spawn. */
57
- function gitSync(args: string[], cwd: string): void {
58
- const proc = Bun.spawnSync(["git", ...args], { cwd, stdin: "ignore", stdout: "ignore", stderr: "ignore" });
59
- if (proc.exitCode !== 0) {
60
- throw new Error(`git ${args[0]} failed in ${cwd}`);
61
- }
62
- }
63
-
64
- /** Read stdout from a git command. */
65
- function gitOutput(args: string[], cwd: string): string {
66
- const proc = Bun.spawnSync(["git", ...args], { cwd, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
67
- return new TextDecoder().decode(proc.stdout).trim();
68
- }
69
-
70
- /**
71
- * Create a temp git repo with two commits so that `git diff storyGitRef HEAD`
72
- * returns at least one test file — needed for the regression gate to activate.
73
- */
74
- function makeGitRepo(): { dir: string; storyGitRef: string } {
75
- const dir = mkdtempSync(join(tmpdir(), "nax-bug026-"));
76
-
77
- gitSync(["init"], dir);
78
- gitSync(["config", "user.email", "test@example.com"], dir);
79
- gitSync(["config", "user.name", "test"], dir);
80
-
81
- // Initial commit → becomes storyGitRef
82
- writeFileSync(join(dir, "src.ts"), "export const x = 1;");
83
- gitSync(["add", "."], dir);
84
- gitSync(["commit", "-m", "initial"], dir);
85
- const storyGitRef = gitOutput(["rev-parse", "HEAD"], dir);
86
-
87
- // Second commit: adds a test file (changed after storyGitRef)
88
- mkdirSync(join(dir, "test"), { recursive: true });
89
- writeFileSync(
90
- join(dir, "test", "example.test.ts"),
91
- 'import { test, expect } from "bun:test";\ntest("x", () => expect(1).toBe(1));',
92
- );
93
- gitSync(["add", "."], dir);
94
- gitSync(["commit", "-m", "add test"], dir);
95
-
96
- return { dir, storyGitRef };
56
+ /** Create a temp directory for test fixtures. */
57
+ function makeTempDir(): string {
58
+ const dir = mkdtempSync(join(tmpdir(), "nax-post-verify-"));
59
+ return dir;
97
60
  }
98
61
 
99
62
  function makeConfig(
@@ -139,6 +102,7 @@ function makeConfig(
139
102
  regressionGate: {
140
103
  enabled: true,
141
104
  timeoutSeconds: 120,
105
+ mode: "per-story",
142
106
  ...regressionGateOverrides,
143
107
  },
144
108
  contextProviderTokenBudget: 2000,
@@ -216,7 +180,6 @@ function makePRD(story: UserStory): PRD {
216
180
 
217
181
  function makeOpts(
218
182
  workdir: string,
219
- storyGitRef: string,
220
183
  config: NaxConfig,
221
184
  story: UserStory,
222
185
  prd: PRD,
@@ -230,7 +193,6 @@ function makeOpts(
230
193
  storiesToExecute: [story],
231
194
  allStoryMetrics: [] as StoryMetrics[],
232
195
  timeoutRetryCountMap: new Map<string, number>(),
233
- storyGitRef,
234
196
  };
235
197
  }
236
198
 
@@ -239,19 +201,14 @@ function makeOpts(
239
201
  // ---------------------------------------------------------------------------
240
202
 
241
203
  let tempDir: string;
242
- let storyGitRef: string;
243
204
 
244
205
  beforeEach(() => {
245
206
  // Wire _postVerifyDeps to mocks
246
207
  _postVerifyDeps.runVerification = mockRunVerification as typeof _postVerifyDeps.runVerification;
247
- _postVerifyDeps.parseTestOutput = () => ({ passCount: 5, failCount: 0, isEnvironmentalFailure: false }) as any;
248
- _postVerifyDeps.getEnvironmentalEscalationThreshold = () => 3;
249
208
  _postVerifyDeps.revertStoriesOnFailure = mockRevertStoriesOnFailure as typeof _postVerifyDeps.revertStoriesOnFailure;
250
209
  _postVerifyDeps.runRectificationLoop = mockRunRectificationLoop as typeof _postVerifyDeps.runRectificationLoop;
251
210
  _postVerifyDeps.getExpectedFiles = () => [];
252
211
  _postVerifyDeps.savePRD = mock(async () => {}) as typeof _postVerifyDeps.savePRD;
253
- _postVerifyDeps.appendProgress = mock(async () => {}) as typeof _postVerifyDeps.appendProgress;
254
- _postVerifyDeps.getTierConfig = () => undefined as any;
255
212
  _postVerifyDeps.parseBunTestOutput = () => ({ failed: 0, passed: 5, failures: [] }) as any;
256
213
  mockRunVerification.mockClear();
257
214
  mockRevertStoriesOnFailure.mockClear();
@@ -259,9 +216,7 @@ beforeEach(() => {
259
216
  _verificationResponses = [];
260
217
  _verificationCallIndex = 0;
261
218
 
262
- const repo = makeGitRepo();
263
- tempDir = repo.dir;
264
- storyGitRef = repo.storyGitRef;
219
+ tempDir = makeTempDir();
265
220
  });
266
221
 
267
222
  afterEach(() => {
@@ -276,9 +231,8 @@ afterEach(() => {
276
231
 
277
232
  describe("BUG-026: regression gate TIMEOUT acceptance", () => {
278
233
  test("TIMEOUT + acceptOnTimeout=true → runPostAgentVerification returns passed", async () => {
279
- // Call 1: scoped verification passes; Call 2: regression gate times out
234
+ // Now only one call: regression gate times out with acceptOnTimeout=true
280
235
  _verificationResponses = [
281
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
282
236
  { success: false, status: "TIMEOUT", countsTowardEscalation: false },
283
237
  ];
284
238
 
@@ -286,14 +240,13 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
286
240
  const story = makeStory();
287
241
  const prd = makePRD(story);
288
242
 
289
- const result = await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
243
+ const result = await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
290
244
 
291
245
  expect(result.passed).toBe(true);
292
246
  });
293
247
 
294
248
  test("TIMEOUT + acceptOnTimeout=true → revertStoriesOnFailure is NOT called", async () => {
295
249
  _verificationResponses = [
296
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
297
250
  { success: false, status: "TIMEOUT", countsTowardEscalation: false },
298
251
  ];
299
252
 
@@ -301,14 +254,13 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
301
254
  const story = makeStory();
302
255
  const prd = makePRD(story);
303
256
 
304
- await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
257
+ await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
305
258
 
306
259
  expect(mockRevertStoriesOnFailure).not.toHaveBeenCalled();
307
260
  });
308
261
 
309
262
  test("TIMEOUT + acceptOnTimeout=false → runPostAgentVerification returns failed", async () => {
310
263
  _verificationResponses = [
311
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
312
264
  { success: false, status: "TIMEOUT", countsTowardEscalation: false },
313
265
  ];
314
266
 
@@ -316,14 +268,13 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
316
268
  const story = makeStory();
317
269
  const prd = makePRD(story);
318
270
 
319
- const result = await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
271
+ const result = await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
320
272
 
321
273
  expect(result.passed).toBe(false);
322
274
  });
323
275
 
324
276
  test("TIMEOUT + acceptOnTimeout=false → revertStoriesOnFailure IS called", async () => {
325
277
  _verificationResponses = [
326
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
327
278
  { success: false, status: "TIMEOUT", countsTowardEscalation: false },
328
279
  ];
329
280
 
@@ -331,14 +282,13 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
331
282
  const story = makeStory();
332
283
  const prd = makePRD(story);
333
284
 
334
- await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
285
+ await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
335
286
 
336
287
  expect(mockRevertStoriesOnFailure).toHaveBeenCalledTimes(1);
337
288
  });
338
289
 
339
290
  test("TIMEOUT + acceptOnTimeout not set → defaults to true → returns passed", async () => {
340
291
  _verificationResponses = [
341
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
342
292
  { success: false, status: "TIMEOUT", countsTowardEscalation: false },
343
293
  ];
344
294
 
@@ -347,14 +297,13 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
347
297
  const story = makeStory();
348
298
  const prd = makePRD(story);
349
299
 
350
- const result = await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
300
+ const result = await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
351
301
 
352
302
  expect(result.passed).toBe(true);
353
303
  });
354
304
 
355
305
  test("TEST_FAILURE in regression gate → returns failed regardless of acceptOnTimeout", async () => {
356
306
  _verificationResponses = [
357
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
358
307
  { success: false, status: "TEST_FAILURE", countsTowardEscalation: true, output: "FAIL 1" },
359
308
  ];
360
309
 
@@ -362,14 +311,13 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
362
311
  const story = makeStory();
363
312
  const prd = makePRD(story);
364
313
 
365
- const result = await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
314
+ const result = await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
366
315
 
367
316
  expect(result.passed).toBe(false);
368
317
  });
369
318
 
370
319
  test("TEST_FAILURE in regression gate → revertStoriesOnFailure IS called", async () => {
371
320
  _verificationResponses = [
372
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
373
321
  { success: false, status: "TEST_FAILURE", countsTowardEscalation: true, output: "FAIL 1" },
374
322
  ];
375
323
 
@@ -377,39 +325,38 @@ describe("BUG-026: regression gate TIMEOUT acceptance", () => {
377
325
  const story = makeStory();
378
326
  const prd = makePRD(story);
379
327
 
380
- await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
328
+ await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
381
329
 
382
330
  expect(mockRevertStoriesOnFailure).toHaveBeenCalledTimes(1);
383
331
  });
384
332
 
385
- test("regression gate runs second runVerification called twice (scoped + full suite)", async () => {
333
+ test("full-suite regression gate passesreturns passed (one call to runVerification)", async () => {
386
334
  _verificationResponses = [
387
335
  { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
388
- { success: false, status: "TIMEOUT", countsTowardEscalation: false },
389
336
  ];
390
337
 
391
338
  const config = makeConfig({ acceptOnTimeout: true });
392
339
  const story = makeStory();
393
340
  const prd = makePRD(story);
394
341
 
395
- await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
342
+ const result = await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
396
343
 
397
- // Once for scoped verification, once for regression gate
398
- expect(mockRunVerification).toHaveBeenCalledTimes(2);
344
+ // Post-verify now ONLY runs the full-suite regression gate (no scoped verification)
345
+ expect(result.passed).toBe(true);
346
+ expect(mockRunVerification).toHaveBeenCalledTimes(1);
399
347
  });
400
348
 
401
- test("regression gate disabled → only scoped test runs (one call to runVerification)", async () => {
402
- _verificationResponses = [
403
- { success: true, status: "SUCCESS", countsTowardEscalation: true, output: "pass 5" },
404
- ];
349
+ test("regression gate disabled → returns passed (skips regression gate)", async () => {
350
+ _verificationResponses = [];
405
351
 
406
352
  const config = makeConfig({ enabled: false, timeoutSeconds: 120 });
407
353
  const story = makeStory();
408
354
  const prd = makePRD(story);
409
355
 
410
- const result = await runPostAgentVerification(makeOpts(tempDir, storyGitRef, config, story, prd));
356
+ const result = await runPostAgentVerification(makeOpts(tempDir, config, story, prd));
411
357
 
412
358
  expect(result.passed).toBe(true);
413
- expect(mockRunVerification).toHaveBeenCalledTimes(1);
359
+ // No verification calls when regression gate is disabled
360
+ expect(mockRunVerification).toHaveBeenCalledTimes(0);
414
361
  });
415
362
  });
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Unit tests for post-verify regression gate (BUG-009)
2
+ * Unit tests for regression gate configuration and behavior
3
3
  *
4
- * Tests the logic for:
5
- * - Running regression gate after scoped verification passes
6
- * - Skipping regression gate when scoped verification already ran full suite
7
- * - Feeding regression failures into rectification loop
4
+ * Tests the configuration and type-level logic for:
5
+ * - Regression gate enabled/disabled state
6
+ * - Timeout configuration and acceptOnTimeout behavior (BUG-026)
7
+ * - Story state transitions on regression failure
8
+ * - Metrics removal on regression failure
9
+ *
10
+ * Behavioral tests are in post-verify-regression.test.ts
8
11
  */
9
12
 
10
13
  import { describe, expect, test } from "bun:test";
@@ -41,44 +44,28 @@ describe("RegressionGateConfig", () => {
41
44
  });
42
45
 
43
46
  describe("Regression Gate Logic", () => {
44
- test("should run regression gate when scoped tests were run (changed files > 0)", () => {
45
- const changedTestFiles = ["test/foo.test.ts", "test/bar.test.ts"];
46
- const regressionGateEnabled = true;
47
- const scopedTestsWereRun = changedTestFiles.length > 0;
48
-
49
- // Logic: regression gate should run
50
- const shouldRunRegressionGate = regressionGateEnabled && scopedTestsWereRun;
51
- expect(shouldRunRegressionGate).toBe(true);
52
- });
53
-
54
- test("should skip regression gate when scoped tests ran full suite (changed files = 0)", () => {
55
- const changedTestFiles: string[] = [];
47
+ test("regression gate should run when enabled (post-verify always runs full suite)", () => {
56
48
  const regressionGateEnabled = true;
57
- const scopedTestsWereRun = changedTestFiles.length > 0;
58
49
 
59
- // Logic: regression gate should NOT run (full suite already ran)
60
- const shouldRunRegressionGate = regressionGateEnabled && scopedTestsWereRun;
61
- expect(shouldRunRegressionGate).toBe(false);
50
+ // Post-verify now ONLY runs full-suite regression gate (no scoped logic)
51
+ expect(regressionGateEnabled).toBe(true);
62
52
  });
63
53
 
64
- test("should skip regression gate when disabled in config", () => {
65
- const changedTestFiles = ["test/foo.test.ts"];
54
+ test("regression gate should skip when disabled in config", () => {
66
55
  const regressionGateEnabled = false;
67
- const scopedTestsWereRun = changedTestFiles.length > 0;
68
56
 
69
- // Logic: regression gate should NOT run (disabled)
70
- const shouldRunRegressionGate = regressionGateEnabled && scopedTestsWereRun;
71
- expect(shouldRunRegressionGate).toBe(false);
57
+ // Logic: regression gate should NOT run
58
+ expect(regressionGateEnabled).toBe(false);
72
59
  });
73
60
 
74
- test("should skip regression gate when both disabled AND no changed files", () => {
75
- const changedTestFiles: string[] = [];
76
- const regressionGateEnabled = false;
77
- const scopedTestsWereRun = changedTestFiles.length > 0;
61
+ test("post-verify removes scoped verification (always runs full suite)", () => {
62
+ // With the removal of scoped verification, post-verify always:
63
+ // 1. Runs the full-suite regression gate (if enabled)
64
+ // 2. Reverts on failure
65
+ // 3. Optionally runs rectification on test failures
78
66
 
79
- // Logic: regression gate should NOT run
80
- const shouldRunRegressionGate = regressionGateEnabled && scopedTestsWereRun;
81
- expect(shouldRunRegressionGate).toBe(false);
67
+ const hasNoScopedVerification = true;
68
+ expect(hasNoScopedVerification).toBe(true);
82
69
  });
83
70
  });
84
71
 
@@ -104,27 +91,20 @@ describe("Regression Failure Handling", () => {
104
91
 
105
92
  describe("Rectification Prompt for Regression", () => {
106
93
  test("should include REGRESSION prefix in rectification prompt", () => {
107
- const basePrompt = `# Rectification Required
108
-
109
- Your changes caused test regressions. Fix these without breaking existing logic.`;
110
-
111
- const regressionPrompt = `# REGRESSION: Cross-Story Test Failures
112
-
113
- Your changes passed scoped tests but broke unrelated tests. Fix these regressions.
94
+ const regressionPrompt = `# REGRESSION: Full-Suite Test Failures
114
95
 
115
- ${basePrompt}`;
96
+ Your changes broke tests in the full suite. Fix these regressions.`;
116
97
 
117
98
  expect(regressionPrompt).toContain("# REGRESSION:");
118
- expect(regressionPrompt).toContain("passed scoped tests but broke unrelated tests");
119
- expect(regressionPrompt).toContain(basePrompt);
99
+ expect(regressionPrompt).toContain("Full-Suite Test Failures");
120
100
  });
121
101
 
122
- test("regression prompt should emphasize cross-story nature", () => {
102
+ test("regression prompt should emphasize full-suite nature", () => {
123
103
  const regressionPrompt =
124
- "# REGRESSION: Cross-Story Test Failures\n\nYour changes passed scoped tests but broke unrelated tests.";
104
+ "# REGRESSION: Full-Suite Test Failures\n\nYour changes broke tests in the full suite.";
125
105
 
126
- expect(regressionPrompt).toContain("Cross-Story");
127
- expect(regressionPrompt).toContain("unrelated tests");
106
+ expect(regressionPrompt).toContain("Full-Suite");
107
+ expect(regressionPrompt).toContain("broke tests");
128
108
  });
129
109
  });
130
110
 
@@ -44,9 +44,8 @@ describe("formatConsole", () => {
44
44
 
45
45
  const output = formatConsole(entry);
46
46
 
47
- // Should not contain brackets around storyId
48
- const bracketCount = (output.match(/\[/g) || []).length;
49
- expect(bracketCount).toBe(2); // Only timestamp and stage
47
+ // Visibility test instead of raw bracket count (avoid ANSI issues)
48
+ expect(output).not.toContain("[user-auth-001]");
50
49
  });
51
50
 
52
51
  test("formats data as pretty JSON on new line", () => {
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ // Access internal functions for testing
3
+ // @ts-ignore
4
+ import { hasShellOperators, validateHookCommand } from "../../../src/hooks/runner";
5
+
6
+ describe("Hook Shell Security (SEC-3)", () => {
7
+ test("hasShellOperators detects backticks", () => {
8
+ // @ts-ignore
9
+ expect(hasShellOperators("echo `whoami`")).toBe(true);
10
+ });
11
+
12
+ test("hasShellOperators detects pipes and redirects", () => {
13
+ // @ts-ignore
14
+ expect(hasShellOperators("echo hi | grep h")).toBe(true);
15
+ // @ts-ignore
16
+ expect(hasShellOperators("echo hi > file.txt")).toBe(true);
17
+ });
18
+
19
+ test("validateHookCommand blocks backtick substitution", () => {
20
+ // @ts-ignore
21
+ expect(() => validateHookCommand("echo `whoami`")).toThrow(/dangerous pattern/);
22
+ });
23
+
24
+ test("validateHookCommand blocks $(...) substitution", () => {
25
+ // @ts-ignore
26
+ expect(() => validateHookCommand("echo $(whoami)")).toThrow(/dangerous pattern/);
27
+ });
28
+
29
+ test("validateHookCommand blocks eval", () => {
30
+ // @ts-ignore
31
+ expect(() => validateHookCommand("eval 'echo hi'")).toThrow(/dangerous pattern/);
32
+ });
33
+
34
+ test("allows safe commands", () => {
35
+ // @ts-ignore
36
+ expect(() => validateHookCommand("echo 'Hello World'")).not.toThrow();
37
+ // @ts-ignore
38
+ expect(() => validateHookCommand("bun test")).not.toThrow();
39
+ });
40
+ });