agent-gauntlet 0.1.10 → 0.1.12

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 (55) hide show
  1. package/README.md +55 -87
  2. package/package.json +4 -2
  3. package/src/bun-plugins.d.ts +4 -0
  4. package/src/cli-adapters/claude.ts +139 -108
  5. package/src/cli-adapters/codex.ts +141 -117
  6. package/src/cli-adapters/cursor.ts +152 -0
  7. package/src/cli-adapters/gemini.ts +171 -139
  8. package/src/cli-adapters/github-copilot.ts +153 -0
  9. package/src/cli-adapters/index.ts +77 -48
  10. package/src/commands/check.test.ts +24 -20
  11. package/src/commands/check.ts +86 -59
  12. package/src/commands/ci/index.ts +15 -0
  13. package/src/commands/ci/init.ts +96 -0
  14. package/src/commands/ci/list-jobs.ts +78 -0
  15. package/src/commands/detect.test.ts +38 -32
  16. package/src/commands/detect.ts +89 -61
  17. package/src/commands/health.test.ts +67 -53
  18. package/src/commands/health.ts +167 -145
  19. package/src/commands/help.test.ts +37 -37
  20. package/src/commands/help.ts +31 -22
  21. package/src/commands/index.ts +10 -9
  22. package/src/commands/init.test.ts +120 -107
  23. package/src/commands/init.ts +514 -417
  24. package/src/commands/list.test.ts +87 -70
  25. package/src/commands/list.ts +28 -24
  26. package/src/commands/rerun.ts +157 -119
  27. package/src/commands/review.test.ts +26 -20
  28. package/src/commands/review.ts +86 -59
  29. package/src/commands/run.test.ts +22 -20
  30. package/src/commands/run.ts +85 -58
  31. package/src/commands/shared.ts +44 -35
  32. package/src/config/ci-loader.ts +33 -0
  33. package/src/config/ci-schema.ts +52 -0
  34. package/src/config/loader.test.ts +112 -90
  35. package/src/config/loader.ts +132 -123
  36. package/src/config/schema.ts +48 -47
  37. package/src/config/types.ts +28 -13
  38. package/src/config/validator.ts +521 -454
  39. package/src/core/change-detector.ts +122 -104
  40. package/src/core/entry-point.test.ts +60 -62
  41. package/src/core/entry-point.ts +120 -74
  42. package/src/core/job.ts +69 -59
  43. package/src/core/runner.ts +264 -230
  44. package/src/gates/check.ts +78 -69
  45. package/src/gates/result.ts +7 -7
  46. package/src/gates/review.test.ts +277 -138
  47. package/src/gates/review.ts +724 -561
  48. package/src/index.ts +18 -15
  49. package/src/output/console.ts +253 -214
  50. package/src/output/logger.ts +66 -52
  51. package/src/templates/run_gauntlet.template.md +18 -0
  52. package/src/templates/workflow.yml +77 -0
  53. package/src/utils/diff-parser.ts +64 -62
  54. package/src/utils/log-parser.ts +227 -206
  55. package/src/utils/sanitizer.ts +1 -1
@@ -1,82 +1,91 @@
1
- import { exec } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- import { CheckGateConfig } from '../config/types.js';
4
- import { GateResult } from './result.js';
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import type { CheckGateConfig } from "../config/types.js";
4
+ import type { GateResult } from "./result.js";
5
5
 
6
6
  const execAsync = promisify(exec);
7
7
  const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
8
8
 
