agent-gauntlet 0.2.2 → 0.4.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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli-adapters/claude.ts +13 -1
- package/src/cli-adapters/gemini.ts +17 -2
- package/src/commands/check.ts +108 -12
- package/src/commands/ci/list-jobs.ts +3 -2
- package/src/commands/clean.ts +29 -0
- package/src/commands/help.ts +1 -1
- package/src/commands/index.ts +2 -1
- package/src/commands/init.ts +4 -4
- package/src/commands/review.ts +108 -12
- package/src/commands/run.ts +109 -12
- package/src/commands/shared.ts +56 -10
- package/src/commands/validate.ts +20 -0
- package/src/config/schema.ts +5 -0
- package/src/config/validator.ts +6 -13
- package/src/core/change-detector.ts +1 -0
- package/src/core/entry-point.ts +48 -7
- package/src/core/runner.ts +90 -56
- package/src/gates/result.ts +32 -0
- package/src/gates/review.ts +428 -162
- package/src/index.ts +4 -2
- package/src/output/console-log.ts +146 -0
- package/src/output/console.ts +103 -9
- package/src/output/logger.ts +52 -8
- package/src/templates/run_gauntlet.template.md +20 -13
- package/src/utils/log-parser.ts +498 -162
- package/src/utils/session-ref.ts +82 -0
- package/src/commands/check.test.ts +0 -29
- package/src/commands/detect.test.ts +0 -43
- package/src/commands/health.test.ts +0 -93
- package/src/commands/help.test.ts +0 -44
- package/src/commands/init.test.ts +0 -130
- package/src/commands/list.test.ts +0 -121
- package/src/commands/rerun.ts +0 -160
- package/src/commands/review.test.ts +0 -31
- package/src/commands/run.test.ts +0 -27
- package/src/config/loader.test.ts +0 -151
- package/src/core/entry-point.test.ts +0 -61
- package/src/gates/review.test.ts +0 -291
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const SESSION_REF_FILENAME = ".session_ref";
|
|
7
|
+
|
|
8
|
+
// Exported for testing - allows injection of mock exec
|
|
9
|
+
export let execFn: (
|
|
10
|
+
cmd: string,
|
|
11
|
+
) => Promise<{ stdout: string; stderr: string }> = promisify(exec);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set the exec function (for testing)
|
|
15
|
+
*/
|
|
16
|
+
export function setExecFn(
|
|
17
|
+
fn: (cmd: string) => Promise<{ stdout: string; stderr: string }>,
|
|
18
|
+
): void {
|
|
19
|
+
execFn = fn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reset the exec function to the real implementation
|
|
24
|
+
*/
|
|
25
|
+
export function resetExecFn(): void {
|
|
26
|
+
execFn = promisify(exec);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Captures the current git state (working tree) as a commit SHA
|
|
31
|
+
* and writes it to the log directory.
|
|
32
|
+
* Uses `git stash create --include-untracked` to capture the state without modifying it.
|
|
33
|
+
*/
|
|
34
|
+
export async function writeSessionRef(logDir: string): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
// Create a stash of the current state (including untracked files)
|
|
37
|
+
// This returns a commit SHA but doesn't modify the working tree
|
|
38
|
+
const { stdout } = await execFn("git stash create --include-untracked");
|
|
39
|
+
let sha = stdout.trim();
|
|
40
|
+
|
|
41
|
+
if (!sha) {
|
|
42
|
+
// If no changes to stash (clean working tree), use HEAD
|
|
43
|
+
const { stdout: headSha } = await execFn("git rev-parse HEAD");
|
|
44
|
+
sha = headSha.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ensure log directory exists
|
|
48
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
49
|
+
await fs.writeFile(path.join(logDir, SESSION_REF_FILENAME), sha);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn(
|
|
52
|
+
"Failed to create session reference:",
|
|
53
|
+
error instanceof Error ? error.message : String(error),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads the stored session reference SHA from the log directory.
|
|
60
|
+
* Returns null if the file doesn't exist.
|
|
61
|
+
*/
|
|
62
|
+
export async function readSessionRef(logDir: string): Promise<string | null> {
|
|
63
|
+
try {
|
|
64
|
+
const refPath = path.join(logDir, SESSION_REF_FILENAME);
|
|
65
|
+
const content = await fs.readFile(refPath, "utf-8");
|
|
66
|
+
return content.trim();
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Removes the session reference file from the log directory.
|
|
74
|
+
*/
|
|
75
|
+
export async function clearSessionRef(logDir: string): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const refPath = path.join(logDir, SESSION_REF_FILENAME);
|
|
78
|
+
await fs.rm(refPath, { force: true });
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore errors
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { registerCheckCommand } from "./check.js";
|
|
4
|
-
|
|
5
|
-
describe("Check Command", () => {
|
|
6
|
-
let program: Command;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
program = new Command();
|
|
10
|
-
registerCheckCommand(program);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("should register the check command", () => {
|
|
14
|
-
const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
|
|
15
|
-
expect(checkCmd).toBeDefined();
|
|
16
|
-
expect(checkCmd?.description()).toBe(
|
|
17
|
-
"Run only applicable checks for detected changes",
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("should have correct options", () => {
|
|
22
|
-
const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
|
|
23
|
-
expect(checkCmd?.options.some((opt) => opt.long === "--gate")).toBe(true);
|
|
24
|
-
expect(checkCmd?.options.some((opt) => opt.long === "--commit")).toBe(true);
|
|
25
|
-
expect(checkCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
|
|
26
|
-
true,
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { registerDetectCommand } from "./detect.js";
|
|
4
|
-
|
|
5
|
-
describe("Detect Command", () => {
|
|
6
|
-
let program: Command;
|
|
7
|
-
const originalConsoleLog = console.log;
|
|
8
|
-
const originalConsoleError = console.error;
|
|
9
|
-
let logs: string[];
|
|
10
|
-
let errors: string[];
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
program = new Command();
|
|
14
|
-
registerDetectCommand(program);
|
|
15
|
-
logs = [];
|
|
16
|
-
errors = [];
|
|
17
|
-
console.log = (...args: unknown[]) => {
|
|
18
|
-
logs.push(args.join(" "));
|
|
19
|
-
};
|
|
20
|
-
console.error = (...args: unknown[]) => {
|
|
21
|
-
errors.push(args.join(" "));
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
console.log = originalConsoleLog;
|
|
27
|
-
console.error = originalConsoleError;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("should register the detect command", () => {
|
|
31
|
-
const detectCmd = program.commands.find((cmd) => cmd.name() === "detect");
|
|
32
|
-
expect(detectCmd).toBeDefined();
|
|
33
|
-
expect(detectCmd?.description()).toBe(
|
|
34
|
-
"Show what gates would run for detected changes (without executing them)",
|
|
35
|
-
);
|
|
36
|
-
expect(detectCmd?.options.some((opt) => opt.long === "--commit")).toBe(
|
|
37
|
-
true,
|
|
38
|
-
);
|
|
39
|
-
expect(detectCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
|
|
40
|
-
true,
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterAll,
|
|
3
|
-
afterEach,
|
|
4
|
-
beforeAll,
|
|
5
|
-
beforeEach,
|
|
6
|
-
describe,
|
|
7
|
-
expect,
|
|
8
|
-
it,
|
|
9
|
-
} from "bun:test";
|
|
10
|
-
import fs from "node:fs/promises";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { Command } from "commander";
|
|
13
|
-
import { registerHealthCommand } from "./health.js";
|
|
14
|
-
|
|
15
|
-
const TEST_DIR = path.join(process.cwd(), `test-health-${Date.now()}`);
|
|
16
|
-
const GAUNTLET_DIR = path.join(TEST_DIR, ".gauntlet");
|
|
17
|
-
const REVIEWS_DIR = path.join(GAUNTLET_DIR, "reviews");
|
|
18
|
-
|
|
19
|
-
describe("Health Command", () => {
|
|
20
|
-
let program: Command;
|
|
21
|
-
const originalConsoleLog = console.log;
|
|
22
|
-
const originalCwd = process.cwd();
|
|
23
|
-
let logs: string[];
|
|
24
|
-
|
|
25
|
-
beforeAll(async () => {
|
|
26
|
-
// Setup test directory structure
|
|
27
|
-
await fs.mkdir(TEST_DIR, { recursive: true });
|
|
28
|
-
await fs.mkdir(GAUNTLET_DIR, { recursive: true });
|
|
29
|
-
await fs.mkdir(REVIEWS_DIR, { recursive: true });
|
|
30
|
-
|
|
31
|
-
// Write config.yml
|
|
32
|
-
await fs.writeFile(
|
|
33
|
-
path.join(GAUNTLET_DIR, "config.yml"),
|
|
34
|
-
`
|
|
35
|
-
base_branch: origin/main
|
|
36
|
-
log_dir: gauntlet_logs
|
|
37
|
-
cli:
|
|
38
|
-
default_preference:
|
|
39
|
-
- gemini
|
|
40
|
-
check_usage_limit: false
|
|
41
|
-
entry_points:
|
|
42
|
-
- path: .
|
|
43
|
-
`,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// Write review definition with CLI preference
|
|
47
|
-
await fs.writeFile(
|
|
48
|
-
path.join(REVIEWS_DIR, "security.md"),
|
|
49
|
-
`---
|
|
50
|
-
cli_preference:
|
|
51
|
-
- gemini
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
# Security Review
|
|
55
|
-
Review for security.
|
|
56
|
-
`,
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterAll(async () => {
|
|
61
|
-
await fs.rm(TEST_DIR, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
program = new Command();
|
|
66
|
-
registerHealthCommand(program);
|
|
67
|
-
logs = [];
|
|
68
|
-
console.log = (...args: unknown[]) => {
|
|
69
|
-
logs.push(args.join(" "));
|
|
70
|
-
};
|
|
71
|
-
process.chdir(TEST_DIR);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
afterEach(() => {
|
|
75
|
-
console.log = originalConsoleLog;
|
|
76
|
-
process.chdir(originalCwd);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("should register the health command", () => {
|
|
80
|
-
const healthCmd = program.commands.find((cmd) => cmd.name() === "health");
|
|
81
|
-
expect(healthCmd).toBeDefined();
|
|
82
|
-
expect(healthCmd?.description()).toBe("Check CLI tool availability");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("should run health check", async () => {
|
|
86
|
-
const healthCmd = program.commands.find((cmd) => cmd.name() === "health");
|
|
87
|
-
await healthCmd?.parseAsync(["health"]);
|
|
88
|
-
|
|
89
|
-
const output = logs.join("\n");
|
|
90
|
-
expect(output).toContain("Config validation:");
|
|
91
|
-
expect(output).toContain("CLI Tool Health Check:");
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { registerHelpCommand } from "./help.js";
|
|
4
|
-
|
|
5
|
-
describe("Help Command", () => {
|
|
6
|
-
let program: Command;
|
|
7
|
-
const originalConsoleLog = console.log;
|
|
8
|
-
let logs: string[];
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
program = new Command();
|
|
12
|
-
registerHelpCommand(program);
|
|
13
|
-
logs = [];
|
|
14
|
-
console.log = (...args: unknown[]) => {
|
|
15
|
-
logs.push(args.join(" "));
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
console.log = originalConsoleLog;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("should register the help command", () => {
|
|
24
|
-
const helpCmd = program.commands.find((cmd) => cmd.name() === "help");
|
|
25
|
-
expect(helpCmd).toBeDefined();
|
|
26
|
-
expect(helpCmd?.description()).toBe("Show help information");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should output help information when executed", async () => {
|
|
30
|
-
const helpCmd = program.commands.find((cmd) => cmd.name() === "help");
|
|
31
|
-
await helpCmd?.parseAsync(["help"]);
|
|
32
|
-
|
|
33
|
-
const output = logs.join("\n");
|
|
34
|
-
expect(output).toContain("Agent Gauntlet");
|
|
35
|
-
expect(output).toContain("Commands:");
|
|
36
|
-
expect(output).toContain("run");
|
|
37
|
-
expect(output).toContain("check");
|
|
38
|
-
expect(output).toContain("review");
|
|
39
|
-
expect(output).toContain("detect");
|
|
40
|
-
expect(output).toContain("list");
|
|
41
|
-
expect(output).toContain("health");
|
|
42
|
-
expect(output).toContain("init");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterAll,
|
|
3
|
-
afterEach,
|
|
4
|
-
beforeAll,
|
|
5
|
-
beforeEach,
|
|
6
|
-
describe,
|
|
7
|
-
expect,
|
|
8
|
-
it,
|
|
9
|
-
mock,
|
|
10
|
-
} from "bun:test";
|
|
11
|
-
import fs from "node:fs/promises";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { Command } from "commander";
|
|
14
|
-
|
|
15
|
-
const TEST_DIR = path.join(process.cwd(), `test-init-${Date.now()}`);
|
|
16
|
-
|
|
17
|
-
// Mock adapters
|
|
18
|
-
const mockAdapters = [
|
|
19
|
-
{
|
|
20
|
-
name: "mock-cli-1",
|
|
21
|
-
isAvailable: async () => true,
|
|
22
|
-
getProjectCommandDir: () => ".mock1",
|
|
23
|
-
getUserCommandDir: () => null,
|
|
24
|
-
getCommandExtension: () => ".sh",
|
|
25
|
-
canUseSymlink: () => false,
|
|
26
|
-
transformCommand: (content: string) => content,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: "mock-cli-2",
|
|
30
|
-
isAvailable: async () => false, // Not available
|
|
31
|
-
getProjectCommandDir: () => ".mock2",
|
|
32
|
-
getUserCommandDir: () => null,
|
|
33
|
-
getCommandExtension: () => ".sh",
|
|
34
|
-
canUseSymlink: () => false,
|
|
35
|
-
transformCommand: (content: string) => content,
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
mock.module("../cli-adapters/index.js", () => ({
|
|
40
|
-
getAllAdapters: () => mockAdapters,
|
|
41
|
-
getProjectCommandAdapters: () => mockAdapters,
|
|
42
|
-
getUserCommandAdapters: () => [],
|
|
43
|
-
getAdapter: (name: string) => mockAdapters.find((a) => a.name === name),
|
|
44
|
-
getValidCLITools: () => mockAdapters.map((a) => a.name),
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
// Import after mocking
|
|
48
|
-
const { registerInitCommand } = await import("./init.js");
|
|
49
|
-
|
|
50
|
-
describe("Init Command", () => {
|
|
51
|
-
let program: Command;
|
|
52
|
-
const originalConsoleLog = console.log;
|
|
53
|
-
const originalCwd = process.cwd();
|
|
54
|
-
let logs: string[];
|
|
55
|
-
|
|
56
|
-
beforeAll(async () => {
|
|
57
|
-
await fs.mkdir(TEST_DIR, { recursive: true });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterAll(async () => {
|
|
61
|
-
await fs.rm(TEST_DIR, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
program = new Command();
|
|
66
|
-
registerInitCommand(program);
|
|
67
|
-
logs = [];
|
|
68
|
-
console.log = (...args: unknown[]) => {
|
|
69
|
-
logs.push(args.join(" "));
|
|
70
|
-
};
|
|
71
|
-
process.chdir(TEST_DIR);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
afterEach(() => {
|
|
75
|
-
console.log = originalConsoleLog;
|
|
76
|
-
process.chdir(originalCwd);
|
|
77
|
-
// Cleanup any created .gauntlet directory
|
|
78
|
-
return fs
|
|
79
|
-
.rm(path.join(TEST_DIR, ".gauntlet"), { recursive: true, force: true })
|
|
80
|
-
.catch(() => {});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("should register the init command", () => {
|
|
84
|
-
const initCmd = program.commands.find((cmd) => cmd.name() === "init");
|
|
85
|
-
expect(initCmd).toBeDefined();
|
|
86
|
-
expect(initCmd?.description()).toBe("Initialize .gauntlet configuration");
|
|
87
|
-
expect(initCmd?.options.some((opt) => opt.long === "--yes")).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should create .gauntlet directory structure with --yes flag", async () => {
|
|
91
|
-
// We expect it to use the available mock-cli-1
|
|
92
|
-
await program.parseAsync(["node", "test", "init", "--yes"]);
|
|
93
|
-
|
|
94
|
-
// Check that files were created
|
|
95
|
-
const gauntletDir = path.join(TEST_DIR, ".gauntlet");
|
|
96
|
-
const configFile = path.join(gauntletDir, "config.yml");
|
|
97
|
-
const reviewsDir = path.join(gauntletDir, "reviews");
|
|
98
|
-
const checksDir = path.join(gauntletDir, "checks");
|
|
99
|
-
const runGauntletFile = path.join(gauntletDir, "run_gauntlet.md");
|
|
100
|
-
|
|
101
|
-
expect(await fs.stat(gauntletDir)).toBeDefined();
|
|
102
|
-
expect(await fs.stat(configFile)).toBeDefined();
|
|
103
|
-
expect(await fs.stat(reviewsDir)).toBeDefined();
|
|
104
|
-
expect(await fs.stat(checksDir)).toBeDefined();
|
|
105
|
-
expect(await fs.stat(runGauntletFile)).toBeDefined();
|
|
106
|
-
|
|
107
|
-
// Verify config content
|
|
108
|
-
const configContent = await fs.readFile(configFile, "utf-8");
|
|
109
|
-
expect(configContent).toContain("base_branch");
|
|
110
|
-
expect(configContent).toContain("log_dir");
|
|
111
|
-
expect(configContent).toContain("mock-cli-1"); // Should be present
|
|
112
|
-
expect(configContent).not.toContain("mock-cli-2"); // Should not be present (unavailable)
|
|
113
|
-
|
|
114
|
-
// Verify review file content
|
|
115
|
-
const reviewFile = path.join(reviewsDir, "code-quality.md");
|
|
116
|
-
const reviewContent = await fs.readFile(reviewFile, "utf-8");
|
|
117
|
-
expect(reviewContent).toContain("mock-cli-1");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("should not create directory if .gauntlet already exists", async () => {
|
|
121
|
-
// Create .gauntlet directory first
|
|
122
|
-
const gauntletDir = path.join(TEST_DIR, ".gauntlet");
|
|
123
|
-
await fs.mkdir(gauntletDir, { recursive: true });
|
|
124
|
-
|
|
125
|
-
await program.parseAsync(["node", "test", "init", "--yes"]);
|
|
126
|
-
|
|
127
|
-
const output = logs.join("\n");
|
|
128
|
-
expect(output).toContain(".gauntlet directory already exists");
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterAll,
|
|
3
|
-
afterEach,
|
|
4
|
-
beforeAll,
|
|
5
|
-
beforeEach,
|
|
6
|
-
describe,
|
|
7
|
-
expect,
|
|
8
|
-
it,
|
|
9
|
-
} from "bun:test";
|
|
10
|
-
import fs from "node:fs/promises";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { Command } from "commander";
|
|
13
|
-
import { registerListCommand } from "./list.js";
|
|
14
|
-
|
|
15
|
-
const TEST_DIR = path.join(process.cwd(), `test-list-${Date.now()}`);
|
|
16
|
-
const GAUNTLET_DIR = path.join(TEST_DIR, ".gauntlet");
|
|
17
|
-
const CHECKS_DIR = path.join(GAUNTLET_DIR, "checks");
|
|
18
|
-
const REVIEWS_DIR = path.join(GAUNTLET_DIR, "reviews");
|
|
19
|
-
|
|
20
|
-
describe("List Command", () => {
|
|
21
|
-
let program: Command;
|
|
22
|
-
const originalConsoleLog = console.log;
|
|
23
|
-
const originalConsoleError = console.error;
|
|
24
|
-
const originalCwd = process.cwd();
|
|
25
|
-
let logs: string[];
|
|
26
|
-
let errors: string[];
|
|
27
|
-
|
|
28
|
-
beforeAll(async () => {
|
|
29
|
-
// Setup test directory structure
|
|
30
|
-
await fs.mkdir(TEST_DIR, { recursive: true });
|
|
31
|
-
await fs.mkdir(GAUNTLET_DIR, { recursive: true });
|
|
32
|
-
await fs.mkdir(CHECKS_DIR, { recursive: true });
|
|
33
|
-
await fs.mkdir(REVIEWS_DIR, { recursive: true });
|
|
34
|
-
|
|
35
|
-
// Write config.yml
|
|
36
|
-
await fs.writeFile(
|
|
37
|
-
path.join(GAUNTLET_DIR, "config.yml"),
|
|
38
|
-
`
|
|
39
|
-
base_branch: origin/main
|
|
40
|
-
log_dir: gauntlet_logs
|
|
41
|
-
cli:
|
|
42
|
-
default_preference:
|
|
43
|
-
- gemini
|
|
44
|
-
check_usage_limit: false
|
|
45
|
-
entry_points:
|
|
46
|
-
- path: src/
|
|
47
|
-
checks:
|
|
48
|
-
- lint
|
|
49
|
-
reviews:
|
|
50
|
-
- security
|
|
51
|
-
`,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
// Write check definition
|
|
55
|
-
await fs.writeFile(
|
|
56
|
-
path.join(CHECKS_DIR, "lint.yml"),
|
|
57
|
-
`
|
|
58
|
-
name: lint
|
|
59
|
-
command: npm run lint
|
|
60
|
-
working_directory: .
|
|
61
|
-
`,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Write review definition
|
|
65
|
-
await fs.writeFile(
|
|
66
|
-
path.join(REVIEWS_DIR, "security.md"),
|
|
67
|
-
`---
|
|
68
|
-
cli_preference:
|
|
69
|
-
- gemini
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
# Security Review
|
|
73
|
-
Review for security.
|
|
74
|
-
`,
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterAll(async () => {
|
|
79
|
-
await fs.rm(TEST_DIR, { recursive: true, force: true });
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
program = new Command();
|
|
84
|
-
registerListCommand(program);
|
|
85
|
-
logs = [];
|
|
86
|
-
errors = [];
|
|
87
|
-
console.log = (...args: unknown[]) => {
|
|
88
|
-
logs.push(args.join(" "));
|
|
89
|
-
};
|
|
90
|
-
console.error = (...args: unknown[]) => {
|
|
91
|
-
errors.push(args.join(" "));
|
|
92
|
-
};
|
|
93
|
-
process.chdir(TEST_DIR);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
afterEach(() => {
|
|
97
|
-
console.log = originalConsoleLog;
|
|
98
|
-
console.error = originalConsoleError;
|
|
99
|
-
process.chdir(originalCwd);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("should register the list command", () => {
|
|
103
|
-
const listCmd = program.commands.find((cmd) => cmd.name() === "list");
|
|
104
|
-
expect(listCmd).toBeDefined();
|
|
105
|
-
expect(listCmd?.description()).toBe("List configured gates");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("should list check gates, review gates, and entry points", async () => {
|
|
109
|
-
const listCmd = program.commands.find((cmd) => cmd.name() === "list");
|
|
110
|
-
await listCmd?.parseAsync(["list"]);
|
|
111
|
-
|
|
112
|
-
const output = logs.join("\n");
|
|
113
|
-
expect(output).toContain("Check Gates:");
|
|
114
|
-
expect(output).toContain("lint");
|
|
115
|
-
expect(output).toContain("Review Gates:");
|
|
116
|
-
expect(output).toContain("security");
|
|
117
|
-
expect(output).toContain("gemini");
|
|
118
|
-
expect(output).toContain("Entry Points:");
|
|
119
|
-
expect(output).toContain("src/");
|
|
120
|
-
});
|
|
121
|
-
});
|
package/src/commands/rerun.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { loadConfig } from "../config/loader.js";
|
|
4
|
-
import { ChangeDetector } from "../core/change-detector.js";
|
|
5
|
-
import { EntryPointExpander } from "../core/entry-point.js";
|
|
6
|
-
import { JobGenerator } from "../core/job.js";
|
|
7
|
-
import { Runner } from "../core/runner.js";
|
|
8
|
-
import { ConsoleReporter } from "../output/console.js";
|
|
9
|
-
import { Logger } from "../output/logger.js";
|
|
10
|
-
import {
|
|
11
|
-
findPreviousFailures,
|
|
12
|
-
type PreviousViolation,
|
|
13
|
-
} from "../utils/log-parser.js";
|
|
14
|
-
import { rotateLogs } from "./shared.js";
|
|
15
|
-
|
|
16
|
-
export function registerRerunCommand(program: Command): void {
|
|
17
|
-
program
|
|
18
|
-
.command("rerun")
|
|
19
|
-
.description(
|
|
20
|
-
"Rerun gates (checks & reviews) with previous failures as context (defaults to uncommitted changes)",
|
|
21
|
-
)
|
|
22
|
-
.option(
|
|
23
|
-
"-b, --base-branch <branch>",
|
|
24
|
-
"Override base branch for change detection",
|
|
25
|
-
)
|
|
26
|
-
.option("-g, --gate <name>", "Run specific gate only")
|
|
27
|
-
.option(
|
|
28
|
-
"-c, --commit <sha>",
|
|
29
|
-
"Use diff for a specific commit (overrides default uncommitted mode)",
|
|
30
|
-
)
|
|
31
|
-
.action(async (options) => {
|
|
32
|
-
try {
|
|
33
|
-
const config = await loadConfig();
|
|
34
|
-
|
|
35
|
-
// Parse previous failures from log files (only for review gates)
|
|
36
|
-
console.log(chalk.dim("Analyzing previous runs..."));
|
|
37
|
-
|
|
38
|
-
// findPreviousFailures handles errors internally and returns empty array on failure
|
|
39
|
-
const previousFailures = await findPreviousFailures(
|
|
40
|
-
config.project.log_dir,
|
|
41
|
-
options.gate,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Create a map: jobId -> (adapterName -> violations)
|
|
45
|
-
const failuresMap = new Map<string, Map<string, PreviousViolation[]>>();
|
|
46
|
-
for (const gateFailure of previousFailures) {
|
|
47
|
-
const adapterMap = new Map<string, PreviousViolation[]>();
|
|
48
|
-
for (const adapterFailure of gateFailure.adapterFailures) {
|
|
49
|
-
adapterMap.set(
|
|
50
|
-
adapterFailure.adapterName,
|
|
51
|
-
adapterFailure.violations,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
failuresMap.set(gateFailure.jobId, adapterMap);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (previousFailures.length > 0) {
|
|
58
|
-
const totalViolations = previousFailures.reduce(
|
|
59
|
-
(sum, gf) =>
|
|
60
|
-
sum +
|
|
61
|
-
gf.adapterFailures.reduce((s, af) => s + af.violations.length, 0),
|
|
62
|
-
0,
|
|
63
|
-
);
|
|
64
|
-
console.log(
|
|
65
|
-
chalk.yellow(
|
|
66
|
-
`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
} else {
|
|
70
|
-
console.log(
|
|
71
|
-
chalk.dim("No previous failures found. Running as normal..."),
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Rotate logs before starting the new run
|
|
76
|
-
await rotateLogs(config.project.log_dir);
|
|
77
|
-
|
|
78
|
-
// Determine effective base branch
|
|
79
|
-
// Priority: CLI override > CI env var > config
|
|
80
|
-
const effectiveBaseBranch =
|
|
81
|
-
options.baseBranch ||
|
|
82
|
-
(process.env.GITHUB_BASE_REF &&
|
|
83
|
-
(process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
|
|
84
|
-
? process.env.GITHUB_BASE_REF
|
|
85
|
-
: null) ||
|
|
86
|
-
config.project.base_branch;
|
|
87
|
-
|
|
88
|
-
// Detect changes (default to uncommitted unless --commit is specified)
|
|
89
|
-
// Note: Rerun defaults to uncommitted changes for faster iteration loops,
|
|
90
|
-
// unlike 'run' which defaults to base_branch comparison.
|
|
91
|
-
const changeOptions = {
|
|
92
|
-
commit: options.commit,
|
|
93
|
-
uncommitted: !options.commit, // Default to uncommitted unless commit is specified
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const changeDetector = new ChangeDetector(
|
|
97
|
-
effectiveBaseBranch,
|
|
98
|
-
changeOptions,
|
|
99
|
-
);
|
|
100
|
-
const expander = new EntryPointExpander();
|
|
101
|
-
const jobGen = new JobGenerator(config);
|
|
102
|
-
|
|
103
|
-
const modeDesc = options.commit
|
|
104
|
-
? `commit ${options.commit}`
|
|
105
|
-
: "uncommitted changes";
|
|
106
|
-
console.log(chalk.dim(`Detecting changes (${modeDesc})...`));
|
|
107
|
-
|
|
108
|
-
const changes = await changeDetector.getChangedFiles();
|
|
109
|
-
|
|
110
|
-
if (changes.length === 0) {
|
|
111
|
-
console.log(chalk.green("No changes detected."));
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
116
|
-
|
|
117
|
-
const entryPoints = await expander.expand(
|
|
118
|
-
config.project.entry_points,
|
|
119
|
-
changes,
|
|
120
|
-
);
|
|
121
|
-
let jobs = jobGen.generateJobs(entryPoints);
|
|
122
|
-
|
|
123
|
-
if (options.gate) {
|
|
124
|
-
jobs = jobs.filter((j) => j.name === options.gate);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (jobs.length === 0) {
|
|
128
|
-
console.log(chalk.yellow("No applicable gates for these changes."));
|
|
129
|
-
process.exit(0);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
console.log(chalk.dim(`Running ${jobs.length} gates...`));
|
|
133
|
-
if (previousFailures.length > 0) {
|
|
134
|
-
console.log(
|
|
135
|
-
chalk.dim(
|
|
136
|
-
"Previous failures will be injected as context for matching reviewers.",
|
|
137
|
-
),
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const logger = new Logger(config.project.log_dir);
|
|
142
|
-
const reporter = new ConsoleReporter();
|
|
143
|
-
const runner = new Runner(
|
|
144
|
-
config,
|
|
145
|
-
logger,
|
|
146
|
-
reporter,
|
|
147
|
-
failuresMap, // Pass previous failures map
|
|
148
|
-
changeOptions, // Pass change detection options
|
|
149
|
-
effectiveBaseBranch, // Pass effective base branch
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const success = await runner.run(jobs);
|
|
153
|
-
process.exit(success ? 0 : 1);
|
|
154
|
-
} catch (error: unknown) {
|
|
155
|
-
const err = error as { message?: string };
|
|
156
|
-
console.error(chalk.red("Error:"), err.message);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|