claude-auto 0.12.4
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/.claude-auto/.claude.hooks.json +25 -0
- package/.claude-auto/reminders/reminder-auto.md +145 -0
- package/.claude-auto/reminders/reminder-documentation.md +30 -0
- package/.claude-auto/reminders/reminder-emergent-design.md +41 -0
- package/.claude-auto/reminders/reminder-extreme-ownership.md +27 -0
- package/.claude-auto/reminders/reminder-ide-diagnostics.md +25 -0
- package/.claude-auto/reminders/reminder-parallelization.md +27 -0
- package/.claude-auto/reminders/reminder-rethink-after-revert.md +25 -0
- package/.claude-auto/reminders/reminder-sub-agent-rules.md +27 -0
- package/.claude-auto/reminders/reminder-test-title-matches-spec.md +37 -0
- package/.claude-auto/validators/appeal-system.md +55 -0
- package/.claude-auto/validators/backwards-compat.md +33 -0
- package/.claude-auto/validators/burst-atomicity.md +37 -0
- package/.claude-auto/validators/coverage-rules.md +34 -0
- package/.claude-auto/validators/dead-code.md +36 -0
- package/.claude-auto/validators/hygiene.md +34 -0
- package/.claude-auto/validators/infra-commit-format.md +37 -0
- package/.claude-auto/validators/ketchup-plan-format.md +42 -0
- package/.claude-auto/validators/new-code-requires-tests.md +36 -0
- package/.claude-auto/validators/no-comments.md +35 -0
- package/.claude-auto/validators/no-dangerous-git.md +35 -0
- package/.claude-auto/validators/tcr-workflow.md +31 -0
- package/.claude-auto/validators/testing-no-state-peeking.md +37 -0
- package/.claude-auto/validators/testing-structure.md +37 -0
- package/.claude-auto/validators/testing-stubs-over-mocks.md +42 -0
- package/.claude-auto/validators/testing-weak-assertions.md +36 -0
- package/.claude-auto/validators/type-organization.md +30 -0
- package/README.md +172 -0
- package/bin/cli.ts +6 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +7 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bundle/scripts/auto-continue.js +5045 -0
- package/dist/bundle/scripts/pre-tool-use.js +11719 -0
- package/dist/bundle/scripts/session-start.js +8571 -0
- package/dist/bundle/scripts/user-prompt-submit.js +8585 -0
- package/dist/scripts/auto-continue.d.ts +3 -0
- package/dist/scripts/auto-continue.d.ts.map +1 -0
- package/dist/scripts/auto-continue.js +65 -0
- package/dist/scripts/auto-continue.js.map +1 -0
- package/dist/scripts/generate-changeset.d.ts +13 -0
- package/dist/scripts/generate-changeset.d.ts.map +1 -0
- package/dist/scripts/generate-changeset.js +322 -0
- package/dist/scripts/generate-changeset.js.map +1 -0
- package/dist/scripts/pre-tool-use.d.ts +3 -0
- package/dist/scripts/pre-tool-use.d.ts.map +1 -0
- package/dist/scripts/pre-tool-use.js +78 -0
- package/dist/scripts/pre-tool-use.js.map +1 -0
- package/dist/scripts/session-start.d.ts +3 -0
- package/dist/scripts/session-start.d.ts.map +1 -0
- package/dist/scripts/session-start.js +76 -0
- package/dist/scripts/session-start.js.map +1 -0
- package/dist/scripts/user-prompt-submit.d.ts +3 -0
- package/dist/scripts/user-prompt-submit.d.ts.map +1 -0
- package/dist/scripts/user-prompt-submit.js +76 -0
- package/dist/scripts/user-prompt-submit.js.map +1 -0
- package/dist/src/activity-logger.d.ts +2 -0
- package/dist/src/activity-logger.d.ts.map +1 -0
- package/dist/src/activity-logger.js +47 -0
- package/dist/src/activity-logger.js.map +1 -0
- package/dist/src/activity-logger.test.d.ts +2 -0
- package/dist/src/activity-logger.test.d.ts.map +1 -0
- package/dist/src/activity-logger.test.js +121 -0
- package/dist/src/activity-logger.test.js.map +1 -0
- package/dist/src/clean-logs.d.ts +6 -0
- package/dist/src/clean-logs.d.ts.map +1 -0
- package/dist/src/clean-logs.js +38 -0
- package/dist/src/clean-logs.js.map +1 -0
- package/dist/src/clean-logs.test.d.ts +2 -0
- package/dist/src/clean-logs.test.d.ts.map +1 -0
- package/dist/src/clean-logs.test.js +101 -0
- package/dist/src/clean-logs.test.js.map +1 -0
- package/dist/src/cli/cli.d.ts +3 -0
- package/dist/src/cli/cli.d.ts.map +1 -0
- package/dist/src/cli/cli.js +32 -0
- package/dist/src/cli/cli.js.map +1 -0
- package/dist/src/cli/cli.test.d.ts +2 -0
- package/dist/src/cli/cli.test.d.ts.map +1 -0
- package/dist/src/cli/cli.test.js +27 -0
- package/dist/src/cli/cli.test.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +7 -0
- package/dist/src/cli/doctor.d.ts.map +1 -0
- package/dist/src/cli/doctor.js +67 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/doctor.test.d.ts +2 -0
- package/dist/src/cli/doctor.test.d.ts.map +1 -0
- package/dist/src/cli/doctor.test.js +87 -0
- package/dist/src/cli/doctor.test.js.map +1 -0
- package/dist/src/cli/install.d.ts +10 -0
- package/dist/src/cli/install.d.ts.map +1 -0
- package/dist/src/cli/install.js +116 -0
- package/dist/src/cli/install.js.map +1 -0
- package/dist/src/cli/install.test.d.ts +2 -0
- package/dist/src/cli/install.test.d.ts.map +1 -0
- package/dist/src/cli/install.test.js +217 -0
- package/dist/src/cli/install.test.js.map +1 -0
- package/dist/src/cli/reminders.d.ts +12 -0
- package/dist/src/cli/reminders.d.ts.map +1 -0
- package/dist/src/cli/reminders.js +52 -0
- package/dist/src/cli/reminders.js.map +1 -0
- package/dist/src/cli/reminders.test.d.ts +2 -0
- package/dist/src/cli/reminders.test.d.ts.map +1 -0
- package/dist/src/cli/reminders.test.js +72 -0
- package/dist/src/cli/reminders.test.js.map +1 -0
- package/dist/src/cli/repair.d.ts +11 -0
- package/dist/src/cli/repair.d.ts.map +1 -0
- package/dist/src/cli/repair.js +91 -0
- package/dist/src/cli/repair.js.map +1 -0
- package/dist/src/cli/repair.test.d.ts +2 -0
- package/dist/src/cli/repair.test.d.ts.map +1 -0
- package/dist/src/cli/repair.test.js +95 -0
- package/dist/src/cli/repair.test.js.map +1 -0
- package/dist/src/cli/status.d.ts +10 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +55 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/status.test.d.ts +2 -0
- package/dist/src/cli/status.test.d.ts.map +1 -0
- package/dist/src/cli/status.test.js +80 -0
- package/dist/src/cli/status.test.js.map +1 -0
- package/dist/src/clue-collector.d.ts +23 -0
- package/dist/src/clue-collector.d.ts.map +1 -0
- package/dist/src/clue-collector.js +221 -0
- package/dist/src/clue-collector.js.map +1 -0
- package/dist/src/clue-collector.test.d.ts +2 -0
- package/dist/src/clue-collector.test.d.ts.map +1 -0
- package/dist/src/clue-collector.test.js +278 -0
- package/dist/src/clue-collector.test.js.map +1 -0
- package/dist/src/commit-validator.d.ts +53 -0
- package/dist/src/commit-validator.d.ts.map +1 -0
- package/dist/src/commit-validator.js +356 -0
- package/dist/src/commit-validator.js.map +1 -0
- package/dist/src/commit-validator.test.d.ts +2 -0
- package/dist/src/commit-validator.test.d.ts.map +1 -0
- package/dist/src/commit-validator.test.js +733 -0
- package/dist/src/commit-validator.test.js.map +1 -0
- package/dist/src/config-loader.d.ts +15 -0
- package/dist/src/config-loader.d.ts.map +1 -0
- package/dist/src/config-loader.js +12 -0
- package/dist/src/config-loader.js.map +1 -0
- package/dist/src/config-loader.test.d.ts +2 -0
- package/dist/src/config-loader.test.d.ts.map +1 -0
- package/dist/src/config-loader.test.js +69 -0
- package/dist/src/config-loader.test.js.map +1 -0
- package/dist/src/debug-logger.d.ts +2 -0
- package/dist/src/debug-logger.d.ts.map +1 -0
- package/dist/src/debug-logger.js +23 -0
- package/dist/src/debug-logger.js.map +1 -0
- package/dist/src/debug-logger.test.d.ts +2 -0
- package/dist/src/debug-logger.test.d.ts.map +1 -0
- package/dist/src/debug-logger.test.js +63 -0
- package/dist/src/debug-logger.test.js.map +1 -0
- package/dist/src/default-validators.test.d.ts +2 -0
- package/dist/src/default-validators.test.d.ts.map +1 -0
- package/dist/src/default-validators.test.js +119 -0
- package/dist/src/default-validators.test.js.map +1 -0
- package/dist/src/deny-list.d.ts +3 -0
- package/dist/src/deny-list.d.ts.map +1 -0
- package/dist/src/deny-list.js +62 -0
- package/dist/src/deny-list.js.map +1 -0
- package/dist/src/deny-list.test.d.ts +2 -0
- package/dist/src/deny-list.test.d.ts.map +1 -0
- package/dist/src/deny-list.test.js +93 -0
- package/dist/src/deny-list.test.js.map +1 -0
- package/dist/src/e2e.test.d.ts +2 -0
- package/dist/src/e2e.test.d.ts.map +1 -0
- package/dist/src/e2e.test.js +82 -0
- package/dist/src/e2e.test.js.map +1 -0
- package/dist/src/gitignore-manager.d.ts +2 -0
- package/dist/src/gitignore-manager.d.ts.map +1 -0
- package/dist/src/gitignore-manager.js +45 -0
- package/dist/src/gitignore-manager.js.map +1 -0
- package/dist/src/gitignore-manager.test.d.ts +2 -0
- package/dist/src/gitignore-manager.test.d.ts.map +1 -0
- package/dist/src/gitignore-manager.test.js +65 -0
- package/dist/src/gitignore-manager.test.js.map +1 -0
- package/dist/src/hook-input.d.ts +9 -0
- package/dist/src/hook-input.d.ts.map +1 -0
- package/dist/src/hook-input.js +7 -0
- package/dist/src/hook-input.js.map +1 -0
- package/dist/src/hook-input.test.d.ts +2 -0
- package/dist/src/hook-input.test.d.ts.map +1 -0
- package/dist/src/hook-input.test.js +20 -0
- package/dist/src/hook-input.test.js.map +1 -0
- package/dist/src/hook-logger.d.ts +16 -0
- package/dist/src/hook-logger.d.ts.map +1 -0
- package/dist/src/hook-logger.js +90 -0
- package/dist/src/hook-logger.js.map +1 -0
- package/dist/src/hook-logger.test.d.ts +2 -0
- package/dist/src/hook-logger.test.d.ts.map +1 -0
- package/dist/src/hook-logger.test.js +205 -0
- package/dist/src/hook-logger.test.js.map +1 -0
- package/dist/src/hook-state.d.ts +44 -0
- package/dist/src/hook-state.d.ts.map +1 -0
- package/dist/src/hook-state.js +128 -0
- package/dist/src/hook-state.js.map +1 -0
- package/dist/src/hook-state.test.d.ts +2 -0
- package/dist/src/hook-state.test.d.ts.map +1 -0
- package/dist/src/hook-state.test.js +204 -0
- package/dist/src/hook-state.test.js.map +1 -0
- package/dist/src/hooks/auto-continue.d.ts +21 -0
- package/dist/src/hooks/auto-continue.d.ts.map +1 -0
- package/dist/src/hooks/auto-continue.js +70 -0
- package/dist/src/hooks/auto-continue.js.map +1 -0
- package/dist/src/hooks/auto-continue.test.d.ts +2 -0
- package/dist/src/hooks/auto-continue.test.d.ts.map +1 -0
- package/dist/src/hooks/auto-continue.test.js +171 -0
- package/dist/src/hooks/auto-continue.test.js.map +1 -0
- package/dist/src/hooks/pre-tool-use.d.ts +14 -0
- package/dist/src/hooks/pre-tool-use.d.ts.map +1 -0
- package/dist/src/hooks/pre-tool-use.js +66 -0
- package/dist/src/hooks/pre-tool-use.js.map +1 -0
- package/dist/src/hooks/pre-tool-use.test.d.ts +2 -0
- package/dist/src/hooks/pre-tool-use.test.d.ts.map +1 -0
- package/dist/src/hooks/pre-tool-use.test.js +255 -0
- package/dist/src/hooks/pre-tool-use.test.js.map +1 -0
- package/dist/src/hooks/session-start.d.ts +20 -0
- package/dist/src/hooks/session-start.d.ts.map +1 -0
- package/dist/src/hooks/session-start.js +27 -0
- package/dist/src/hooks/session-start.js.map +1 -0
- package/dist/src/hooks/session-start.test.d.ts +2 -0
- package/dist/src/hooks/session-start.test.d.ts.map +1 -0
- package/dist/src/hooks/session-start.test.js +125 -0
- package/dist/src/hooks/session-start.test.js.map +1 -0
- package/dist/src/hooks/user-prompt-submit.d.ts +17 -0
- package/dist/src/hooks/user-prompt-submit.d.ts.map +1 -0
- package/dist/src/hooks/user-prompt-submit.js +28 -0
- package/dist/src/hooks/user-prompt-submit.js.map +1 -0
- package/dist/src/hooks/user-prompt-submit.test.d.ts +2 -0
- package/dist/src/hooks/user-prompt-submit.test.d.ts.map +1 -0
- package/dist/src/hooks/user-prompt-submit.test.js +119 -0
- package/dist/src/hooks/user-prompt-submit.test.js.map +1 -0
- package/dist/src/hooks/validate-commit.d.ts +12 -0
- package/dist/src/hooks/validate-commit.d.ts.map +1 -0
- package/dist/src/hooks/validate-commit.js +58 -0
- package/dist/src/hooks/validate-commit.js.map +1 -0
- package/dist/src/hooks/validate-commit.test.d.ts +2 -0
- package/dist/src/hooks/validate-commit.test.d.ts.map +1 -0
- package/dist/src/hooks/validate-commit.test.js +150 -0
- package/dist/src/hooks/validate-commit.test.js.map +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +42 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/linker.d.ts +6 -0
- package/dist/src/linker.d.ts.map +1 -0
- package/dist/src/linker.js +78 -0
- package/dist/src/linker.js.map +1 -0
- package/dist/src/linker.test.d.ts +2 -0
- package/dist/src/linker.test.d.ts.map +1 -0
- package/dist/src/linker.test.js +192 -0
- package/dist/src/linker.test.js.map +1 -0
- package/dist/src/logger.d.ts +21 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +117 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/logger.test.d.ts +2 -0
- package/dist/src/logger.test.d.ts.map +1 -0
- package/dist/src/logger.test.js +159 -0
- package/dist/src/logger.test.js.map +1 -0
- package/dist/src/path-resolver.d.ts +9 -0
- package/dist/src/path-resolver.d.ts.map +1 -0
- package/dist/src/path-resolver.js +52 -0
- package/dist/src/path-resolver.js.map +1 -0
- package/dist/src/reminder-loader.d.ts +24 -0
- package/dist/src/reminder-loader.d.ts.map +1 -0
- package/dist/src/reminder-loader.js +84 -0
- package/dist/src/reminder-loader.js.map +1 -0
- package/dist/src/reminder-loader.test.d.ts +2 -0
- package/dist/src/reminder-loader.test.d.ts.map +1 -0
- package/dist/src/reminder-loader.test.js +152 -0
- package/dist/src/reminder-loader.test.js.map +1 -0
- package/dist/src/root-finder.d.ts +2 -0
- package/dist/src/root-finder.d.ts.map +1 -0
- package/dist/src/root-finder.js +71 -0
- package/dist/src/root-finder.js.map +1 -0
- package/dist/src/root-finder.test.d.ts +2 -0
- package/dist/src/root-finder.test.d.ts.map +1 -0
- package/dist/src/root-finder.test.js +111 -0
- package/dist/src/root-finder.test.js.map +1 -0
- package/dist/src/settings-merger.d.ts +2 -0
- package/dist/src/settings-merger.d.ts.map +1 -0
- package/dist/src/settings-merger.js +133 -0
- package/dist/src/settings-merger.js.map +1 -0
- package/dist/src/settings-merger.test.d.ts +2 -0
- package/dist/src/settings-merger.test.d.ts.map +1 -0
- package/dist/src/settings-merger.test.js +379 -0
- package/dist/src/settings-merger.test.js.map +1 -0
- package/dist/src/settings-template.test.d.ts +2 -0
- package/dist/src/settings-template.test.d.ts.map +1 -0
- package/dist/src/settings-template.test.js +88 -0
- package/dist/src/settings-template.test.js.map +1 -0
- package/dist/src/state-manager.d.ts +5 -0
- package/dist/src/state-manager.d.ts.map +1 -0
- package/dist/src/state-manager.js +55 -0
- package/dist/src/state-manager.js.map +1 -0
- package/dist/src/state-manager.test.d.ts +2 -0
- package/dist/src/state-manager.test.d.ts.map +1 -0
- package/dist/src/state-manager.test.js +85 -0
- package/dist/src/state-manager.test.js.map +1 -0
- package/dist/src/subagent-classifier.d.ts +4 -0
- package/dist/src/subagent-classifier.d.ts.map +1 -0
- package/dist/src/subagent-classifier.js +53 -0
- package/dist/src/subagent-classifier.js.map +1 -0
- package/dist/src/subagent-classifier.test.d.ts +2 -0
- package/dist/src/subagent-classifier.test.d.ts.map +1 -0
- package/dist/src/subagent-classifier.test.js +84 -0
- package/dist/src/subagent-classifier.test.js.map +1 -0
- package/dist/src/validator-loader.d.ts +9 -0
- package/dist/src/validator-loader.d.ts.map +1 -0
- package/dist/src/validator-loader.js +71 -0
- package/dist/src/validator-loader.js.map +1 -0
- package/dist/src/validator-loader.test.d.ts +2 -0
- package/dist/src/validator-loader.test.d.ts.map +1 -0
- package/dist/src/validator-loader.test.js +140 -0
- package/dist/src/validator-loader.test.js.map +1 -0
- package/package.json +91 -0
- package/scripts/auto-continue.ts +39 -0
- package/scripts/generate-changeset.ts +405 -0
- package/scripts/pre-tool-use.ts +44 -0
- package/scripts/session-start.ts +42 -0
- package/scripts/tail-logs.sh +17 -0
- package/scripts/test-hooks.sh +910 -0
- package/scripts/user-prompt-submit.ts +42 -0
- package/templates/settings.json +48 -0
- package/templates/settings.local.json +48 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("node:fs"));
|
|
37
|
+
const os = __importStar(require("node:os"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const vitest_1 = require("vitest");
|
|
40
|
+
const commit_validator_js_1 = require("./commit-validator.js");
|
|
41
|
+
function claudeJson(inner) {
|
|
42
|
+
return JSON.stringify({ type: 'result', subtype: 'success', result: JSON.stringify(inner) });
|
|
43
|
+
}
|
|
44
|
+
function claudeBatchJson(results) {
|
|
45
|
+
return JSON.stringify({ type: 'result', subtype: 'success', result: JSON.stringify(results) });
|
|
46
|
+
}
|
|
47
|
+
(0, vitest_1.describe)('extractAppeal', () => {
|
|
48
|
+
(0, vitest_1.it)('extracts appeal from commit message with [appeal: reason]', () => {
|
|
49
|
+
const message = 'feat: add feature [appeal: coherence]';
|
|
50
|
+
const result = (0, commit_validator_js_1.extractAppeal)(message);
|
|
51
|
+
(0, vitest_1.expect)(result).toBe('coherence');
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)('returns null when no appeal present', () => {
|
|
54
|
+
const message = 'feat: add feature';
|
|
55
|
+
const result = (0, commit_validator_js_1.extractAppeal)(message);
|
|
56
|
+
(0, vitest_1.expect)(result).toBe(null);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.describe)('isCommitCommand', () => {
|
|
60
|
+
(0, vitest_1.it)('detects simple git commit', () => {
|
|
61
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.isCommitCommand)('git commit -m "message"')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('detects git commit with add', () => {
|
|
64
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.isCommitCommand)('git add -A && git commit -m "message"')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('detects git commit with heredoc', () => {
|
|
67
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.isCommitCommand)('git commit -m "$(cat <<\'EOF\'\nmessage\nEOF\n)"')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('returns false for git status', () => {
|
|
70
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.isCommitCommand)('git status')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)('returns false for git diff', () => {
|
|
73
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.isCommitCommand)('git diff')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('returns false for non-git commands', () => {
|
|
76
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.isCommitCommand)('npm test')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.describe)('getCommitContext', () => {
|
|
80
|
+
let tempDir;
|
|
81
|
+
(0, vitest_1.beforeEach)(() => {
|
|
82
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ketchup-commit-'));
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.afterEach)(() => {
|
|
85
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)('extracts diff, files, and message from staged changes', () => {
|
|
88
|
+
const { execSync } = require('node:child_process');
|
|
89
|
+
execSync('git init', { cwd: tempDir, stdio: 'pipe' });
|
|
90
|
+
execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: 'pipe' });
|
|
91
|
+
execSync('git config user.name "Test"', { cwd: tempDir, stdio: 'pipe' });
|
|
92
|
+
fs.writeFileSync(path.join(tempDir, 'test.txt'), 'hello world');
|
|
93
|
+
execSync('git add test.txt', { cwd: tempDir, stdio: 'pipe' });
|
|
94
|
+
const result = (0, commit_validator_js_1.getCommitContext)(tempDir, 'git commit -m "Add test file"');
|
|
95
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
96
|
+
diff: vitest_1.expect.stringContaining('+hello world'),
|
|
97
|
+
files: ['test.txt'],
|
|
98
|
+
message: 'Add test file',
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)('returns empty message when no -m flag present', () => {
|
|
102
|
+
const { execSync } = require('node:child_process');
|
|
103
|
+
execSync('git init', { cwd: tempDir, stdio: 'pipe' });
|
|
104
|
+
execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: 'pipe' });
|
|
105
|
+
execSync('git config user.name "Test"', { cwd: tempDir, stdio: 'pipe' });
|
|
106
|
+
fs.writeFileSync(path.join(tempDir, 'test.txt'), 'hello');
|
|
107
|
+
execSync('git add test.txt', { cwd: tempDir, stdio: 'pipe' });
|
|
108
|
+
const result = (0, commit_validator_js_1.getCommitContext)(tempDir, 'git commit');
|
|
109
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
110
|
+
diff: vitest_1.expect.stringContaining('+hello'),
|
|
111
|
+
files: ['test.txt'],
|
|
112
|
+
message: '',
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
(0, vitest_1.it)('uses cd target from command when cwd is a non-repo parent', () => {
|
|
116
|
+
// Previously this threw because getCommitContext ran git diff --cached
|
|
117
|
+
// in the parent dir (not a repo). Now it extracts the cd target.
|
|
118
|
+
const { execSync } = require('node:child_process');
|
|
119
|
+
const parentDir = tempDir; // not a git repo
|
|
120
|
+
const repoDir = path.join(parentDir, 'sub-repo');
|
|
121
|
+
fs.mkdirSync(repoDir);
|
|
122
|
+
execSync('git init', { cwd: repoDir, stdio: 'pipe' });
|
|
123
|
+
execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
|
|
124
|
+
execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'pipe' });
|
|
125
|
+
fs.writeFileSync(path.join(repoDir, 'file.ts'), 'const x = 1;');
|
|
126
|
+
execSync('git add file.ts', { cwd: repoDir, stdio: 'pipe' });
|
|
127
|
+
const command = `cd ${repoDir} && git add file.ts && git commit -m "test: no-op"`;
|
|
128
|
+
const result = (0, commit_validator_js_1.getCommitContext)(parentDir, command);
|
|
129
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
130
|
+
diff: vitest_1.expect.stringContaining('+const x = 1;'),
|
|
131
|
+
files: ['file.ts'],
|
|
132
|
+
message: 'test: no-op',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
(0, vitest_1.describe)('extractCdTarget', () => {
|
|
137
|
+
(0, vitest_1.it)('extracts path from cd command', () => {
|
|
138
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.extractCdTarget)('cd /foo/bar && git commit -m "msg"')).toBe('/foo/bar');
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.it)('returns null when no cd prefix', () => {
|
|
141
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.extractCdTarget)('git commit -m "msg"')).toBe(null);
|
|
142
|
+
});
|
|
143
|
+
(0, vitest_1.it)('extracts path when cd is followed by &&', () => {
|
|
144
|
+
(0, vitest_1.expect)((0, commit_validator_js_1.extractCdTarget)('cd /a/b/c && git add . && git commit -m "x"')).toBe('/a/b/c');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
(0, vitest_1.describe)('parseClaudeJsonOutput', () => {
|
|
148
|
+
(0, vitest_1.it)('extracts inner result and total token usage from claude json wrapper', () => {
|
|
149
|
+
const stdout = JSON.stringify({
|
|
150
|
+
type: 'result',
|
|
151
|
+
subtype: 'success',
|
|
152
|
+
result: '{"decision":"ACK"}',
|
|
153
|
+
usage: { input_tokens: 3, output_tokens: 9, cache_read_input_tokens: 21684, cache_creation_input_tokens: 6728 },
|
|
154
|
+
});
|
|
155
|
+
const parsed = (0, commit_validator_js_1.parseClaudeJsonOutput)(stdout);
|
|
156
|
+
(0, vitest_1.expect)(parsed).toEqual({ decision: 'ACK', inputTokens: 28415, outputTokens: 9 });
|
|
157
|
+
});
|
|
158
|
+
(0, vitest_1.it)('extracts NACK with reason and tokens from wrapper', () => {
|
|
159
|
+
const stdout = JSON.stringify({
|
|
160
|
+
type: 'result',
|
|
161
|
+
subtype: 'success',
|
|
162
|
+
result: '{"decision":"NACK","reason":"Missing tests"}',
|
|
163
|
+
usage: { input_tokens: 150, output_tokens: 12 },
|
|
164
|
+
});
|
|
165
|
+
const parsed = (0, commit_validator_js_1.parseClaudeJsonOutput)(stdout);
|
|
166
|
+
(0, vitest_1.expect)(parsed).toEqual({ decision: 'NACK', reason: 'Missing tests', inputTokens: 150, outputTokens: 12 });
|
|
167
|
+
});
|
|
168
|
+
(0, vitest_1.it)('returns outer object when result is not a string', () => {
|
|
169
|
+
const stdout = '{"decision":"ACK"}';
|
|
170
|
+
const parsed = (0, commit_validator_js_1.parseClaudeJsonOutput)(stdout);
|
|
171
|
+
(0, vitest_1.expect)(parsed).toEqual({ decision: 'ACK' });
|
|
172
|
+
});
|
|
173
|
+
(0, vitest_1.it)('defaults to NACK when decision is missing', () => {
|
|
174
|
+
const stdout = JSON.stringify({
|
|
175
|
+
type: 'result',
|
|
176
|
+
subtype: 'success',
|
|
177
|
+
result: '{"some":"garbage"}',
|
|
178
|
+
});
|
|
179
|
+
const parsed = (0, commit_validator_js_1.parseClaudeJsonOutput)(stdout);
|
|
180
|
+
(0, vitest_1.expect)(parsed).toEqual({ decision: 'NACK', reason: 'validator returned invalid response (no ACK decision)' });
|
|
181
|
+
});
|
|
182
|
+
(0, vitest_1.it)('defaults to NACK when decision is not ACK or NACK', () => {
|
|
183
|
+
const stdout = JSON.stringify({
|
|
184
|
+
type: 'result',
|
|
185
|
+
subtype: 'success',
|
|
186
|
+
result: '{"decision":"MAYBE"}',
|
|
187
|
+
});
|
|
188
|
+
const parsed = (0, commit_validator_js_1.parseClaudeJsonOutput)(stdout);
|
|
189
|
+
(0, vitest_1.expect)(parsed).toEqual({ decision: 'NACK', reason: 'validator returned invalid response (no ACK decision)' });
|
|
190
|
+
});
|
|
191
|
+
(0, vitest_1.it)('defaults to NACK when inner result is not valid JSON', () => {
|
|
192
|
+
const stdout = JSON.stringify({
|
|
193
|
+
type: 'result',
|
|
194
|
+
subtype: 'success',
|
|
195
|
+
result: 'just some text response',
|
|
196
|
+
});
|
|
197
|
+
const parsed = (0, commit_validator_js_1.parseClaudeJsonOutput)(stdout);
|
|
198
|
+
(0, vitest_1.expect)(parsed).toEqual({ decision: 'NACK', reason: 'validator returned invalid response (no ACK decision)' });
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
(0, vitest_1.describe)('parseBatchedOutput', () => {
|
|
202
|
+
(0, vitest_1.it)('parses valid batched JSON array from claude envelope', () => {
|
|
203
|
+
const stdout = claudeBatchJson([
|
|
204
|
+
{ id: 'coverage-rules', decision: 'ACK' },
|
|
205
|
+
{ id: 'no-comments', decision: 'NACK', reason: 'Found comments' },
|
|
206
|
+
]);
|
|
207
|
+
const results = (0, commit_validator_js_1.parseBatchedOutput)(stdout, ['coverage-rules', 'no-comments']);
|
|
208
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
209
|
+
{ validator: 'coverage-rules', decision: 'ACK' },
|
|
210
|
+
{ validator: 'no-comments', decision: 'NACK', reason: 'Found comments' },
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
(0, vitest_1.it)('extracts JSON array from markdown code fences', () => {
|
|
214
|
+
const inner = '```json\n[{"id":"v1","decision":"ACK"}]\n```';
|
|
215
|
+
const stdout = JSON.stringify({ type: 'result', subtype: 'success', result: inner });
|
|
216
|
+
const results = (0, commit_validator_js_1.parseBatchedOutput)(stdout, ['v1']);
|
|
217
|
+
(0, vitest_1.expect)(results).toEqual([{ validator: 'v1', decision: 'ACK' }]);
|
|
218
|
+
});
|
|
219
|
+
(0, vitest_1.it)('NACKs validators missing from response', () => {
|
|
220
|
+
const stdout = claudeBatchJson([{ id: 'v1', decision: 'ACK' }]);
|
|
221
|
+
const results = (0, commit_validator_js_1.parseBatchedOutput)(stdout, ['v1', 'v2']);
|
|
222
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
223
|
+
{ validator: 'v1', decision: 'ACK' },
|
|
224
|
+
{ validator: 'v2', decision: 'NACK', reason: 'validator missing or invalid in batched response' },
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
(0, vitest_1.it)('NACKs all when response is unparseable', () => {
|
|
228
|
+
const stdout = JSON.stringify({ type: 'result', subtype: 'success', result: 'not json at all' });
|
|
229
|
+
const results = (0, commit_validator_js_1.parseBatchedOutput)(stdout, ['v1', 'v2']);
|
|
230
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
231
|
+
{ validator: 'v1', decision: 'NACK', reason: 'batched validator returned unparseable response' },
|
|
232
|
+
{ validator: 'v2', decision: 'NACK', reason: 'batched validator returned unparseable response' },
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
(0, vitest_1.it)('accepts validator key as alias for id', () => {
|
|
236
|
+
const inner = [{ validator: 'v1', decision: 'ACK' }];
|
|
237
|
+
const stdout = JSON.stringify({ type: 'result', subtype: 'success', result: JSON.stringify(inner) });
|
|
238
|
+
const results = (0, commit_validator_js_1.parseBatchedOutput)(stdout, ['v1']);
|
|
239
|
+
(0, vitest_1.expect)(results).toEqual([{ validator: 'v1', decision: 'ACK' }]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
(0, vitest_1.describe)('runValidator', () => {
|
|
243
|
+
(0, vitest_1.it)('calls executor with formatted prompt', async () => {
|
|
244
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
245
|
+
status: 0,
|
|
246
|
+
stdout: claudeJson({ decision: 'ACK' }),
|
|
247
|
+
});
|
|
248
|
+
const validator = {
|
|
249
|
+
name: 'test-validator',
|
|
250
|
+
description: 'Test',
|
|
251
|
+
enabled: true,
|
|
252
|
+
content: 'Check the commit',
|
|
253
|
+
path: '/validators/test.md',
|
|
254
|
+
};
|
|
255
|
+
const context = {
|
|
256
|
+
diff: '+hello world',
|
|
257
|
+
files: ['test.txt'],
|
|
258
|
+
message: 'Add test',
|
|
259
|
+
};
|
|
260
|
+
await (0, commit_validator_js_1.runValidator)(validator, context, executor);
|
|
261
|
+
(0, vitest_1.expect)(executor).toHaveBeenCalledWith('claude', ['-p', '--no-session-persistence', vitest_1.expect.stringContaining('<diff>'), '--output-format', 'json'], vitest_1.expect.objectContaining({ encoding: 'utf8' }));
|
|
262
|
+
});
|
|
263
|
+
(0, vitest_1.it)('includes validator content in prompt', async () => {
|
|
264
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
265
|
+
status: 0,
|
|
266
|
+
stdout: claudeJson({ decision: 'ACK' }),
|
|
267
|
+
});
|
|
268
|
+
const validator = {
|
|
269
|
+
name: 'test-validator',
|
|
270
|
+
description: 'Test',
|
|
271
|
+
enabled: true,
|
|
272
|
+
content: 'Check that tests pass',
|
|
273
|
+
path: '/validators/test.md',
|
|
274
|
+
};
|
|
275
|
+
const context = {
|
|
276
|
+
diff: '+hello',
|
|
277
|
+
files: ['test.txt'],
|
|
278
|
+
message: 'Test commit',
|
|
279
|
+
};
|
|
280
|
+
await (0, commit_validator_js_1.runValidator)(validator, context, executor);
|
|
281
|
+
const prompt = executor.mock.calls[0][1][2];
|
|
282
|
+
(0, vitest_1.expect)(prompt).toContain('Check that tests pass');
|
|
283
|
+
(0, vitest_1.expect)(prompt).toContain('<diff>');
|
|
284
|
+
(0, vitest_1.expect)(prompt).toContain('+hello');
|
|
285
|
+
(0, vitest_1.expect)(prompt).toContain('<commit-message>');
|
|
286
|
+
(0, vitest_1.expect)(prompt).toContain('Test commit');
|
|
287
|
+
(0, vitest_1.expect)(prompt).toContain('<files>');
|
|
288
|
+
(0, vitest_1.expect)(prompt).toContain('test.txt');
|
|
289
|
+
});
|
|
290
|
+
(0, vitest_1.it)('parses ACK response from claude json wrapper', async () => {
|
|
291
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
292
|
+
status: 0,
|
|
293
|
+
stdout: JSON.stringify({
|
|
294
|
+
type: 'result',
|
|
295
|
+
subtype: 'success',
|
|
296
|
+
result: '{"decision":"ACK"}',
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const validator = {
|
|
300
|
+
name: 'test',
|
|
301
|
+
description: 'Test',
|
|
302
|
+
enabled: true,
|
|
303
|
+
content: 'Check',
|
|
304
|
+
path: '/test.md',
|
|
305
|
+
};
|
|
306
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
307
|
+
const result = await (0, commit_validator_js_1.runValidator)(validator, context, executor);
|
|
308
|
+
(0, vitest_1.expect)(result).toEqual({ decision: 'ACK' });
|
|
309
|
+
});
|
|
310
|
+
(0, vitest_1.it)('parses NACK response with reason', async () => {
|
|
311
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
312
|
+
status: 0,
|
|
313
|
+
stdout: claudeJson({ decision: 'NACK', reason: 'Missing tests' }),
|
|
314
|
+
});
|
|
315
|
+
const validator = {
|
|
316
|
+
name: 'test',
|
|
317
|
+
description: 'Test',
|
|
318
|
+
enabled: true,
|
|
319
|
+
content: 'Check',
|
|
320
|
+
path: '/test.md',
|
|
321
|
+
};
|
|
322
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
323
|
+
const result = await (0, commit_validator_js_1.runValidator)(validator, context, executor);
|
|
324
|
+
(0, vitest_1.expect)(result).toEqual({ decision: 'NACK', reason: 'Missing tests' });
|
|
325
|
+
});
|
|
326
|
+
(0, vitest_1.it)('retries once when response is invalid then succeeds', async () => {
|
|
327
|
+
const executor = vitest_1.vi
|
|
328
|
+
.fn()
|
|
329
|
+
.mockReturnValueOnce({
|
|
330
|
+
status: 0,
|
|
331
|
+
stdout: claudeJson({ some: 'garbage' }),
|
|
332
|
+
})
|
|
333
|
+
.mockReturnValueOnce({
|
|
334
|
+
status: 0,
|
|
335
|
+
stdout: claudeJson({ decision: 'ACK' }),
|
|
336
|
+
});
|
|
337
|
+
const validator = {
|
|
338
|
+
name: 'test',
|
|
339
|
+
description: 'Test',
|
|
340
|
+
enabled: true,
|
|
341
|
+
content: 'Check',
|
|
342
|
+
path: '/test.md',
|
|
343
|
+
};
|
|
344
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
345
|
+
const result = await (0, commit_validator_js_1.runValidator)(validator, context, executor);
|
|
346
|
+
(0, vitest_1.expect)(result).toEqual({ decision: 'ACK' });
|
|
347
|
+
(0, vitest_1.expect)(executor).toHaveBeenCalledTimes(2);
|
|
348
|
+
});
|
|
349
|
+
(0, vitest_1.it)('returns NACK after retry also fails', async () => {
|
|
350
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
351
|
+
status: 0,
|
|
352
|
+
stdout: claudeJson({ some: 'garbage' }),
|
|
353
|
+
});
|
|
354
|
+
const validator = {
|
|
355
|
+
name: 'test',
|
|
356
|
+
description: 'Test',
|
|
357
|
+
enabled: true,
|
|
358
|
+
content: 'Check',
|
|
359
|
+
path: '/test.md',
|
|
360
|
+
};
|
|
361
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
362
|
+
const result = await (0, commit_validator_js_1.runValidator)(validator, context, executor);
|
|
363
|
+
(0, vitest_1.expect)(result).toEqual({ decision: 'NACK', reason: 'validator returned invalid response (no ACK decision)' });
|
|
364
|
+
(0, vitest_1.expect)(executor).toHaveBeenCalledTimes(2);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
(0, vitest_1.describe)('runAppealValidator', () => {
|
|
368
|
+
(0, vitest_1.it)('includes validator results and appeal in prompt', async () => {
|
|
369
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
370
|
+
status: 0,
|
|
371
|
+
stdout: claudeJson({ decision: 'ACK' }),
|
|
372
|
+
});
|
|
373
|
+
const appealValidator = {
|
|
374
|
+
name: 'appeal-system',
|
|
375
|
+
description: 'Evaluate appeals',
|
|
376
|
+
enabled: true,
|
|
377
|
+
content: 'Judge the appeal',
|
|
378
|
+
path: '/appeal.md',
|
|
379
|
+
};
|
|
380
|
+
const context = {
|
|
381
|
+
diff: '+hello',
|
|
382
|
+
files: ['test.txt'],
|
|
383
|
+
message: 'feat: add feature [appeal: coherence]',
|
|
384
|
+
};
|
|
385
|
+
const results = [
|
|
386
|
+
{ validator: 'coverage-rules', decision: 'NACK', reason: 'Missing tests', appealable: true },
|
|
387
|
+
];
|
|
388
|
+
const appeal = 'these files are tightly coupled';
|
|
389
|
+
await (0, commit_validator_js_1.runAppealValidator)(appealValidator, context, results, appeal, executor);
|
|
390
|
+
const prompt = executor.mock.calls[0][1][2];
|
|
391
|
+
(0, vitest_1.expect)(prompt).toContain('<validator-results>');
|
|
392
|
+
(0, vitest_1.expect)(prompt).toContain('coverage-rules: NACK - Missing tests');
|
|
393
|
+
(0, vitest_1.expect)(prompt).toContain('<appeal>');
|
|
394
|
+
(0, vitest_1.expect)(prompt).toContain('these files are tightly coupled');
|
|
395
|
+
(0, vitest_1.expect)(prompt).toContain('Judge the appeal');
|
|
396
|
+
});
|
|
397
|
+
(0, vitest_1.it)('returns ACK when appeal is valid', async () => {
|
|
398
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
399
|
+
status: 0,
|
|
400
|
+
stdout: claudeJson({ decision: 'ACK' }),
|
|
401
|
+
});
|
|
402
|
+
const appealValidator = {
|
|
403
|
+
name: 'appeal-system',
|
|
404
|
+
description: 'Evaluate appeals',
|
|
405
|
+
enabled: true,
|
|
406
|
+
content: 'Judge',
|
|
407
|
+
path: '/appeal.md',
|
|
408
|
+
};
|
|
409
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
410
|
+
const results = [{ validator: 'v1', decision: 'NACK', reason: 'reason', appealable: true }];
|
|
411
|
+
const result = await (0, commit_validator_js_1.runAppealValidator)(appealValidator, context, results, 'coherence', executor);
|
|
412
|
+
(0, vitest_1.expect)(result).toEqual({ decision: 'ACK' });
|
|
413
|
+
});
|
|
414
|
+
(0, vitest_1.it)('returns NACK when appeal is invalid', async () => {
|
|
415
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
416
|
+
status: 0,
|
|
417
|
+
stdout: claudeJson({ decision: 'NACK', reason: 'Appeal does not justify violation' }),
|
|
418
|
+
});
|
|
419
|
+
const appealValidator = {
|
|
420
|
+
name: 'appeal-system',
|
|
421
|
+
description: 'Evaluate appeals',
|
|
422
|
+
enabled: true,
|
|
423
|
+
content: 'Judge',
|
|
424
|
+
path: '/appeal.md',
|
|
425
|
+
};
|
|
426
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
427
|
+
const results = [{ validator: 'v1', decision: 'NACK', reason: 'reason', appealable: true }];
|
|
428
|
+
const result = await (0, commit_validator_js_1.runAppealValidator)(appealValidator, context, results, 'please let me in', executor);
|
|
429
|
+
(0, vitest_1.expect)(result).toEqual({ decision: 'NACK', reason: 'Appeal does not justify violation' });
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
(0, vitest_1.describe)('validateCommit', () => {
|
|
433
|
+
(0, vitest_1.it)('builds batched prompt with validator tags and stripped boilerplate', async () => {
|
|
434
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
435
|
+
status: 0,
|
|
436
|
+
stdout: claudeBatchJson([{ id: 'v1', decision: 'ACK' }]),
|
|
437
|
+
});
|
|
438
|
+
const boilerplateContent = `You are a commit validator. You MUST respond with ONLY a JSON object, no other text.
|
|
439
|
+
|
|
440
|
+
Valid responses:
|
|
441
|
+
{"decision":"ACK"}
|
|
442
|
+
{"decision":"NACK","reason":"one sentence explanation"}
|
|
443
|
+
|
|
444
|
+
Check for tests.
|
|
445
|
+
|
|
446
|
+
RESPOND WITH JSON ONLY - NO PROSE, NO MARKDOWN, NO EXPLANATION OUTSIDE THE JSON.`;
|
|
447
|
+
const validators = [
|
|
448
|
+
{ name: 'v1', description: 'd1', enabled: true, content: boilerplateContent, path: '/v1.md' },
|
|
449
|
+
];
|
|
450
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
451
|
+
await (0, commit_validator_js_1.validateCommit)(validators, context, executor);
|
|
452
|
+
const prompt = executor.mock.calls[0][1][2];
|
|
453
|
+
(0, vitest_1.expect)(prompt).toContain('<validator id="v1">');
|
|
454
|
+
(0, vitest_1.expect)(prompt).toContain('Check for tests.');
|
|
455
|
+
(0, vitest_1.expect)(prompt).not.toContain('You are a commit validator. You MUST');
|
|
456
|
+
(0, vitest_1.expect)(prompt).not.toContain('RESPOND WITH JSON ONLY');
|
|
457
|
+
(0, vitest_1.expect)(prompt).not.toContain('Valid responses:');
|
|
458
|
+
(0, vitest_1.expect)(prompt).toContain('Respond with ONLY a JSON array');
|
|
459
|
+
});
|
|
460
|
+
(0, vitest_1.it)('runs batches in parallel and returns flattened results', async () => {
|
|
461
|
+
const callLog = [];
|
|
462
|
+
const asyncExecutor = vitest_1.vi.fn().mockImplementation((_cmd, args) => {
|
|
463
|
+
const prompt = args[2];
|
|
464
|
+
const batchName = prompt.includes('"v1"') ? 'batch-0' : prompt.includes('"v2"') ? 'batch-1' : 'batch-2';
|
|
465
|
+
callLog.push(`start:${batchName}`);
|
|
466
|
+
return new Promise((resolve) => {
|
|
467
|
+
setTimeout(() => {
|
|
468
|
+
callLog.push(`end:${batchName}`);
|
|
469
|
+
const id = batchName === 'batch-0' ? 'v1' : batchName === 'batch-1' ? 'v2' : 'v3';
|
|
470
|
+
resolve({ status: 0, stdout: claudeBatchJson([{ id, decision: 'ACK' }]) });
|
|
471
|
+
}, 50);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
const validators = [
|
|
475
|
+
{ name: 'v1', description: 'd1', enabled: true, content: 'c1', path: '/v1.md' },
|
|
476
|
+
{ name: 'v2', description: 'd2', enabled: true, content: 'c2', path: '/v2.md' },
|
|
477
|
+
{ name: 'v3', description: 'd3', enabled: true, content: 'c3', path: '/v3.md' },
|
|
478
|
+
];
|
|
479
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
480
|
+
const results = await (0, commit_validator_js_1.validateCommit)(validators, context, asyncExecutor);
|
|
481
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
482
|
+
{ validator: 'v1', decision: 'ACK', appealable: true },
|
|
483
|
+
{ validator: 'v2', decision: 'ACK', appealable: true },
|
|
484
|
+
{ validator: 'v3', decision: 'ACK', appealable: true },
|
|
485
|
+
]);
|
|
486
|
+
(0, vitest_1.expect)(asyncExecutor).toHaveBeenCalledTimes(3);
|
|
487
|
+
(0, vitest_1.expect)(callLog).toEqual([
|
|
488
|
+
'start:batch-0',
|
|
489
|
+
'start:batch-1',
|
|
490
|
+
'start:batch-2',
|
|
491
|
+
'end:batch-0',
|
|
492
|
+
'end:batch-1',
|
|
493
|
+
'end:batch-2',
|
|
494
|
+
]);
|
|
495
|
+
});
|
|
496
|
+
(0, vitest_1.it)('aggregates NACK reasons from batched responses', async () => {
|
|
497
|
+
const executor = vitest_1.vi
|
|
498
|
+
.fn()
|
|
499
|
+
.mockReturnValueOnce({ status: 0, stdout: claudeBatchJson([{ id: 'v1', decision: 'ACK' }]) })
|
|
500
|
+
.mockReturnValueOnce({
|
|
501
|
+
status: 0,
|
|
502
|
+
stdout: claudeBatchJson([{ id: 'v2', decision: 'NACK', reason: 'Missing tests' }]),
|
|
503
|
+
})
|
|
504
|
+
.mockReturnValueOnce({
|
|
505
|
+
status: 0,
|
|
506
|
+
stdout: claudeBatchJson([{ id: 'v3', decision: 'NACK', reason: 'No coverage' }]),
|
|
507
|
+
});
|
|
508
|
+
const validators = [
|
|
509
|
+
{ name: 'v1', description: 'd1', enabled: true, content: 'c1', path: '/v1.md' },
|
|
510
|
+
{ name: 'v2', description: 'd2', enabled: true, content: 'c2', path: '/v2.md' },
|
|
511
|
+
{ name: 'v3', description: 'd3', enabled: true, content: 'c3', path: '/v3.md' },
|
|
512
|
+
];
|
|
513
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
514
|
+
const results = await (0, commit_validator_js_1.validateCommit)(validators, context, executor);
|
|
515
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
516
|
+
{ validator: 'v1', decision: 'ACK', appealable: true },
|
|
517
|
+
{ validator: 'v2', decision: 'NACK', reason: 'Missing tests', appealable: true },
|
|
518
|
+
{ validator: 'v3', decision: 'NACK', reason: 'No coverage', appealable: true },
|
|
519
|
+
]);
|
|
520
|
+
});
|
|
521
|
+
(0, vitest_1.it)('marks no-dangerous-git as not appealable', async () => {
|
|
522
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
523
|
+
status: 0,
|
|
524
|
+
stdout: claudeBatchJson([{ id: 'no-dangerous-git', decision: 'NACK', reason: '--force is forbidden' }]),
|
|
525
|
+
});
|
|
526
|
+
const validators = [
|
|
527
|
+
{ name: 'no-dangerous-git', description: 'd', enabled: true, content: 'c', path: '/v.md' },
|
|
528
|
+
];
|
|
529
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
530
|
+
const results = await (0, commit_validator_js_1.validateCommit)(validators, context, executor);
|
|
531
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
532
|
+
{ validator: 'no-dangerous-git', decision: 'NACK', reason: '--force is forbidden', appealable: false },
|
|
533
|
+
]);
|
|
534
|
+
});
|
|
535
|
+
(0, vitest_1.it)('NACKs all validators in a batch when executor throws', async () => {
|
|
536
|
+
const executor = vitest_1.vi.fn().mockRejectedValue(new Error('connection refused'));
|
|
537
|
+
const validators = [{ name: 'v1', description: 'd', enabled: true, content: 'c', path: '/v.md' }];
|
|
538
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
539
|
+
const results = await (0, commit_validator_js_1.validateCommit)(validators, context, executor);
|
|
540
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
541
|
+
{ validator: 'v1', decision: 'NACK', reason: 'validator crashed: Error: connection refused', appealable: false },
|
|
542
|
+
]);
|
|
543
|
+
});
|
|
544
|
+
(0, vitest_1.it)('logs shared token counts for all validators in a batch', async () => {
|
|
545
|
+
const stdout = JSON.stringify({
|
|
546
|
+
type: 'result',
|
|
547
|
+
subtype: 'success',
|
|
548
|
+
result: JSON.stringify([{ id: 'v1', decision: 'ACK' }]),
|
|
549
|
+
usage: { input_tokens: 100, output_tokens: 20, cache_read_input_tokens: 50 },
|
|
550
|
+
});
|
|
551
|
+
const executor = vitest_1.vi.fn().mockReturnValue({ status: 0, stdout });
|
|
552
|
+
const logs = [];
|
|
553
|
+
const onLog = (event, name, detail) => {
|
|
554
|
+
logs.push({ event, name, detail });
|
|
555
|
+
};
|
|
556
|
+
const validators = [{ name: 'v1', description: 'd', enabled: true, content: 'c', path: '/v.md' }];
|
|
557
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
558
|
+
await (0, commit_validator_js_1.validateCommit)(validators, context, executor, onLog);
|
|
559
|
+
(0, vitest_1.expect)(logs).toEqual([
|
|
560
|
+
{ event: 'spawn', name: 'batch-0', detail: 'validators: v1' },
|
|
561
|
+
{ event: 'complete', name: 'v1', detail: 'ACK (in:150 out:20)' },
|
|
562
|
+
]);
|
|
563
|
+
});
|
|
564
|
+
(0, vitest_1.it)('uses provided batchCount to control chunk size', async () => {
|
|
565
|
+
const executor = vitest_1.vi.fn().mockImplementation((_cmd, args) => {
|
|
566
|
+
const prompt = args[2];
|
|
567
|
+
if (prompt.includes('"v1"') && prompt.includes('"v2"')) {
|
|
568
|
+
return {
|
|
569
|
+
status: 0,
|
|
570
|
+
stdout: claudeBatchJson([
|
|
571
|
+
{ id: 'v1', decision: 'ACK' },
|
|
572
|
+
{ id: 'v2', decision: 'ACK' },
|
|
573
|
+
]),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
status: 0,
|
|
578
|
+
stdout: claudeBatchJson([
|
|
579
|
+
{ id: 'v3', decision: 'ACK' },
|
|
580
|
+
{ id: 'v4', decision: 'ACK' },
|
|
581
|
+
]),
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
const validators = [
|
|
585
|
+
{ name: 'v1', description: 'd', enabled: true, content: 'c1', path: '/v1.md' },
|
|
586
|
+
{ name: 'v2', description: 'd', enabled: true, content: 'c2', path: '/v2.md' },
|
|
587
|
+
{ name: 'v3', description: 'd', enabled: true, content: 'c3', path: '/v3.md' },
|
|
588
|
+
{ name: 'v4', description: 'd', enabled: true, content: 'c4', path: '/v4.md' },
|
|
589
|
+
];
|
|
590
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'msg' };
|
|
591
|
+
const results = await (0, commit_validator_js_1.validateCommit)(validators, context, executor, undefined, 2);
|
|
592
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
593
|
+
{ validator: 'v1', decision: 'ACK', appealable: true },
|
|
594
|
+
{ validator: 'v2', decision: 'ACK', appealable: true },
|
|
595
|
+
{ validator: 'v3', decision: 'ACK', appealable: true },
|
|
596
|
+
{ validator: 'v4', decision: 'ACK', appealable: true },
|
|
597
|
+
]);
|
|
598
|
+
(0, vitest_1.expect)(executor).toHaveBeenCalledTimes(2);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
(0, vitest_1.describe)('handleCommitValidation', () => {
|
|
602
|
+
(0, vitest_1.it)('allows commit when all validators ACK', async () => {
|
|
603
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
604
|
+
status: 0,
|
|
605
|
+
stdout: claudeBatchJson([{ id: 'v1', decision: 'ACK' }]),
|
|
606
|
+
});
|
|
607
|
+
const validators = [{ name: 'v1', description: 'd', enabled: true, content: 'c', path: '/v.md' }];
|
|
608
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'feat: add feature' };
|
|
609
|
+
const result = await (0, commit_validator_js_1.handleCommitValidation)(validators, context, executor);
|
|
610
|
+
(0, vitest_1.expect)(result).toEqual({ allowed: true, results: vitest_1.expect.any(Array) });
|
|
611
|
+
});
|
|
612
|
+
(0, vitest_1.it)('blocks commit when validator NACKs without appeal', async () => {
|
|
613
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
614
|
+
status: 0,
|
|
615
|
+
stdout: claudeBatchJson([{ id: 'coverage-rules', decision: 'NACK', reason: 'Missing tests' }]),
|
|
616
|
+
});
|
|
617
|
+
const validators = [
|
|
618
|
+
{ name: 'coverage-rules', description: 'd', enabled: true, content: 'c', path: '/v.md' },
|
|
619
|
+
];
|
|
620
|
+
const context = { diff: '+a', files: ['a.txt'], message: 'feat: add feature' };
|
|
621
|
+
const result = await (0, commit_validator_js_1.handleCommitValidation)(validators, context, executor);
|
|
622
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
623
|
+
allowed: false,
|
|
624
|
+
results: vitest_1.expect.any(Array),
|
|
625
|
+
blockedBy: ['coverage-rules'],
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
(0, vitest_1.it)('allows commit when appeal validator ACKs the appeal', async () => {
|
|
629
|
+
const executor = vitest_1.vi
|
|
630
|
+
.fn()
|
|
631
|
+
.mockReturnValueOnce({
|
|
632
|
+
status: 0,
|
|
633
|
+
stdout: claudeBatchJson([{ id: 'coverage-rules', decision: 'NACK', reason: 'Missing tests' }]),
|
|
634
|
+
})
|
|
635
|
+
.mockReturnValueOnce({
|
|
636
|
+
status: 0,
|
|
637
|
+
stdout: claudeJson({ decision: 'ACK' }),
|
|
638
|
+
});
|
|
639
|
+
const validators = [
|
|
640
|
+
{ name: 'coverage-rules', description: 'd', enabled: true, content: 'c', path: '/v.md' },
|
|
641
|
+
];
|
|
642
|
+
const appealValidator = {
|
|
643
|
+
name: 'appeal-system',
|
|
644
|
+
description: 'Appeal validator',
|
|
645
|
+
enabled: true,
|
|
646
|
+
content: 'Evaluate appeal',
|
|
647
|
+
path: '/appeal.md',
|
|
648
|
+
};
|
|
649
|
+
const context = {
|
|
650
|
+
diff: '+a',
|
|
651
|
+
files: ['a.txt'],
|
|
652
|
+
message: 'feat: add feature [appeal: these files are tightly coupled]',
|
|
653
|
+
};
|
|
654
|
+
const result = await (0, commit_validator_js_1.handleCommitValidation)(validators, context, executor, appealValidator);
|
|
655
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
656
|
+
allowed: true,
|
|
657
|
+
results: vitest_1.expect.any(Array),
|
|
658
|
+
appeal: 'these files are tightly coupled',
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
(0, vitest_1.it)('blocks commit when appeal validator NACKs the appeal', async () => {
|
|
662
|
+
const executor = vitest_1.vi
|
|
663
|
+
.fn()
|
|
664
|
+
.mockReturnValueOnce({
|
|
665
|
+
status: 0,
|
|
666
|
+
stdout: claudeBatchJson([{ id: 'coverage-rules', decision: 'NACK', reason: 'Missing tests' }]),
|
|
667
|
+
})
|
|
668
|
+
.mockReturnValueOnce({
|
|
669
|
+
status: 0,
|
|
670
|
+
stdout: claudeJson({ decision: 'NACK', reason: 'Appeal does not justify violation' }),
|
|
671
|
+
});
|
|
672
|
+
const validators = [
|
|
673
|
+
{ name: 'coverage-rules', description: 'd', enabled: true, content: 'c', path: '/v.md' },
|
|
674
|
+
];
|
|
675
|
+
const appealValidator = {
|
|
676
|
+
name: 'appeal-system',
|
|
677
|
+
description: 'Appeal validator',
|
|
678
|
+
enabled: true,
|
|
679
|
+
content: 'Evaluate appeal',
|
|
680
|
+
path: '/appeal.md',
|
|
681
|
+
};
|
|
682
|
+
const context = {
|
|
683
|
+
diff: '+a',
|
|
684
|
+
files: ['a.txt'],
|
|
685
|
+
message: 'feat: add feature [appeal: please let this through]',
|
|
686
|
+
};
|
|
687
|
+
const result = await (0, commit_validator_js_1.handleCommitValidation)(validators, context, executor, appealValidator);
|
|
688
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
689
|
+
allowed: false,
|
|
690
|
+
results: vitest_1.expect.any(Array),
|
|
691
|
+
blockedBy: ['coverage-rules'],
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
(0, vitest_1.it)('blocks commit when no appeal validator provided and NACK with appeal', async () => {
|
|
695
|
+
const executor = vitest_1.vi.fn().mockReturnValue({
|
|
696
|
+
status: 0,
|
|
697
|
+
stdout: claudeBatchJson([{ id: 'coverage-rules', decision: 'NACK', reason: 'Missing tests' }]),
|
|
698
|
+
});
|
|
699
|
+
const validators = [
|
|
700
|
+
{ name: 'coverage-rules', description: 'd', enabled: true, content: 'c', path: '/v.md' },
|
|
701
|
+
];
|
|
702
|
+
const context = {
|
|
703
|
+
diff: '+a',
|
|
704
|
+
files: ['a.txt'],
|
|
705
|
+
message: 'feat: add feature [appeal: coherence]',
|
|
706
|
+
};
|
|
707
|
+
const result = await (0, commit_validator_js_1.handleCommitValidation)(validators, context, executor);
|
|
708
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
709
|
+
allowed: false,
|
|
710
|
+
results: vitest_1.expect.any(Array),
|
|
711
|
+
blockedBy: ['coverage-rules'],
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
(0, vitest_1.describe)('formatBlockMessage', () => {
|
|
716
|
+
(0, vitest_1.it)('formats block message with validator reasons and appeal instructions', () => {
|
|
717
|
+
const results = [
|
|
718
|
+
{ validator: 'coverage-rules', decision: 'NACK', reason: 'Missing tests', appealable: true },
|
|
719
|
+
];
|
|
720
|
+
const message = (0, commit_validator_js_1.formatBlockMessage)(results);
|
|
721
|
+
(0, vitest_1.expect)(message).toContain('coverage-rules: Missing tests');
|
|
722
|
+
(0, vitest_1.expect)(message).toContain('[appeal: your justification]');
|
|
723
|
+
});
|
|
724
|
+
(0, vitest_1.it)('omits appeal instructions for non-appealable validators', () => {
|
|
725
|
+
const results = [
|
|
726
|
+
{ validator: 'no-dangerous-git', decision: 'NACK', reason: '--force forbidden', appealable: false },
|
|
727
|
+
];
|
|
728
|
+
const message = (0, commit_validator_js_1.formatBlockMessage)(results);
|
|
729
|
+
(0, vitest_1.expect)(message).toContain('no-dangerous-git: --force forbidden');
|
|
730
|
+
(0, vitest_1.expect)(message).toContain('cannot be appealed');
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
//# sourceMappingURL=commit-validator.test.js.map
|