-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.js
More file actions
336 lines (281 loc) · 13 KB
/
Copy pathverify.js
File metadata and controls
336 lines (281 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
#!/usr/bin/env node
// avoid-ai -- test suite
// Run: node tests/verify.js
const fs = require("fs");
const os = require("os");
const path = require("path");
const { execSync, spawnSync } = require("child_process");
const ROOT = path.join(__dirname, "..");
const CHECK = path.join(ROOT, "src/scripts/check.js");
const PREWRITE = path.join(ROOT, "src/hooks/avoid-ai-prewrite.js");
const CONFIG = path.join(ROOT, "src/hooks/avoid-ai-config.js");
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "avoid-ai-test-"));
let passed = 0;
let failed = 0;
function test(label, fn) {
try {
fn();
console.log(" PASS " + label);
passed++;
} catch (e) {
console.log(" FAIL " + label);
console.log(" " + e.message);
failed++;
}
}
function assert(cond, msg) {
if (!cond) throw new Error(msg || "assertion failed");
}
function tmpFile(name, content) {
const p = path.join(tmpDir, name);
fs.writeFileSync(p, content, "utf8");
return p;
}
function runCheck(filePath) {
return spawnSync("node", [CHECK, filePath], { encoding: "utf8" });
}
function runPrewrite(toolName, content) {
const input = JSON.stringify({
tool_name: toolName,
tool_input: toolName === "Write"
? { content }
: toolName === "Edit"
? { new_string: content }
: { edits: [{ new_string: content }] }
});
const flagPath = path.join(tmpDir, ".avoid-ai-active");
fs.writeFileSync(flagPath, "on", "utf8");
const env = Object.assign({}, process.env, { CLAUDE_CONFIG_DIR: tmpDir });
return spawnSync("node", [PREWRITE], { input, encoding: "utf8", env });
}
// ── check.js tests ────────────────────────────────────────────────────────
console.log("\ncheck.js");
test("clean file -> exit 0, prints OK", () => {
const f = tmpFile("clean.md", "Hello world. No issues here.\n");
const r = runCheck(f);
assert(r.status === 0, "expected exit 0, got " + r.status);
assert(r.stdout.includes("OK"), "expected OK in output");
});
test("em dash -> exit 1, P1 finding", () => {
const f = tmpFile("emdash.md", "Some text \u2014 more text\n");
const r = runCheck(f);
assert(r.status === 1, "expected exit 1");
assert(r.stdout.includes("P1"), "expected P1 in output");
assert(r.stdout.includes("EM DASH"), "expected EM DASH in output");
});
test("non-breaking space -> exit 1, P0 finding", () => {
const f = tmpFile("nbsp.md", "Word word\n");
const r = runCheck(f);
assert(r.status === 1, "expected exit 1");
assert(r.stdout.includes("P0"), "expected P0 in output");
assert(r.stdout.includes("NON-BREAKING"), "expected NON-BREAKING in output");
});
test("zero-width space -> exit 1, P0 finding", () => {
const f = tmpFile("zwsp.md", "Wordword\n");
const r = runCheck(f);
assert(r.status === 1, "expected exit 1");
assert(r.stdout.includes("U+200B"), "expected U+200B in output");
});
test("correct line:col reported", () => {
const f = tmpFile("linecol.md", "line1\nline2 \u2014 here\n");
const r = runCheck(f);
assert(r.stdout.includes("line 2:7"), "expected line 2:7, got: " + r.stdout);
});
test("multiple findings counted in summary", () => {
const f = tmpFile("multi.md", "a\u2014b\u2014c\n");
const r = runCheck(f);
assert(r.stdout.includes("2 P1"), "expected 2 P1 in summary");
});
test("BOM at start -> P0", () => {
const f = tmpFile("bom.md", "Hello\n");
const r = runCheck(f);
assert(r.stdout.includes("P0"), "expected P0");
assert(r.stdout.includes("BOM"), "expected BOM label");
});
test("--fix replaces em dash with keyboard hyphen", () => {
const f = tmpFile("fix.md", "text \u2014 more\n");
spawnSync("node", [CHECK, f, "--fix"], { encoding: "utf8" });
const fixed = f.replace(".md", ".fixed.md");
assert(fs.existsSync(fixed), ".fixed.md not created");
const content = fs.readFileSync(fixed, "utf8");
assert(content.includes("text - more"), "expected hyphen replacement");
assert(!content.includes("\u2014"), "literal em dash still present");
});
test("--fix replaces ellipsis with three dots", () => {
const f = tmpFile("fix-ellipsis.md", "wait\u2026 and see\n");
spawnSync("node", [CHECK, f, "--fix"], { encoding: "utf8" });
const fixed = f.replace(".md", ".fixed.md");
const content = fs.readFileSync(fixed, "utf8");
assert(content.includes("..."), "expected ... replacement");
assert(!content.includes("\u2026"), "literal ellipsis still present");
});
test("--fix strips invisible chars, preserves text", () => {
const f = tmpFile("fix-invis.md", "hello\u200Bworld\n");
spawnSync("node", [CHECK, f, "--fix"], { encoding: "utf8" });
const fixed = f.replace(".md", ".fixed.md");
const content = fs.readFileSync(fixed, "utf8");
assert(content.includes("helloworld"), "expected invisible char stripped");
assert(!content.includes("\u200B"), "zero-width space still present");
});
// ── prewrite hook tests ───────────────────────────────────────────────────
console.log("\nprewrite hook");
test("Write with em dash -> blocked", () => {
const r = runPrewrite("Write", "hello \u2014 world");
const out = JSON.parse(r.stdout);
assert(out.decision === "block", "expected block, got: " + JSON.stringify(out));
});
test("Write without em dash -> approve (exit 0, no block)", () => {
const r = runPrewrite("Write", "clean content here");
const out = r.stdout.trim();
if (out) {
const parsed = JSON.parse(out);
assert(parsed.decision !== "block", "got unexpected block");
}
assert(r.status === 0, "expected exit 0");
});
test("Edit new_string with em dash -> blocked", () => {
const r = runPrewrite("Edit", "new \u2014 value");
const out = JSON.parse(r.stdout);
assert(out.decision === "block", "expected block");
});
test("MultiEdit with em dash -> blocked", () => {
const r = runPrewrite("MultiEdit", "text \u2014 here");
const out = JSON.parse(r.stdout);
assert(out.decision === "block", "expected block");
});
test("non-Write tool -> approve (exit 0)", () => {
const input = JSON.stringify({ tool_name: "Bash", tool_input: { command: "echo hi" } });
const flagPath = path.join(tmpDir, ".avoid-ai-active");
fs.writeFileSync(flagPath, "on", "utf8");
const env = Object.assign({}, process.env, { CLAUDE_CONFIG_DIR: tmpDir });
const r = spawnSync("node", [PREWRITE], { input, encoding: "utf8", env });
assert(r.status === 0, "expected exit 0 for non-write tool");
assert(!r.stdout.includes("block"), "should not block non-write tool");
});
test("avoid-ai inactive -> approve even with em dash", () => {
const flagPath = path.join(tmpDir, ".avoid-ai-active");
try { fs.unlinkSync(flagPath); } catch (e) {}
const input = JSON.stringify({ tool_name: "Write", tool_input: { content: "text \u2014 here" } });
const env = Object.assign({}, process.env, { CLAUDE_CONFIG_DIR: tmpDir });
const r = spawnSync("node", [PREWRITE], { input, encoding: "utf8", env });
assert(r.status === 0, "expected exit 0 when inactive");
assert(!r.stdout.includes("block"), "should not block when inactive");
});
// ── config tests ──────────────────────────────────────────────────────────
console.log("\nconfig");
test("getDefaultMode returns valid mode", () => {
const { getDefaultMode, VALID_MODES } = require(CONFIG);
const mode = getDefaultMode();
assert(VALID_MODES.includes(mode), "invalid mode: " + mode);
});
test("safeWriteFlag + readFlag roundtrip", () => {
const { safeWriteFlag, readFlag } = require(CONFIG);
const fp = path.join(tmpDir, ".avoid-ai-test-flag");
safeWriteFlag(fp, "strict");
const read = readFlag(fp);
assert(read === "strict", "expected strict, got " + read);
fs.unlinkSync(fp);
});
test("readFlag returns null for missing file", () => {
const { readFlag } = require(CONFIG);
const fp = path.join(tmpDir, ".nonexistent-flag");
assert(readFlag(fp) === null, "expected null for missing flag");
});
test("en dash -> P1 finding", () => {
const f = tmpFile("endash.md", "pages 10–15\n");
const r = runCheck(f);
assert(r.status === 1, "expected exit 1");
assert(r.stdout.includes("EN DASH"), "expected EN DASH");
});
test("ellipsis char -> P1 finding", () => {
const f = tmpFile("ellipsis.md", "wait…\n");
const r = runCheck(f);
assert(r.stdout.includes("ELLIPSIS"), "expected ELLIPSIS");
});
test("typographic apostrophe -> P1 finding", () => {
const f = tmpFile("apos.md", "don’t\n");
const r = runCheck(f);
assert(r.stdout.includes("APOSTROPHE"), "expected APOSTROPHE");
});
test("cyrillic homoglyph in latin text -> P0 finding", () => {
const f = tmpFile("homo.md", "heоllo\n");
const r = runCheck(f);
assert(r.status === 1, "expected exit 1");
assert(r.stdout.includes("HOMOGLYPH"), "expected HOMOGLYPH");
assert(r.stdout.includes("P0"), "expected P0 severity");
});
test("word joiner -> P0 finding", () => {
const f = tmpFile("wj.md", "wordjoiner\n");
const r = runCheck(f);
assert(r.stdout.includes("WORD JOINER"), "expected WORD JOINER");
});
// ── entropy tests ────────────────────────────────────────────────────────
console.log("\nentropy");
test("uniform paragraphs -> ENTROPY warning", () => {
// 4 paragraphs of nearly identical length
const body = [
"This is paragraph one with some content here to fill it out nicely.",
"This is paragraph two with some content here to fill it out nicely.",
"This is paragraph three with content here to fill it out just right.",
"This is paragraph four with some content here to fill it out nicely.",
].join("\n\n");
const f = tmpFile("entropy-para.md", body);
const r = runCheck(f);
assert(r.stdout.includes("ENTROPY"), "expected ENTROPY warning for uniform paragraphs: " + r.stdout);
});
test("high connector count -> ENTROPY warning", () => {
const body = "Moreover this is true. Furthermore it holds. Additionally we note. However it is complex. Therefore we conclude.";
const f = tmpFile("entropy-connectors.md", body);
const r = runCheck(f);
assert(r.stdout.includes("ENTROPY"), "expected ENTROPY warning for connectors: " + r.stdout);
});
test("varied text -> no ENTROPY warning", () => {
const body = "Short.\n\nThis paragraph is considerably longer and covers much more ground than the first one does, with multiple clauses and ideas.\n\nMedium length paragraph here with a couple of sentences. It rounds things out.";
const f = tmpFile("entropy-ok.md", body);
const r = runCheck(f);
assert(!r.stdout.includes("ENTROPY"), "unexpected ENTROPY warning for varied text");
});
// golden fixture tests
// tests/fixtures/golden.txt is generated by Python with exact byte layout.
// Every line:col assertion here is derived from known codepoint positions,
// not from running the scanner and copying output.
//
// golden.txt layout:
// line 1: "hello [EM DASH] world" U+2014 at codepoint col 7
// line 2: "word [ZWSP]word" U+200B at codepoint col 6
// line 3: "long word [NBSP]here" U+00A0 at codepoint col 10
// line 4: "[emoji]text [EM DASH] end" U+2014 at codepoint col 7 (emoji counts as 1)
// line 5: "clean line no issues" no findings
console.log("\ngolden fixture");
const GOLDEN = path.join(ROOT, "tests/fixtures/golden.txt");
test("golden: em dash on line 1 at col 7", () => {
const r = runCheck(GOLDEN);
assert(r.status === 1, "expected exit 1");
assert(r.stdout.includes("line 1:7"), "expected line 1:7, got:\n" + r.stdout);
assert(r.stdout.includes("EM DASH"), "expected EM DASH label");
});
test("golden: ZWSP on line 2 at col 6", () => {
const r = runCheck(GOLDEN);
assert(r.stdout.includes("line 2:6"), "expected line 2:6, got:\n" + r.stdout);
assert(r.stdout.includes("ZERO-WIDTH SPACE"), "expected ZERO-WIDTH SPACE label");
});
test("golden: NBSP on line 3 at col 10", () => {
const r = runCheck(GOLDEN);
assert(r.stdout.includes("line 3:10"), "expected line 3:10, got:\n" + r.stdout);
assert(r.stdout.includes("NON-BREAKING"), "expected NON-BREAKING label");
});
test("golden: em dash on line 4 at codepoint col 7 (emoji before it counts as 1)", () => {
const r = runCheck(GOLDEN);
assert(r.stdout.includes("line 4:7"), "expected line 4:7 (codepoint col), got:\n" + r.stdout);
});
test("golden: exactly 4 findings total (line 5 is clean)", () => {
const r = runCheck(GOLDEN);
const matches = r.stdout.match(/(\d+) P0.*?(\d+) P1/);
assert(matches, "summary line not found");
const total = parseInt(matches[1]) + parseInt(matches[2]);
assert(total === 4, "expected exactly 4 findings, got " + total);
});
// ── cleanup ───────────────────────────────────────────────────────────────
fs.rmSync(tmpDir, { recursive: true, force: true });
console.log("\n" + passed + " passed " + failed + " failed");
process.exit(failed > 0 ? 1 : 0);