claude-crap 0.1.2
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/CHANGELOG.md +308 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/bin/claude-crap.mjs +141 -0
- package/dist/adapters/bandit.d.ts +48 -0
- package/dist/adapters/bandit.d.ts.map +1 -0
- package/dist/adapters/bandit.js +145 -0
- package/dist/adapters/bandit.js.map +1 -0
- package/dist/adapters/common.d.ts +73 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +78 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/eslint.d.ts +52 -0
- package/dist/adapters/eslint.d.ts.map +1 -0
- package/dist/adapters/eslint.js +142 -0
- package/dist/adapters/eslint.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +64 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/semgrep.d.ts +30 -0
- package/dist/adapters/semgrep.d.ts.map +1 -0
- package/dist/adapters/semgrep.js +130 -0
- package/dist/adapters/semgrep.js.map +1 -0
- package/dist/adapters/stryker.d.ts +55 -0
- package/dist/adapters/stryker.d.ts.map +1 -0
- package/dist/adapters/stryker.js +165 -0
- package/dist/adapters/stryker.js.map +1 -0
- package/dist/ast/cyclomatic.d.ts +48 -0
- package/dist/ast/cyclomatic.d.ts.map +1 -0
- package/dist/ast/cyclomatic.js +106 -0
- package/dist/ast/cyclomatic.js.map +1 -0
- package/dist/ast/index.d.ts +26 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +23 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/language-config.d.ts +70 -0
- package/dist/ast/language-config.d.ts.map +1 -0
- package/dist/ast/language-config.js +192 -0
- package/dist/ast/language-config.js.map +1 -0
- package/dist/ast/tree-sitter-engine.d.ts +133 -0
- package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
- package/dist/ast/tree-sitter-engine.js +270 -0
- package/dist/ast/tree-sitter-engine.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/crap-config.d.ts +97 -0
- package/dist/crap-config.d.ts.map +1 -0
- package/dist/crap-config.js +144 -0
- package/dist/crap-config.js.map +1 -0
- package/dist/dashboard/server.d.ts +65 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +147 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/crap.d.ts +71 -0
- package/dist/metrics/crap.d.ts.map +1 -0
- package/dist/metrics/crap.js +67 -0
- package/dist/metrics/crap.js.map +1 -0
- package/dist/metrics/index.d.ts +31 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +27 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/score.d.ts +143 -0
- package/dist/metrics/score.d.ts.map +1 -0
- package/dist/metrics/score.js +224 -0
- package/dist/metrics/score.js.map +1 -0
- package/dist/metrics/tdr.d.ts +106 -0
- package/dist/metrics/tdr.d.ts.map +1 -0
- package/dist/metrics/tdr.js +117 -0
- package/dist/metrics/tdr.js.map +1 -0
- package/dist/metrics/workspace-walker.d.ts +43 -0
- package/dist/metrics/workspace-walker.d.ts.map +1 -0
- package/dist/metrics/workspace-walker.js +137 -0
- package/dist/metrics/workspace-walker.js.map +1 -0
- package/dist/sarif/index.d.ts +21 -0
- package/dist/sarif/index.d.ts.map +1 -0
- package/dist/sarif/index.js +19 -0
- package/dist/sarif/index.js.map +1 -0
- package/dist/sarif/sarif-builder.d.ts +128 -0
- package/dist/sarif/sarif-builder.d.ts.map +1 -0
- package/dist/sarif/sarif-builder.js +79 -0
- package/dist/sarif/sarif-builder.js.map +1 -0
- package/dist/sarif/sarif-store.d.ts +205 -0
- package/dist/sarif/sarif-store.d.ts.map +1 -0
- package/dist/sarif/sarif-store.js +246 -0
- package/dist/sarif/sarif-store.js.map +1 -0
- package/dist/sarif/sarif-validator.d.ts +45 -0
- package/dist/sarif/sarif-validator.d.ts.map +1 -0
- package/dist/sarif/sarif-validator.js +138 -0
- package/dist/sarif/sarif-validator.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +216 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -0
- package/dist/schemas/tool-schemas.js +208 -0
- package/dist/schemas/tool-schemas.js.map +1 -0
- package/dist/sdk.d.ts +45 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/test-harness.d.ts +75 -0
- package/dist/tools/test-harness.d.ts.map +1 -0
- package/dist/tools/test-harness.js +137 -0
- package/dist/tools/test-harness.js.map +1 -0
- package/dist/workspace-guard.d.ts +53 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +61 -0
- package/dist/workspace-guard.js.map +1 -0
- package/package.json +133 -0
- package/plugin/.claude-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +18 -0
- package/plugin/CLAUDE.md +143 -0
- package/plugin/bundle/dashboard/public/index.html +368 -0
- package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/plugin/bundle/mcp-server.mjs +8718 -0
- package/plugin/bundle/mcp-server.mjs.map +7 -0
- package/plugin/bundle/tdr-engine.mjs +50 -0
- package/plugin/bundle/tdr-engine.mjs.map +7 -0
- package/plugin/hooks/hooks.json +62 -0
- package/plugin/hooks/lib/crap-config.mjs +152 -0
- package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
- package/plugin/hooks/lib/hook-io.mjs +151 -0
- package/plugin/hooks/lib/quality-gate.mjs +329 -0
- package/plugin/hooks/lib/test-harness.mjs +152 -0
- package/plugin/hooks/post-tool-use.mjs +245 -0
- package/plugin/hooks/pre-tool-use.mjs +290 -0
- package/plugin/hooks/session-start.mjs +109 -0
- package/plugin/hooks/stop-quality-gate.mjs +226 -0
- package/plugin/package.json +18 -0
- package/plugin/skills/adopt/SKILL.md +74 -0
- package/plugin/skills/analyze/SKILL.md +77 -0
- package/plugin/skills/check-test/SKILL.md +50 -0
- package/plugin/skills/score/SKILL.md +31 -0
- package/scripts/bug-report.mjs +328 -0
- package/scripts/build-fast.mjs +130 -0
- package/scripts/bundle-plugin.mjs +74 -0
- package/scripts/doctor.mjs +320 -0
- package/scripts/install.mjs +192 -0
- package/scripts/lib/cli-ui.mjs +122 -0
- package/scripts/postinstall.mjs +127 -0
- package/scripts/run-tests.mjs +95 -0
- package/scripts/status.mjs +110 -0
- package/scripts/uninstall.mjs +72 -0
- package/src/adapters/bandit.ts +191 -0
- package/src/adapters/common.ts +133 -0
- package/src/adapters/eslint.ts +187 -0
- package/src/adapters/index.ts +78 -0
- package/src/adapters/semgrep.ts +150 -0
- package/src/adapters/stryker.ts +218 -0
- package/src/ast/cyclomatic.ts +131 -0
- package/src/ast/index.ts +33 -0
- package/src/ast/language-config.ts +231 -0
- package/src/ast/tree-sitter-engine.ts +385 -0
- package/src/config.ts +109 -0
- package/src/crap-config.ts +196 -0
- package/src/dashboard/public/index.html +368 -0
- package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/src/dashboard/server.ts +205 -0
- package/src/index.ts +696 -0
- package/src/metrics/crap.ts +101 -0
- package/src/metrics/index.ts +51 -0
- package/src/metrics/score.ts +329 -0
- package/src/metrics/tdr.ts +155 -0
- package/src/metrics/workspace-walker.ts +146 -0
- package/src/sarif/index.ts +31 -0
- package/src/sarif/sarif-builder.ts +139 -0
- package/src/sarif/sarif-store.ts +347 -0
- package/src/sarif/sarif-validator.ts +145 -0
- package/src/schemas/tool-schemas.ts +225 -0
- package/src/sdk.ts +110 -0
- package/src/tests/adapters/bandit.test.ts +111 -0
- package/src/tests/adapters/dispatch.test.ts +100 -0
- package/src/tests/adapters/eslint.test.ts +138 -0
- package/src/tests/adapters/semgrep.test.ts +125 -0
- package/src/tests/adapters/stryker.test.ts +103 -0
- package/src/tests/crap-config.test.ts +228 -0
- package/src/tests/crap.test.ts +59 -0
- package/src/tests/cyclomatic.test.ts +87 -0
- package/src/tests/dashboard-http.test.ts +108 -0
- package/src/tests/dashboard-integrity.test.ts +128 -0
- package/src/tests/integration/mcp-server.integration.test.ts +352 -0
- package/src/tests/pre-tool-use-hook.test.ts +178 -0
- package/src/tests/sarif-store.test.ts +241 -0
- package/src/tests/sarif-validator.test.ts +164 -0
- package/src/tests/score.test.ts +260 -0
- package/src/tests/skills-frontmatter.test.ts +172 -0
- package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
- package/src/tests/tdr.test.ts +86 -0
- package/src/tests/test-harness.test.ts +153 -0
- package/src/tests/workspace-guard.test.ts +111 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/test-harness.ts +158 -0
- package/src/workspace-guard.ts +64 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the project score engine.
|
|
3
|
+
*
|
|
4
|
+
* Builds in-memory `SarifStore` instances with hand-crafted finding
|
|
5
|
+
* sets so we can verify each dimension's letter-grade boundaries and
|
|
6
|
+
* the overall worst-of aggregation in isolation, with no filesystem.
|
|
7
|
+
*
|
|
8
|
+
* @module tests/score.test
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, before, after } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { SarifStore, type PersistedSarif } from "../sarif/sarif-store.js";
|
|
18
|
+
import {
|
|
19
|
+
computeProjectScore,
|
|
20
|
+
renderProjectScoreMarkdown,
|
|
21
|
+
type ProjectScore,
|
|
22
|
+
} from "../metrics/score.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a minimal SARIF doc with one finding. The `ruleId`, `level`,
|
|
26
|
+
* and `effortMinutes` parameters drive how the score engine classifies
|
|
27
|
+
* the finding (security vs reliability, severity, TDR contribution).
|
|
28
|
+
*/
|
|
29
|
+
function makeSarif(opts: {
|
|
30
|
+
ruleId: string;
|
|
31
|
+
uri?: string;
|
|
32
|
+
line?: number;
|
|
33
|
+
column?: number;
|
|
34
|
+
level?: "error" | "warning" | "note";
|
|
35
|
+
effortMinutes?: number;
|
|
36
|
+
sourceTool?: string;
|
|
37
|
+
}): PersistedSarif {
|
|
38
|
+
return {
|
|
39
|
+
version: "2.1.0",
|
|
40
|
+
runs: [
|
|
41
|
+
{
|
|
42
|
+
tool: { driver: { name: opts.sourceTool ?? "test", version: "0" } },
|
|
43
|
+
results: [
|
|
44
|
+
{
|
|
45
|
+
ruleId: opts.ruleId,
|
|
46
|
+
level: opts.level ?? "warning",
|
|
47
|
+
message: { text: opts.ruleId },
|
|
48
|
+
locations: [
|
|
49
|
+
{
|
|
50
|
+
physicalLocation: {
|
|
51
|
+
artifactLocation: { uri: opts.uri ?? "src/foo.ts" },
|
|
52
|
+
region: { startLine: opts.line ?? 1, startColumn: opts.column ?? 1 },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
properties: { effortMinutes: opts.effortMinutes ?? 0 },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Construct a fresh in-memory SarifStore in a temp directory and ingest
|
|
66
|
+
* a list of findings. Each doc carries its own `tool.driver.name`, and
|
|
67
|
+
* we pass that exact name to `ingestRun()` so the per-tool aggregation
|
|
68
|
+
* tests can distinguish between scanners.
|
|
69
|
+
*/
|
|
70
|
+
async function buildStore(workspace: string, docs: PersistedSarif[]): Promise<SarifStore> {
|
|
71
|
+
const store = new SarifStore({ workspaceRoot: workspace, outputDir: "reports" });
|
|
72
|
+
await store.loadLatest();
|
|
73
|
+
for (const doc of docs) {
|
|
74
|
+
const sourceTool = doc.runs[0]?.tool?.driver?.name ?? "test-tool";
|
|
75
|
+
store.ingestRun(doc, sourceTool);
|
|
76
|
+
}
|
|
77
|
+
return store;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Helper that runs the score engine with a minimal sane config.
|
|
82
|
+
*/
|
|
83
|
+
function score(store: SarifStore, workspaceRoot: string, loc = 1000, files = 10): ProjectScore {
|
|
84
|
+
return computeProjectScore({
|
|
85
|
+
workspaceRoot,
|
|
86
|
+
minutesPerLoc: 30,
|
|
87
|
+
tdrMaxRating: "C",
|
|
88
|
+
workspace: { physicalLoc: loc, fileCount: files },
|
|
89
|
+
sarifStore: store,
|
|
90
|
+
dashboardUrl: "http://127.0.0.1:5117",
|
|
91
|
+
sarifReportPath: store.consolidatedReportPath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("computeProjectScore", () => {
|
|
96
|
+
let workspace = "";
|
|
97
|
+
|
|
98
|
+
before(async () => {
|
|
99
|
+
workspace = await mkdtemp(join(tmpdir(), "claude-crap-score-"));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
after(async () => {
|
|
103
|
+
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rates an empty project as A across the board", async () => {
|
|
107
|
+
const store = await buildStore(workspace, []);
|
|
108
|
+
const s = score(store, workspace);
|
|
109
|
+
assert.equal(s.maintainability.rating, "A");
|
|
110
|
+
assert.equal(s.reliability.rating, "A");
|
|
111
|
+
assert.equal(s.security.rating, "A");
|
|
112
|
+
assert.equal(s.overall.rating, "A");
|
|
113
|
+
assert.equal(s.overall.passes, true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("classifies a SQL injection rule as security", async () => {
|
|
117
|
+
const store = await buildStore(workspace, [
|
|
118
|
+
makeSarif({ ruleId: "python.lang.sql-injection", level: "error" }),
|
|
119
|
+
]);
|
|
120
|
+
const s = score(store, workspace);
|
|
121
|
+
assert.equal(s.security.errorFindings, 1);
|
|
122
|
+
assert.equal(s.reliability.errorFindings, 0);
|
|
123
|
+
assert.equal(s.security.rating, "D"); // 1 error → D
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("classifies a non-security rule as reliability", async () => {
|
|
127
|
+
const store = await buildStore(workspace, [
|
|
128
|
+
makeSarif({ ruleId: "ts.unused-variable", level: "warning" }),
|
|
129
|
+
]);
|
|
130
|
+
const s = score(store, workspace);
|
|
131
|
+
assert.equal(s.reliability.warningFindings, 1);
|
|
132
|
+
assert.equal(s.security.warningFindings, 0);
|
|
133
|
+
assert.equal(s.reliability.rating, "C"); // 1 warning → C
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("escalates reliability to E with 3+ errors", async () => {
|
|
137
|
+
const store = await buildStore(workspace, [
|
|
138
|
+
makeSarif({ ruleId: "rule.a", level: "error", line: 1 }),
|
|
139
|
+
makeSarif({ ruleId: "rule.b", level: "error", line: 2 }),
|
|
140
|
+
makeSarif({ ruleId: "rule.c", level: "error", line: 3 }),
|
|
141
|
+
]);
|
|
142
|
+
const s = score(store, workspace);
|
|
143
|
+
assert.equal(s.reliability.rating, "E");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("collapses overall to the worst dimension", async () => {
|
|
147
|
+
const store = await buildStore(workspace, [
|
|
148
|
+
// 1 security error → security D, reliability A, maintainability A → overall D
|
|
149
|
+
makeSarif({ ruleId: "auth.broken", level: "error" }),
|
|
150
|
+
]);
|
|
151
|
+
const s = score(store, workspace);
|
|
152
|
+
assert.equal(s.security.rating, "D");
|
|
153
|
+
assert.equal(s.reliability.rating, "A");
|
|
154
|
+
assert.equal(s.maintainability.rating, "A");
|
|
155
|
+
assert.equal(s.overall.rating, "D");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("marks overall as failing when worse than the policy ceiling", async () => {
|
|
159
|
+
const store = await buildStore(workspace, [
|
|
160
|
+
makeSarif({ ruleId: "auth.broken", level: "error" }),
|
|
161
|
+
]);
|
|
162
|
+
const s = score(store, workspace);
|
|
163
|
+
// Overall = D, policy ceiling = C → fails
|
|
164
|
+
assert.equal(s.overall.passes, false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("marks overall as passing when within policy", async () => {
|
|
168
|
+
const store = await buildStore(workspace, [
|
|
169
|
+
makeSarif({ ruleId: "ts.style", level: "warning" }),
|
|
170
|
+
]);
|
|
171
|
+
const s = score(store, workspace);
|
|
172
|
+
// Reliability = C, ceiling = C → equal, not worse, so passes
|
|
173
|
+
assert.equal(s.overall.rating, "C");
|
|
174
|
+
assert.equal(s.overall.passes, true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("derives maintainability rating from TDR boundaries", async () => {
|
|
178
|
+
// 360 minutes of remediation over 1000 LOC × 30 min/LOC = 30000 cost
|
|
179
|
+
// → TDR = 360 / 30000 = 1.2% → A
|
|
180
|
+
const store = await buildStore(workspace, [
|
|
181
|
+
makeSarif({ ruleId: "ts.todo", level: "note", effortMinutes: 360 }),
|
|
182
|
+
]);
|
|
183
|
+
const s = score(store, workspace);
|
|
184
|
+
assert.ok(s.maintainability.tdrPercent < 5);
|
|
185
|
+
assert.equal(s.maintainability.rating, "A");
|
|
186
|
+
|
|
187
|
+
// 6000 minutes / 30000 = 20% → C
|
|
188
|
+
const store2 = await buildStore(workspace, [
|
|
189
|
+
makeSarif({ ruleId: "ts.todo2", level: "note", effortMinutes: 6000 }),
|
|
190
|
+
]);
|
|
191
|
+
const s2 = score(store2, workspace);
|
|
192
|
+
assert.equal(s2.maintainability.rating, "C");
|
|
193
|
+
|
|
194
|
+
// 18000 minutes / 30000 = 60% → E
|
|
195
|
+
const store3 = await buildStore(workspace, [
|
|
196
|
+
makeSarif({ ruleId: "ts.todo3", level: "note", effortMinutes: 18000 }),
|
|
197
|
+
]);
|
|
198
|
+
const s3 = score(store3, workspace);
|
|
199
|
+
assert.equal(s3.maintainability.rating, "E");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("aggregates findings by tool and by file", async () => {
|
|
203
|
+
const store = await buildStore(workspace, [
|
|
204
|
+
makeSarif({ ruleId: "rule.a", uri: "src/a.ts", line: 1, sourceTool: "semgrep" }),
|
|
205
|
+
makeSarif({ ruleId: "rule.b", uri: "src/a.ts", line: 2, sourceTool: "semgrep" }),
|
|
206
|
+
makeSarif({ ruleId: "rule.c", uri: "src/b.ts", line: 1, sourceTool: "eslint" }),
|
|
207
|
+
]);
|
|
208
|
+
const s = score(store, workspace);
|
|
209
|
+
assert.equal(s.findings.byTool.semgrep, 2);
|
|
210
|
+
assert.equal(s.findings.byTool.eslint, 1);
|
|
211
|
+
assert.equal(s.findings.byFile["src/a.ts"], 2);
|
|
212
|
+
assert.equal(s.findings.byFile["src/b.ts"], 1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("propagates the dashboard URL into the location block", async () => {
|
|
216
|
+
const store = await buildStore(workspace, []);
|
|
217
|
+
const s = score(store, workspace);
|
|
218
|
+
assert.equal(s.location.dashboardUrl, "http://127.0.0.1:5117");
|
|
219
|
+
assert.ok(s.location.sarifReportPath.endsWith("latest.sarif"));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("renderProjectScoreMarkdown", () => {
|
|
224
|
+
let workspace = "";
|
|
225
|
+
|
|
226
|
+
before(async () => {
|
|
227
|
+
workspace = await mkdtemp(join(tmpdir(), "claude-crap-score-md-"));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
after(async () => {
|
|
231
|
+
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("renders a compact summary that includes the overall rating and dashboard URL", async () => {
|
|
235
|
+
const store = await buildStore(workspace, [
|
|
236
|
+
makeSarif({ ruleId: "ts.style", level: "warning" }),
|
|
237
|
+
]);
|
|
238
|
+
const s = score(store, workspace);
|
|
239
|
+
const md = renderProjectScoreMarkdown(s);
|
|
240
|
+
assert.match(md, /## claude-crap :: project score/);
|
|
241
|
+
assert.match(md, /\*\*Overall: C\*\*/);
|
|
242
|
+
assert.match(md, /Dashboard:.*127\.0\.0\.1:5117/);
|
|
243
|
+
assert.match(md, /Report:.*latest\.sarif/);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("renders a fallback line when no dashboard URL is configured", async () => {
|
|
247
|
+
const store = await buildStore(workspace, []);
|
|
248
|
+
const s = computeProjectScore({
|
|
249
|
+
workspaceRoot: workspace,
|
|
250
|
+
minutesPerLoc: 30,
|
|
251
|
+
tdrMaxRating: "C",
|
|
252
|
+
workspace: { physicalLoc: 100, fileCount: 1 },
|
|
253
|
+
sarifStore: store,
|
|
254
|
+
dashboardUrl: null,
|
|
255
|
+
sarifReportPath: store.consolidatedReportPath,
|
|
256
|
+
});
|
|
257
|
+
const md = renderProjectScoreMarkdown(s);
|
|
258
|
+
assert.match(md, /not running/);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter contract for claude-crap's shipped skills.
|
|
3
|
+
*
|
|
4
|
+
* Every directory under `skills/` at the plugin root must contain a
|
|
5
|
+
* SKILL.md file with YAML frontmatter declaring at minimum `name`
|
|
6
|
+
* and `description`. The `name` has to match the directory name so
|
|
7
|
+
* Claude Code's slash-command namespace (`/claude-crap:<name>`)
|
|
8
|
+
* resolves cleanly. The `description` drives model-invocation
|
|
9
|
+
* triggering — the skill-creator skill's guidance is emphatic that
|
|
10
|
+
* undertriggering is the common failure mode, so descriptions need
|
|
11
|
+
* "pushy" language like "use this skill whenever..." to bias Claude
|
|
12
|
+
* toward invoking them when context matches.
|
|
13
|
+
*
|
|
14
|
+
* These tests pin the shape of every SKILL.md the plugin ships so
|
|
15
|
+
* a future drive-by edit that removes the `description` field or
|
|
16
|
+
* renames a directory without updating the frontmatter cannot slip
|
|
17
|
+
* through CI silently. They also pin the minimum substantive length
|
|
18
|
+
* of descriptions (>100 chars) so terse one-liners like
|
|
19
|
+
* "run a command" do not count as valid skills.
|
|
20
|
+
*
|
|
21
|
+
* @module tests/skills-frontmatter.test
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it } from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
27
|
+
import { dirname, join, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const SKILLS_DIR = resolve(HERE, "..", "..", "plugin", "skills");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimal YAML frontmatter parser covering the subset claude-crap
|
|
35
|
+
* actually uses: top-level string scalars only, no nested objects,
|
|
36
|
+
* no arrays, no quoting edge cases. The full-fat YAML parsers in
|
|
37
|
+
* the ecosystem would add 100+ KB of dependencies for something we
|
|
38
|
+
* can solve in 20 lines; this stays self-contained.
|
|
39
|
+
*
|
|
40
|
+
* @param content Raw file contents of a SKILL.md file.
|
|
41
|
+
* @returns Parsed frontmatter fields, or `null` if the file
|
|
42
|
+
* does not start with a `---` delimited block.
|
|
43
|
+
*/
|
|
44
|
+
function parseFrontmatter(
|
|
45
|
+
content: string,
|
|
46
|
+
): { name?: string; description?: string; body: string } | null {
|
|
47
|
+
const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
|
|
48
|
+
if (!match) return null;
|
|
49
|
+
const [, yaml, body] = match as unknown as [string, string, string];
|
|
50
|
+
const result: { name?: string; description?: string; body: string } = { body };
|
|
51
|
+
for (const rawLine of yaml.split(/\r?\n/)) {
|
|
52
|
+
const line = rawLine.trim();
|
|
53
|
+
if (!line || line.startsWith("#")) continue;
|
|
54
|
+
const kv = line.match(/^([a-zA-Z_-]+)\s*:\s*(.+)$/);
|
|
55
|
+
if (!kv) continue;
|
|
56
|
+
const [, key, value] = kv as unknown as [string, string, string];
|
|
57
|
+
const clean = value.replace(/^['"]|['"]$/g, "");
|
|
58
|
+
if (key === "name") result.name = clean;
|
|
59
|
+
if (key === "description") result.description = clean;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Return every immediate subdirectory of `skills/`. If the root
|
|
66
|
+
* itself does not exist, returns an empty list so the test suite
|
|
67
|
+
* can still run on a fresh clone before any skills have been added.
|
|
68
|
+
*/
|
|
69
|
+
async function listSkillDirs(): Promise<string[]> {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(SKILLS_DIR, { withFileTypes: true });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const error = err as NodeJS.ErrnoException;
|
|
75
|
+
if (error.code === "ENOENT") return [];
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("claude-crap skills — frontmatter contract", () => {
|
|
82
|
+
it("ships at least one user-invocable skill", async () => {
|
|
83
|
+
const dirs = await listSkillDirs();
|
|
84
|
+
assert.ok(
|
|
85
|
+
dirs.length > 0,
|
|
86
|
+
"expected at least one skill directory under skills/ — v0.1.1 introduces /claude-crap:score, /claude-crap:check-test, /claude-crap:analyze, /claude-crap:adopt",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("every skills/<name>/ directory contains a SKILL.md file", async () => {
|
|
91
|
+
const dirs = await listSkillDirs();
|
|
92
|
+
for (const dir of dirs) {
|
|
93
|
+
const skillMd = join(SKILLS_DIR, dir, "SKILL.md");
|
|
94
|
+
const content = await readFile(skillMd, "utf8").catch(() => null);
|
|
95
|
+
assert.ok(content, `skills/${dir}/SKILL.md is missing`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("every SKILL.md starts with a --- delimited YAML frontmatter block", async () => {
|
|
100
|
+
const dirs = await listSkillDirs();
|
|
101
|
+
for (const dir of dirs) {
|
|
102
|
+
const content = await readFile(join(SKILLS_DIR, dir, "SKILL.md"), "utf8");
|
|
103
|
+
const fm = parseFrontmatter(content);
|
|
104
|
+
assert.ok(
|
|
105
|
+
fm,
|
|
106
|
+
`skills/${dir}/SKILL.md must start with '---' ... '---' YAML frontmatter`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("every frontmatter declares a non-empty name matching the directory", async () => {
|
|
112
|
+
const dirs = await listSkillDirs();
|
|
113
|
+
for (const dir of dirs) {
|
|
114
|
+
const content = await readFile(join(SKILLS_DIR, dir, "SKILL.md"), "utf8");
|
|
115
|
+
const fm = parseFrontmatter(content);
|
|
116
|
+
assert.ok(fm?.name, `skills/${dir}/SKILL.md frontmatter must have a 'name' field`);
|
|
117
|
+
assert.equal(
|
|
118
|
+
fm.name,
|
|
119
|
+
dir,
|
|
120
|
+
`skills/${dir}/SKILL.md frontmatter name '${fm.name}' must match the directory basename '${dir}'`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("every frontmatter declares a substantive description (>100 chars)", async () => {
|
|
126
|
+
const dirs = await listSkillDirs();
|
|
127
|
+
for (const dir of dirs) {
|
|
128
|
+
const content = await readFile(join(SKILLS_DIR, dir, "SKILL.md"), "utf8");
|
|
129
|
+
const fm = parseFrontmatter(content);
|
|
130
|
+
assert.ok(
|
|
131
|
+
fm?.description,
|
|
132
|
+
`skills/${dir}/SKILL.md frontmatter must have a 'description' field`,
|
|
133
|
+
);
|
|
134
|
+
assert.ok(
|
|
135
|
+
fm.description.length > 100,
|
|
136
|
+
`skills/${dir}/SKILL.md description is only ${fm.description.length} chars; Claude Code's trigger matcher needs substantive context to invoke the skill. Rewrite it with 'use this skill whenever ...' phrasing and at least one example context.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("every description uses 'use this skill when/whenever' trigger language", async () => {
|
|
142
|
+
// Rationale: the skill-creator guidance flags undertriggering as the
|
|
143
|
+
// common failure mode and recommends 'pushy' descriptions that bias
|
|
144
|
+
// Claude toward invoking the skill. Enforcing a minimal version of
|
|
145
|
+
// that discipline at the lint level means every future skill has to
|
|
146
|
+
// at least consider when it should trigger before merging.
|
|
147
|
+
const dirs = await listSkillDirs();
|
|
148
|
+
for (const dir of dirs) {
|
|
149
|
+
const content = await readFile(join(SKILLS_DIR, dir, "SKILL.md"), "utf8");
|
|
150
|
+
const fm = parseFrontmatter(content);
|
|
151
|
+
assert.ok(fm?.description);
|
|
152
|
+
assert.match(
|
|
153
|
+
fm.description,
|
|
154
|
+
/use this skill (when|whenever)/i,
|
|
155
|
+
`skills/${dir}/SKILL.md description should include 'use this skill when/whenever ...' trigger language (see the skill-creator guidance on combating undertriggering)`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("every SKILL.md body is non-empty", async () => {
|
|
161
|
+
const dirs = await listSkillDirs();
|
|
162
|
+
for (const dir of dirs) {
|
|
163
|
+
const content = await readFile(join(SKILLS_DIR, dir, "SKILL.md"), "utf8");
|
|
164
|
+
const fm = parseFrontmatter(content);
|
|
165
|
+
assert.ok(fm, `${dir}/SKILL.md must parse`);
|
|
166
|
+
assert.ok(
|
|
167
|
+
fm.body.trim().length > 0,
|
|
168
|
+
`skills/${dir}/SKILL.md must have instructions after the frontmatter`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for the Stop quality-gate hook under each of the
|
|
3
|
+
* three supported strictness modes.
|
|
4
|
+
*
|
|
5
|
+
* The test spawns `hooks/stop-quality-gate.mjs` as a subprocess with
|
|
6
|
+
* a hand-crafted fixture workspace containing:
|
|
7
|
+
*
|
|
8
|
+
* - `.claude-crap/reports/latest.sarif` — one error-level finding
|
|
9
|
+
* so the gate has a reason to fail.
|
|
10
|
+
* - (optional) `.claude-crap.json` — exercises file-based config.
|
|
11
|
+
* - One small `.ts` file so the workspace walker returns a non-zero
|
|
12
|
+
* LOC denominator and TDR math does not divide by one.
|
|
13
|
+
*
|
|
14
|
+
* The test then asserts on the subprocess exit code and where the
|
|
15
|
+
* verdict was written (stdout vs stderr) for each mode, matching the
|
|
16
|
+
* design in the CHANGELOG:
|
|
17
|
+
*
|
|
18
|
+
* - `strict` — exit 2, verdict on stderr (hard block)
|
|
19
|
+
* - `warn` — exit 0, verdict on stdout (soft nudge, agent sees it)
|
|
20
|
+
* - `advisory` — exit 0, one-liner on stdout (minimal pressure)
|
|
21
|
+
*
|
|
22
|
+
* These tests require `dist/` to be built because the Stop hook
|
|
23
|
+
* imports `dist/metrics/tdr.js` at runtime. The suite skips cleanly
|
|
24
|
+
* on fresh checkouts before the first build.
|
|
25
|
+
*
|
|
26
|
+
* @module tests/stop-quality-gate-strictness.test
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, before, after } from "node:test";
|
|
30
|
+
import assert from "node:assert/strict";
|
|
31
|
+
import { spawn } from "node:child_process";
|
|
32
|
+
import { promises as fs, statSync } from "node:fs";
|
|
33
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
34
|
+
import { tmpdir } from "node:os";
|
|
35
|
+
import { dirname, join, resolve } from "node:path";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
37
|
+
|
|
38
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const PLUGIN_ROOT = resolve(HERE, "..", "..");
|
|
40
|
+
const HOOK_PATH = join(PLUGIN_ROOT, "plugin", "hooks", "stop-quality-gate.mjs");
|
|
41
|
+
const TDR_ENTRY = process.env.SONAR_TDR_ENTRY
|
|
42
|
+
? resolve(process.env.SONAR_TDR_ENTRY)
|
|
43
|
+
: join(PLUGIN_ROOT, "plugin", "bundle", "tdr-engine.mjs");
|
|
44
|
+
|
|
45
|
+
let bundleBuilt = false;
|
|
46
|
+
try {
|
|
47
|
+
statSync(TDR_ENTRY);
|
|
48
|
+
bundleBuilt = true;
|
|
49
|
+
} catch {
|
|
50
|
+
bundleBuilt = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface HookResult {
|
|
54
|
+
readonly code: number;
|
|
55
|
+
readonly stdout: string;
|
|
56
|
+
readonly stderr: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Spawn the Stop hook against a fixture workspace, passing an empty
|
|
61
|
+
* JSON payload on stdin (the Stop hook does not depend on the hook
|
|
62
|
+
* input — the verdict is entirely a function of the on-disk SARIF
|
|
63
|
+
* plus the workspace LOC walk).
|
|
64
|
+
*/
|
|
65
|
+
function runStopHook(
|
|
66
|
+
workspace: string,
|
|
67
|
+
envOverrides: Record<string, string | undefined> = {},
|
|
68
|
+
): Promise<HookResult> {
|
|
69
|
+
return new Promise((resolvePromise, reject) => {
|
|
70
|
+
const env: Record<string, string> = {
|
|
71
|
+
PATH: process.env.PATH ?? "",
|
|
72
|
+
NODE_ENV: "test",
|
|
73
|
+
CLAUDE_PROJECT_DIR: workspace,
|
|
74
|
+
};
|
|
75
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
76
|
+
if (value === undefined) delete env[key];
|
|
77
|
+
else env[key] = value;
|
|
78
|
+
}
|
|
79
|
+
const child = spawn(process.execPath, [HOOK_PATH], {
|
|
80
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
81
|
+
cwd: workspace,
|
|
82
|
+
env,
|
|
83
|
+
});
|
|
84
|
+
let stdout = "";
|
|
85
|
+
let stderr = "";
|
|
86
|
+
child.stdout.on("data", (chunk) => {
|
|
87
|
+
stdout += chunk.toString();
|
|
88
|
+
});
|
|
89
|
+
child.stderr.on("data", (chunk) => {
|
|
90
|
+
stderr += chunk.toString();
|
|
91
|
+
});
|
|
92
|
+
child.on("error", reject);
|
|
93
|
+
child.on("exit", (code) => {
|
|
94
|
+
resolvePromise({ code: code ?? -1, stdout, stderr });
|
|
95
|
+
});
|
|
96
|
+
child.stdin.write("{}");
|
|
97
|
+
child.stdin.end();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build a fixture workspace with a failing SARIF report on disk and
|
|
103
|
+
* one minimal source file so the LOC walk returns a sensible
|
|
104
|
+
* denominator for the TDR computation.
|
|
105
|
+
*/
|
|
106
|
+
async function createFailingFixture(): Promise<string> {
|
|
107
|
+
const workspace = await mkdtemp(join(tmpdir(), "claude-crap-strict-"));
|
|
108
|
+
const reportsDir = join(workspace, ".claude-crap", "reports");
|
|
109
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
110
|
+
|
|
111
|
+
// One error-level finding guarantees the SONAR-GATE-ERRORS policy fails.
|
|
112
|
+
const sarif = {
|
|
113
|
+
version: "2.1.0",
|
|
114
|
+
runs: [
|
|
115
|
+
{
|
|
116
|
+
tool: { driver: { name: "claude-crap-fixture", version: "0.0.0" } },
|
|
117
|
+
results: [
|
|
118
|
+
{
|
|
119
|
+
ruleId: "FIXTURE-ERR-001",
|
|
120
|
+
level: "error",
|
|
121
|
+
message: { text: "fixture error so the gate has something to block on" },
|
|
122
|
+
locations: [
|
|
123
|
+
{
|
|
124
|
+
physicalLocation: {
|
|
125
|
+
artifactLocation: { uri: "src/a.ts" },
|
|
126
|
+
region: { startLine: 1, startColumn: 1 },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
properties: {
|
|
131
|
+
sourceTool: "claude-crap-fixture",
|
|
132
|
+
effortMinutes: 90,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
await fs.writeFile(
|
|
140
|
+
join(reportsDir, "latest.sarif"),
|
|
141
|
+
JSON.stringify(sarif, null, 2),
|
|
142
|
+
"utf8",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// A non-zero physical LOC means TDR math is well-defined.
|
|
146
|
+
await fs.mkdir(join(workspace, "src"), { recursive: true });
|
|
147
|
+
await fs.writeFile(
|
|
148
|
+
join(workspace, "src", "a.ts"),
|
|
149
|
+
"export const answer = 42;\n",
|
|
150
|
+
"utf8",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return workspace;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe(
|
|
157
|
+
"stop-quality-gate hook — strictness matrix",
|
|
158
|
+
{ skip: !bundleBuilt },
|
|
159
|
+
() => {
|
|
160
|
+
let workspace = "";
|
|
161
|
+
|
|
162
|
+
before(async () => {
|
|
163
|
+
workspace = await createFailingFixture();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
after(async () => {
|
|
167
|
+
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("default (no env, no file) → exit 2 + stderr box (strict is the default)", async () => {
|
|
171
|
+
const result = await runStopHook(workspace, {
|
|
172
|
+
CLAUDE_CRAP_STRICTNESS: undefined,
|
|
173
|
+
});
|
|
174
|
+
assert.equal(result.code, 2, `stderr was: ${result.stderr}`);
|
|
175
|
+
assert.match(result.stderr, /Stop quality gate BLOCKED/);
|
|
176
|
+
assert.match(result.stderr, /SONAR-GATE-ERRORS/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("CLAUDE_CRAP_STRICTNESS=strict → exit 2 + stderr box", async () => {
|
|
180
|
+
const result = await runStopHook(workspace, {
|
|
181
|
+
CLAUDE_CRAP_STRICTNESS: "strict",
|
|
182
|
+
});
|
|
183
|
+
assert.equal(result.code, 2);
|
|
184
|
+
assert.match(result.stderr, /Stop quality gate BLOCKED/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("CLAUDE_CRAP_STRICTNESS=warn → exit 0 + full verdict on stdout", async () => {
|
|
188
|
+
const result = await runStopHook(workspace, {
|
|
189
|
+
CLAUDE_CRAP_STRICTNESS: "warn",
|
|
190
|
+
});
|
|
191
|
+
assert.equal(result.code, 0, `stderr was: ${result.stderr}`);
|
|
192
|
+
// The full verdict must still reach the hook transcript so the
|
|
193
|
+
// agent can choose to remediate on its next turn.
|
|
194
|
+
assert.match(result.stdout, /Stop quality gate WARNING/);
|
|
195
|
+
assert.match(result.stdout, /SONAR-GATE-ERRORS/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("CLAUDE_CRAP_STRICTNESS=advisory → exit 0 + one-line summary on stdout", async () => {
|
|
199
|
+
const result = await runStopHook(workspace, {
|
|
200
|
+
CLAUDE_CRAP_STRICTNESS: "advisory",
|
|
201
|
+
});
|
|
202
|
+
assert.equal(result.code, 0, `stderr was: ${result.stderr}`);
|
|
203
|
+
assert.match(result.stdout, /Stop quality gate ADVISORY/);
|
|
204
|
+
// Advisory must NOT render the heavy multi-line verdict box —
|
|
205
|
+
// the point is minimal pressure. Walking the stdout for the
|
|
206
|
+
// "policy failure(s)" decorator from the blocking/warning box
|
|
207
|
+
// would be a robust negative assertion.
|
|
208
|
+
assert.doesNotMatch(result.stdout, /policy failure\(s\)/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it(".claude-crap.json with strictness='warn' is honored when env is unset", async () => {
|
|
212
|
+
const configPath = join(workspace, ".claude-crap.json");
|
|
213
|
+
await fs.writeFile(configPath, JSON.stringify({ strictness: "warn" }), "utf8");
|
|
214
|
+
try {
|
|
215
|
+
const result = await runStopHook(workspace, {
|
|
216
|
+
CLAUDE_CRAP_STRICTNESS: undefined,
|
|
217
|
+
});
|
|
218
|
+
assert.equal(result.code, 0, `stderr was: ${result.stderr}`);
|
|
219
|
+
assert.match(result.stdout, /Stop quality gate WARNING/);
|
|
220
|
+
} finally {
|
|
221
|
+
await fs.unlink(configPath);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("env variable wins over .claude-crap.json even when the file disagrees", async () => {
|
|
226
|
+
const configPath = join(workspace, ".claude-crap.json");
|
|
227
|
+
await fs.writeFile(
|
|
228
|
+
configPath,
|
|
229
|
+
JSON.stringify({ strictness: "advisory" }),
|
|
230
|
+
"utf8",
|
|
231
|
+
);
|
|
232
|
+
try {
|
|
233
|
+
const result = await runStopHook(workspace, {
|
|
234
|
+
CLAUDE_CRAP_STRICTNESS: "strict",
|
|
235
|
+
});
|
|
236
|
+
assert.equal(result.code, 2, `stderr was: ${result.stderr}`);
|
|
237
|
+
assert.match(result.stderr, /Stop quality gate BLOCKED/);
|
|
238
|
+
} finally {
|
|
239
|
+
await fs.unlink(configPath);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
);
|