agent-gauntlet 0.1.10 → 0.1.11
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 +1 -1
- package/package.json +4 -2
- package/src/cli-adapters/claude.ts +139 -108
- package/src/cli-adapters/codex.ts +141 -117
- package/src/cli-adapters/cursor.ts +152 -0
- package/src/cli-adapters/gemini.ts +171 -139
- package/src/cli-adapters/github-copilot.ts +153 -0
- package/src/cli-adapters/index.ts +77 -48
- package/src/commands/check.test.ts +24 -20
- package/src/commands/check.ts +65 -59
- package/src/commands/detect.test.ts +38 -32
- package/src/commands/detect.ts +74 -61
- package/src/commands/health.test.ts +67 -53
- package/src/commands/health.ts +167 -145
- package/src/commands/help.test.ts +37 -37
- package/src/commands/help.ts +30 -22
- package/src/commands/index.ts +9 -9
- package/src/commands/init.test.ts +118 -107
- package/src/commands/init.ts +514 -417
- package/src/commands/list.test.ts +87 -70
- package/src/commands/list.ts +28 -24
- package/src/commands/rerun.ts +142 -119
- package/src/commands/review.test.ts +26 -20
- package/src/commands/review.ts +65 -59
- package/src/commands/run.test.ts +22 -20
- package/src/commands/run.ts +64 -58
- package/src/commands/shared.ts +44 -35
- package/src/config/loader.test.ts +112 -90
- package/src/config/loader.ts +132 -123
- package/src/config/schema.ts +49 -47
- package/src/config/types.ts +15 -13
- package/src/config/validator.ts +521 -454
- package/src/core/change-detector.ts +122 -104
- package/src/core/entry-point.test.ts +60 -62
- package/src/core/entry-point.ts +76 -67
- package/src/core/job.ts +69 -59
- package/src/core/runner.ts +261 -230
- package/src/gates/check.ts +78 -69
- package/src/gates/result.ts +7 -7
- package/src/gates/review.test.ts +174 -138
- package/src/gates/review.ts +716 -561
- package/src/index.ts +16 -15
- package/src/output/console.ts +253 -214
- package/src/output/logger.ts +64 -52
- package/src/templates/run_gauntlet.template.md +18 -0
- package/src/utils/diff-parser.ts +64 -62
- package/src/utils/log-parser.ts +227 -206
- package/src/utils/sanitizer.ts +1 -1
package/src/commands/review.ts
CHANGED
|
@@ -1,72 +1,78 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { loadConfig } from
|
|
4
|
-
import { ChangeDetector } from
|
|
5
|
-
import { EntryPointExpander } from
|
|
6
|
-
import { JobGenerator } from
|
|
7
|
-
import { Runner } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { rotateLogs } from
|
|
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 { rotateLogs } from "./shared.js";
|
|
11
11
|
|
|
12
12
|
export function registerReviewCommand(program: Command): void {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
program
|
|
14
|
+
.command("review")
|
|
15
|
+
.description("Run only applicable reviews for detected changes")
|
|
16
|
+
.option("-g, --gate <name>", "Run specific review gate only")
|
|
17
|
+
.option("-c, --commit <sha>", "Use diff for a specific commit")
|
|
18
|
+
.option(
|
|
19
|
+
"-u, --uncommitted",
|
|
20
|
+
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
21
|
+
)
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
const config = await loadConfig();
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// Rotate logs before starting
|
|
27
|
+
await rotateLogs(config.project.log_dir);
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.log(chalk.dim('Detecting changes...'));
|
|
34
|
-
const changes = await changeDetector.getChangedFiles();
|
|
35
|
-
|
|
36
|
-
if (changes.length === 0) {
|
|
37
|
-
console.log(chalk.green('No changes detected.'));
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
29
|
+
const changeDetector = new ChangeDetector(config.project.base_branch, {
|
|
30
|
+
commit: options.commit,
|
|
31
|
+
uncommitted: options.uncommitted,
|
|
32
|
+
});
|
|
33
|
+
const expander = new EntryPointExpander();
|
|
34
|
+
const jobGen = new JobGenerator(config);
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
console.log(chalk.dim("Detecting changes..."));
|
|
37
|
+
const changes = await changeDetector.getChangedFiles();
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
if (changes.length === 0) {
|
|
40
|
+
console.log(chalk.green("No changes detected."));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
jobs = jobs.filter(j => j.name === options.gate);
|
|
51
|
-
}
|
|
44
|
+
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
const entryPoints = await expander.expand(
|
|
47
|
+
config.project.entry_points,
|
|
48
|
+
changes,
|
|
49
|
+
);
|
|
50
|
+
let jobs = jobGen.generateJobs(entryPoints);
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
// Filter to only reviews
|
|
53
|
+
jobs = jobs.filter((j) => j.type === "review");
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
if (options.gate) {
|
|
56
|
+
jobs = jobs.filter((j) => j.name === options.gate);
|
|
57
|
+
}
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
if (jobs.length === 0) {
|
|
60
|
+
console.log(chalk.yellow("No applicable reviews for these changes."));
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
console.log(chalk.dim(`Running ${jobs.length} review(s)...`));
|
|
65
|
+
|
|
66
|
+
const logger = new Logger(config.project.log_dir);
|
|
67
|
+
const reporter = new ConsoleReporter();
|
|
68
|
+
const runner = new Runner(config, logger, reporter);
|
|
69
|
+
|
|
70
|
+
const success = await runner.run(jobs);
|
|
71
|
+
process.exit(success ? 0 : 1);
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
const err = error as { message?: string };
|
|
74
|
+
console.error(chalk.red("Error:"), err.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
72
78
|
}
|
package/src/commands/run.test.ts
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { registerRunCommand } from
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerRunCommand } from "./run.js";
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
|
|
5
|
+
describe("Run Command", () => {
|
|
6
|
+
let program: Command;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
program = new Command();
|
|
10
|
+
registerRunCommand(program);
|
|
11
|
+
});
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
it("should register the run command", () => {
|
|
14
|
+
const runCmd = program.commands.find((cmd) => cmd.name() === "run");
|
|
15
|
+
expect(runCmd).toBeDefined();
|
|
16
|
+
expect(runCmd?.description()).toBe("Run gates for detected changes");
|
|
17
|
+
});
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
it("should have correct options", () => {
|
|
20
|
+
const runCmd = program.commands.find((cmd) => cmd.name() === "run");
|
|
21
|
+
expect(runCmd?.options.some((opt) => opt.long === "--gate")).toBe(true);
|
|
22
|
+
expect(runCmd?.options.some((opt) => opt.long === "--commit")).toBe(true);
|
|
23
|
+
expect(runCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
|
|
24
|
+
true,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
25
27
|
});
|
package/src/commands/run.ts
CHANGED
|
@@ -1,69 +1,75 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { loadConfig } from
|
|
4
|
-
import { ChangeDetector } from
|
|
5
|
-
import { EntryPointExpander } from
|
|
6
|
-
import { JobGenerator } from
|
|
7
|
-
import { Runner } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { rotateLogs } from
|
|
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 { rotateLogs } from "./shared.js";
|
|
11
11
|
|
|
12
12
|
export function registerRunCommand(program: Command): void {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
program
|
|
14
|
+
.command("run")
|
|
15
|
+
.description("Run gates for detected changes")
|
|
16
|
+
.option("-g, --gate <name>", "Run specific gate only")
|
|
17
|
+
.option("-c, --commit <sha>", "Use diff for a specific commit")
|
|
18
|
+
.option(
|
|
19
|
+
"-u, --uncommitted",
|
|
20
|
+
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
21
|
+
)
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
const config = await loadConfig();
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
uncommitted: options.uncommitted
|
|
29
|
-
});
|
|
30
|
-
const expander = new EntryPointExpander();
|
|
31
|
-
const jobGen = new JobGenerator(config);
|
|
32
|
-
|
|
33
|
-
console.log(chalk.dim('Detecting changes...'));
|
|
34
|
-
const changes = await changeDetector.getChangedFiles();
|
|
35
|
-
|
|
36
|
-
if (changes.length === 0) {
|
|
37
|
-
console.log(chalk.green('No changes detected.'));
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
26
|
+
// Rotate logs before starting
|
|
27
|
+
await rotateLogs(config.project.log_dir);
|
|
42
28
|
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
const changeDetector = new ChangeDetector(config.project.base_branch, {
|
|
30
|
+
commit: options.commit,
|
|
31
|
+
uncommitted: options.uncommitted,
|
|
32
|
+
});
|
|
33
|
+
const expander = new EntryPointExpander();
|
|
34
|
+
const jobGen = new JobGenerator(config);
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
36
|
+
console.log(chalk.dim("Detecting changes..."));
|
|
37
|
+
const changes = await changeDetector.getChangedFiles();
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
if (changes.length === 0) {
|
|
40
|
+
console.log(chalk.green("No changes detected."));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
const entryPoints = await expander.expand(
|
|
47
|
+
config.project.entry_points,
|
|
48
|
+
changes,
|
|
49
|
+
);
|
|
50
|
+
let jobs = jobGen.generateJobs(entryPoints);
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
if (options.gate) {
|
|
53
|
+
jobs = jobs.filter((j) => j.name === options.gate);
|
|
54
|
+
}
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
if (jobs.length === 0) {
|
|
57
|
+
console.log(chalk.yellow("No applicable gates for these changes."));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.dim(`Running ${jobs.length} gates...`));
|
|
62
|
+
|
|
63
|
+
const logger = new Logger(config.project.log_dir);
|
|
64
|
+
const reporter = new ConsoleReporter();
|
|
65
|
+
const runner = new Runner(config, logger, reporter);
|
|
66
|
+
|
|
67
|
+
const success = await runner.run(jobs);
|
|
68
|
+
process.exit(success ? 0 : 1);
|
|
69
|
+
} catch (error: unknown) {
|
|
70
|
+
const err = error as { message?: string };
|
|
71
|
+
console.error(chalk.red("Error:"), err.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
69
75
|
}
|
package/src/commands/shared.ts
CHANGED
|
@@ -1,44 +1,53 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export async function exists(path: string): Promise<boolean> {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
try {
|
|
6
|
+
await fs.stat(path);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export async function rotateLogs(logDir: string): Promise<void> {
|
|
14
|
-
|
|
14
|
+
const previousDir = path.join(logDir, "previous");
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
try {
|
|
17
|
+
// 1. Ensure logDir exists (if not, nothing to rotate, but we should create it for future use if needed,
|
|
18
|
+
// though usually the logger creates it. If it doesn't exist, we can just return).
|
|
19
|
+
if (!(await exists(logDir))) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
// 2. Clear .gauntlet_logs/previous if it exists
|
|
24
|
+
if (await exists(previousDir)) {
|
|
25
|
+
const previousFiles = await fs.readdir(previousDir);
|
|
26
|
+
await Promise.all(
|
|
27
|
+
previousFiles.map((file) =>
|
|
28
|
+
fs.rm(path.join(previousDir, file), { recursive: true, force: true }),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
} else {
|
|
32
|
+
await fs.mkdir(previousDir, { recursive: true });
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// 3. Move all existing files in .gauntlet_logs/ to .gauntlet_logs/previous
|
|
36
|
+
const files = await fs.readdir(logDir);
|
|
37
|
+
await Promise.all(
|
|
38
|
+
files
|
|
39
|
+
.filter((file) => file !== "previous")
|
|
40
|
+
.map((file) =>
|
|
41
|
+
fs.rename(path.join(logDir, file), path.join(previousDir, file)),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// Log warning but don't crash the run as log rotation failure isn't critical
|
|
46
|
+
console.warn(
|
|
47
|
+
"Failed to rotate logs in",
|
|
48
|
+
logDir,
|
|
49
|
+
":",
|
|
50
|
+
error instanceof Error ? error.message : error,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
44
53
|
}
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import fs from
|
|
3
|
-
import path from
|
|
4
|
-
import { loadConfig } from
|
|
5
|
-
|
|
6
|
-
const TEST_DIR = path.join(process.cwd(),
|
|
7
|
-
const GAUNTLET_DIR = path.join(TEST_DIR,
|
|
8
|
-
const CHECKS_DIR = path.join(GAUNTLET_DIR,
|
|
9
|
-
const REVIEWS_DIR = path.join(GAUNTLET_DIR,
|
|
10
|
-
|
|
11
|
-
describe(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadConfig } from "./loader.js";
|
|
5
|
+
|
|
6
|
+
const TEST_DIR = path.join(process.cwd(), `test-env-${Date.now()}`);
|
|
7
|
+
const GAUNTLET_DIR = path.join(TEST_DIR, ".gauntlet");
|
|
8
|
+
const CHECKS_DIR = path.join(GAUNTLET_DIR, "checks");
|
|
9
|
+
const REVIEWS_DIR = path.join(GAUNTLET_DIR, "reviews");
|
|
10
|
+
|
|
11
|
+
describe("Config Loader", () => {
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
// Setup directory structure
|
|
14
|
+
await fs.mkdir(TEST_DIR);
|
|
15
|
+
await fs.mkdir(GAUNTLET_DIR);
|
|
16
|
+
await fs.mkdir(CHECKS_DIR);
|
|
17
|
+
await fs.mkdir(REVIEWS_DIR);
|
|
18
|
+
|
|
19
|
+
// Write config.yml
|
|
20
|
+
await fs.writeFile(
|
|
21
|
+
path.join(GAUNTLET_DIR, "config.yml"),
|
|
22
|
+
`
|
|
21
23
|
base_branch: origin/dev
|
|
22
24
|
log_dir: test_logs
|
|
23
25
|
cli:
|
|
@@ -31,99 +33,119 @@ entry_points:
|
|
|
31
33
|
- lint
|
|
32
34
|
reviews:
|
|
33
35
|
- security
|
|
34
|
-
|
|
36
|
+
`,
|
|
37
|
+
);
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
// Write a check definition
|
|
40
|
+
await fs.writeFile(
|
|
41
|
+
path.join(CHECKS_DIR, "lint.yml"),
|
|
42
|
+
`
|
|
38
43
|
name: lint
|
|
39
44
|
command: npm run lint
|
|
40
45
|
working_directory: .
|
|
41
|
-
|
|
46
|
+
`,
|
|
47
|
+
);
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
// Write a review definition
|
|
50
|
+
await fs.writeFile(
|
|
51
|
+
path.join(REVIEWS_DIR, "security.md"),
|
|
52
|
+
`---
|
|
45
53
|
cli_preference:
|
|
46
54
|
- gemini
|
|
47
55
|
---
|
|
48
56
|
|
|
49
57
|
# Security Review
|
|
50
58
|
Check for vulnerabilities.
|
|
51
|
-
|
|
59
|
+
`,
|
|
60
|
+
);
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
// Write a review definition without preference
|
|
63
|
+
await fs.writeFile(
|
|
64
|
+
path.join(REVIEWS_DIR, "style.md"),
|
|
65
|
+
`---
|
|
55
66
|
num_reviews: 1
|
|
56
67
|
---
|
|
57
68
|
|
|
58
69
|
# Style Review
|
|
59
70
|
Check style.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
71
|
+
`,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
// Cleanup
|
|
77
|
+
await fs.rm(TEST_DIR, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should load project configuration correctly", async () => {
|
|
81
|
+
const config = await loadConfig(TEST_DIR);
|
|
82
|
+
|
|
83
|
+
expect(config.project.base_branch).toBe("origin/dev");
|
|
84
|
+
expect(config.project.log_dir).toBe("test_logs");
|
|
85
|
+
expect(config.project.entry_points).toHaveLength(1);
|
|
86
|
+
expect(config.project.entry_points[0].path).toBe("src/");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should load check gates correctly", async () => {
|
|
90
|
+
const config = await loadConfig(TEST_DIR);
|
|
91
|
+
|
|
92
|
+
expect(Object.keys(config.checks)).toContain("lint");
|
|
93
|
+
expect(config.checks.lint.command).toBe("npm run lint");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should load review gates correctly", async () => {
|
|
97
|
+
const config = await loadConfig(TEST_DIR);
|
|
98
|
+
|
|
99
|
+
expect(Object.keys(config.reviews)).toContain("security");
|
|
100
|
+
expect(config.reviews.security.name).toBe("security");
|
|
101
|
+
expect(config.reviews.security.cli_preference).toEqual(["gemini"]);
|
|
102
|
+
expect(config.reviews.security.promptContent).toContain(
|
|
103
|
+
"Check for vulnerabilities.",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should merge default cli preference", async () => {
|
|
108
|
+
const config = await loadConfig(TEST_DIR);
|
|
109
|
+
|
|
110
|
+
expect(Object.keys(config.reviews)).toContain("style");
|
|
111
|
+
expect(config.reviews.style.cli_preference).toEqual(["claude", "gemini"]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should reject check gate with fail_fast when parallel is true", async () => {
|
|
115
|
+
await fs.writeFile(
|
|
116
|
+
path.join(CHECKS_DIR, "invalid.yml"),
|
|
117
|
+
`
|
|
102
118
|
name: invalid
|
|
103
119
|
command: echo test
|
|
104
120
|
parallel: true
|
|
105
121
|
fail_fast: true
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await expect(loadConfig(TEST_DIR)).rejects.toThrow(
|
|
126
|
+
/fail_fast can only be used when parallel is false/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should accept check gate with fail_fast when parallel is false", async () => {
|
|
131
|
+
// Clean up the invalid file first
|
|
132
|
+
try {
|
|
133
|
+
await fs.unlink(path.join(CHECKS_DIR, "invalid.yml"));
|
|
134
|
+
} catch {}
|
|
135
|
+
|
|
136
|
+
await fs.writeFile(
|
|
137
|
+
path.join(CHECKS_DIR, "valid.yml"),
|
|
138
|
+
`
|
|
118
139
|
name: valid
|
|
119
140
|
command: echo test
|
|
120
141
|
parallel: false
|
|
121
142
|
fail_fast: true
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const config = await loadConfig(TEST_DIR);
|
|
147
|
+
expect(config.checks.valid).toBeDefined();
|
|
148
|
+
expect(config.checks.valid.fail_fast).toBe(true);
|
|
149
|
+
expect(config.checks.valid.parallel).toBe(false);
|
|
150
|
+
});
|
|
129
151
|
});
|