@os-eco/overstory-cli 0.7.0 → 0.7.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.
- package/README.md +6 -5
- package/agents/builder.md +1 -1
- package/agents/coordinator.md +12 -11
- package/agents/lead.md +6 -6
- package/agents/monitor.md +4 -4
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +5 -5
- package/agents/supervisor.md +36 -32
- package/package.json +1 -1
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/hooks-deployer.ts +7 -90
- package/src/agents/overlay.test.ts +7 -7
- package/src/agents/overlay.ts +5 -5
- package/src/commands/agents.test.ts +5 -0
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/coordinator.test.ts +1 -0
- package/src/commands/coordinator.ts +15 -11
- package/src/commands/costs.test.ts +5 -0
- package/src/commands/init.test.ts +1 -2
- package/src/commands/init.ts +1 -8
- package/src/commands/inspect.test.ts +14 -0
- package/src/commands/log.test.ts +14 -0
- package/src/commands/log.ts +39 -0
- package/src/commands/mail.test.ts +5 -0
- package/src/commands/monitor.ts +15 -11
- package/src/commands/nudge.test.ts +1 -0
- package/src/commands/prime.test.ts +2 -0
- package/src/commands/prime.ts +6 -2
- package/src/commands/run.test.ts +1 -0
- package/src/commands/sling.test.ts +15 -1
- package/src/commands/sling.ts +44 -21
- package/src/commands/status.test.ts +1 -0
- package/src/commands/stop.test.ts +1 -0
- package/src/commands/supervisor.ts +19 -12
- package/src/commands/trace.test.ts +1 -0
- package/src/commands/worktree.test.ts +9 -0
- package/src/config.ts +29 -0
- package/src/doctor/consistency.test.ts +14 -0
- package/src/e2e/init-sling-lifecycle.test.ts +3 -5
- package/src/index.ts +1 -1
- package/src/mail/broadcast.test.ts +1 -0
- package/src/merge/resolver.ts +23 -4
- package/src/runtimes/claude.test.ts +1 -1
- package/src/runtimes/pi-guards.test.ts +433 -0
- package/src/runtimes/pi-guards.ts +349 -0
- package/src/runtimes/pi.test.ts +620 -0
- package/src/runtimes/pi.ts +244 -0
- package/src/runtimes/registry.test.ts +33 -0
- package/src/runtimes/registry.ts +15 -2
- package/src/runtimes/types.ts +63 -0
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/compat.ts +1 -0
- package/src/sessions/store.test.ts +31 -0
- package/src/sessions/store.ts +37 -4
- package/src/types.ts +17 -0
- package/src/watchdog/daemon.test.ts +7 -4
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -0
- package/src/watchdog/triage.ts +14 -4
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { INTERACTIVE_TOOLS, NATIVE_TEAM_TOOLS } from "../agents/guard-rules.ts";
|
|
3
|
+
import { PiRuntime } from "./pi.ts";
|
|
4
|
+
import { generatePiGuardExtension } from "./pi-guards.ts";
|
|
5
|
+
import type { HooksDef } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
const WORKTREE = "/project/.overstory/worktrees/test-agent";
|
|
8
|
+
|
|
9
|
+
function builderHooks(name = "test-builder"): HooksDef {
|
|
10
|
+
return { agentName: name, capability: "builder", worktreePath: WORKTREE };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scoutHooks(name = "test-scout"): HooksDef {
|
|
14
|
+
return { agentName: name, capability: "scout", worktreePath: WORKTREE };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function coordinatorHooks(name = "test-coordinator"): HooksDef {
|
|
18
|
+
return { agentName: name, capability: "coordinator", worktreePath: WORKTREE };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("generatePiGuardExtension", () => {
|
|
22
|
+
describe("header and identity", () => {
|
|
23
|
+
test("embeds agent name in generated code", () => {
|
|
24
|
+
const generated = generatePiGuardExtension(builderHooks("my-builder"));
|
|
25
|
+
expect(generated).toContain('const AGENT_NAME = "my-builder";');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("embeds worktree path in generated code", () => {
|
|
29
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
30
|
+
expect(generated).toContain(`const WORKTREE_PATH = "${WORKTREE}";`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("embeds capability in file header comment", () => {
|
|
34
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
35
|
+
expect(generated).toContain("Capability: builder");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("imports Pi Extension type", () => {
|
|
39
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
40
|
+
expect(generated).toContain(
|
|
41
|
+
'import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";',
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("exports a default Pi Extension factory", () => {
|
|
46
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
47
|
+
expect(generated).toContain("export default function (pi: ExtensionAPI) {");
|
|
48
|
+
expect(generated).toContain('pi.on("tool_call", async (event) => {');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("TEAM_BLOCKED / INTERACTIVE_BLOCKED — separate sets per category (all capabilities)", () => {
|
|
53
|
+
test("all NATIVE_TEAM_TOOLS appear in TEAM_BLOCKED for builder", () => {
|
|
54
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
55
|
+
const teamSection = extractTeamBlockedSection(generated);
|
|
56
|
+
for (const tool of NATIVE_TEAM_TOOLS) {
|
|
57
|
+
expect(teamSection).toContain(`"${tool}"`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("all INTERACTIVE_TOOLS appear in INTERACTIVE_BLOCKED for builder", () => {
|
|
62
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
63
|
+
const interactiveSection = extractInteractiveBlockedSection(generated);
|
|
64
|
+
for (const tool of INTERACTIVE_TOOLS) {
|
|
65
|
+
expect(interactiveSection).toContain(`"${tool}"`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("TEAM_BLOCKED and INTERACTIVE_BLOCKED checks use has() for efficiency", () => {
|
|
70
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
71
|
+
expect(generated).toContain("TEAM_BLOCKED.has(event.toolName)");
|
|
72
|
+
expect(generated).toContain("INTERACTIVE_BLOCKED.has(event.toolName)");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("team tools use delegation block reason", () => {
|
|
76
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
77
|
+
expect(generated).toContain("Overstory agents must use 'ov sling' for delegation");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("interactive tools use human-interaction block reason", () => {
|
|
81
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
82
|
+
expect(generated).toContain(
|
|
83
|
+
"requires human interaction — use ov mail (--type question) to escalate",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("Builder — implementation capability", () => {
|
|
89
|
+
test("write tools are NOT in TEAM_BLOCKED or INTERACTIVE_BLOCKED for builder", () => {
|
|
90
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
91
|
+
const teamSection = extractTeamBlockedSection(generated);
|
|
92
|
+
const interactiveSection = extractInteractiveBlockedSection(generated);
|
|
93
|
+
expect(teamSection).not.toContain('"Write"');
|
|
94
|
+
expect(teamSection).not.toContain('"Edit"');
|
|
95
|
+
expect(teamSection).not.toContain('"NotebookEdit"');
|
|
96
|
+
expect(interactiveSection).not.toContain('"Write"');
|
|
97
|
+
expect(interactiveSection).not.toContain('"Edit"');
|
|
98
|
+
expect(interactiveSection).not.toContain('"NotebookEdit"');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("WRITE_BLOCKED constant is absent for builder", () => {
|
|
102
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
103
|
+
expect(generated).not.toContain("const WRITE_BLOCKED =");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("has FILE_MODIFYING_PATTERNS section", () => {
|
|
107
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
108
|
+
expect(generated).toContain("FILE_MODIFYING_PATTERNS.some");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("has SAFE_PREFIXES array", () => {
|
|
112
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
113
|
+
expect(generated).toContain("const SAFE_PREFIXES =");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("does NOT have DANGEROUS_PATTERNS blocklist guard", () => {
|
|
117
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
118
|
+
expect(generated).not.toContain("DANGEROUS_PATTERNS.some");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("Bash path boundary check for file-modifying commands", () => {
|
|
122
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
123
|
+
expect(generated).toContain("FILE_MODIFYING_PATTERNS.some((re) => re.test(cmd))");
|
|
124
|
+
expect(generated).toContain("Bash path boundary violation");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("builder Bash path boundary uses + '/' and exact match", () => {
|
|
128
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
129
|
+
expect(generated).toContain('!p.startsWith(WORKTREE_PATH + "/")');
|
|
130
|
+
expect(generated).toContain("p !== WORKTREE_PATH");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("builder does NOT use cmd.trimStart() (no safe prefix check)", () => {
|
|
134
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
135
|
+
expect(generated).not.toContain("cmd.trimStart()");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("Scout — non-implementation capability", () => {
|
|
140
|
+
test("write tools ARE in WRITE_BLOCKED for scout", () => {
|
|
141
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
142
|
+
const writeSection = extractWriteBlockedSection(generated);
|
|
143
|
+
expect(writeSection).toContain('"Write"');
|
|
144
|
+
expect(writeSection).toContain('"Edit"');
|
|
145
|
+
expect(writeSection).toContain('"NotebookEdit"');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("WRITE_BLOCKED uses capability-specific block reason", () => {
|
|
149
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
150
|
+
expect(generated).toContain("scout agents cannot modify files");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("has whitelist+blocklist pattern (SAFE_PREFIXES then DANGEROUS_PATTERNS)", () => {
|
|
154
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
155
|
+
expect(generated).toContain("SAFE_PREFIXES.some((p) => trimmed.startsWith(p))");
|
|
156
|
+
expect(generated).toContain("DANGEROUS_PATTERNS.some((re) => re.test(cmd))");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("safe prefix check uses cmd.trimStart() for leading whitespace tolerance", () => {
|
|
160
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
161
|
+
expect(generated).toContain("const trimmed = cmd.trimStart();");
|
|
162
|
+
expect(generated).toContain("trimmed.startsWith(p)");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("SAFE_PREFIXES check comes before DANGEROUS_PATTERNS check", () => {
|
|
166
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
167
|
+
const safeIdx = generated.indexOf("SAFE_PREFIXES.some");
|
|
168
|
+
const dangerIdx = generated.indexOf("DANGEROUS_PATTERNS.some");
|
|
169
|
+
expect(safeIdx).toBeGreaterThan(-1);
|
|
170
|
+
expect(dangerIdx).toBeGreaterThan(-1);
|
|
171
|
+
expect(safeIdx).toBeLessThan(dangerIdx);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("does NOT have FILE_MODIFYING_PATTERNS guard", () => {
|
|
175
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
176
|
+
expect(generated).not.toContain("FILE_MODIFYING_PATTERNS.some");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("block reason references capability name", () => {
|
|
180
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
181
|
+
expect(generated).toContain("scout agents cannot modify files");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("Coordinator — coordination capability", () => {
|
|
186
|
+
test("safe prefixes include git add and git commit", () => {
|
|
187
|
+
const generated = generatePiGuardExtension(coordinatorHooks());
|
|
188
|
+
const safePrefixesSection = extractSafePrefixesSection(generated);
|
|
189
|
+
expect(safePrefixesSection).toContain('"git add"');
|
|
190
|
+
expect(safePrefixesSection).toContain('"git commit"');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("write tools are in WRITE_BLOCKED (coordination is non-implementation)", () => {
|
|
194
|
+
const generated = generatePiGuardExtension(coordinatorHooks());
|
|
195
|
+
const writeSection = extractWriteBlockedSection(generated);
|
|
196
|
+
expect(writeSection).toContain('"Write"');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("builder does NOT have git add/commit in safe prefixes", () => {
|
|
200
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
201
|
+
const safePrefixesSection = extractSafePrefixesSection(generated);
|
|
202
|
+
expect(safePrefixesSection).not.toContain('"git add"');
|
|
203
|
+
expect(safePrefixesSection).not.toContain('"git commit"');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("path boundary guards (all capabilities)", () => {
|
|
208
|
+
test("WRITE_SCOPE_TOOLS constant is always present", () => {
|
|
209
|
+
for (const hooks of [builderHooks(), scoutHooks(), coordinatorHooks()]) {
|
|
210
|
+
const generated = generatePiGuardExtension(hooks);
|
|
211
|
+
expect(generated).toContain(
|
|
212
|
+
'const WRITE_SCOPE_TOOLS = new Set<string>(["write", "edit", "Write", "Edit", "NotebookEdit"]);',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("path boundary check uses WORKTREE_PATH + '/' for subpath safety", () => {
|
|
218
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
219
|
+
expect(generated).toContain('filePath.startsWith(WORKTREE_PATH + "/")');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("path boundary allows exact worktree path match", () => {
|
|
223
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
224
|
+
expect(generated).toContain("filePath !== WORKTREE_PATH");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("path boundary checks file_path and notebook_path fields", () => {
|
|
228
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
229
|
+
expect(generated).toContain("file_path");
|
|
230
|
+
expect(generated).toContain("notebook_path");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("path boundary block reason is clear", () => {
|
|
234
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
235
|
+
expect(generated).toContain(
|
|
236
|
+
"Path boundary violation: file is outside your assigned worktree",
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("universal Bash danger guards (all capabilities)", () => {
|
|
242
|
+
test("blocks git push for builder", () => {
|
|
243
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
244
|
+
expect(generated).toContain("git push is blocked");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("blocks git push for scout", () => {
|
|
248
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
249
|
+
expect(generated).toContain("git push is blocked");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("blocks git reset --hard", () => {
|
|
253
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
254
|
+
expect(generated).toContain("git reset --hard is not allowed");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("enforces branch naming convention using AGENT_NAME", () => {
|
|
258
|
+
const generated = generatePiGuardExtension(builderHooks("my-agent"));
|
|
259
|
+
// These strings intentionally contain literal ${...} — they appear in the generated code
|
|
260
|
+
// as template literal expressions, not as interpolations in this test file.
|
|
261
|
+
expect(generated).toContain("overstory/$" + "{AGENT_NAME}/");
|
|
262
|
+
expect(generated).toContain(
|
|
263
|
+
"Branch must follow overstory/$" + "{AGENT_NAME}/{task-id} convention",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("bash guard matches both Bash and bash tool names", () => {
|
|
268
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
269
|
+
expect(generated).toContain('event.toolName === "Bash"');
|
|
270
|
+
expect(generated).toContain('event.toolName === "bash"');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("quality gate prefixes", () => {
|
|
275
|
+
test("custom quality gate commands appear in SAFE_PREFIXES", () => {
|
|
276
|
+
const hooks: HooksDef = {
|
|
277
|
+
agentName: "test-reviewer",
|
|
278
|
+
capability: "reviewer",
|
|
279
|
+
worktreePath: WORKTREE,
|
|
280
|
+
qualityGates: [
|
|
281
|
+
{ name: "Tests", command: "bun test", description: "all tests must pass" },
|
|
282
|
+
{ name: "Lint", command: "bun run lint", description: "lint clean" },
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
const generated = generatePiGuardExtension(hooks);
|
|
286
|
+
const safePrefixesSection = extractSafePrefixesSection(generated);
|
|
287
|
+
expect(safePrefixesSection).toContain('"bun test"');
|
|
288
|
+
expect(safePrefixesSection).toContain('"bun run lint"');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("default quality gates provide SAFE_PREFIXES entries", () => {
|
|
292
|
+
// Without custom gates, DEFAULT_QUALITY_GATES are used
|
|
293
|
+
const generated = generatePiGuardExtension(scoutHooks());
|
|
294
|
+
expect(generated).toContain("const SAFE_PREFIXES =");
|
|
295
|
+
// bun test is the default quality gate command
|
|
296
|
+
const safePrefixesSection = extractSafePrefixesSection(generated);
|
|
297
|
+
expect(safePrefixesSection).toContain('"bun test"');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("generated code is self-contained", () => {
|
|
302
|
+
test("output is non-empty TypeScript string", () => {
|
|
303
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
304
|
+
expect(typeof generated).toBe("string");
|
|
305
|
+
expect(generated.length).toBeGreaterThan(500);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("output ends with newline", () => {
|
|
309
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
310
|
+
expect(generated.endsWith("\n")).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("DANGEROUS_PATTERNS constant is always present", () => {
|
|
314
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
315
|
+
expect(generated).toContain("const DANGEROUS_PATTERNS =");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("FILE_MODIFYING_PATTERNS constant is always present", () => {
|
|
319
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
320
|
+
expect(generated).toContain("const FILE_MODIFYING_PATTERNS =");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("returns { type: 'allow' } as default", () => {
|
|
324
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
325
|
+
// Pi's ExtensionAPI uses implicit undefined return for allow (no explicit { type: "allow" } needed).
|
|
326
|
+
// The generated code uses a comment marker "// Default: allow." instead.
|
|
327
|
+
expect(generated).toContain("// Default: allow.");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("uses String() for safe property access on event.input", () => {
|
|
331
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
332
|
+
expect(generated).toContain("String(");
|
|
333
|
+
expect(generated).toContain("event.input as Record<string, unknown>");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("deterministic output for same inputs", () => {
|
|
337
|
+
const hooks = builderHooks("consistent-builder");
|
|
338
|
+
const g1 = generatePiGuardExtension(hooks);
|
|
339
|
+
const g2 = generatePiGuardExtension(hooks);
|
|
340
|
+
expect(g1).toBe(g2);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("activity tracking events", () => {
|
|
345
|
+
test('generated code contains pi.on("tool_call", ...)', () => {
|
|
346
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
347
|
+
expect(generated).toContain('pi.on("tool_call",');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("generated code contains pi.exec ov log tool-start in tool_call handler", () => {
|
|
351
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
352
|
+
expect(generated).toContain('pi.exec("ov", ["log", "tool-start", "--agent", AGENT_NAME])');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('generated code contains pi.on("tool_execution_end", ...)', () => {
|
|
356
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
357
|
+
expect(generated).toContain('pi.on("tool_execution_end",');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("generated code contains pi.exec ov log tool-end in tool_execution_end handler", () => {
|
|
361
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
362
|
+
expect(generated).toContain('pi.exec("ov", ["log", "tool-end", "--agent", AGENT_NAME])');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('generated code contains pi.on("session_shutdown", ...)', () => {
|
|
366
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
367
|
+
expect(generated).toContain('pi.on("session_shutdown",');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("generated code awaits pi.exec ov log session-end in session_shutdown handler", () => {
|
|
371
|
+
const generated = generatePiGuardExtension(builderHooks());
|
|
372
|
+
expect(generated).toContain(
|
|
373
|
+
'await pi.exec("ov", ["log", "session-end", "--agent", AGENT_NAME])',
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("PiRuntime integration", () => {
|
|
379
|
+
test("PiRuntime.requiresBeaconVerification() returns false", () => {
|
|
380
|
+
const runtime = new PiRuntime();
|
|
381
|
+
expect(runtime.requiresBeaconVerification()).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// --- Helpers ---
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Extract the TEAM_BLOCKED Set literal section from generated code.
|
|
390
|
+
* Returns the text between "TEAM_BLOCKED = new Set" and the first "]);"
|
|
391
|
+
* after that point.
|
|
392
|
+
*/
|
|
393
|
+
function extractTeamBlockedSection(generated: string): string {
|
|
394
|
+
const start = generated.indexOf("TEAM_BLOCKED = new Set");
|
|
395
|
+
const end = generated.indexOf("]);", start);
|
|
396
|
+
if (start === -1 || end === -1) return "";
|
|
397
|
+
return generated.slice(start, end + 3);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Extract the INTERACTIVE_BLOCKED Set literal section from generated code.
|
|
402
|
+
* Returns the text between "INTERACTIVE_BLOCKED = new Set" and the first "]);"
|
|
403
|
+
* after that point.
|
|
404
|
+
*/
|
|
405
|
+
function extractInteractiveBlockedSection(generated: string): string {
|
|
406
|
+
const start = generated.indexOf("INTERACTIVE_BLOCKED = new Set");
|
|
407
|
+
const end = generated.indexOf("]);", start);
|
|
408
|
+
if (start === -1 || end === -1) return "";
|
|
409
|
+
return generated.slice(start, end + 3);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Extract the WRITE_BLOCKED Set literal section from generated code.
|
|
414
|
+
* Returns the text between "WRITE_BLOCKED = new Set" and the first "]);"
|
|
415
|
+
* after that point.
|
|
416
|
+
*/
|
|
417
|
+
function extractWriteBlockedSection(generated: string): string {
|
|
418
|
+
const start = generated.indexOf("WRITE_BLOCKED = new Set");
|
|
419
|
+
const end = generated.indexOf("]);", start);
|
|
420
|
+
if (start === -1 || end === -1) return "";
|
|
421
|
+
return generated.slice(start, end + 3);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Extract the SAFE_PREFIXES array literal section from generated code.
|
|
426
|
+
* Returns the text between "SAFE_PREFIXES =" and the next "];"
|
|
427
|
+
*/
|
|
428
|
+
function extractSafePrefixesSection(generated: string): string {
|
|
429
|
+
const start = generated.indexOf("const SAFE_PREFIXES =");
|
|
430
|
+
const end = generated.indexOf("];", start);
|
|
431
|
+
if (start === -1 || end === -1) return "";
|
|
432
|
+
return generated.slice(start, end + 2);
|
|
433
|
+
}
|