flake-update-20260505
  1/**
  2 * Tests for Guardrails extension
  3 *
  4 * Run with: bun test guardrails.test.ts
  5 */
  6
  7import { describe, expect, test } from "bun:test";
  8import {
  9	stripQuotedContent,
 10	matchCommandRule,
 11	commandRules,
 12	dangerousBashWrites,
 13	protectedPaths,
 14	softProtectedPaths,
 15	buildModifyReason,
 16	buildRejectReason,
 17	buildWriteModifyReason,
 18	buildWriteRejectReason,
 19} from "./utils";
 20
 21// ============================================================================
 22// stripQuotedContent
 23// ============================================================================
 24
 25describe("stripQuotedContent", () => {
 26	test("strips double-quoted strings", () => {
 27		expect(stripQuotedContent('git commit -m "rm -rf everything"')).toBe('git commit -m ""');
 28	});
 29
 30	test("strips single-quoted strings", () => {
 31		expect(stripQuotedContent("echo 'sudo rm -rf /'")).toBe("echo ''");
 32	});
 33
 34	test("handles escaped quotes in double-quoted strings", () => {
 35		expect(stripQuotedContent('echo "say \\"hello\\""')).toBe('echo ""');
 36	});
 37
 38	test("preserves unquoted content", () => {
 39		expect(stripQuotedContent("rm -rf /tmp/test")).toBe("rm -rf /tmp/test");
 40	});
 41
 42	test("handles mixed quotes", () => {
 43		const result = stripQuotedContent(`kubectl apply -f "config.yaml" --namespace 'prod'`);
 44		expect(result).toBe(`kubectl apply -f "" --namespace ''`);
 45	});
 46});
 47
 48// ============================================================================
 49// Command Rule Matching
 50// ============================================================================
 51
 52describe("matchCommandRule", () => {
 53	test("matches recursive rm", () => {
 54		const match = matchCommandRule("rm -rf /tmp/test");
 55		expect(match).not.toBeNull();
 56		expect(match!.desc).toBe("recursive delete");
 57		expect(match!.action).toBe("confirm");
 58	});
 59
 60	test("matches sudo", () => {
 61		const match = matchCommandRule("sudo apt update");
 62		expect(match).not.toBeNull();
 63		expect(match!.desc).toBe("sudo command");
 64	});
 65
 66	test("matches kubectl", () => {
 67		const match = matchCommandRule("kubectl get pods");
 68		expect(match).not.toBeNull();
 69		expect(match!.desc).toBe("kubectl command");
 70		expect(match!.action).toBe("confirm");
 71	});
 72
 73	test("blocks nixos-rebuild switch", () => {
 74		const match = matchCommandRule("nixos-rebuild switch");
 75		expect(match).not.toBeNull();
 76		expect(match!.action).toBe("block");
 77		expect(match!.suggestion).toContain("make");
 78	});
 79
 80	test("blocks home-manager switch", () => {
 81		const match = matchCommandRule("home-manager switch");
 82		expect(match).not.toBeNull();
 83		expect(match!.action).toBe("block");
 84	});
 85
 86	test("matches gh pr", () => {
 87		const match = matchCommandRule("gh pr list");
 88		expect(match).not.toBeNull();
 89		expect(match!.desc).toContain("gh CLI");
 90		expect(match!.suggestion).toContain("github");
 91	});
 92
 93	test("matches nix eval", () => {
 94		const match = matchCommandRule("nix eval .#foo");
 95		expect(match).not.toBeNull();
 96		expect(match!.desc).toContain("nix eval");
 97	});
 98
 99	test("matches nix flake update", () => {
100		const match = matchCommandRule("nix flake update");
101		expect(match).not.toBeNull();
102		expect(match!.desc).toContain("nix flake update");
103	});
104
105	test("matches nix run", () => {
106		const match = matchCommandRule("nix run nixpkgs#hello");
107		expect(match).not.toBeNull();
108		expect(match!.desc).toContain("nix run");
109	});
110
111	test("does not match safe commands", () => {
112		expect(matchCommandRule("ls -la")).toBeNull();
113		expect(matchCommandRule("cat README.md")).toBeNull();
114		expect(matchCommandRule("echo hello")).toBeNull();
115		expect(matchCommandRule("git status")).toBeNull();
116	});
117
118	test("does not match quoted content", () => {
119		// stripQuotedContent should be applied before matching
120		const stripped = stripQuotedContent('git commit -m "sudo rm -rf"');
121		expect(matchCommandRule(stripped)).toBeNull();
122	});
123});
124
125// ============================================================================
126// Dangerous Bash Writes
127// ============================================================================
128
129describe("dangerousBashWrites", () => {
130	test("detects redirect to .env", () => {
131		const matches = dangerousBashWrites.some((p) => p.test("> .env"));
132		expect(matches).toBe(true);
133	});
134
135	test("detects tee to .env", () => {
136		const matches = dangerousBashWrites.some((p) => p.test("tee .env"));
137		expect(matches).toBe(true);
138	});
139
140	test("detects redirect to .pem", () => {
141		const matches = dangerousBashWrites.some((p) => p.test("> cert.pem"));
142		expect(matches).toBe(true);
143	});
144
145	test("does not match safe writes", () => {
146		const matches = dangerousBashWrites.some((p) => p.test("> output.txt"));
147		expect(matches).toBe(false);
148	});
149});
150
151// ============================================================================
152// Protected Paths
153// ============================================================================
154
155describe("protectedPaths", () => {
156	test("matches .env files", () => {
157		const matches = protectedPaths.some((p) => p.pattern.test(".env"));
158		expect(matches).toBe(true);
159	});
160
161	test("matches .env.local but not .env.example", () => {
162		const matchesLocal = protectedPaths.some((p) => p.pattern.test(".env.local"));
163		expect(matchesLocal).toBe(true);
164		const matchesExample = protectedPaths.some((p) => p.pattern.test(".env.example"));
165		expect(matchesExample).toBe(false);
166	});
167
168	test("matches node_modules", () => {
169		const matches = protectedPaths.some((p) => p.pattern.test("node_modules/foo/bar.js"));
170		expect(matches).toBe(true);
171	});
172
173	test("matches .git directory", () => {
174		const matches = protectedPaths.some((p) => p.pattern.test(".git/config"));
175		expect(matches).toBe(true);
176	});
177
178	test("matches SSH keys", () => {
179		const matches = protectedPaths.some((p) => p.pattern.test("id_ed25519"));
180		expect(matches).toBe(true);
181	});
182
183	test("does not match normal files", () => {
184		const matches = protectedPaths.some((p) => p.pattern.test("src/main.ts"));
185		expect(matches).toBe(false);
186	});
187});
188
189describe("softProtectedPaths", () => {
190	test("matches package-lock.json", () => {
191		const matches = softProtectedPaths.some((p) => p.pattern.test("package-lock.json"));
192		expect(matches).toBe(true);
193	});
194
195	test("matches yarn.lock", () => {
196		const matches = softProtectedPaths.some((p) => p.pattern.test("yarn.lock"));
197		expect(matches).toBe(true);
198	});
199
200	test("matches pnpm-lock.yaml", () => {
201		const matches = softProtectedPaths.some((p) => p.pattern.test("pnpm-lock.yaml"));
202		expect(matches).toBe(true);
203	});
204
205	test("does not match package.json", () => {
206		const matches = softProtectedPaths.some((p) => p.pattern.test("package.json"));
207		expect(matches).toBe(false);
208	});
209});
210
211// ============================================================================
212// Approval Gate Integration
213// ============================================================================
214
215describe("buildModifyReason", () => {
216	test("tells LLM to ask user for changes", () => {
217		const reason = buildModifyReason("kubectl command");
218		expect(reason).toContain("modify");
219		expect(reason).toContain("Ask the user");
220		expect(reason).toContain("retry");
221		expect(reason).toContain("kubectl command");
222	});
223});
224
225describe("buildRejectReason", () => {
226	test("tells LLM not to retry", () => {
227		const reason = buildRejectReason("kubectl command");
228		expect(reason).toContain("rejected");
229		expect(reason).toContain("Do NOT retry");
230		expect(reason).toContain("kubectl command");
231	});
232});
233
234describe("buildWriteModifyReason", () => {
235	test("tells LLM to ask user for changes with file context", () => {
236		const reason = buildWriteModifyReason("package-lock.json", "/project/package-lock.json");
237		expect(reason).toContain("modify");
238		expect(reason).toContain("Ask the user");
239		expect(reason).toContain("package-lock.json");
240	});
241});
242
243describe("buildWriteRejectReason", () => {
244	test("tells LLM not to retry with file context", () => {
245		const reason = buildWriteRejectReason("yarn.lock", "/project/yarn.lock");
246		expect(reason).toContain("rejected");
247		expect(reason).toContain("Do NOT retry");
248		expect(reason).toContain("yarn.lock");
249	});
250});