@hasna/hooks 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/bin/index.js +315 -67
  2. package/bin/serve.js +12 -0
  3. package/dist/index.js +52 -4
  4. package/dist/lib/installer.d.ts +40 -1
  5. package/dist/lib/registration.d.ts +75 -0
  6. package/dist/lib/registry.d.ts +14 -0
  7. package/hooks/hook-agent-rules-version-check/README.md +1 -1
  8. package/hooks/hook-trash-guard/README.md +132 -0
  9. package/hooks/hook-trash-guard/package.json +12 -0
  10. package/hooks/hook-trash-guard/src/hook.ts +1114 -0
  11. package/package.json +4 -2
  12. package/hooks/codewith-native-common.test.ts +0 -1935
  13. package/hooks/hook-affected-tests/tsconfig.json +0 -25
  14. package/hooks/hook-agent-rules-version-check/src/hook.test.ts +0 -104
  15. package/hooks/hook-agent-rules-version-check/tsconfig.json +0 -25
  16. package/hooks/hook-announce-start/tsconfig.json +0 -25
  17. package/hooks/hook-announce-stop/tsconfig.json +0 -25
  18. package/hooks/hook-autoformat/tsconfig.json +0 -25
  19. package/hooks/hook-branchprotect/tsconfig.json +0 -25
  20. package/hooks/hook-checkbugs/tsconfig.json +0 -15
  21. package/hooks/hook-checkdocs/tsconfig.json +0 -15
  22. package/hooks/hook-checkfiles/tsconfig.json +0 -15
  23. package/hooks/hook-checklint/tsconfig.json +0 -15
  24. package/hooks/hook-checkpoint/tsconfig.json +0 -25
  25. package/hooks/hook-checksecurity/tsconfig.json +0 -15
  26. package/hooks/hook-checktasks/tsconfig.json +0 -20
  27. package/hooks/hook-checktests/tsconfig.json +0 -15
  28. package/hooks/hook-conflict-detect/tsconfig.json +0 -25
  29. package/hooks/hook-contextrefresh/tsconfig.json +0 -25
  30. package/hooks/hook-desktopnotify/tsconfig.json +0 -25
  31. package/hooks/hook-dm-inject/tsconfig.json +0 -25
  32. package/hooks/hook-envsetup/tsconfig.json +0 -25
  33. package/hooks/hook-failure-to-task/tsconfig.json +0 -25
  34. package/hooks/hook-filelock/tsconfig.json +0 -25
  35. package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +0 -302
  36. package/hooks/hook-fleet-blockers-gate/tsconfig.json +0 -25
  37. package/hooks/hook-fleet-catchup/src/hook.test.ts +0 -156
  38. package/hooks/hook-fleet-catchup/tsconfig.json +0 -25
  39. package/hooks/hook-gitguard/tsconfig.json +0 -25
  40. package/hooks/hook-knowledge-context/src/hook.test.ts +0 -379
  41. package/hooks/hook-packageage/tsconfig.json +0 -25
  42. package/hooks/hook-permissionguard/tsconfig.json +0 -25
  43. package/hooks/hook-phonenotify/tsconfig.json +0 -25
  44. package/hooks/hook-precompact/tsconfig.json +0 -25
  45. package/hooks/hook-protectfiles/tsconfig.json +0 -25
  46. package/hooks/hook-scanoutput/src/hook.test.ts +0 -217
  47. package/hooks/hook-spiral-detector/src/hook.test.ts +0 -72
  48. package/hooks/hook-stylescheck/tsconfig.json +0 -25
  49. package/hooks/hook-typecheck-gate/tsconfig.json +0 -25
  50. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +0 -466
  51. package/hooks/hook-workspace-repos-guard/tsconfig.json +0 -21
  52. package/hooks/mention-context/src/hook.test.ts +0 -68
