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