9
9
  export class CheckGateExecutor {
10
- async execute(
11
- jobId: string,
12
- config: CheckGateConfig,
13
- workingDirectory: string,
14
- logger: (output: string) => Promise<void>
15
- ): Promise<GateResult> {
16
- const startTime = Date.now();
17
-
18
- try {
19
- await logger(`[${new Date().toISOString()}] Starting check: ${config.name}\n`);
20
- await logger(`Executing command: ${config.command}\n`);
21
- await logger(`Working directory: ${workingDirectory}\n\n`);
10
+ async execute(
11
+ jobId: string,
12
+ config: CheckGateConfig,
13
+ workingDirectory: string,
14
+ logger: (output: string) => Promise<void>,
15
+ ): Promise<GateResult> {
16
+ const startTime = Date.now();
22
17
 
23
- const { stdout, stderr } = await execAsync(config.command, {
24
- cwd: workingDirectory,
25
- timeout: config.timeout ? config.timeout * 1000 : undefined,
26
- maxBuffer: MAX_BUFFER_BYTES
27
- });
18
+ try {
19
+ await logger(
20
+ `[${new Date().toISOString()}] Starting check: ${config.name}\n`,
21
+ );
22
+ await logger(`Executing command: ${config.command}\n`);
23
+ await logger(`Working directory: ${workingDirectory}\n\n`);
28
24
 
29
- if (stdout) await logger(stdout);
30
- if (stderr) await logger(`\nSTDERR:\n${stderr}`);
25
+ const { stdout, stderr } = await execAsync(config.command, {
26
+ cwd: workingDirectory,
27
+ timeout: config.timeout ? config.timeout * 1000 : undefined,
28
+ maxBuffer: MAX_BUFFER_BYTES,
29
+ });
31
30
 
32
- const result: GateResult = {
33
- jobId,
34
- status: 'pass',
35
- duration: Date.now() - startTime,
36
- message: 'Command exited with code 0'
37
- };
31
+ if (stdout) await logger(stdout);
32
+ if (stderr) await logger(`\nSTDERR:\n${stderr}`);
38
33
 
39
- await logger(`Result: ${result.status} - ${result.message}\n`);
40
- return result;
41
- } catch (error: any) {
42
- if (error.stdout) await logger(error.stdout);
43
- if (error.stderr) await logger(`\nSTDERR:\n${error.stderr}`);
44
-
45
- await logger(`\nCommand failed: ${error.message}`);
34
+ const result: GateResult = {
35
+ jobId,
36
+ status: "pass",
37
+ duration: Date.now() - startTime,
38
+ message: "Command exited with code 0",
39
+ };
46
40
 
47
- // If it's a timeout
48
- if (error.signal === 'SIGTERM' && config.timeout) {
49
- const result: GateResult = {
50
- jobId,
51
- status: 'fail',
52
- duration: Date.now() - startTime,
53
- message: `Timed out after ${config.timeout}s`
54
- };
55
- await logger(`Result: ${result.status} - ${result.message}\n`);
56
- return result;
57
- }
41
+ await logger(`Result: ${result.status} - ${result.message}\n`);
42
+ return result;
43
+ } catch (error: unknown) {
44
+ const err = error as {
45
+ stdout?: string;
46
+ stderr?: string;
47
+ message?: string;
48
+ signal?: string;
49
+ code?: number;
50
+ };
51
+ if (err.stdout) await logger(err.stdout);
52
+ if (err.stderr) await logger(`\nSTDERR:\n${err.stderr}`);
58
53
 
59
- // If it's a non-zero exit code
60
- if (typeof error.code === 'number') {
61
- const result: GateResult = {
62
- jobId,
63
- status: 'fail',
64
- duration: Date.now() - startTime,
65
- message: `Exited with code ${error.code}`
66
- };
67
- await logger(`Result: ${result.status} - ${result.message}\n`);
68
- return result;
69
- }
54
+ await logger(`\nCommand failed: ${err.message}`);
70
55
 
71
- // Other errors
72
- const result: GateResult = {
73
- jobId,
74
- status: 'error',
75
- duration: Date.now() - startTime,
76
- message: error.message || 'Unknown error'
77
- };
78
- await logger(`Result: ${result.status} - ${result.message}\n`);
79
- return result;
80
- }
81
- }
56
+ // If it's a timeout
57
+ if (err.signal === "SIGTERM" && config.timeout) {
58
+ const result: GateResult = {
59
+ jobId,
60
+ status: "fail",
61
+ duration: Date.now() - startTime,
62
+ message: `Timed out after ${config.timeout}s`,
63
+ };
64
+ await logger(`Result: ${result.status} - ${result.message}\n`);
65
+ return result;
66
+ }
67
+
68
+ // If it's a non-zero exit code
69
+ if (typeof err.code === "number") {
70
+ const result: GateResult = {
71
+ jobId,
72
+ status: "fail",
73
+ duration: Date.now() - startTime,
74
+ message: `Exited with code ${err.code}`,
75
+ };
76
+ await logger(`Result: ${result.status} - ${result.message}\n`);
77
+ return result;
78
+ }
79
+
80
+ // Other errors
81
+ const result: GateResult = {
82
+ jobId,
83
+ status: "error",
84
+ duration: Date.now() - startTime,
85
+ message: err.message || "Unknown error",
86
+ };
87
+ await logger(`Result: ${result.status} - ${result.message}\n`);
88
+ return result;
89
+ }
90
+ }
82
91
  }
