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});