@@ -1,466 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { homedir } from "os";
3
- import { join } from "path";
4
- import { evaluate, resolveAllowedOrgs } from "./hook";
5
-
6
- const HOME = homedir();
7
- const REPOS = join(HOME, ".hasna", "repos", "clones");
8
- const LEGACY = join(HOME, "workspace", "repos");
9
-
10
- function preToolUse(tool_name: string, tool_input: Record<string, unknown>, cwd = join(REPOS, "hasna", "apps")) {
11
- return { hook_event_name: "PreToolUse", tool_name, tool_input, cwd };
12
- }
13
-
14
- function isBlocked(result: { output: { decision?: string; reason?: string } }): string | null {
15
- if (result.output.decision === "block") return result.output.reason ?? "blocked";
16
- return null;
17
- }describe("hook-workspace-repos-guard", () => {
18
- describe("resolveAllowedOrgs", () => {
19
- test("defaults to the three public canonical GitHub orgs", () => {
20
- expect([...resolveAllowedOrgs({})].sort()).toEqual(["hasna", "hasna-products", "hasnaxyz"]);
21
- });
22
-
23
- test("parses comma-separated env override, trimming whitespace and empties", () => {
24
- expect([...resolveAllowedOrgs({ WORKSPACE_REPOS_GUARD_ORGS: " hasna , myorg ,, " })].sort()).toEqual(["hasna", "myorg"]);
25
- });
26
- });
27
-
28
- describe("positive controls — must continue", () => {
29
- test("Write deep inside an allowed org checkout continues", () => {
30
- const result = evaluate(preToolUse("Write", { file_path: join(REPOS, "hasna", "apps", "apps", "foo", "src", "index.ts") }));
31
- expect(result.output.continue).toBe(true);
32
- expect(isBlocked(result)).toBeNull();
33
- });
34
-
35
- test("Edit inside a non-default org is blocked", () => {
36
- const result = evaluate(preToolUse("Edit", { file_path: join(REPOS, "notanorg", "platform", "docs", "readme.md") }));
37
- expect(isBlocked(result)).not.toBeNull();
38
- });
39
-
40
- test("MultiEdit with relative file_path resolves against cwd and continues inside allowed org", () => {
41
- const result = evaluate(preToolUse("MultiEdit", { file_path: "apps/hooks/src/lib/registry.ts" }));
42
- expect(result.output.continue).toBe(true);
43
- });
44
-
45
- test("NotebookEdit inside allowed org continues", () => {
46
- const result = evaluate(preToolUse("NotebookEdit", { notebook_path: join(REPOS, "hasnaxyz", "iapp-probe", "probe.ipynb") }));
47
- expect(result.output.continue).toBe(true);
48
- });
49
-
50
- test("Bash read commands under repos are never blocked", () => {
51
- const reads = [
52
- `ls ${REPOS}`,
53
- `cat ${join(REPOS, "hasna", "apps", "README.md")}`,
54
- `git -C ${join(REPOS, "hasna", "apps")} status`,
55
- `cd ${REPOS} && git pull`,
56
- `find ${join(REPOS, "hasna")} -name AGENTS.md`,
57
- ];
58
- for (const command of reads) {
59
- const result = evaluate(preToolUse("Bash", { command }));
60
- expect(result.output.continue).toBe(true);
61
- }
62
- });
63
-
64
- test("Bash write deep inside an allowed org checkout continues", () => {
65
- const commands = [
66
- `echo note > ${join(REPOS, "hasna", "apps", "scratch.md")}`,
67
- `touch ${join(REPOS, "hasna", "apps", "apps", "foo", "x.txt")}`,
68
- `git clone https://github.com/hasna/foo.git ${join(REPOS, "hasna", "foo")}`,
69
- `mkdir -p ${join(REPOS, "hasnaxyz", "iapp-probe", "docs")}`,
70
- ];
71
- for (const command of commands) {
72
- const result = evaluate(preToolUse("Bash", { command }));
73
- expect(result.output.continue).toBe(true);
74
- }
75
- });
76
-
77
- test("tilde/$HOME spellings of deep allowed-org writes continue", () => {
78
- const commands = [
79
- `touch ~/.hasna/repos/clones/hasna/apps/foo.md`,
80
- `echo x > ~/.hasna/repos/clones/hasnaxyz/iapp-probe/notes.md`,
81
- `mkdir -p "${HOME}/.hasna/repos/clones/hasnaxyz/iapp-probe/notes"`,
82
- `touch "$HOME"/.hasna/repos/clones/hasna/apps/x.txt`,
83
- `echo x > "\${HOME}"/.hasna/repos/clones/hasnaxyz/iapp-probe/notes.md`,
84
- ];
85
- for (const command of commands) {
86
- const result = evaluate(preToolUse("Bash", { command }));
87
- expect(result.output.continue).toBe(true);
88
- }
89
- });
90
-
91
- test("file-tool tilde/$HOME paths deep inside an allowed org continue", () => {
92
- const results = [
93
- evaluate(preToolUse("Write", { file_path: `~/.hasna/repos/clones/hasna/apps/src/x.ts` })),
94
- evaluate(preToolUse("Edit", { file_path: `$HOME/.hasna/repos/clones/hasnaxyz/iapp-probe/n.ipynb` })),
95
- evaluate(preToolUse("Write", { file_path: `"${HOME}/.hasna/repos/clones/hasna/apps/notes.md"` })),
96
- evaluate(preToolUse("Write", { file_path: `"$HOME"/.hasna/repos/clones/hasna/apps/notes2.md` })),
97
- ];
98
- for (const result of results) {
99
- expect(result.output.continue).toBe(true);
100
- }
101
- });
102
-
103
- test("apply_patch Add/Update File deep inside an allowed org continues", () => {
104
- const result = evaluate(
105
- preToolUse("apply_patch", {
106
- patch: `*** Begin Patch\n*** Add File: apps/hooks/src/lib/registry.ts\n+export const x = 1;\n*** End Patch`,
107
- })
108
- );
109
- expect(result.output.continue).toBe(true);
110
- });
111
-
112
- test("non-mutating sed without -i, and interpreter without a path, continue", () => {
113
- const commands = [
114
- `sed 's/a/b/' ${join(REPOS, "hasna", "apps", "README.md")}`,
115
- `node -e "console.log('no path here')"`,
116
- `python3 -c "print('no path here')"`,
117
- ];
118
- for (const command of commands) {
119
- const result = evaluate(preToolUse("Bash", { command }));
120
- expect(result.output.continue).toBe(true);
121
- }
122
- });
123
-
124
- test("non-PreToolUse events and non-file tools are ignored", () => {
125
- const result = evaluate({ hook_event_name: "PostToolUse", tool_name: "Write", tool_input: { file_path: join(REPOS, "hasna", "x.ts") } });
126
- expect(result.output.continue).toBe(true);
127
- const read = evaluate(preToolUse("Read", { file_path: join(REPOS, "hasna", "apps", "README.md") }));
128
- expect(read.output.continue).toBe(true);
129
- });
130
- });
131
-
132
- describe("negative controls — must block", () => {
133
- test("Write to ~/.hasna/repos/clones itself is blocked", () => {
134
- const result = evaluate(preToolUse("Write", { file_path: join(REPOS, "stray.txt") }));
135
- const reason = isBlocked(result);
136
- expect(reason).not.toBeNull();
137
- expect(reason).toContain("workspace-repos-guard");
138
- });
139
-
140
- test("Write to the repos root directory entry is blocked", () => {
141
- const result = evaluate(preToolUse("Edit", { file_path: REPOS }));
142
- expect(isBlocked(result)).not.toBeNull();
143
- });
144
-
145
- test("Write whose second segment is not an allowed org is blocked", () => {
146
- const result = evaluate(preToolUse("Write", { file_path: join(REPOS, "notanorg", "x", "file.ts") }));
147
- const reason = isBlocked(result);
148
- expect(reason).not.toBeNull();
149
- expect(reason).toContain("notanorg");
150
- });
151
-
152
- test("Write creating a top-level entry under repos (org-level, no deeper path) is blocked", () => {
153
- const result = evaluate(preToolUse("Write", { file_path: join(REPOS, "hasna") }));
154
- expect(isBlocked(result)).not.toBeNull();
155
- });
156
-
157
- test("rm -rf deep inside an allowed org checkout is blocked", () => {
158
- const result = evaluate(preToolUse("Bash", { command: `rm -rf ${join(REPOS, "hasna", "apps", ".git")}` }));
159
- const reason = isBlocked(result);
160
- expect(reason).not.toBeNull();
161
- expect(reason).toContain("delete");
162
- });
163
-
164
- test("rmdir of an org folder is blocked", () => {
165
- const result = evaluate(preToolUse("Bash", { command: `rmdir ${join(REPOS, "oldorg")}` }));
166
- expect(isBlocked(result)).not.toBeNull();
167
- });
168
-
169
- test("git clean inside a checkout is blocked", () => {
170
- const result = evaluate(preToolUse("Bash", { command: `git -C ${join(REPOS, "hasna", "apps")} clean -fd` }));
171
- expect(isBlocked(result)).not.toBeNull();
172
- });
173
-
174
- test("rm -rf of the repos root itself is blocked", () => {
175
- const result = evaluate(preToolUse("Bash", { command: `rm -rf ${REPOS}` }));
176
- expect(isBlocked(result)).not.toBeNull();
177
- });
178
-
179
- test("cd into repos followed by rm -rf . is blocked via relative resolution", () => {
180
- const result = evaluate(preToolUse("Bash", { command: `cd ${REPOS} && rm -rf .` }));
181
- expect(isBlocked(result)).not.toBeNull();
182
- });
183
-
184
- test("top-level non-org folder creation via Bash is blocked", () => {
185
- const commands = [
186
- `mkdir -p ${join(REPOS, "notanorg")}`,
187
- `mkdir -p ${join(REPOS, "notanorg", "sub")}`,
188
- `touch ${join(REPOS, "neworg", "x")}`,
189
- `mv ${join(REPOS, "hasna", "apps", "a")} ${join(REPOS, "anotherorg")}`,
190
- ];
191
- for (const command of commands) {
192
- const result = evaluate(preToolUse("Bash", { command }));
193
- expect(isBlocked(result)).not.toBeNull();
194
- }
195
- });
196
-
197
- test("Bash write into a non-allowed org folder is blocked", () => {
198
- const result = evaluate(preToolUse("Bash", { command: `echo x > ${join(REPOS, "notanorg", "deep", "f.ts")}` }));
199
- const reason = isBlocked(result);
200
- expect(reason).not.toBeNull();
201
- expect(reason).toContain("notanorg");
202
- });
203
-
204
- test("delete via git rm is blocked", () => {
205
- const result = evaluate(preToolUse("Bash", { command: `git -C ${join(REPOS, "hasna", "apps")} rm src/foo.ts` }));
206
- expect(isBlocked(result)).not.toBeNull();
207
- });
208
-
209
- test("tilde/$HOME spellings of the protected path are blocked", () => {
210
- const commands = [
211
- `rm -rf ~/.hasna/repos/clones`,
212
- `rm -rf ~/.hasna/repos/clones/hasna/apps`,
213
- `rm -rf $HOME/.hasna/repos/clones/hasna`,
214
- `rm -rf "${HOME}/.hasna/repos/clones"`,
215
- `rm -rf "$HOME"/.hasna/repos/clones`,
216
- `rm -rf "$HOME"/.hasna/repos/clones/hasna/apps`,
217
- `rm -rf "\${HOME}"/.hasna/repos/clones/hasna`,
218
- `rm -rf "${HOME}"/.hasna/repos/clones/hasna`,
219
- `touch ~/.hasna/repos/clones/stray.txt`,
220
- `mkdir -p ~/.hasna/repos/clones/notanorg`,
221
- `echo x > ~/.hasna/repos/clones/notanorg/f.ts`,
222
- `mv /tmp/x ~/.hasna/repos/clones`,
223
- `truncate -s 0 ~/.hasna/repos/clones/stray.log`,
224
- `rsync -a /tmp/x ~/.hasna/repos/clones/`,
225
- `scp f.txt ~/.hasna/repos/clones/`,
226
- ];
227
- for (const command of commands) {
228
- const result = evaluate(preToolUse("Bash", { command }));
229
- expect(isBlocked(result), `expected BLOCK for: ${command}`).not.toBeNull();
230
- }
231
- });
232
-
233
- test("file-tool tilde/$HOME spellings are blocked", () => {
234
- const results = [
235
- evaluate(preToolUse("Write", { file_path: `~/.hasna/repos/clones/stray.txt` })),
236
- evaluate(preToolUse("Write", { file_path: `$HOME/.hasna/repos/clones/notanorg/f.ts` })),
237
- evaluate(preToolUse("Edit", { file_path: `~/.hasna/repos/clones/hasna` })),
238
- evaluate(preToolUse("Write", { file_path: `~/.hasna/repos/clones` })),
239
- evaluate(preToolUse("Write", { file_path: `"$HOME"/.hasna/repos/clones/stray.txt` })),
240
- ];
241
- for (const result of results) {
242
- expect(isBlocked(result)).not.toBeNull();
243
- }
244
- });
245
-
246
- test("subshell-grouped cd + rm is blocked", () => {
247
- const commands = [
248
- `(cd ~/.hasna/repos/clones && rm -rf hasna)`,
249
- `(cd ${REPOS} && rm -rf hasna)`,
250
- `( cd ${REPOS} && touch stray.txt )`,
251
- ];
252
- for (const command of commands) {
253
- const result = evaluate(preToolUse("Bash", { command }));
254
- expect(isBlocked(result), `expected BLOCK for: ${command}`).not.toBeNull();
255
- }
256
- });
257
-
258
- test("cwd-relative deletes without an explicit cd are blocked", () => {
259
- const fromCheckout = evaluate(preToolUse("Bash", { command: `rm -rf ../` }, join(REPOS, "hasna", "apps")));
260
- expect(isBlocked(fromCheckout)).not.toBeNull();
261
- const fromRoot = evaluate(preToolUse("Bash", { command: `mkdir -p notanorg` }, REPOS));
262
- expect(isBlocked(fromRoot)).not.toBeNull();
263
- const bareRm = evaluate(preToolUse("Bash", { command: `rm -rf .` }, join(REPOS, "hasnaxyz")));
264
- expect(isBlocked(bareRm)).not.toBeNull();
265
- });
266
-
267
- test("mutating verbs writing under the protected root are blocked", () => {
268
- const commands = [
269
- `sed -i 's/x/y/' ~/.hasna/repos/clones/stray.txt`,
270
- `rsync -a ${join(REPOS, "hasna")} /tmp/out`,
271
- `scp -r ${join(REPOS, "hasna")} other:/tmp/`,
272
- `truncate -s 0 ${join(REPOS, "notanorg", "f.ts")}`,
273
- `python3 -c "open('~/.hasna/repos/clones/stray.txt','w')"`,
274
- `node -e "require('fs').writeFileSync('${join(REPOS, "notanorg", "f.ts")}','x')"`,
275
- ];
276
- for (const command of commands) {
277
- const result = evaluate(preToolUse("Bash", { command }));
278
- expect(isBlocked(result), `expected BLOCK for: ${command}`).not.toBeNull();
279
- }
280
- });
281
-
282
- test("apply_patch Delete File and shallow Add File are blocked", () => {
283
- const del = evaluate(
284
- preToolUse("ApplyPatch", {
285
- patch: `*** Begin Patch\n*** Delete File: src/foo.ts\n*** End Patch`,
286
- })
287
- );
288
- expect(isBlocked(del)).not.toBeNull();
289
- const shallow = evaluate(
290
- preToolUse("functions.apply_patch", {
291
- patch: `*** Begin Patch\n*** Add File: ../../stray.txt\n+x\n*** End Patch`,
292
- })
293
- );
294
- expect(isBlocked(shallow)).not.toBeNull();
295
- });
296
- });
297
-
298
- describe("orgs env override", () => {
299
- test("override widens and narrows the allowed set", () => {
300
- const previous = process.env.WORKSPACE_REPOS_GUARD_ORGS;
301
- try {
302
- process.env.WORKSPACE_REPOS_GUARD_ORGS = "hasna,myorg";
303
- const allowed = evaluate(preToolUse("Write", { file_path: join(REPOS, "myorg", "proj", "f.ts") }));
304
- expect(allowed.output.continue).toBe(true);
305
- const narrowed = evaluate(preToolUse("Write", { file_path: join(REPOS, "hasnaxyz", "iapp-x", "f.ts") }));
306
- expect(isBlocked(narrowed)).not.toBeNull();
307
- } finally {
308
- if (previous === undefined) delete process.env.WORKSPACE_REPOS_GUARD_ORGS;
309
- else process.env.WORKSPACE_REPOS_GUARD_ORGS = previous;
310
- }
311
- });
312
- });
313
-
314
- describe("legacy ~/workspace/repos root during the migration window — positive controls (must continue)", () => {
315
- test("Write deep inside an allowed org checkout under the legacy root continues", () => {
316
- const result = evaluate(preToolUse("Write", { file_path: join(LEGACY, "hasna", "apps", "apps", "foo", "src", "index.ts") }));
317
- expect(result.output.continue).toBe(true);
318
- expect(isBlocked(result)).toBeNull();
319
- });
320
-
321
- test("Bash write deep inside an allowed org checkout under the legacy root continues", () => {
322
- const commands = [
323
- `echo note > ${join(LEGACY, "hasna", "apps", "scratch.md")}`,
324
- `git clone https://github.com/hasna/foo.git ${join(LEGACY, "hasna", "foo")}`,
325
- `mkdir -p ${join(LEGACY, "hasnaxyz", "iapp-probe", "docs")}`,
326
- ];
327
- for (const command of commands) {
328
- const result = evaluate(preToolUse("Bash", { command }));
329
- expect(result.output.continue).toBe(true);
330
- }
331
- });
332
-
333
- test("tilde/$HOME spellings of deep allowed-org writes under the legacy root continue", () => {
334
- const commands = [
335
- `touch ~/workspace/repos/hasna/apps/foo.md`,
336
- `echo x > ~/workspace/repos/hasnaxyz/iapp-probe/notes.md`,
337
- `mkdir -p "${HOME}/workspace/repos/hasnaxyz/iapp-probe/notes"`,
338
- `touch "$HOME"/workspace/repos/hasna/apps/x.txt`,
339
- `echo x > "\${HOME}"/workspace/repos/hasnaxyz/iapp-probe/notes.md`,
340
- ];
341
- for (const command of commands) {
342
- const result = evaluate(preToolUse("Bash", { command }));
343
- expect(result.output.continue).toBe(true);
344
- }
345
- });
346
-
347
- test("file-tool tilde/$HOME paths deep inside an allowed org under the legacy root continue", () => {
348
- const results = [
349
- evaluate(preToolUse("Write", { file_path: `~/workspace/repos/hasna/apps/src/x.ts` })),
350
- evaluate(preToolUse("Edit", { file_path: `$HOME/workspace/repos/hasnaxyz/iapp-probe/n.ipynb` })),
351
- evaluate(preToolUse("Write", { file_path: `"${HOME}/workspace/repos/hasna/apps/notes.md"` })),
352
- evaluate(preToolUse("Write", { file_path: `"$HOME"/workspace/repos/hasna/apps/notes2.md` })),
353
- ];
354
- for (const result of results) {
355
- expect(result.output.continue).toBe(true);
356
- }
357
- });
358
- });
359
-
360
- describe("legacy ~/workspace/repos root during the migration window — negative controls restored (must block)", () => {
361
- test("rm -rf of the legacy root itself is blocked", () => {
362
- const result = evaluate(preToolUse("Bash", { command: `rm -rf ~/workspace/repos` }));
363
- const reason = isBlocked(result);
364
- expect(reason).not.toBeNull();
365
- expect(reason).toContain("workspace-repos-guard");
366
- });
367
-
368
- test("rm -rf deep inside an allowed org checkout under the legacy root is blocked", () => {
369
- const result = evaluate(preToolUse("Bash", { command: `rm -rf ~/workspace/repos/hasna/apps` }));
370
- expect(isBlocked(result)).not.toBeNull();
371
- });
372
-
373
- test("tilde/$HOME spellings of the legacy root are blocked", () => {
374
- const commands = [
375
- `rm -rf ~/workspace/repos`,
376
- `rm -rf ~/workspace/repos/hasna/apps`,
377
- `rm -rf $HOME/workspace/repos/hasna`,
378
- `rm -rf "${HOME}/workspace/repos"`,
379
- `rm -rf "$HOME"/workspace/repos`,
380
- `rm -rf "$HOME"/workspace/repos/hasna/apps`,
381
- `rm -rf "\${HOME}"/workspace/repos/hasna`,
382
- `rm -rf "${HOME}"/workspace/repos/hasna`,
383
- `touch ~/workspace/repos/stray.txt`,
384
- `mkdir -p ~/workspace/repos/notanorg`,
385
- `echo x > ~/workspace/repos/notanorg/f.ts`,
386
- `mv /tmp/x ~/workspace/repos`,
387
- `truncate -s 0 ~/workspace/repos/stray.log`,
388
- `rsync -a /tmp/x ~/workspace/repos/`,
389
- `scp f.txt ~/workspace/repos/`,
390
- ];
391
- for (const command of commands) {
392
- const result = evaluate(preToolUse("Bash", { command }));
393
- expect(isBlocked(result), `expected BLOCK for: ${command}`).not.toBeNull();
394
- }
395
- });
396
-
397
- test("file-tool tilde/$HOME spellings under the legacy root are blocked", () => {
398
- const results = [
399
- evaluate(preToolUse("Write", { file_path: `~/workspace/repos/stray.txt` })),
400
- evaluate(preToolUse("Write", { file_path: `$HOME/workspace/repos/notanorg/f.ts` })),
401
- evaluate(preToolUse("Edit", { file_path: `~/workspace/repos/hasna` })),
402
- evaluate(preToolUse("Write", { file_path: `~/workspace/repos` })),
403
- evaluate(preToolUse("Write", { file_path: `"$HOME"/workspace/repos/stray.txt` })),
404
- ];
405
- for (const result of results) {
406
- expect(isBlocked(result)).not.toBeNull();
407
- }
408
- });
409
-
410
- test("subshell-grouped cd + rm under the legacy root is blocked", () => {
411
- const commands = [
412
- `(cd ~/workspace/repos && rm -rf hasna)`,
413
- `(cd ${LEGACY} && rm -rf hasna)`,
414
- `( cd ${LEGACY} && touch stray.txt )`,
415
- ];
416
- for (const command of commands) {
417
- const result = evaluate(preToolUse("Bash", { command }));
418
- expect(isBlocked(result), `expected BLOCK for: ${command}`).not.toBeNull();
419
- }
420
- });
421
-
422
- test("cwd-relative deletes under the legacy root without an explicit cd are blocked", () => {
423
- const fromRoot = evaluate(preToolUse("Bash", { command: `rm -rf .` }, LEGACY));
424
- expect(isBlocked(fromRoot)).not.toBeNull();
425
- const fromOrg = evaluate(preToolUse("Bash", { command: `rm -rf .` }, join(LEGACY, "hasna")));
426
- expect(isBlocked(fromOrg)).not.toBeNull();
427
- });
428
-
429
- test("git clean inside a legacy checkout is blocked", () => {
430
- const result = evaluate(preToolUse("Bash", { command: `git -C ${join(LEGACY, "hasna", "apps")} clean -fd` }));
431
- expect(isBlocked(result)).not.toBeNull();
432
- });
433
-
434
- test("top-level non-org folder creation under the legacy root is blocked", () => {
435
- const commands = [
436
- `mkdir -p ${join(LEGACY, "notanorg")}`,
437
- `mkdir -p ${join(LEGACY, "notanorg", "sub")}`,
438
- `touch ${join(LEGACY, "neworg", "x")}`,
439
- `mv ${join(LEGACY, "hasna", "apps", "a")} ${join(LEGACY, "anotherorg")}`,
440
- ];
441
- for (const command of commands) {
442
- const result = evaluate(preToolUse("Bash", { command }));
443
- expect(isBlocked(result)).not.toBeNull();
444
- }
445
- });
446
-
447
- test("Bash write into a non-allowed org folder under the legacy root is blocked", () => {
448
- const result = evaluate(preToolUse("Bash", { command: `echo x > ${join(LEGACY, "notanorg", "deep", "f.ts")}` }));
449
- expect(isBlocked(result)).not.toBeNull();
450
- });
451
-
452
- test("mutating verbs writing under the legacy root are blocked", () => {
453
- const commands = [
454
- `sed -i 's/x/y/' ~/workspace/repos/stray.txt`,
455
- `python3 -c "open('~/workspace/repos/stray.txt','w')"`,
456
- `truncate -s 0 ${join(LEGACY, "notanorg", "f.ts")}`,
457
- `rsync -a ${join(LEGACY, "hasna")} /tmp/out`,
458
- `scp -r ${join(LEGACY, "hasna")} other:/tmp/`,
459
- ];
460
- for (const command of commands) {
461
- const result = evaluate(preToolUse("Bash", { command }));
462
- expect(isBlocked(result), `expected BLOCK for: ${command}`).not.toBeNull();
463
- }
464
- });
465
- });
466
- });
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "lib": ["ESNext"],
6
- "moduleResolution": "bundler",
7
- "allowImportingTsExtensions": true,
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "noEmit": true,
15
- "noUnusedLocals": true,
16
- "noUnusedParameters": true,
17
- "types": ["bun-types"]
18
- },
19
- "include": ["src/**/*"],
20
- "exclude": ["node_modules", "dist"]
21
- }
@@ -1,68 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- /**
6
- * The mention-context hook is Python, and its regression suite is Python. This
7
- * wrapper exists so that suite runs under the repository's existing `bun test`
8
- * step, with no CI workflow change and no second command for a contributor to
9
- * remember.
10
- *
11
- * It deliberately FAILS rather than skips when python3 is missing. A skipped
12
- * test and a passing one are indistinguishable in the summary line, and this
13
- * suite gates a defect that already reached production once: repositories were
14
- * reported carrying each other's git HEAD, because concurrent probes shared one
15
- * temp file.
16
- */
17
-
18
- const SUITE = join(import.meta.dir, "test_run_capture.py");
19
-
20
- describe("mention-context python regression suite", () => {
21
- test("python3 is available to run it", () => {
22
- const found = Bun.which("python3");
23
- expect(
24
- found,
25
- "python3 is not on PATH; the mention-context regression suite cannot run, " +
26
- "and a silent skip here would read as a pass",
27
- ).toBeTruthy();
28
- });
29
-
30
- test("the suite file is present", () => {
31
- expect(existsSync(SUITE), `missing suite at ${SUITE}`).toBe(true);
32
- });
33
-
34
- test(
35
- "concurrent captures do not cross temp files",
36
- async () => {
37
- const python = Bun.which("python3");
38
- if (!python) throw new Error("python3 not on PATH");
39
-
40
- const proc = Bun.spawn([python, SUITE, "-v"], {
41
- stdout: "pipe",
42
- stderr: "pipe",
43
- });
44
- const [stdout, stderr, exitCode] = await Promise.all([
45
- new Response(proc.stdout).text(),
46
- new Response(proc.stderr).text(),
47
- proc.exited,
48
- ]);
49
-
50
- // unittest writes its report to stderr; surface both so a failure here is
51
- // diagnosable from the bun test output alone rather than needing a re-run.
52
- if (exitCode !== 0) {
53
- throw new Error(
54
- `python regression suite failed (exit ${exitCode})\n` +
55
- `--- stderr ---\n${stderr}\n--- stdout ---\n${stdout}`,
56
- );
57
- }
58
-
59
- // Assert the suite actually ran tests. `unittest` exits 0 when it collects
60
- // nothing, so a zero exit alone cannot distinguish "all passed" from
61
- // "nothing ran" — which is the same class of vacuous check this suite was
62
- // written to close.
63
- expect(stderr).toMatch(/Ran [1-9]\d* tests?/);
64
- expect(stderr).toMatch(/\bOK\b/);
65
- },
66
- 120_000, // the suite forces timed interleavings; a loaded machine needs room
67
- );
68
- });