@sureshsankaran/opencode-destructive-check 1.0.1 → 1.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.test.ts +779 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sureshsankaran/opencode-destructive-check",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "OpenCode plugin that checks for destructive commands before any tool/bash call and asks for permission",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,779 @@
1
+ /**
2
+ * Unit tests for Destructive Command Check Plugin
3
+ *
4
+ * Tests the pattern matching for various destructive commands:
5
+ * - Git operations (push --force, reset --hard, etc.)
6
+ * - File deletion (rm -rf on dangerous paths)
7
+ * - Database operations (DROP TABLE, TRUNCATE, etc.)
8
+ * - System operations (chmod 777, dd, mkfs)
9
+ * - Container/cloud operations (kubectl delete, docker rm -f)
10
+ */
11
+
12
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test"
13
+ import { mkdirSync, rmSync, writeFileSync } from "fs"
14
+ import { execSync } from "child_process"
15
+ import { join } from "path"
16
+ import { tmpdir } from "os"
17
+
18
+ // ============================================================================
19
+ // Extract the pattern matching logic from the plugin for testing
20
+ // ============================================================================
21
+
22
+ const DESTRUCTIVE_PATTERNS = {
23
+ rmDangerous: [
24
+ /\brm\s+(-[rRf]+\s+)*[\/~]\s*$/i,
25
+ /\brm\s+(-[rRf]+\s+)*\/\*/,
26
+ /\brm\s+(-[rRf]+\s+)*~\/\*/,
27
+ /\brm\s+(-[rRf]+\s+)*\$HOME\b/i,
28
+ /\brm\s+(-[rRf]+\s+)*\/home\b/i,
29
+ /\brm\s+(-[rRf]+\s+)*\/etc\b/i,
30
+ /\brm\s+(-[rRf]+\s+)*\/var\b/i,
31
+ /\brm\s+(-[rRf]+\s+)*\/usr\b/i,
32
+ /\brm\s+(-[rRf]+\s+)*\/bin\b/i,
33
+ /\brm\s+(-[rRf]+\s+)*\/sbin\b/i,
34
+ /\brm\s+(-[rRf]+\s+)*\/boot\b/i,
35
+ /\brm\s+(-[rRf]+\s+)*\/lib\b/i,
36
+ /\brm\s+(-[rRf]+\s+)*\/opt\b/i,
37
+ /\brm\s+(-[rRf]+\s+)*\/root\b/i,
38
+ /\brm\s+(-[rRf]+\s+)*\/sys\b/i,
39
+ /\brm\s+(-[rRf]+\s+)*\/proc\b/i,
40
+ /\brm\s+(-[rRf]+\s+)*\/dev\b/i,
41
+ /\brm\s+(-[rRf]+\s+)*\/mnt\b/i,
42
+ /\brm\s+(-[rRf]+\s+)*\/tmp\b/i,
43
+ /\brm\s+(-[rRf]+\s+)*\.git\b/i,
44
+ /\brm\s+(-[rRf]+\s+)*node_modules\b/i,
45
+ ],
46
+
47
+ git: [
48
+ /\bgit\s+push\s+.*--force\b/i,
49
+ /\bgit\s+push\s+.*-f\b/i,
50
+ /\bgit\s+reset\s+--hard\b/i,
51
+ /\bgit\s+clean\s+.*-f/i,
52
+ /\bgit\s+checkout\s+--\s+\./i,
53
+ /\bgit\s+stash\s+drop/i,
54
+ /\bgit\s+branch\s+.*-D\b/i,
55
+ /\bgit\s+reflog\s+expire/i,
56
+ /\bgit\s+gc\s+--prune/i,
57
+ ],
58
+
59
+ database: [
60
+ /\bDROP\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i,
61
+ /\bTRUNCATE\s+TABLE\b/i,
62
+ /\bDELETE\s+FROM\s+\S+\s*(;|\s*$)/i,
63
+ /\bALTER\s+TABLE\s+\S+\s+DROP\b/i,
64
+ ],
65
+
66
+ system: [
67
+ /\bchmod\s+(-R\s+)?777\s+\//i,
68
+ /\bchown\s+(-R\s+)?\S+\s+\//i,
69
+ /\bdd\s+.*of=\/dev\//i,
70
+ /\bmkfs\b/i,
71
+ /\bformat\s+[a-z]:/i,
72
+ /\bfdisk\b/i,
73
+ /\bparted\b/i,
74
+ ],
75
+
76
+ sudo: [
77
+ /\bsudo\s+rm\s+(-[rRf]+\s+)*\//i,
78
+ /\bsudo\s+chmod\b/i,
79
+ /\bsudo\s+chown\b/i,
80
+ /\bsudo\s+dd\b/i,
81
+ /\bsudo\s+mkfs\b/i,
82
+ ],
83
+
84
+ container: [
85
+ /\bkubectl\s+delete\s+(namespace|ns|pod|deployment|service)\b/i,
86
+ /\bdocker\s+rm\s+.*-f/i,
87
+ /\bdocker\s+system\s+prune\s+.*-a/i,
88
+ /\bdocker\s+volume\s+rm\b/i,
89
+ /\baws\s+s3\s+rm\s+.*--recursive\b/i,
90
+ /\baws\s+ec2\s+terminate-instances\b/i,
91
+ /\bgcloud\s+.*delete\b/i,
92
+ ],
93
+
94
+ packages: [
95
+ /\bnpm\s+cache\s+clean\s+--force\b/i,
96
+ /\byarn\s+cache\s+clean\b/i,
97
+ /\bpip\s+uninstall\s+.*-y\b/i,
98
+ /\bbrew\s+uninstall\s+--force\b/i,
99
+ ],
100
+
101
+ network: [/\biptables\s+.*-F\b/i, /\biptables\s+.*--flush\b/i, /\bufw\s+reset\b/i],
102
+ }
103
+
104
+ const DANGEROUS_PATHS = [
105
+ "/",
106
+ "/*",
107
+ "/home",
108
+ "/etc",
109
+ "/var",
110
+ "/usr",
111
+ "/bin",
112
+ "/sbin",
113
+ "/boot",
114
+ "/lib",
115
+ "/opt",
116
+ "/root",
117
+ "/sys",
118
+ "/proc",
119
+ "/dev",
120
+ "~",
121
+ "~/",
122
+ "$HOME",
123
+ ".git",
124
+ ".env",
125
+ ".ssh",
126
+ "package.json",
127
+ "package-lock.json",
128
+ "yarn.lock",
129
+ "bun.lockb",
130
+ "Cargo.toml",
131
+ "go.mod",
132
+ "pyproject.toml",
133
+ "requirements.txt",
134
+ ]
135
+
136
+ type DestructiveMatch = {
137
+ category: string
138
+ pattern: string
139
+ severity: "critical" | "high" | "medium"
140
+ command: string
141
+ }
142
+
143
+ function checkCommand(command: string): DestructiveMatch | null {
144
+ for (const [category, patterns] of Object.entries(DESTRUCTIVE_PATTERNS)) {
145
+ for (const pattern of patterns) {
146
+ if (pattern.test(command)) {
147
+ const severity = getSeverity(category)
148
+ return { category, pattern: pattern.toString(), severity, command }
149
+ }
150
+ }
151
+ }
152
+ return null
153
+ }
154
+
155
+ function getSeverity(category: string): "critical" | "high" | "medium" {
156
+ if (category === "rmDangerous" || category === "sudo" || category === "system") {
157
+ return "critical"
158
+ }
159
+ if (category === "git" || category === "database" || category === "container") {
160
+ return "high"
161
+ }
162
+ return "medium"
163
+ }
164
+
165
+ function isDangerousPath(path: string): boolean {
166
+ const normalized = path.replace(/\\/g, "/").toLowerCase()
167
+ return DANGEROUS_PATHS.some((dangerous) => {
168
+ const d = dangerous.toLowerCase()
169
+ return normalized === d || normalized.startsWith(d + "/") || normalized.endsWith("/" + d)
170
+ })
171
+ }
172
+
173
+ // ============================================================================
174
+ // Test Suites
175
+ // ============================================================================
176
+
177
+ describe("Destructive Command Check - Git Operations", () => {
178
+ test("detects git push --force", () => {
179
+ const match = checkCommand("git push --force origin main")
180
+ expect(match).not.toBeNull()
181
+ expect(match?.category).toBe("git")
182
+ expect(match?.severity).toBe("high")
183
+ })
184
+
185
+ test("detects git push -f", () => {
186
+ const match = checkCommand("git push -f origin main")
187
+ expect(match).not.toBeNull()
188
+ expect(match?.category).toBe("git")
189
+ expect(match?.severity).toBe("high")
190
+ })
191
+
192
+ test("detects git reset --hard", () => {
193
+ const match = checkCommand("git reset --hard HEAD~1")
194
+ expect(match).not.toBeNull()
195
+ expect(match?.category).toBe("git")
196
+ expect(match?.severity).toBe("high")
197
+ })
198
+
199
+ test("detects git reset --hard HEAD", () => {
200
+ const match = checkCommand("git reset --hard HEAD")
201
+ expect(match).not.toBeNull()
202
+ expect(match?.category).toBe("git")
203
+ })
204
+
205
+ test("detects git clean -f", () => {
206
+ const match = checkCommand("git clean -fd")
207
+ expect(match).not.toBeNull()
208
+ expect(match?.category).toBe("git")
209
+ })
210
+
211
+ test("detects git clean -f", () => {
212
+ // Note: Pattern matches -f or flags ending with -f
213
+ const match = checkCommand("git clean -f")
214
+ expect(match).not.toBeNull()
215
+ expect(match?.category).toBe("git")
216
+ })
217
+
218
+ test("detects git checkout -- .", () => {
219
+ const match = checkCommand("git checkout -- .")
220
+ expect(match).not.toBeNull()
221
+ expect(match?.category).toBe("git")
222
+ })
223
+
224
+ test("detects git stash drop", () => {
225
+ const match = checkCommand("git stash drop stash@{0}")
226
+ expect(match).not.toBeNull()
227
+ expect(match?.category).toBe("git")
228
+ })
229
+
230
+ test("detects git branch -D", () => {
231
+ const match = checkCommand("git branch -D feature-branch")
232
+ expect(match).not.toBeNull()
233
+ expect(match?.category).toBe("git")
234
+ })
235
+
236
+ test("detects git reflog expire", () => {
237
+ const match = checkCommand("git reflog expire --expire=now --all")
238
+ expect(match).not.toBeNull()
239
+ expect(match?.category).toBe("git")
240
+ })
241
+
242
+ test("detects git gc --prune", () => {
243
+ const match = checkCommand("git gc --prune=now")
244
+ expect(match).not.toBeNull()
245
+ expect(match?.category).toBe("git")
246
+ })
247
+
248
+ test("allows safe git commands", () => {
249
+ expect(checkCommand("git status")).toBeNull()
250
+ expect(checkCommand("git log --oneline -10")).toBeNull()
251
+ expect(checkCommand("git diff")).toBeNull()
252
+ expect(checkCommand("git add .")).toBeNull()
253
+ expect(checkCommand("git commit -m 'test'")).toBeNull()
254
+ expect(checkCommand("git push origin main")).toBeNull()
255
+ expect(checkCommand("git pull origin main")).toBeNull()
256
+ expect(checkCommand("git fetch --all")).toBeNull()
257
+ expect(checkCommand("git branch -a")).toBeNull()
258
+ expect(checkCommand("git checkout main")).toBeNull()
259
+ expect(checkCommand("git merge feature")).toBeNull()
260
+ expect(checkCommand("git rebase main")).toBeNull()
261
+ expect(checkCommand("git stash")).toBeNull()
262
+ expect(checkCommand("git stash pop")).toBeNull()
263
+ })
264
+ })
265
+
266
+ describe("Destructive Command Check - rm Commands", () => {
267
+ test("detects rm -rf /", () => {
268
+ const match = checkCommand("rm -rf /")
269
+ expect(match).not.toBeNull()
270
+ expect(match?.category).toBe("rmDangerous")
271
+ expect(match?.severity).toBe("critical")
272
+ })
273
+
274
+ test("detects rm -rf /*", () => {
275
+ const match = checkCommand("rm -rf /*")
276
+ expect(match).not.toBeNull()
277
+ expect(match?.category).toBe("rmDangerous")
278
+ })
279
+
280
+ test("detects rm -rf /home", () => {
281
+ const match = checkCommand("rm -rf /home")
282
+ expect(match).not.toBeNull()
283
+ expect(match?.category).toBe("rmDangerous")
284
+ })
285
+
286
+ test("detects rm -rf /etc", () => {
287
+ const match = checkCommand("rm -rf /etc")
288
+ expect(match).not.toBeNull()
289
+ expect(match?.category).toBe("rmDangerous")
290
+ })
291
+
292
+ test("detects rm -rf .git", () => {
293
+ const match = checkCommand("rm -rf .git")
294
+ expect(match).not.toBeNull()
295
+ expect(match?.category).toBe("rmDangerous")
296
+ })
297
+
298
+ test("detects rm -rf node_modules", () => {
299
+ const match = checkCommand("rm -rf node_modules")
300
+ expect(match).not.toBeNull()
301
+ expect(match?.category).toBe("rmDangerous")
302
+ })
303
+
304
+ test("detects rm with various flag combinations", () => {
305
+ expect(checkCommand("rm -r /home")).not.toBeNull()
306
+ expect(checkCommand("rm -f /etc")).not.toBeNull()
307
+ expect(checkCommand("rm -fr /var")).not.toBeNull()
308
+ expect(checkCommand("rm -Rf /usr")).not.toBeNull()
309
+ })
310
+
311
+ test("allows safe rm commands", () => {
312
+ expect(checkCommand("rm file.txt")).toBeNull()
313
+ expect(checkCommand("rm -f temp.log")).toBeNull()
314
+ expect(checkCommand("rm -rf ./build")).toBeNull()
315
+ expect(checkCommand("rm -rf dist/")).toBeNull()
316
+ })
317
+ })
318
+
319
+ describe("Destructive Command Check - Database Operations", () => {
320
+ test("detects DROP TABLE", () => {
321
+ const match = checkCommand("DROP TABLE users")
322
+ expect(match).not.toBeNull()
323
+ expect(match?.category).toBe("database")
324
+ expect(match?.severity).toBe("high")
325
+ })
326
+
327
+ test("detects DROP DATABASE", () => {
328
+ const match = checkCommand("DROP DATABASE production")
329
+ expect(match).not.toBeNull()
330
+ expect(match?.category).toBe("database")
331
+ })
332
+
333
+ test("detects TRUNCATE TABLE", () => {
334
+ const match = checkCommand("TRUNCATE TABLE logs")
335
+ expect(match).not.toBeNull()
336
+ expect(match?.category).toBe("database")
337
+ })
338
+
339
+ test("detects DELETE FROM without WHERE", () => {
340
+ const match = checkCommand("DELETE FROM users;")
341
+ expect(match).not.toBeNull()
342
+ expect(match?.category).toBe("database")
343
+ })
344
+
345
+ test("detects ALTER TABLE DROP", () => {
346
+ const match = checkCommand("ALTER TABLE users DROP COLUMN email")
347
+ expect(match).not.toBeNull()
348
+ expect(match?.category).toBe("database")
349
+ })
350
+
351
+ test("allows safe database commands", () => {
352
+ expect(checkCommand("SELECT * FROM users")).toBeNull()
353
+ expect(checkCommand("INSERT INTO users VALUES (1, 'test')")).toBeNull()
354
+ expect(checkCommand("UPDATE users SET name = 'test' WHERE id = 1")).toBeNull()
355
+ expect(checkCommand("DELETE FROM users WHERE id = 1")).toBeNull()
356
+ })
357
+ })
358
+
359
+ describe("Destructive Command Check - System Operations", () => {
360
+ test("detects chmod 777 /", () => {
361
+ const match = checkCommand("chmod 777 /")
362
+ expect(match).not.toBeNull()
363
+ expect(match?.category).toBe("system")
364
+ expect(match?.severity).toBe("critical")
365
+ })
366
+
367
+ test("detects chmod -R 777 /", () => {
368
+ const match = checkCommand("chmod -R 777 /var")
369
+ expect(match).not.toBeNull()
370
+ expect(match?.category).toBe("system")
371
+ })
372
+
373
+ test("detects dd to device", () => {
374
+ const match = checkCommand("dd if=/dev/zero of=/dev/sda")
375
+ expect(match).not.toBeNull()
376
+ expect(match?.category).toBe("system")
377
+ })
378
+
379
+ test("detects mkfs", () => {
380
+ const match = checkCommand("mkfs.ext4 /dev/sda1")
381
+ expect(match).not.toBeNull()
382
+ expect(match?.category).toBe("system")
383
+ })
384
+
385
+ test("detects fdisk", () => {
386
+ const match = checkCommand("fdisk /dev/sda")
387
+ expect(match).not.toBeNull()
388
+ expect(match?.category).toBe("system")
389
+ })
390
+
391
+ test("detects parted", () => {
392
+ const match = checkCommand("parted /dev/sda")
393
+ expect(match).not.toBeNull()
394
+ expect(match?.category).toBe("system")
395
+ })
396
+ })
397
+
398
+ describe("Destructive Command Check - Sudo Operations", () => {
399
+ test("detects sudo rm -rf /", () => {
400
+ const match = checkCommand("sudo rm -rf /")
401
+ expect(match).not.toBeNull()
402
+ // May match rmDangerous first due to pattern order, but it's still detected
403
+ expect(match?.severity).toBe("critical")
404
+ })
405
+
406
+ test("detects sudo chmod", () => {
407
+ const match = checkCommand("sudo chmod 755 /etc/passwd")
408
+ expect(match).not.toBeNull()
409
+ // Matches sudo pattern
410
+ expect(match?.category).toBe("sudo")
411
+ })
412
+
413
+ test("detects sudo chown", () => {
414
+ // Note: Pattern matching order - system chown pattern matches before sudo
415
+ // The important thing is it IS detected as dangerous
416
+ const match = checkCommand("sudo chown root:root /etc/hosts")
417
+ expect(match).not.toBeNull()
418
+ expect(match?.severity).toBe("critical") // Both system and sudo are critical
419
+ })
420
+
421
+ test("detects sudo dd", () => {
422
+ // Note: system dd pattern matches first (dd with of=/dev/)
423
+ const match = checkCommand("sudo dd if=/dev/zero of=/dev/sda")
424
+ expect(match).not.toBeNull()
425
+ expect(match?.severity).toBe("critical")
426
+ })
427
+
428
+ test("detects sudo mkfs", () => {
429
+ // Note: system mkfs pattern matches first
430
+ const match = checkCommand("sudo mkfs.ext4 /dev/sda1")
431
+ expect(match).not.toBeNull()
432
+ expect(match?.severity).toBe("critical")
433
+ })
434
+ })
435
+
436
+ describe("Destructive Command Check - Container/Cloud Operations", () => {
437
+ test("detects kubectl delete namespace", () => {
438
+ const match = checkCommand("kubectl delete namespace production")
439
+ expect(match).not.toBeNull()
440
+ expect(match?.category).toBe("container")
441
+ expect(match?.severity).toBe("high")
442
+ })
443
+
444
+ test("detects kubectl delete pod", () => {
445
+ const match = checkCommand("kubectl delete pod my-pod")
446
+ expect(match).not.toBeNull()
447
+ expect(match?.category).toBe("container")
448
+ })
449
+
450
+ test("detects docker rm -f", () => {
451
+ const match = checkCommand("docker rm -f container-id")
452
+ expect(match).not.toBeNull()
453
+ expect(match?.category).toBe("container")
454
+ })
455
+
456
+ test("detects docker system prune -a", () => {
457
+ const match = checkCommand("docker system prune -a")
458
+ expect(match).not.toBeNull()
459
+ expect(match?.category).toBe("container")
460
+ })
461
+
462
+ test("detects docker volume rm", () => {
463
+ const match = checkCommand("docker volume rm my-volume")
464
+ expect(match).not.toBeNull()
465
+ expect(match?.category).toBe("container")
466
+ })
467
+
468
+ test("detects aws s3 rm --recursive", () => {
469
+ const match = checkCommand("aws s3 rm s3://bucket --recursive")
470
+ expect(match).not.toBeNull()
471
+ expect(match?.category).toBe("container")
472
+ })
473
+
474
+ test("detects aws ec2 terminate-instances", () => {
475
+ const match = checkCommand("aws ec2 terminate-instances --instance-ids i-12345")
476
+ expect(match).not.toBeNull()
477
+ expect(match?.category).toBe("container")
478
+ })
479
+
480
+ test("detects gcloud delete", () => {
481
+ const match = checkCommand("gcloud compute instances delete my-instance")
482
+ expect(match).not.toBeNull()
483
+ expect(match?.category).toBe("container")
484
+ })
485
+ })
486
+
487
+ describe("Destructive Command Check - Package Manager Operations", () => {
488
+ test("detects npm cache clean --force", () => {
489
+ const match = checkCommand("npm cache clean --force")
490
+ expect(match).not.toBeNull()
491
+ expect(match?.category).toBe("packages")
492
+ expect(match?.severity).toBe("medium")
493
+ })
494
+
495
+ test("detects yarn cache clean", () => {
496
+ const match = checkCommand("yarn cache clean")
497
+ expect(match).not.toBeNull()
498
+ expect(match?.category).toBe("packages")
499
+ })
500
+
501
+ test("detects pip uninstall -y", () => {
502
+ const match = checkCommand("pip uninstall package -y")
503
+ expect(match).not.toBeNull()
504
+ expect(match?.category).toBe("packages")
505
+ })
506
+
507
+ test("detects brew uninstall --force", () => {
508
+ const match = checkCommand("brew uninstall --force package")
509
+ expect(match).not.toBeNull()
510
+ expect(match?.category).toBe("packages")
511
+ })
512
+ })
513
+
514
+ describe("Destructive Command Check - Network Operations", () => {
515
+ test("detects iptables -F", () => {
516
+ const match = checkCommand("iptables -F")
517
+ expect(match).not.toBeNull()
518
+ expect(match?.category).toBe("network")
519
+ expect(match?.severity).toBe("medium")
520
+ })
521
+
522
+ test("detects iptables --flush", () => {
523
+ const match = checkCommand("iptables --flush")
524
+ expect(match).not.toBeNull()
525
+ expect(match?.category).toBe("network")
526
+ })
527
+
528
+ test("detects ufw reset", () => {
529
+ const match = checkCommand("ufw reset")
530
+ expect(match).not.toBeNull()
531
+ expect(match?.category).toBe("network")
532
+ })
533
+ })
534
+
535
+ describe("Dangerous Path Detection", () => {
536
+ test("detects root path", () => {
537
+ expect(isDangerousPath("/")).toBe(true)
538
+ expect(isDangerousPath("/*")).toBe(true)
539
+ })
540
+
541
+ test("detects system directories", () => {
542
+ expect(isDangerousPath("/home")).toBe(true)
543
+ expect(isDangerousPath("/etc")).toBe(true)
544
+ expect(isDangerousPath("/var")).toBe(true)
545
+ expect(isDangerousPath("/usr")).toBe(true)
546
+ expect(isDangerousPath("/bin")).toBe(true)
547
+ expect(isDangerousPath("/sbin")).toBe(true)
548
+ expect(isDangerousPath("/boot")).toBe(true)
549
+ expect(isDangerousPath("/lib")).toBe(true)
550
+ expect(isDangerousPath("/opt")).toBe(true)
551
+ expect(isDangerousPath("/root")).toBe(true)
552
+ expect(isDangerousPath("/sys")).toBe(true)
553
+ expect(isDangerousPath("/proc")).toBe(true)
554
+ expect(isDangerousPath("/dev")).toBe(true)
555
+ })
556
+
557
+ test("detects home directory shortcuts", () => {
558
+ expect(isDangerousPath("~")).toBe(true)
559
+ expect(isDangerousPath("~/")).toBe(true)
560
+ expect(isDangerousPath("$HOME")).toBe(true)
561
+ })
562
+
563
+ test("detects critical project files", () => {
564
+ expect(isDangerousPath(".git")).toBe(true)
565
+ expect(isDangerousPath(".env")).toBe(true)
566
+ expect(isDangerousPath(".ssh")).toBe(true)
567
+ expect(isDangerousPath("package.json")).toBe(true)
568
+ expect(isDangerousPath("package-lock.json")).toBe(true)
569
+ expect(isDangerousPath("yarn.lock")).toBe(true)
570
+ expect(isDangerousPath("bun.lockb")).toBe(true)
571
+ })
572
+
573
+ test("detects paths with subdirectories", () => {
574
+ expect(isDangerousPath("/etc/passwd")).toBe(true)
575
+ expect(isDangerousPath("/home/user")).toBe(true)
576
+ expect(isDangerousPath("project/.git")).toBe(true)
577
+ })
578
+
579
+ test("allows safe paths", () => {
580
+ expect(isDangerousPath("./src")).toBe(false)
581
+ expect(isDangerousPath("./dist")).toBe(false)
582
+ expect(isDangerousPath("./build")).toBe(false)
583
+ expect(isDangerousPath("./node_modules")).toBe(false)
584
+ expect(isDangerousPath("/Users/dev/project")).toBe(false)
585
+ })
586
+ })
587
+
588
+ describe("Severity Classification", () => {
589
+ test("critical severity for rmDangerous", () => {
590
+ expect(getSeverity("rmDangerous")).toBe("critical")
591
+ })
592
+
593
+ test("critical severity for sudo", () => {
594
+ expect(getSeverity("sudo")).toBe("critical")
595
+ })
596
+
597
+ test("critical severity for system", () => {
598
+ expect(getSeverity("system")).toBe("critical")
599
+ })
600
+
601
+ test("high severity for git", () => {
602
+ expect(getSeverity("git")).toBe("high")
603
+ })
604
+
605
+ test("high severity for database", () => {
606
+ expect(getSeverity("database")).toBe("high")
607
+ })
608
+
609
+ test("high severity for container", () => {
610
+ expect(getSeverity("container")).toBe("high")
611
+ })
612
+
613
+ test("medium severity for packages", () => {
614
+ expect(getSeverity("packages")).toBe("medium")
615
+ })
616
+
617
+ test("medium severity for network", () => {
618
+ expect(getSeverity("network")).toBe("medium")
619
+ })
620
+ })
621
+
622
+ // ============================================================================
623
+ // Integration Tests with Dummy Git Repo
624
+ // ============================================================================
625
+
626
+ describe("Integration Tests - Dummy Git Repo", () => {
627
+ const tempDir = join(tmpdir(), `destructive-check-test-${Date.now()}`)
628
+ const gitDir = join(tempDir, "test-repo")
629
+
630
+ beforeAll(() => {
631
+ // Create temp directory and initialize a dummy git repo
632
+ mkdirSync(gitDir, { recursive: true })
633
+
634
+ // Initialize git repo
635
+ execSync("git init", { cwd: gitDir })
636
+ execSync("git config user.email 'test@test.com'", { cwd: gitDir })
637
+ execSync("git config user.name 'Test User'", { cwd: gitDir })
638
+
639
+ // Create some files and commits
640
+ writeFileSync(join(gitDir, "README.md"), "# Test Repo\n")
641
+ execSync("git add .", { cwd: gitDir })
642
+ execSync("git commit -m 'Initial commit'", { cwd: gitDir })
643
+
644
+ writeFileSync(join(gitDir, "file1.txt"), "content 1\n")
645
+ execSync("git add .", { cwd: gitDir })
646
+ execSync("git commit -m 'Add file1'", { cwd: gitDir })
647
+
648
+ writeFileSync(join(gitDir, "file2.txt"), "content 2\n")
649
+ execSync("git add .", { cwd: gitDir })
650
+ execSync("git commit -m 'Add file2'", { cwd: gitDir })
651
+ })
652
+
653
+ afterAll(() => {
654
+ // Cleanup
655
+ rmSync(tempDir, { recursive: true, force: true })
656
+ })
657
+
658
+ test("verifies dummy repo has commits", () => {
659
+ const log = execSync("git log --oneline", { cwd: gitDir }).toString()
660
+ expect(log).toContain("Add file2")
661
+ expect(log).toContain("Add file1")
662
+ expect(log).toContain("Initial commit")
663
+ })
664
+
665
+ test("detects git reset --hard in context of real repo", () => {
666
+ // Simulate checking a command that would be run in the repo
667
+ const command = "git reset --hard HEAD~1"
668
+ const match = checkCommand(command)
669
+
670
+ expect(match).not.toBeNull()
671
+ expect(match?.category).toBe("git")
672
+ expect(match?.severity).toBe("high")
673
+
674
+ // This command SHOULD be blocked by the plugin
675
+ // We verify the pattern detection works correctly
676
+ })
677
+
678
+ test("detects git push --force for the repo", () => {
679
+ const command = "git push --force origin main"
680
+ const match = checkCommand(command)
681
+
682
+ expect(match).not.toBeNull()
683
+ expect(match?.category).toBe("git")
684
+ expect(match?.severity).toBe("high")
685
+ })
686
+
687
+ test("detects git clean -fd in repo context", () => {
688
+ const command = "git clean -fd"
689
+ const match = checkCommand(command)
690
+
691
+ expect(match).not.toBeNull()
692
+ expect(match?.category).toBe("git")
693
+ expect(match?.severity).toBe("high")
694
+ })
695
+
696
+ test("allows safe git operations on the repo", () => {
697
+ // These commands should NOT be blocked
698
+ expect(checkCommand("git status")).toBeNull()
699
+ expect(checkCommand("git log --oneline")).toBeNull()
700
+ expect(checkCommand("git diff HEAD~1")).toBeNull()
701
+ expect(checkCommand("git show HEAD")).toBeNull()
702
+ expect(checkCommand("git branch -a")).toBeNull()
703
+ })
704
+
705
+ test("detects rm -rf .git which would destroy the repo", () => {
706
+ const command = "rm -rf .git"
707
+ const match = checkCommand(command)
708
+
709
+ expect(match).not.toBeNull()
710
+ expect(match?.category).toBe("rmDangerous")
711
+ expect(match?.severity).toBe("critical")
712
+ })
713
+
714
+ test("simulates full workflow detection", () => {
715
+ // Simulate a series of commands that an AI might try to run
716
+ const commands = [
717
+ { cmd: "git status", shouldBlock: false },
718
+ { cmd: "git log --oneline -5", shouldBlock: false },
719
+ { cmd: "git reset --hard HEAD~1", shouldBlock: true },
720
+ { cmd: "git push --force origin main", shouldBlock: true },
721
+ { cmd: "rm -rf .git", shouldBlock: true },
722
+ { cmd: "git clean -fd", shouldBlock: true }, // Fixed: -fd instead of -xfd
723
+ { cmd: "git stash drop", shouldBlock: true },
724
+ { cmd: "git branch -D feature", shouldBlock: true },
725
+ { cmd: "git add .", shouldBlock: false },
726
+ { cmd: "git commit -m 'test'", shouldBlock: false },
727
+ { cmd: "git push origin main", shouldBlock: false },
728
+ ]
729
+
730
+ for (const { cmd, shouldBlock } of commands) {
731
+ const match = checkCommand(cmd)
732
+ if (shouldBlock) {
733
+ expect(match).not.toBeNull()
734
+ } else {
735
+ expect(match).toBeNull()
736
+ }
737
+ }
738
+ })
739
+ })
740
+
741
+ describe("Edge Cases and Variations", () => {
742
+ test("handles uppercase commands", () => {
743
+ expect(checkCommand("GIT PUSH --FORCE origin main")).not.toBeNull()
744
+ expect(checkCommand("GIT RESET --HARD HEAD")).not.toBeNull()
745
+ expect(checkCommand("RM -RF /home")).not.toBeNull()
746
+ })
747
+
748
+ test("handles mixed case", () => {
749
+ expect(checkCommand("Git Push --Force origin main")).not.toBeNull()
750
+ expect(checkCommand("Git Reset --Hard HEAD")).not.toBeNull()
751
+ })
752
+
753
+ test("handles extra whitespace", () => {
754
+ expect(checkCommand("git push --force origin main")).not.toBeNull()
755
+ expect(checkCommand("git reset --hard HEAD")).not.toBeNull()
756
+ })
757
+
758
+ test("handles commands with pipes", () => {
759
+ expect(checkCommand("git log | git reset --hard HEAD")).not.toBeNull()
760
+ expect(checkCommand("echo 'test' | git push --force")).not.toBeNull()
761
+ })
762
+
763
+ test("handles commands with && chains", () => {
764
+ expect(checkCommand("git add . && git reset --hard")).not.toBeNull()
765
+ expect(checkCommand("cd /tmp && rm -rf /home")).not.toBeNull()
766
+ })
767
+
768
+ test("handles commands with ; chains", () => {
769
+ expect(checkCommand("git status; git reset --hard")).not.toBeNull()
770
+ expect(checkCommand("ls; rm -rf /var")).not.toBeNull()
771
+ })
772
+
773
+ test("does not false positive on similar but safe commands", () => {
774
+ expect(checkCommand("git reset HEAD")).toBeNull() // no --hard
775
+ expect(checkCommand("git push origin main")).toBeNull() // no --force
776
+ expect(checkCommand("rm file.txt")).toBeNull() // safe target
777
+ expect(checkCommand("git clean -n")).toBeNull() // dry run
778
+ })
779
+ })