@uniglot/wont-let-you-see 0.1.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.
@@ -0,0 +1,808 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { addEntry } from "../mapping";
3
+ import { plugin } from "../index";
4
+ import { resetConfig } from "../config";
5
+ import type { Hooks } from "@opencode-ai/plugin";
6
+
7
+ describe("tool.execute.before hook", () => {
8
+ const sessionId = "test-session-hooks";
9
+ const originalEnv = { ...process.env };
10
+ let hooks: Hooks;
11
+
12
+ beforeEach(async () => {
13
+ process.env.WONT_LET_YOU_SEE_REVEALED_PATTERNS = "";
14
+ resetConfig();
15
+ hooks = await plugin({} as any);
16
+
17
+ addEntry(sessionId, "vpc", "vpc-abc123");
18
+ addEntry(sessionId, "subnet", "subnet-xyz789");
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.env = { ...originalEnv };
23
+ resetConfig();
24
+ });
25
+
26
+ describe("AWS command detection", () => {
27
+ it("should detect aws commands", async () => {
28
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-1" };
29
+ const output = { args: { command: "aws ec2 describe-vpcs" } };
30
+
31
+ const hook = hooks["tool.execute.before"];
32
+ expect(hook).toBeDefined();
33
+
34
+ if (hook) {
35
+ await hook(input, output);
36
+ expect(output.args.command).toBe("aws ec2 describe-vpcs");
37
+ }
38
+ });
39
+
40
+ it("should ignore non-aws commands", async () => {
41
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-2" };
42
+ const output = { args: { command: "ls -la" } };
43
+
44
+ const hook = hooks["tool.execute.before"];
45
+ if (hook) {
46
+ await hook(input, output);
47
+ expect(output.args.command).toBe("ls -la");
48
+ }
49
+ });
50
+
51
+ it("should ignore git commands", async () => {
52
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-3" };
53
+ const output = { args: { command: "git status" } };
54
+
55
+ const hook = hooks["tool.execute.before"];
56
+ if (hook) {
57
+ await hook(input, output);
58
+ expect(output.args.command).toBe("git status");
59
+ }
60
+ });
61
+
62
+ it("should ignore non-bash tools", async () => {
63
+ const input = { tool: "read", sessionID: sessionId, callID: "call-4" };
64
+ const output = { args: { command: "aws ec2 describe-vpcs" } };
65
+
66
+ const hook = hooks["tool.execute.before"];
67
+ if (hook) {
68
+ await hook(input, output);
69
+ expect(output.args.command).toBe("aws ec2 describe-vpcs");
70
+ }
71
+ });
72
+ });
73
+
74
+ describe("Token unmasking", () => {
75
+ it("should unmask single token in aws command", async () => {
76
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-5" };
77
+ const output = {
78
+ args: { command: "aws ec2 describe-vpcs --vpc-ids #(vpc-1)" },
79
+ };
80
+
81
+ const hook = hooks["tool.execute.before"];
82
+ if (hook) {
83
+ await hook(input, output);
84
+ expect(output.args.command).toBe(
85
+ "aws ec2 describe-vpcs --vpc-ids vpc-abc123",
86
+ );
87
+ }
88
+ });
89
+
90
+ it("should unmask multiple tokens in aws command", async () => {
91
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-6" };
92
+ const output = {
93
+ args: {
94
+ command:
95
+ "aws ec2 describe-subnets --subnet-ids #(subnet-1) --filters Name=vpc-id,Values=#(vpc-1)",
96
+ },
97
+ };
98
+
99
+ const hook = hooks["tool.execute.before"];
100
+ if (hook) {
101
+ await hook(input, output);
102
+ expect(output.args.command).toBe(
103
+ "aws ec2 describe-subnets --subnet-ids subnet-xyz789 --filters Name=vpc-id,Values=vpc-abc123",
104
+ );
105
+ }
106
+ });
107
+
108
+ it("should handle unknown tokens with warning", async () => {
109
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
110
+
111
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-7" };
112
+ const output = {
113
+ args: { command: "aws ec2 describe-vpcs --vpc-ids #(vpc-999)" },
114
+ };
115
+
116
+ const hook = hooks["tool.execute.before"];
117
+ if (hook) {
118
+ await hook(input, output);
119
+
120
+ expect(warnSpy).toHaveBeenCalledWith("Unknown token: #(vpc-999)");
121
+
122
+ expect(output.args.command).toBe(
123
+ "aws ec2 describe-vpcs --vpc-ids #(vpc-999)",
124
+ );
125
+ }
126
+
127
+ warnSpy.mockRestore();
128
+ });
129
+
130
+ it("should not modify command structure", async () => {
131
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-8" };
132
+ const output = {
133
+ args: {
134
+ command: "aws ec2 describe-vpcs --vpc-ids #(vpc-1) --output json",
135
+ },
136
+ };
137
+
138
+ const hook = hooks["tool.execute.before"];
139
+ if (hook) {
140
+ await hook(input, output);
141
+
142
+ expect(output.args.command).toBe(
143
+ "aws ec2 describe-vpcs --vpc-ids vpc-abc123 --output json",
144
+ );
145
+ expect(output.args.command).toContain("--output json");
146
+ expect(output.args.command).toContain("--vpc-ids");
147
+ }
148
+ });
149
+
150
+ it("should handle aws commands without tokens", async () => {
151
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-9" };
152
+ const output = { args: { command: "aws ec2 describe-vpcs" } };
153
+
154
+ const hook = hooks["tool.execute.before"];
155
+ if (hook) {
156
+ await hook(input, output);
157
+ expect(output.args.command).toBe("aws ec2 describe-vpcs");
158
+ }
159
+ });
160
+ });
161
+
162
+ describe("Edge cases", () => {
163
+ it("should handle aws command with mixed content", async () => {
164
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-10" };
165
+ const output = {
166
+ args: {
167
+ command:
168
+ "aws ec2 create-tags --resources #(vpc-1) --tags Key=Name,Value=MyVPC",
169
+ },
170
+ };
171
+
172
+ const hook = hooks["tool.execute.before"];
173
+ if (hook) {
174
+ await hook(input, output);
175
+ expect(output.args.command).toBe(
176
+ "aws ec2 create-tags --resources vpc-abc123 --tags Key=Name,Value=MyVPC",
177
+ );
178
+ }
179
+ });
180
+
181
+ it("should handle command starting with whitespace", async () => {
182
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-11" };
183
+ const output = {
184
+ args: { command: " aws ec2 describe-vpcs --vpc-ids #(vpc-1)" },
185
+ };
186
+
187
+ const hook = hooks["tool.execute.before"];
188
+ if (hook) {
189
+ await hook(input, output);
190
+ expect(output.args.command).toBe(
191
+ " aws ec2 describe-vpcs --vpc-ids vpc-abc123",
192
+ );
193
+ }
194
+ });
195
+
196
+ it("should handle AWS in uppercase", async () => {
197
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-12" };
198
+ const output = {
199
+ args: { command: "AWS ec2 describe-vpcs --vpc-ids #(vpc-1)" },
200
+ };
201
+
202
+ const hook = hooks["tool.execute.before"];
203
+ if (hook) {
204
+ await hook(input, output);
205
+ expect(output.args.command).toBe(
206
+ "AWS ec2 describe-vpcs --vpc-ids #(vpc-1)",
207
+ );
208
+ }
209
+ });
210
+
211
+ it("should detect aws in piped commands", async () => {
212
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-13" };
213
+ const output = {
214
+ args: { command: "cat data.json | aws s3 cp - s3://bucket/#(vpc-1)" },
215
+ };
216
+
217
+ const hook = hooks["tool.execute.before"];
218
+ if (hook) {
219
+ await hook(input, output);
220
+ expect(output.args.command).toBe(
221
+ "cat data.json | aws s3 cp - s3://bucket/vpc-abc123",
222
+ );
223
+ }
224
+ });
225
+
226
+ it("should detect aws after environment variables", async () => {
227
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-14" };
228
+ const output = {
229
+ args: {
230
+ command: "AWS_PROFILE=prod aws ec2 describe-vpcs --vpc-ids #(vpc-1)",
231
+ },
232
+ };
233
+
234
+ const hook = hooks["tool.execute.before"];
235
+ if (hook) {
236
+ await hook(input, output);
237
+ expect(output.args.command).toBe(
238
+ "AWS_PROFILE=prod aws ec2 describe-vpcs --vpc-ids vpc-abc123",
239
+ );
240
+ }
241
+ });
242
+
243
+ it("should detect aws in chained commands", async () => {
244
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-15" };
245
+ const output = {
246
+ args: {
247
+ command:
248
+ "echo 'starting' && aws ec2 describe-vpcs --vpc-ids #(vpc-1)",
249
+ },
250
+ };
251
+
252
+ const hook = hooks["tool.execute.before"];
253
+ if (hook) {
254
+ await hook(input, output);
255
+ expect(output.args.command).toBe(
256
+ "echo 'starting' && aws ec2 describe-vpcs --vpc-ids vpc-abc123",
257
+ );
258
+ }
259
+ });
260
+
261
+ it("should detect terraform commands", async () => {
262
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-16" };
263
+ const output = {
264
+ args: { command: "terraform plan -var vpc_id=#(vpc-1)" },
265
+ };
266
+
267
+ const hook = hooks["tool.execute.before"];
268
+ if (hook) {
269
+ await hook(input, output);
270
+ expect(output.args.command).toBe(
271
+ "terraform plan -var vpc_id=vpc-abc123",
272
+ );
273
+ }
274
+ });
275
+
276
+ it("should detect kubectl commands", async () => {
277
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-17" };
278
+ const output = {
279
+ args: { command: "kubectl get pods -n #(vpc-1)" },
280
+ };
281
+
282
+ const hook = hooks["tool.execute.before"];
283
+ if (hook) {
284
+ await hook(input, output);
285
+ expect(output.args.command).toBe("kubectl get pods -n vpc-abc123");
286
+ }
287
+ });
288
+
289
+ it("should detect helm commands", async () => {
290
+ const input = { tool: "bash", sessionID: sessionId, callID: "call-18" };
291
+ const output = {
292
+ args: { command: "helm install myapp --set vpc=#(vpc-1)" },
293
+ };
294
+
295
+ const hook = hooks["tool.execute.before"];
296
+ if (hook) {
297
+ await hook(input, output);
298
+ expect(output.args.command).toBe(
299
+ "helm install myapp --set vpc=vpc-abc123",
300
+ );
301
+ }
302
+ });
303
+ });
304
+ });
305
+
306
+ describe("tool.execute.after hook", () => {
307
+ const sessionId = "test-session-after-hooks";
308
+ const originalEnv = { ...process.env };
309
+ let hooks: Hooks;
310
+
311
+ beforeEach(async () => {
312
+ process.env.WONT_LET_YOU_SEE_REVEALED_PATTERNS = "";
313
+ resetConfig();
314
+ hooks = await plugin({} as any);
315
+ });
316
+
317
+ afterEach(() => {
318
+ process.env = { ...originalEnv };
319
+ resetConfig();
320
+ });
321
+
322
+ describe("AWS CLI JSON output masking", () => {
323
+ it("should mask AWS resource IDs in JSON output", async () => {
324
+ const callID = "call-after-1";
325
+ const input = { tool: "bash", sessionID: sessionId, callID };
326
+
327
+ // Simulate before hook to mark this as an AWS command
328
+ const beforeOutput = { args: { command: "aws ec2 describe-vpcs" } };
329
+ const beforeHook = hooks["tool.execute.before"];
330
+ if (beforeHook) {
331
+ await beforeHook(input, beforeOutput);
332
+ }
333
+
334
+ const output = {
335
+ title: "AWS CLI Output",
336
+ output: JSON.stringify({
337
+ Vpcs: [{ VpcId: "vpc-0abc1234", CidrBlock: "10.0.0.0/16" }],
338
+ }),
339
+ metadata: {},
340
+ };
341
+
342
+ const hook = hooks["tool.execute.after"];
343
+ expect(hook).toBeDefined();
344
+
345
+ if (hook) {
346
+ await hook(input, output);
347
+
348
+ const parsed = JSON.parse(output.output);
349
+ expect(parsed.Vpcs[0].VpcId).toMatch(/^#\(vpc-\d+\)$/);
350
+ expect(parsed.Vpcs[0].CidrBlock).toMatch(/^#\(ipv4-\d+\)\/16$/);
351
+ }
352
+ });
353
+
354
+ it("should preserve JSON validity after masking", async () => {
355
+ const callID = "call-after-2";
356
+ const input = { tool: "bash", sessionID: sessionId, callID };
357
+
358
+ const beforeOutput = { args: { command: "aws ec2 describe-subnets" } };
359
+ const beforeHook = hooks["tool.execute.before"];
360
+ if (beforeHook) {
361
+ await beforeHook(input, beforeOutput);
362
+ }
363
+
364
+ const output = {
365
+ title: "AWS CLI Output",
366
+ output: JSON.stringify({
367
+ Subnets: [{ SubnetId: "subnet-9abcd890", VpcId: "vpc-0abc1234" }],
368
+ }),
369
+ metadata: {},
370
+ };
371
+
372
+ const hook = hooks["tool.execute.after"];
373
+ if (hook) {
374
+ await hook(input, output);
375
+
376
+ expect(() => JSON.parse(output.output)).not.toThrow();
377
+
378
+ const parsed = JSON.parse(output.output);
379
+ expect(parsed.Subnets).toBeDefined();
380
+ expect(parsed.Subnets[0].SubnetId).toMatch(/^#\(subnet-\d+\)$/);
381
+ }
382
+ });
383
+ });
384
+
385
+ describe("AWS CLI text output masking", () => {
386
+ it("should mask AWS resource IDs in text output", async () => {
387
+ const callID = "call-after-3";
388
+ const input = { tool: "bash", sessionID: sessionId, callID };
389
+
390
+ const beforeOutput = { args: { command: "aws ec2 describe-vpcs" } };
391
+ const beforeHook = hooks["tool.execute.before"];
392
+ if (beforeHook) {
393
+ await beforeHook(input, beforeOutput);
394
+ }
395
+
396
+ const output = {
397
+ title: "AWS CLI Output",
398
+ output: "VPC ID: vpc-0abc1234\nSubnet ID: subnet-9abcd890",
399
+ metadata: {},
400
+ };
401
+
402
+ const hook = hooks["tool.execute.after"];
403
+ if (hook) {
404
+ await hook(input, output);
405
+
406
+ expect(output.output).toMatch(/VPC ID: #\(vpc-\d+\)/);
407
+ expect(output.output).toMatch(/Subnet ID: #\(subnet-\d+\)/);
408
+ expect(output.output).not.toContain("vpc-0abc1234");
409
+ expect(output.output).not.toContain("subnet-9abcd890");
410
+ }
411
+ });
412
+
413
+ it("should mask IP addresses in text output", async () => {
414
+ const callID = "call-after-4";
415
+ const input = { tool: "bash", sessionID: sessionId, callID };
416
+
417
+ const beforeOutput = { args: { command: "aws ec2 describe-instances" } };
418
+ const beforeHook = hooks["tool.execute.before"];
419
+ if (beforeHook) {
420
+ await beforeHook(input, beforeOutput);
421
+ }
422
+
423
+ const output = {
424
+ title: "AWS CLI Output",
425
+ output: "Instance IP: 192.168.1.100\nCIDR: 10.0.0.0/16",
426
+ metadata: {},
427
+ };
428
+
429
+ const hook = hooks["tool.execute.after"];
430
+ if (hook) {
431
+ await hook(input, output);
432
+
433
+ expect(output.output).toMatch(/Instance IP: #\(ipv4-\d+\)/);
434
+ expect(output.output).toMatch(/CIDR: #\(ipv4-\d+\)\/16/);
435
+ }
436
+ });
437
+ });
438
+
439
+ describe("AWS CLI error output masking", () => {
440
+ it("should mask AWS resource IDs in error messages", async () => {
441
+ const callID = "call-after-5";
442
+ const input = { tool: "bash", sessionID: sessionId, callID };
443
+
444
+ const beforeOutput = {
445
+ args: { command: "aws ec2 describe-vpcs --vpc-ids vpc-0abc1234" },
446
+ };
447
+ const beforeHook = hooks["tool.execute.before"];
448
+ if (beforeHook) {
449
+ await beforeHook(input, beforeOutput);
450
+ }
451
+
452
+ const output = {
453
+ title: "AWS CLI Error",
454
+ output: "Error: VPC vpc-0abc1234 not found",
455
+ metadata: {},
456
+ };
457
+
458
+ const hook = hooks["tool.execute.after"];
459
+ if (hook) {
460
+ await hook(input, output);
461
+
462
+ expect(output.output).toMatch(/Error: VPC #\(vpc-\d+\) not found/);
463
+ expect(output.output).not.toContain("vpc-0abc1234");
464
+ }
465
+ });
466
+ });
467
+
468
+ describe("Idempotent behavior", () => {
469
+ it("should not double-mask already masked output", async () => {
470
+ const callID = "call-after-6";
471
+ const input = { tool: "bash", sessionID: sessionId, callID };
472
+
473
+ const beforeOutput = { args: { command: "aws ec2 describe-vpcs" } };
474
+ const beforeHook = hooks["tool.execute.before"];
475
+ if (beforeHook) {
476
+ await beforeHook(input, beforeOutput);
477
+ }
478
+
479
+ const output = {
480
+ title: "AWS CLI Output",
481
+ output: "VPC ID: #(vpc-1)",
482
+ metadata: {},
483
+ };
484
+
485
+ const hook = hooks["tool.execute.after"];
486
+ if (hook) {
487
+ await hook(input, output);
488
+
489
+ expect(output.output).toBe("VPC ID: #(vpc-1)");
490
+ }
491
+ });
492
+
493
+ it("should handle mixed masked and unmasked content", async () => {
494
+ const callID = "call-after-7";
495
+ const input = { tool: "bash", sessionID: sessionId, callID };
496
+
497
+ const beforeOutput = { args: { command: "aws ec2 describe-subnets" } };
498
+ const beforeHook = hooks["tool.execute.before"];
499
+ if (beforeHook) {
500
+ await beforeHook(input, beforeOutput);
501
+ }
502
+
503
+ const output = {
504
+ title: "AWS CLI Output",
505
+ output: "VPC ID: #(vpc-1), Subnet ID: subnet-9abcd890",
506
+ metadata: {},
507
+ };
508
+
509
+ const hook = hooks["tool.execute.after"];
510
+ if (hook) {
511
+ await hook(input, output);
512
+
513
+ expect(output.output).toContain("#(vpc-1)");
514
+ expect(output.output).toMatch(/Subnet ID: #\(subnet-\d+\)/);
515
+ expect(output.output).not.toContain("subnet-9abcd890");
516
+ }
517
+ });
518
+ });
519
+
520
+ describe("Non-AWS command handling", () => {
521
+ it("should not mask output from non-bash tools", async () => {
522
+ const input = {
523
+ tool: "read",
524
+ sessionID: sessionId,
525
+ callID: "call-after-8",
526
+ };
527
+ const output = {
528
+ title: "File Content",
529
+ output: "VPC ID: vpc-abc123",
530
+ metadata: {},
531
+ };
532
+
533
+ const hook = hooks["tool.execute.after"];
534
+ if (hook) {
535
+ await hook(input, output);
536
+
537
+ expect(output.output).toBe("VPC ID: vpc-abc123");
538
+ }
539
+ });
540
+
541
+ it("should not mask output from non-AWS bash commands", async () => {
542
+ const input = {
543
+ tool: "bash",
544
+ sessionID: sessionId,
545
+ callID: "call-after-9",
546
+ };
547
+ const output = {
548
+ title: "Git Status",
549
+ output: "On branch main\nnothing to commit",
550
+ metadata: {},
551
+ };
552
+
553
+ const hook = hooks["tool.execute.after"];
554
+ if (hook) {
555
+ await hook(input, output);
556
+
557
+ expect(output.output).toBe("On branch main\nnothing to commit");
558
+ }
559
+ });
560
+ });
561
+ });
562
+ describe("chat.message hook (UserPromptSubmit)", () => {
563
+ const sessionId = "test-session-chat-message";
564
+ const originalEnv = { ...process.env };
565
+ let hooks: Hooks;
566
+
567
+ beforeEach(async () => {
568
+ process.env.WONT_LET_YOU_SEE_REVEALED_PATTERNS = "";
569
+ resetConfig();
570
+ hooks = await plugin({} as any);
571
+ });
572
+
573
+ afterEach(() => {
574
+ process.env = { ...originalEnv };
575
+ resetConfig();
576
+ });
577
+
578
+ describe("User prompt masking", () => {
579
+ it("should mask sensitive data in user prompt", async () => {
580
+ const input = { sessionID: sessionId };
581
+ const output = {
582
+ message: { role: "user" } as any,
583
+ parts: [
584
+ {
585
+ type: "text" as const,
586
+ text: "Check VPC vpc-0abc1234 and subnet subnet-9abc7890",
587
+ },
588
+ ] as any,
589
+ };
590
+
591
+ const hook = hooks["chat.message"];
592
+ expect(hook).toBeDefined();
593
+
594
+ if (hook) {
595
+ await hook(input, output);
596
+
597
+ expect(output.parts[0].text).toMatch(/Check VPC #\(vpc-\d+\)/);
598
+ expect(output.parts[0].text).toMatch(/subnet #\(subnet-\d+\)/);
599
+ expect(output.parts[0].text).not.toContain("vpc-0abc1234");
600
+ expect(output.parts[0].text).not.toContain("subnet-9abc7890");
601
+ }
602
+ });
603
+
604
+ it("should handle ARNs pasted by user", async () => {
605
+ const input = { sessionID: sessionId };
606
+ const output = {
607
+ message: { role: "user" } as any,
608
+ parts: [
609
+ {
610
+ type: "text" as const,
611
+ text: "Check this ARN: arn:aws:ec2:us-east-1:123456789012:vpc/vpc-abc123",
612
+ },
613
+ ] as any,
614
+ };
615
+
616
+ const hook = hooks["chat.message"];
617
+ if (hook) {
618
+ await hook(input, output);
619
+
620
+ expect(output.parts[0].text).toMatch(/Check this ARN: #\(arn-\d+\)/);
621
+ expect(output.parts[0].text).not.toContain(
622
+ "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-abc123",
623
+ );
624
+ }
625
+ });
626
+
627
+ it("should handle VPC IDs pasted by user", async () => {
628
+ const input = { sessionID: sessionId };
629
+ const output = {
630
+ message: { role: "user" } as any,
631
+ parts: [
632
+ {
633
+ type: "text" as const,
634
+ text: "I need help with vpc-0abc1234def5678",
635
+ },
636
+ ] as any,
637
+ };
638
+
639
+ const hook = hooks["chat.message"];
640
+ if (hook) {
641
+ await hook(input, output);
642
+
643
+ expect(output.parts[0].text).toMatch(/I need help with #\(vpc-\d+\)/);
644
+ expect(output.parts[0].text).not.toContain("vpc-0abc1234def5678");
645
+ }
646
+ });
647
+
648
+ it("should handle IP addresses pasted by user", async () => {
649
+ const input = { sessionID: sessionId };
650
+ const output = {
651
+ message: { role: "user" } as any,
652
+ parts: [
653
+ { type: "text" as const, text: "Server at 192.168.1.100 is down" },
654
+ ] as any,
655
+ };
656
+
657
+ const hook = hooks["chat.message"];
658
+ if (hook) {
659
+ await hook(input, output);
660
+
661
+ expect(output.parts[0].text).toMatch(/Server at #\(ipv4-\d+\) is down/);
662
+ expect(output.parts[0].text).not.toContain("192.168.1.100");
663
+ }
664
+ });
665
+
666
+ it("should handle CIDR blocks pasted by user", async () => {
667
+ const input = { sessionID: sessionId };
668
+ const output = {
669
+ message: { role: "user" } as any,
670
+ parts: [
671
+ { type: "text" as const, text: "Configure network with 10.0.0.0/16" },
672
+ ] as any,
673
+ };
674
+
675
+ const hook = hooks["chat.message"];
676
+ if (hook) {
677
+ await hook(input, output);
678
+
679
+ expect(output.parts[0].text).toMatch(
680
+ /Configure network with #\(ipv4-\d+\)\/16/,
681
+ );
682
+ expect(output.parts[0].text).not.toContain("10.0.0.0");
683
+ }
684
+ });
685
+ });
686
+
687
+ describe("Idempotent behavior", () => {
688
+ it("should not double-mask already masked prompts", async () => {
689
+ const input = { sessionID: sessionId };
690
+ const output = {
691
+ message: { role: "user" } as any,
692
+ parts: [
693
+ { type: "text" as const, text: "Check VPC #(vpc-1) status" },
694
+ ] as any,
695
+ };
696
+
697
+ const hook = hooks["chat.message"];
698
+ if (hook) {
699
+ await hook(input, output);
700
+
701
+ expect(output.parts[0].text).toBe("Check VPC #(vpc-1) status");
702
+ }
703
+ });
704
+
705
+ it("should handle mixed masked and unmasked content", async () => {
706
+ const input = { sessionID: sessionId };
707
+ const output = {
708
+ message: { role: "user" } as any,
709
+ parts: [
710
+ {
711
+ type: "text" as const,
712
+ text: "VPC #(vpc-1) and subnet subnet-9abc7890",
713
+ },
714
+ ] as any,
715
+ };
716
+
717
+ const hook = hooks["chat.message"];
718
+ if (hook) {
719
+ await hook(input, output);
720
+
721
+ expect(output.parts[0].text).toContain("#(vpc-1)");
722
+ expect(output.parts[0].text).toMatch(/subnet #\(subnet-\d+\)/);
723
+ expect(output.parts[0].text).not.toContain("subnet-9abc7890");
724
+ }
725
+ });
726
+ });
727
+
728
+ describe("Edge cases", () => {
729
+ it("should handle empty message content", async () => {
730
+ const input = { sessionID: sessionId };
731
+ const output = {
732
+ message: { role: "user" } as any,
733
+ parts: [{ type: "text" as const, text: "" }] as any,
734
+ };
735
+
736
+ const hook = hooks["chat.message"];
737
+ if (hook) {
738
+ await hook(input, output);
739
+
740
+ expect(output.parts[0].text).toBe("");
741
+ }
742
+ });
743
+
744
+ it("should handle message with no sensitive data", async () => {
745
+ const input = { sessionID: sessionId };
746
+ const output = {
747
+ message: { role: "user" } as any,
748
+ parts: [{ type: "text" as const, text: "Hello, how are you?" }] as any,
749
+ };
750
+
751
+ const hook = hooks["chat.message"];
752
+ if (hook) {
753
+ await hook(input, output);
754
+
755
+ expect(output.parts[0].text).toBe("Hello, how are you?");
756
+ }
757
+ });
758
+
759
+ it("should handle multiple sensitive items in one prompt", async () => {
760
+ const input = { sessionID: sessionId };
761
+ const output = {
762
+ message: { role: "user" } as any,
763
+ parts: [
764
+ {
765
+ type: "text" as const,
766
+ text: "Check vpc-0abc1234, subnet-9abc7890, and IP 192.168.1.100",
767
+ },
768
+ ] as any,
769
+ };
770
+
771
+ const hook = hooks["chat.message"];
772
+ if (hook) {
773
+ await hook(input, output);
774
+
775
+ expect(output.parts[0].text).toMatch(/#\(vpc-\d+\)/);
776
+ expect(output.parts[0].text).toMatch(/#\(subnet-\d+\)/);
777
+ expect(output.parts[0].text).toMatch(/#\(ipv4-\d+\)/);
778
+ expect(output.parts[0].text).not.toContain("vpc-0abc1234");
779
+ expect(output.parts[0].text).not.toContain("subnet-9abc7890");
780
+ expect(output.parts[0].text).not.toContain("192.168.1.100");
781
+ }
782
+ });
783
+
784
+ it("should not modify prompt structure beyond masking", async () => {
785
+ const input = { sessionID: sessionId };
786
+ const output = {
787
+ message: { role: "user" } as any,
788
+ parts: [
789
+ {
790
+ type: "text" as const,
791
+ text: "Line 1: vpc-0abc1234\nLine 2: subnet-9abc7890\nLine 3: Done",
792
+ },
793
+ ] as any,
794
+ };
795
+
796
+ const hook = hooks["chat.message"];
797
+ if (hook) {
798
+ await hook(input, output);
799
+
800
+ const lines = output.parts[0].text.split("\n");
801
+ expect(lines).toHaveLength(3);
802
+ expect(lines[0]).toMatch(/Line 1: #\(vpc-\d+\)/);
803
+ expect(lines[1]).toMatch(/Line 2: #\(subnet-\d+\)/);
804
+ expect(lines[2]).toBe("Line 3: Done");
805
+ }
806
+ });
807
+ });
808
+ });