@selesai/code 0.13.22 → 0.13.23

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,609 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
5
+ import graftExtension from "./index.ts";
6
+ import { TELEMETRY_ENV } from "./cli.ts";
7
+ import { INJECTION_ENTRY_TYPE, MODE_ENTRY_TYPE, type RetrievalMode } from "./state.ts";
8
+ import { handlerFor, makeCtx, makePi, notified, OK, toolFor, type PiHarness } from "./test-support.ts";
9
+
10
+ const ASK_JSON = JSON.stringify({
11
+ mode: "lexical",
12
+ coverage: 0.8,
13
+ hits: [{ kind: "symbol", title: "verify · function", pointer: "src/auth.ts:L1-L9", snippet: "checks", code: "code" }],
14
+ });
15
+
16
+ let root: string;
17
+ let repo: string;
18
+ let agentDir: string;
19
+
20
+ beforeAll(() => {
21
+ root = mkdtempSync(join(tmpdir(), "graft-lifecycle-"));
22
+ agentDir = join(root, "agent");
23
+ repo = join(root, "repo");
24
+ mkdirSync(agentDir, { recursive: true });
25
+ mkdirSync(join(repo, ".selesai"), { recursive: true });
26
+ mkdirSync(join(repo, "graft", ".graph"), { recursive: true });
27
+ writeFileSync(join(repo, "graft", ".graph", "wiring.json"), "{}", "utf-8");
28
+ });
29
+
30
+ afterAll(() => rmSync(root, { recursive: true, force: true }));
31
+
32
+ const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
33
+
34
+ function writeProjectSettings(value: unknown): void {
35
+ writeFileSync(join(repo, ".selesai", "settings.json"), JSON.stringify({ graft: value }), "utf-8");
36
+ }
37
+
38
+ /** A git-root + graft-version aware exec stub. */
39
+ function exec(graftOutput = ASK_JSON) {
40
+ return async (command: string, args: string[]) => {
41
+ if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${repo}\n` };
42
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
43
+ return { ...OK, stdout: graftOutput };
44
+ };
45
+ }
46
+
47
+ interface Booted extends PiHarness {
48
+ ctx: ReturnType<typeof makeCtx>;
49
+ reset(): Promise<void>;
50
+ }
51
+
52
+ /**
53
+ * Start a session the way Selesai does, with the project settings and branch
54
+ * entries the test needs, and return the harness for further events.
55
+ */
56
+ async function bootSession(
57
+ options: { settings?: Record<string, unknown>; branch?: unknown[]; hasUI?: boolean; trusted?: boolean } = {},
58
+ ): Promise<Booted> {
59
+ process.env.PI_CODING_AGENT_DIR = agentDir;
60
+ writeProjectSettings(options.settings ?? {});
61
+ const harness = makePi(exec());
62
+ graftExtension(harness.pi as never);
63
+ const ctx = makeCtx({ cwd: repo, trusted: options.trusted ?? true, hasUI: options.hasUI ?? true, branch: options.branch });
64
+ await (handlerFor(harness, "session_start") as (event: unknown, ctx: unknown) => Promise<void>)(
65
+ { type: "session_start", reason: "startup" },
66
+ ctx,
67
+ );
68
+ return {
69
+ ...harness,
70
+ ctx,
71
+ reset: async () => {
72
+ await (handlerFor(harness, "session_shutdown") as (event: unknown, ctx: unknown) => Promise<void>)(
73
+ { type: "session_shutdown", reason: "quit" },
74
+ ctx,
75
+ );
76
+ },
77
+ };
78
+ }
79
+
80
+ afterEach(() => {
81
+ if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
82
+ else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
83
+ delete process.env[TELEMETRY_ENV];
84
+ vi.restoreAllMocks();
85
+ });
86
+
87
+ describe("session start", () => {
88
+ it("resolves the Git working tree, probes the executable, and publishes a status line", async () => {
89
+ const session = await bootSession();
90
+ expect(session.commandsRun()).toEqual(["git rev-parse --show-toplevel", "graft --version"]);
91
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0");
92
+ await session.reset();
93
+ });
94
+
95
+ it("does nothing at all when the extension is disabled by settings", async () => {
96
+ const session = await bootSession({ settings: { enabled: false } });
97
+ expect(session.exec).not.toHaveBeenCalled();
98
+ expect(session.ctx.statuses.get("graft")).toBeUndefined();
99
+ await session.reset();
100
+ });
101
+
102
+ it("ignores project settings until the project is trusted", async () => {
103
+ const session = await bootSession({ settings: { enabled: false }, trusted: false });
104
+ // Untrusted: the project's `enabled: false` must not be honored, so the
105
+ // extension still runs its probe.
106
+ expect(session.commandsRun()).toContain("graft --version");
107
+ await session.reset();
108
+ });
109
+
110
+ it("defaults Graft to telemetry opt-out without touching Graft's own settings", async () => {
111
+ delete process.env[TELEMETRY_ENV];
112
+ const session = await bootSession();
113
+ expect(process.env[TELEMETRY_ENV]).toBe("1");
114
+ await session.reset();
115
+ expect(process.env[TELEMETRY_ENV]).toBeUndefined();
116
+ });
117
+
118
+ it("leaves an explicit telemetry preference alone", async () => {
119
+ const session = await bootSession({ settings: { telemetry: "inherit" } });
120
+ expect(process.env[TELEMETRY_ENV]).toBeUndefined();
121
+ await session.reset();
122
+ });
123
+
124
+ it("keeps a user's own DO_NOT_TRACK setting across the session", async () => {
125
+ process.env[TELEMETRY_ENV] = "0";
126
+ const session = await bootSession();
127
+ expect(process.env[TELEMETRY_ENV]).toBe("0");
128
+ await session.reset();
129
+ expect(process.env[TELEMETRY_ENV]).toBe("0");
130
+ });
131
+
132
+ it("restores the retrieval mode chosen on this branch", async () => {
133
+ const session = await bootSession({
134
+ branch: [{ type: "custom", customType: MODE_ENTRY_TYPE, data: { mode: "hybrid" } }],
135
+ });
136
+ expect(session.ctx.statuses.get("graft-mode")).toBe("graft mode: hybrid");
137
+ await session.reset();
138
+ });
139
+
140
+ it("does not leak the telemetry default when a session starts twice without a shutdown", async () => {
141
+ delete process.env[TELEMETRY_ENV];
142
+ const session = await bootSession();
143
+ expect(process.env[TELEMETRY_ENV]).toBe("1");
144
+
145
+ // A second start with no shutdown in between must still be undone later.
146
+ await (handlerFor(session, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
147
+ { type: "session_start", reason: "reload" },
148
+ session.ctx,
149
+ );
150
+ await session.reset();
151
+ expect(process.env[TELEMETRY_ENV]).toBeUndefined();
152
+ });
153
+
154
+ it("automatically installs a missing CLI once, then rechecks it", async () => {
155
+ process.env.PI_CODING_AGENT_DIR = agentDir;
156
+ writeProjectSettings({});
157
+ let installed = false;
158
+ const harness = makePi(async (command: string, args: string[]) => {
159
+ if (command === "git") return { ...OK, stdout: `${repo}\n` };
160
+ if (command === "npm") {
161
+ installed = true;
162
+ return { ...OK };
163
+ }
164
+ if (args.includes("--version")) {
165
+ if (!installed) throw new Error("spawn graft ENOENT");
166
+ return { ...OK, stdout: "0.18.0\n" };
167
+ }
168
+ return { ...OK };
169
+ });
170
+ graftExtension(harness.pi as never);
171
+ const ctx = makeCtx({ cwd: repo });
172
+ const start = handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>;
173
+ await start({ type: "session_start", reason: "startup" }, ctx);
174
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
175
+ expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.18");
176
+
177
+ await start({ type: "session_start", reason: "reload" }, ctx);
178
+ expect(harness.commandsRun().filter((command) => command.startsWith("npm install"))).toHaveLength(1);
179
+ });
180
+
181
+ it("reports a failed automatic install without failing the session", async () => {
182
+ process.env.PI_CODING_AGENT_DIR = agentDir;
183
+ writeProjectSettings({});
184
+ const harness = makePi(async (command: string, args: string[]) => {
185
+ if (command === "git") return { ...OK, stdout: `${repo}\n` };
186
+ if (command === "npm") return { ...OK, code: 1, stderr: "npm access denied" };
187
+ if (args.includes("--version")) throw new Error("spawn graft ENOENT");
188
+ return { ...OK };
189
+ });
190
+ graftExtension(harness.pi as never);
191
+ const ctx = makeCtx({ cwd: repo });
192
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
193
+ { type: "session_start", reason: "startup" },
194
+ ctx,
195
+ );
196
+ await vi.waitFor(() => expect(notified(ctx)).toContain("npm access denied"));
197
+ expect(ctx.statuses.get("graft")).toBe("graft: ✕ no cli");
198
+ });
199
+
200
+ it("reports that Graft needs a Git working tree without probing anything else", async () => {
201
+ process.env.PI_CODING_AGENT_DIR = agentDir;
202
+ writeProjectSettings({});
203
+ const harness = makePi(async (command: string) => {
204
+ if (command === "git") return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
205
+ return { ...OK, stdout: "0.18.0\n" };
206
+ });
207
+ graftExtension(harness.pi as never);
208
+ const ctx = makeCtx({ cwd: repo });
209
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
210
+ { type: "session_start", reason: "startup" },
211
+ ctx,
212
+ );
213
+ expect(ctx.statuses.get("graft")).toBe("graft: ✕ n/a");
214
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+
218
+ describe("mutation awareness", () => {
219
+ const toolResultEvent = (toolName: string, input: Record<string, unknown>, isError = false) => ({
220
+ type: "tool_result",
221
+ toolName,
222
+ toolCallId: "c1",
223
+ input,
224
+ content: [],
225
+ isError,
226
+ });
227
+ const onToolResult = (session: Booted, toolName: string, input: Record<string, unknown>, isError = false) =>
228
+ (handlerFor(session, "tool_result") as (e: unknown, c: unknown) => Promise<void>)(
229
+ toolResultEvent(toolName, input, isError),
230
+ session.ctx,
231
+ );
232
+
233
+ it("marks the graph changed after a successful edit", async () => {
234
+ const session = await bootSession();
235
+ await onToolResult(session, "edit", { path: "src/a.ts" });
236
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.18.0");
237
+ await session.reset();
238
+ });
239
+
240
+ it("marks the graph changed after a clearly mutating bash command", async () => {
241
+ const session = await bootSession();
242
+ await onToolResult(session, "bash", { command: "rm -rf dist" });
243
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.18.0");
244
+ await session.reset();
245
+ });
246
+
247
+ it("does not mark anything changed for read-only work or failed calls", async () => {
248
+ const session = await bootSession();
249
+ await onToolResult(session, "read", { path: "src/a.ts" });
250
+ await onToolResult(session, "bash", { command: "git status" });
251
+ await onToolResult(session, "edit", { path: "src/a.ts" }, true);
252
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0");
253
+ await session.reset();
254
+ });
255
+ });
256
+
257
+ describe("proactive refresh", () => {
258
+ it("does nothing when the graph was never marked changed", async () => {
259
+ const session = await bootSession();
260
+ const before = session.exec.mock.calls.length;
261
+ await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
262
+ expect(session.exec.mock.calls.length).toBe(before);
263
+ await session.reset();
264
+ });
265
+
266
+ it("never creates a graph that does not exist", async () => {
267
+ process.env.PI_CODING_AGENT_DIR = agentDir;
268
+ const bare = join(root, "bare-repo");
269
+ mkdirSync(join(bare, ".selesai"), { recursive: true });
270
+ writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
271
+ const harness = makePi(async (command: string, args: string[]) => {
272
+ if (command === "git") return { ...OK, stdout: `${bare}\n` };
273
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
274
+ return { ...OK, stdout: ASK_JSON };
275
+ });
276
+ graftExtension(harness.pi as never);
277
+ const ctx = makeCtx({ cwd: bare });
278
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
279
+ { type: "session_start", reason: "startup" },
280
+ ctx,
281
+ );
282
+ expect(ctx.statuses.get("graft")).toBe("graft: ○ unbuilt v0.18.0");
283
+ const before = harness.exec.mock.calls.length;
284
+ await (handlerFor(harness, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, ctx);
285
+ expect(harness.exec.mock.calls.length).toBe(before);
286
+ });
287
+
288
+ it("re-indexes after edits, shows the sync phase, and returns to fresh", async () => {
289
+ const session = await bootSession({ settings: { refreshDebounceSeconds: 0 } });
290
+ await (handlerFor(session, "tool_result") as (e: unknown, c: unknown) => Promise<void>)(
291
+ { type: "tool_result", toolName: "read", toolCallId: "c", input: {}, content: [], isError: false },
292
+ session.ctx,
293
+ );
294
+ await (handlerFor(session, "tool_result") as (e: unknown, c: unknown) => Promise<void>)(
295
+ { type: "tool_result", toolName: "write", toolCallId: "c1", input: {}, content: [], isError: false },
296
+ session.ctx,
297
+ );
298
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.18.0");
299
+
300
+ await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
301
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.18.0");
302
+ await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
303
+ expect(session.commandsRun()).toContain("graft build");
304
+ await session.reset();
305
+ });
306
+
307
+ it("honors the debounce window across consecutive turns", async () => {
308
+ const session = await bootSession({ settings: { refreshDebounceSeconds: 600 } });
309
+ const handler = handlerFor(session, "tool_result") as (e: unknown, c: unknown) => Promise<void>;
310
+ await handler({ type: "tool_result", toolName: "write", toolCallId: "c", input: {}, content: [], isError: false }, session.ctx);
311
+ await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
312
+ await vi.waitFor(() => expect(session.commandsRun()).toContain("graft build"));
313
+
314
+ const builds = () => session.commandsRun().filter((line) => line.startsWith("graft build")).length;
315
+ expect(builds()).toBe(1);
316
+
317
+ // A second turn inside the debounce window must not queue another rebuild.
318
+ await handler({ type: "tool_result", toolName: "write", toolCallId: "c2", input: {}, content: [], isError: false }, session.ctx);
319
+ await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
320
+ expect(builds()).toBe(1);
321
+ await session.reset();
322
+ });
323
+
324
+ it("survives a refresh that fails, and reports it", async () => {
325
+ process.env.PI_CODING_AGENT_DIR = agentDir;
326
+ writeProjectSettings({ refreshDebounceSeconds: 0 });
327
+ const harness = makePi(async (command: string, args: string[]) => {
328
+ if (command === "git") return { ...OK, stdout: `${repo}\n` };
329
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
330
+ if (args.includes("build")) return { code: 1, stdout: "", stderr: "✗ index failed", killed: false };
331
+ return { ...OK, stdout: ASK_JSON };
332
+ });
333
+ graftExtension(harness.pi as never);
334
+ const ctx = makeCtx({ cwd: repo });
335
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
336
+ { type: "session_start", reason: "startup" },
337
+ ctx,
338
+ );
339
+ await (handlerFor(harness, "tool_result") as (e: unknown, c: unknown) => Promise<void>)(
340
+ { type: "tool_result", toolName: "write", toolCallId: "c", input: {}, content: [], isError: false },
341
+ ctx,
342
+ );
343
+ await (handlerFor(harness, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, ctx);
344
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.18.0"));
345
+ });
346
+ });
347
+
348
+ describe("retrieval strategy", () => {
349
+ const beforeAgentStart = (session: Booted, prompt: string) =>
350
+ (handlerFor(session, "before_agent_start") as (e: unknown, c: unknown) => Promise<unknown>)(
351
+ { type: "before_agent_start", prompt },
352
+ session.ctx,
353
+ );
354
+
355
+ it("injects a source-backed pack in push mode, which is the default", async () => {
356
+ const session = await bootSession();
357
+ const result = await beforeAgentStart(session, "Fix the login crash");
358
+ expect(result).toBeDefined();
359
+ expect(session.entries).toHaveLength(1);
360
+ await session.reset();
361
+ });
362
+
363
+ it("injects one bounded, declared pack for an eligible task in push mode", async () => {
364
+ const session = await bootSession({ settings: { mode: "push" } });
365
+ const result = (await beforeAgentStart(session, "Fix the login crash in src/auth.ts")) as {
366
+ message: { customType: string; content: string; display: boolean; details: Record<string, unknown> };
367
+ };
368
+
369
+ expect(result.message.customType).toBe("graft-context");
370
+ expect(result.message.display).toBe(true);
371
+ expect(result.message.content).toContain('<graft-context source="graft" mode="push"');
372
+ expect(result.message.content).toContain("src/auth.ts:L1-L9");
373
+ expect(session.commandsRun().join(" ")).toContain("ask");
374
+ expect(session.commandsRun().join(" ")).toContain("--source");
375
+
376
+ const entry = session.entries.find((e) => e.customType === INJECTION_ENTRY_TYPE);
377
+ expect(entry?.data).toMatchObject({
378
+ mode: "push",
379
+ state: "fresh-structural",
380
+ references: ["src/auth.ts:L1-L9"],
381
+ truncated: false,
382
+ coverage: 0.8,
383
+ });
384
+ await session.reset();
385
+ });
386
+
387
+ it("injects only once per turn even if the hook is re-entered", async () => {
388
+ const session = await bootSession({ settings: { mode: "push" } });
389
+ const first = await beforeAgentStart(session, "Fix the login crash");
390
+ const second = await beforeAgentStart(session, "Fix the login crash");
391
+ expect(first).toBeDefined();
392
+ expect(second).toBeUndefined();
393
+ expect(session.entries.filter((e) => e.customType === INJECTION_ENTRY_TYPE)).toHaveLength(1);
394
+ await session.reset();
395
+ });
396
+
397
+ it("injects again on the next turn", async () => {
398
+ const session = await bootSession({ settings: { mode: "push" } });
399
+ await beforeAgentStart(session, "Fix the login crash");
400
+ await (handlerFor(session, "turn_start") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
401
+ expect(await beforeAgentStart(session, "Now add a regression test")).toBeDefined();
402
+ await session.reset();
403
+ });
404
+
405
+ it("never injects for chat, even in hybrid mode", async () => {
406
+ const session = await bootSession({ settings: { mode: "hybrid" } });
407
+ expect(await beforeAgentStart(session, "thanks!")).toBeUndefined();
408
+ expect(session.entries).toHaveLength(0);
409
+ await session.reset();
410
+ });
411
+
412
+ it("drops a pack the relevance gate rejects rather than adding prompt noise", async () => {
413
+ process.env.PI_CODING_AGENT_DIR = agentDir;
414
+ writeProjectSettings({ mode: "push" });
415
+ const weak = JSON.stringify({ mode: "lexical", coverage: 0.01, hits: [{ pointer: "src/a.ts", title: "a", snippet: "" }] });
416
+ const harness = makePi(exec(weak));
417
+ graftExtension(harness.pi as never);
418
+ const ctx = makeCtx({ cwd: repo });
419
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
420
+ { type: "session_start", reason: "startup" },
421
+ ctx,
422
+ );
423
+ expect(
424
+ await (handlerFor(harness, "before_agent_start") as (e: unknown, c: unknown) => Promise<unknown>)(
425
+ { type: "before_agent_start", prompt: "Fix the login crash" },
426
+ ctx,
427
+ ),
428
+ ).toBeUndefined();
429
+ });
430
+
431
+ it("never breaks a turn when the Graft query fails", async () => {
432
+ process.env.PI_CODING_AGENT_DIR = agentDir;
433
+ writeProjectSettings({ mode: "push" });
434
+ const harness = makePi(async (command: string, args: string[]) => {
435
+ if (command === "git") return { ...OK, stdout: `${repo}\n` };
436
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
437
+ return { code: 1, stdout: "", stderr: "✗ graph missing", killed: false };
438
+ });
439
+ graftExtension(harness.pi as never);
440
+ const ctx = makeCtx({ cwd: repo });
441
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
442
+ { type: "session_start", reason: "startup" },
443
+ ctx,
444
+ );
445
+ const result = await (handlerFor(harness, "before_agent_start") as (e: unknown, c: unknown) => Promise<unknown>)(
446
+ { type: "before_agent_start", prompt: "Fix the login crash" },
447
+ ctx,
448
+ );
449
+ expect(result).toBeUndefined();
450
+ expect(notified(ctx)).toContain("Graft context unavailable");
451
+ });
452
+
453
+ it("injects nothing when the graph has never been built, and says how to fix it", async () => {
454
+ process.env.PI_CODING_AGENT_DIR = agentDir;
455
+ const bare = join(root, "unbuilt-repo");
456
+ mkdirSync(join(bare, ".selesai"), { recursive: true });
457
+ writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: { mode: "push" } }), "utf-8");
458
+ const harness = makePi(async (command: string, args: string[]) => {
459
+ if (command === "git") return { ...OK, stdout: `${bare}\n` };
460
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
461
+ return { ...OK, stdout: ASK_JSON };
462
+ });
463
+ graftExtension(harness.pi as never);
464
+ const ctx = makeCtx({ cwd: bare });
465
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
466
+ { type: "session_start", reason: "startup" },
467
+ ctx,
468
+ );
469
+ expect(
470
+ await (handlerFor(harness, "before_agent_start") as (e: unknown, c: unknown) => Promise<unknown>)(
471
+ { type: "before_agent_start", prompt: "Fix the login crash" },
472
+ ctx,
473
+ ),
474
+ ).toBeUndefined();
475
+ });
476
+ });
477
+
478
+ describe("session shutdown", () => {
479
+ it("clears status, restores the environment, and stops all further work", async () => {
480
+ const session = await bootSession({ settings: { mode: "push" } });
481
+ await session.reset();
482
+
483
+ expect(session.ctx.statuses.get("graft")).toBeUndefined();
484
+ expect(session.ctx.statuses.get("graft-mode")).toBeUndefined();
485
+ expect(process.env[TELEMETRY_ENV]).toBeUndefined();
486
+
487
+ const before = session.exec.mock.calls.length;
488
+ await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
489
+ await (handlerFor(session, "before_agent_start") as (e: unknown, c: unknown) => Promise<unknown>)(
490
+ { type: "before_agent_start", prompt: "Fix the login crash" },
491
+ session.ctx,
492
+ );
493
+ expect(session.exec.mock.calls.length).toBe(before);
494
+ });
495
+
496
+ it("refuses tool calls after the session is torn down", async () => {
497
+ const session = await bootSession();
498
+ const tool = toolFor(session, "graft_repo_map");
499
+ await session.reset();
500
+ await expect(
501
+ (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", {}, undefined, undefined, session.ctx),
502
+ ).rejects.toThrow(/no active Selesai session/);
503
+ });
504
+ });
505
+
506
+ describe("tool readiness", () => {
507
+ it("fails with recovery guidance when the executable is missing", async () => {
508
+ process.env.PI_CODING_AGENT_DIR = agentDir;
509
+ writeProjectSettings({});
510
+ const harness = makePi(async (command: string) => {
511
+ if (command === "git") return { ...OK, stdout: `${repo}\n` };
512
+ throw new Error("spawn graft ENOENT");
513
+ });
514
+ graftExtension(harness.pi as never);
515
+ const ctx = makeCtx({ cwd: repo });
516
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
517
+ { type: "session_start", reason: "startup" },
518
+ ctx,
519
+ );
520
+ const tool = toolFor(harness, "graft_find_code");
521
+ await expect(
522
+ (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", { question: "x" }, undefined, undefined, ctx),
523
+ ).rejects.toThrow(/Run \/graft doctor/);
524
+ });
525
+
526
+ it("tells the user to build before answering from a graph that is not there", async () => {
527
+ process.env.PI_CODING_AGENT_DIR = agentDir;
528
+ const bare = join(root, "unbuilt-tools");
529
+ mkdirSync(join(bare, ".selesai"), { recursive: true });
530
+ writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
531
+ const harness = makePi(async (command: string, args: string[]) => {
532
+ if (command === "git") return { ...OK, stdout: `${bare}\n` };
533
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
534
+ return { ...OK, stdout: ASK_JSON };
535
+ });
536
+ graftExtension(harness.pi as never);
537
+ const ctx = makeCtx({ cwd: bare });
538
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
539
+ { type: "session_start", reason: "startup" },
540
+ ctx,
541
+ );
542
+ const tool = toolFor(harness, "graft_find_code");
543
+ await expect(
544
+ (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", { question: "x" }, undefined, undefined, ctx),
545
+ ).rejects.toThrow(/graft build/);
546
+ });
547
+
548
+ it("re-resolves lazily when an earlier attempt found no repository", async () => {
549
+ process.env.PI_CODING_AGENT_DIR = agentDir;
550
+ writeProjectSettings({});
551
+ let gitCalls = 0;
552
+ const harness = makePi(async (command: string, args: string[]) => {
553
+ if (command === "git") {
554
+ gitCalls += 1;
555
+ return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
556
+ }
557
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
558
+ return { ...OK, stdout: ASK_JSON };
559
+ });
560
+ graftExtension(harness.pi as never);
561
+ const ctx = makeCtx({ cwd: repo });
562
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
563
+ { type: "session_start", reason: "startup" },
564
+ ctx,
565
+ );
566
+ const tool = toolFor(harness, "graft_repo_map");
567
+ await expect(
568
+ (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", {}, undefined, undefined, ctx),
569
+ ).rejects.toThrow(/needs a Git repository/);
570
+ // Session start plus one lazy re-resolve: a failed resolution is not cached
571
+ // as permanent, because the user may have fixed it a moment ago.
572
+ expect(gitCalls).toBe(2);
573
+ });
574
+
575
+ it("refuses a tool call that arrives before any session exists", async () => {
576
+ const harness = makePi(exec());
577
+ graftExtension(harness.pi as never);
578
+ const tool = toolFor(harness, "graft_repo_map");
579
+ await expect(
580
+ (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", {}, undefined, undefined, makeCtx({ cwd: repo })),
581
+ ).rejects.toThrow(/no active Selesai session/);
582
+ expect(harness.exec).not.toHaveBeenCalled();
583
+ });
584
+
585
+ it("refuses tool calls when the extension is disabled", async () => {
586
+ const session = await bootSession({ settings: { enabled: false } });
587
+ const tool = toolFor(session, "graft_repo_map");
588
+ await expect(
589
+ (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", {}, undefined, undefined, session.ctx),
590
+ ).rejects.toThrow(/disabled by settings/);
591
+ });
592
+ });
593
+
594
+ describe("mode selection through commands", () => {
595
+ it("persists the chosen mode and applies it to the next turn", async () => {
596
+ const session = await bootSession();
597
+ const handler = session.commands.get("graft")!.handler as (args: string, ctx: unknown) => Promise<void>;
598
+ await handler("mode hybrid", session.ctx);
599
+ expect(session.entries).toContainEqual({ customType: MODE_ENTRY_TYPE, data: { mode: "hybrid" as RetrievalMode } });
600
+ expect(session.ctx.statuses.get("graft-mode")).toBe("graft mode: hybrid");
601
+
602
+ const result = await (handlerFor(session, "before_agent_start") as (e: unknown, c: unknown) => Promise<unknown>)(
603
+ { type: "before_agent_start", prompt: "Refactor src/auth.ts" },
604
+ session.ctx,
605
+ );
606
+ expect(result).toBeDefined();
607
+ await session.reset();
608
+ });
609
+ });