@@ -1,10 +1,10 @@
1
- export type GateStatus = 'pass' | 'fail' | 'error';
1
+ export type GateStatus = "pass" | "fail" | "error";
2
2
 
3
3
  export interface GateResult {
4
- jobId: string;
5
- status: GateStatus;
6
- duration: number; // ms
7
- message?: string; // summary message
8
- logPath?: string; // path to full log
9
- logPaths?: string[]; // paths to multiple logs (e.g. per-agent logs)
4
+ jobId: string;
5
+ status: GateStatus;
6
+ duration: number; // ms
7
+ message?: string; // summary message
8
+ logPath?: string; // path to full log
9
+ logPaths?: string[]; // paths to multiple logs (e.g. per-agent logs)
10
10
  }
@@ -1,152 +1,291 @@
1
- import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { ReviewGateExecutor } from './review.js';
5
- import { Logger } from '../output/logger.js';
6
- import * as cliAdapters from '../cli-adapters/index.js';
7
- import type { CLIAdapter } from '../cli-adapters/index.js';
8
-
9
- const TEST_DIR = path.join(process.cwd(), 'test-review-logs-' + Date.now());
10
- const LOG_DIR = path.join(TEST_DIR, 'logs');
11
-
12
- describe('ReviewGateExecutor Logging', () => {
13
- let logger: Logger;
14
- let executor: ReviewGateExecutor;
15
-
16
- beforeEach(async () => {
17
- await fs.mkdir(TEST_DIR, { recursive: true });
18
- await fs.mkdir(LOG_DIR, { recursive: true });
19
- logger = new Logger(LOG_DIR);
20
- executor = new ReviewGateExecutor();
21
-
22
- // Mock getAdapter
23
- mock.module('../cli-adapters/index.js', () => ({
24
- getAdapter: (name: string) => ({
25
- name,
26
- isAvailable: async () => true,
27
- checkHealth: async () => ({ status: 'healthy' }),
28
- // execute returns the raw string output from the LLM, which is then parsed by the executor.
29
- // The real adapter returns a string. In this test, we return a JSON string to simulate
30
- // the LLM returning structured data. This IS intentional and matches the expected contract
31
- // where execute() -> Promise<string>.
32
- execute: async () => {
33
- await new Promise(r => setTimeout(r, 1)); // Simulate async work
34
- return JSON.stringify({ status: 'pass', message: 'OK' });
35
- },
36
- getProjectCommandDir: () => null,
37
- getUserCommandDir: () => null,
38
- getCommandExtension: () => 'md',
39
- canUseSymlink: () => false,
40
- transformCommand: (c: string) => c
41
- } as unknown as CLIAdapter)
42
- }));
43
-
44
- // Mock git commands via util.promisify(exec)
45
- mock.module('node:util', () => ({
46
- promisify: (fn: Function) => {
47
- // Only mock exec, let others pass (though in this test env we likely only use exec)
48
- if (fn.name === 'exec') {
49
- return async (cmd: string) => {
50
- if (/^git diff/.test(cmd)) return 'diff content';
51
- if (/^git ls-files/.test(cmd)) return 'file.ts';
52
- return { stdout: '', stderr: '' };
53
- };
54
- }
55
- // Fallback for other functions if needed
56
- return async () => {};
57
- }
58
- }));
59
- });
60
-
61
- afterEach(async () => {
62
- await fs.rm(TEST_DIR, { recursive: true, force: true });
63
- mock.restore();
64
- });
65
-
66
- it('should only create adapter-specific logs and no generic log', async () => {
67
- const jobId = 'review:src:code-quality';
68
- const config = {
69
- name: 'code-quality',
70
- cli_preference: ['codex', 'claude'],
71
- num_reviews: 2
72
- };
73
-
74
- const loggerFactory = logger.createLoggerFactory(jobId);
75
-
76
- // We need to mock getDiff since it uses execAsync which we mocked
77
- // Actually ReviewGateExecutor is a class, we can mock its private method if needed
78
- // or just let it run if the mock promisify works.
79
-
80
- const result = await executor.execute(
81
- jobId,
82
- config as any,
83
- 'src/',
84
- loggerFactory,
85
- 'main'
86
- );
87
-
88
- expect(result.status).toBe('pass');
89
- expect(result.logPaths).toBeDefined();
90
- expect(result.logPaths).toHaveLength(2);
91
- expect(result.logPaths?.[0]).toContain('review_src_code-quality_codex.log');
92
- expect(result.logPaths?.[1]).toContain('review_src_code-quality_claude.log');
93
-
94
- const files = await fs.readdir(LOG_DIR);
95
- expect(files).toContain('review_src_code-quality_codex.log');
96
- expect(files).toContain('review_src_code-quality_claude.log');
97
- expect(files).not.toContain('review_src_code-quality.log');
98
-
99
- // Verify multiplexed content
100
- const codexLog = await fs.readFile(path.join(LOG_DIR, 'review_src_code-quality_codex.log'), 'utf-8');
101
- expect(codexLog).toContain('Starting review: code-quality');
102
- expect(codexLog).toContain('Review result (codex): pass');
103
-
104
- const claudeLog = await fs.readFile(path.join(LOG_DIR, 'review_src_code-quality_claude.log'), 'utf-8');
105
- expect(claudeLog).toContain('Starting review: code-quality');
106
- expect(claudeLog).toContain('Review result (claude): pass');
107
- });
108
-
109
- it('should be handled correctly by ConsoleReporter', async () => {
110
- const jobId = 'review:src:code-quality';
111
- const codexPath = path.join(LOG_DIR, 'review_src_code-quality_codex.log');
112
- const claudePath = path.join(LOG_DIR, 'review_src_code-quality_claude.log');
113
-
114
- await fs.writeFile(codexPath, `
1
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { CLIAdapter } from "../cli-adapters/index.js";
5
+ import type {
6
+ ReviewGateConfig,
7
+ ReviewPromptFrontmatter,
8
+ } from "../config/types.js";
9
+ import { Logger } from "../output/logger.js";
10
+ import type { ReviewGateExecutor } from "./review.js";
11
+
12
+ const TEST_DIR = path.join(process.cwd(), `test-review-logs-${Date.now()}`);
13
+
14
+ describe("ReviewGateExecutor Logging", () => {
15
+ let logger: Logger;
16
+ let executor: ReviewGateExecutor;
17
+ let originalCI: string | undefined;
18
+ let originalGithubActions: string | undefined;
19
+ let originalCwd: string;
20
+
21
+ beforeEach(async () => {
22
+ await fs.mkdir(TEST_DIR, { recursive: true });
23
+
24
+ // Save and disable CI mode for this test to avoid complex git ref issues
25
+ originalCI = process.env.CI;
26
+ originalGithubActions = process.env.GITHUB_ACTIONS;
27
+ originalCwd = process.cwd();
28
+ delete process.env.CI;
29
+ delete process.env.GITHUB_ACTIONS;
30
+
31
+ // Change to test directory with its own git repo to avoid issues with the main repo
32
+ process.chdir(TEST_DIR);
33
+ // Initialize a minimal git repo for the test
34
+ const { exec } = await import("node:child_process");
35
+ const { promisify } = await import("node:util");
36
+ const execAsync = promisify(exec);
37
+ await execAsync("git init");
38
+ await execAsync('git config user.email "test@test.com"');
39
+ await execAsync('git config user.name "Test"');
40
+ // Create an initial commit so we have a history
41
+ await fs.writeFile("test.txt", "initial");
42
+ await execAsync("git add test.txt");
43
+ await execAsync('git commit -m "initial"');
44
+ // Create a "main" branch
45
+ await execAsync("git branch -M main");
46
+ // Create src directory for our test
47
+ await fs.mkdir("src", { recursive: true });
48
+ await fs.writeFile("src/test.ts", "test content");
49
+ await execAsync("git add src/test.ts");
50
+ await execAsync('git commit -m "add src"');
51
+
52
+ // Make uncommitted changes so the diff isn't empty
53
+ await fs.writeFile("src/test.ts", "modified test content");
54
+
55
+ // Now create the log directory and logger in the test directory
56
+ await fs.mkdir("logs", { recursive: true });
57
+ logger = new Logger(path.join(process.cwd(), "logs"));
58
+
59
+ // Create a factory function for mock adapters that returns the correct name
60
+ const createMockAdapter = (name: string): CLIAdapter =>
61
+ ({
62
+ name,
63
+ isAvailable: async () => true,
64
+ checkHealth: async () => ({ status: "healthy" }),
65
+ // execute returns the raw string output from the LLM, which is then parsed by the executor.
66
+ // The real adapter returns a string. In this test, we return a JSON string to simulate
67
+ // the LLM returning structured data. This IS intentional and matches the expected contract
68
+ // where execute() -> Promise<string>.
69
+ execute: async () => {
70
+ await new Promise((r) => setTimeout(r, 1)); // Simulate async work
71
+ return JSON.stringify({ status: "pass", message: "OK" });
72
+ },
73
+ getProjectCommandDir: () => null,
74
+ getUserCommandDir: () => null,
75
+ getCommandExtension: () => "md",
76
+ canUseSymlink: () => false,
77
+ transformCommand: (c: string) => c,
78
+ }) as unknown as CLIAdapter;
79
+
80
+ // Mock getAdapter and other exports that may be imported by other modules
81
+ mock.module("../cli-adapters/index.js", () => ({
82
+ getAdapter: (name: string) => createMockAdapter(name),
83
+ getAllAdapters: () => [
84
+ createMockAdapter("codex"),
85
+ createMockAdapter("claude"),
86
+ ],
87
+ getProjectCommandAdapters: () => [
88
+ createMockAdapter("codex"),
89
+ createMockAdapter("claude"),
90
+ ],
91
+ getUserCommandAdapters: () => [
92
+ createMockAdapter("codex"),
93
+ createMockAdapter("claude"),
94
+ ],
95
+ getValidCLITools: () => ["codex", "claude", "gemini"],
96
+ }));
97
+
98
+ const { ReviewGateExecutor } = await import("./review.js");
99
+ executor = new ReviewGateExecutor();
100
+ });
101
+
102
+ afterEach(async () => {
103
+ // Restore working directory first
104
+ process.chdir(originalCwd);
105
+
106
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
107
+ mock.restore();
108
+
109
+ // Restore CI env vars
110
+ if (originalCI !== undefined) {
111
+ process.env.CI = originalCI;
112
+ }
113
+ if (originalGithubActions !== undefined) {
114
+ process.env.GITHUB_ACTIONS = originalGithubActions;
115
+ }
116
+ });
117
+
118
+ it("should only create adapter-specific logs and no generic log", async () => {
119
+ const jobId = "review:src:code-quality";
120
+ const config: ReviewGateConfig & ReviewPromptFrontmatter = {
121
+ name: "code-quality",
122
+ cli_preference: ["codex", "claude"],
123
+ num_reviews: 2,
124
+ };
125
+
126
+ const loggerFactory = logger.createLoggerFactory(jobId);
127
+
128
+ // We need to mock getDiff since it uses execAsync which we mocked
129
+ // Actually ReviewGateExecutor is a class, we can mock its private method if needed
130
+ // or just let it run if the mock promisify works.
131
+
132
+ const result = await executor.execute(
133
+ jobId,
134
+ config,
135
+ "src/",
136
+ loggerFactory,
137
+ "main",
138
+ );
139
+
140
+ // Enhanced error messages for better debugging
141
+ if (result.status !== "pass") {
142
+ throw new Error(
143
+ `Expected result.status to be "pass" but got "${result.status}". Message: ${result.message || "none"}. Duration: ${result.duration}ms`,
144
+ );
145
+ }
146
+
147
+ if (!result.logPaths) {
148
+ throw new Error(
149
+ `Expected result.logPaths to be defined but got ${JSON.stringify(result.logPaths)}`,
150
+ );
151
+ }
152
+
153
+ if (result.logPaths.length !== 2) {
154
+ throw new Error(
155
+ `Expected result.logPaths to have length 2 but got ${result.logPaths.length}. Paths: ${JSON.stringify(result.logPaths)}`,
156
+ );
157
+ }
158
+
159
+ if (!result.logPaths[0]?.includes("review_src_code-quality_codex.log")) {
160
+ throw new Error(
161
+ `Expected result.logPaths[0] to contain "review_src_code-quality_codex.log" but got "${result.logPaths[0]}"`,
162
+ );
163
+ }
164
+
165
+ if (!result.logPaths[1]?.includes("review_src_code-quality_claude.log")) {
166
+ throw new Error(
167
+ `Expected result.logPaths[1] to contain "review_src_code-quality_claude.log" but got "${result.logPaths[1]}"`,
168
+ );
169
+ }
170
+
171
+ const files = await fs.readdir("logs");
172
+ const filesList = files.join(", ");
173
+
174
+ if (!files.includes("review_src_code-quality_codex.log")) {
175
+ throw new Error(
176
+ `Expected log directory to contain "review_src_code-quality_codex.log" but only found: [${filesList}]`,
177
+ );
178
+ }
179
+
180
+ if (!files.includes("review_src_code-quality_claude.log")) {
181
+ throw new Error(
182
+ `Expected log directory to contain "review_src_code-quality_claude.log" but only found: [${filesList}]`,
183
+ );
184
+ }
185
+
186
+ if (files.includes("review_src_code-quality.log")) {
187
+ throw new Error(
188
+ `Expected log directory NOT to contain generic log "review_src_code-quality.log" but it was found. All files: [${filesList}]`,
189
+ );
190
+ }
191
+
192
+ // Verify multiplexed content
193
+ const codexLog = await fs.readFile(
194
+ "logs/review_src_code-quality_codex.log",
195
+ "utf-8",
196
+ );
197
+ if (!codexLog.includes("Starting review: code-quality")) {
198
+ throw new Error(
199
+ `Expected codex log to contain "Starting review: code-quality" but got: ${codexLog.substring(0, 200)}...`,
200
+ );
201
+ }
202
+ if (!codexLog.includes("Review result (codex): pass")) {
203
+ throw new Error(
204
+ `Expected codex log to contain "Review result (codex): pass" but got: ${codexLog.substring(0, 200)}...`,
205
+ );
206
+ }
207
+
208
+ const claudeLog = await fs.readFile(
209
+ "logs/review_src_code-quality_claude.log",
210
+ "utf-8",
211
+ );
212
+ if (!claudeLog.includes("Starting review: code-quality")) {
213
+ throw new Error(
214
+ `Expected claude log to contain "Starting review: code-quality" but got: ${claudeLog.substring(0, 200)}...`,
215
+ );
216
+ }
217
+ if (!claudeLog.includes("Review result (claude): pass")) {
218
+ throw new Error(
219
+ `Expected claude log to contain "Review result (claude): pass" but got: ${claudeLog.substring(0, 200)}...`,
220
+ );
221
+ }
222
+ });
223
+
224
+ it("should be handled correctly by ConsoleReporter", async () => {
225
+ const jobId = "review:src:code-quality";
226
+ const codexPath = "logs/review_src_code-quality_codex.log";
227
+ const claudePath = "logs/review_src_code-quality_claude.log";
228
+
229
+ await fs.writeFile(
230
+ codexPath,
231
+ `
115
232
  [2026-01-14T10:00:00.000Z] Starting review: code-quality
116
233
  --- Parsed Result (codex) ---
117
234
  Status: FAIL
118
235
  Violations:
119
236
  1. src/index.ts:10 - Security risk
120
237
  Fix: Use a safer method
121
- `);
238
+ `,
239
+ );
122
240
 
123
- await fs.writeFile(claudePath, `
241
+ await fs.writeFile(
242
+ claudePath,
243
+ `
124
244
  [2026-01-14T10:00:00.000Z] Starting review: code-quality
125
245
  --- Parsed Result (claude) ---
126
246
  Status: FAIL
127
247
  Violations:
128
248
  1. src/main.ts:20 - Style issue
129
249
  Fix: Rename variable
130
- `);
131
-
132
- const result = {
133
- jobId,
134
- status: 'fail' as const,
135
- duration: 1000,
136
- message: 'Found violations',
137
- logPaths: [codexPath, claudePath]
138
- };
139
-
140
- const { ConsoleReporter } = await import('../output/console.js');
141
- const reporter = new ConsoleReporter();
142
-
143
- // We can access extractFailureDetails directly as it is public
144
- const details = await reporter.extractFailureDetails(result);
145
-
146
- // Check for presence of key information rather than exact counts
147
- expect(details.some((d: string) => d.includes('src/index.ts') && d.includes('10') && d.includes('Security risk'))).toBe(true);
148
- expect(details.some((d: string) => d.includes('Use a safer method'))).toBe(true);
149
- expect(details.some((d: string) => d.includes('src/main.ts') && d.includes('20') && d.includes('Style issue'))).toBe(true);
150
- expect(details.some((d: string) => d.includes('Rename variable'))).toBe(true);
151
- });
250
+ `,
251
+ );
252
+
253
+ const result = {
254
+ jobId,
255
+ status: "fail" as const,
256
+ duration: 1000,
257
+ message: "Found violations",
258
+ logPaths: [codexPath, claudePath],
259
+ };
260
+
261
+ const { ConsoleReporter } = await import("../output/console.js");
262
+ const reporter = new ConsoleReporter();
263
+
264
+ // We can access extractFailureDetails directly as it is public
265
+ const details = await reporter.extractFailureDetails(result);
266
+
267
+ // Check for presence of key information rather than exact counts
268
+ expect(
269
+ details.some(
270
+ (d: string) =>
271
+ d.includes("src/index.ts") &&
272
+ d.includes("10") &&
273
+ d.includes("Security risk"),
274
+ ),
275
+ ).toBe(true);
276
+ expect(details.some((d: string) => d.includes("Use a safer method"))).toBe(
277
+ true,
278
+ );
279
+ expect(
280
+ details.some(
281
+ (d: string) =>
282
+ d.includes("src/main.ts") &&
283
+ d.includes("20") &&
284
+ d.includes("Style issue"),
285
+ ),
286
+ ).toBe(true);
287
+ expect(details.some((d: string) => d.includes("Rename variable"))).toBe(
288
+ true,
289
+ );
290
+ });
152
291
  });