@opencode-trace/plugin 0.0.3 → 0.0.5
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 +4 -11
- package/dist/__tests__/tracer.test.d.ts +2 -0
- package/dist/__tests__/tracer.test.d.ts.map +1 -0
- package/dist/__tests__/tracer.test.js +276 -0
- package/dist/__tests__/tracer.test.js.map +1 -0
- package/dist/integration.test.js +508 -11
- package/dist/integration.test.js.map +1 -1
- package/dist/plugin-instance.d.ts +33 -9
- package/dist/plugin-instance.d.ts.map +1 -1
- package/dist/plugin-instance.js +256 -65
- package/dist/plugin-instance.js.map +1 -1
- package/dist/plugin-instance.test.js +672 -25
- package/dist/plugin-instance.test.js.map +1 -1
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js +212 -25
- package/dist/trace.js.map +1 -1
- package/dist/trace.test.js +741 -16
- package/dist/trace.test.js.map +1 -1
- package/dist/tracer.d.ts +20 -0
- package/dist/tracer.d.ts.map +1 -0
- package/dist/tracer.js +12 -0
- package/dist/tracer.js.map +1 -0
- package/dist/write-queue.d.ts +27 -2
- package/dist/write-queue.d.ts.map +1 -1
- package/dist/write-queue.js +99 -14
- package/dist/write-queue.js.map +1 -1
- package/dist/write-queue.test.js +373 -6
- package/dist/write-queue.test.js.map +1 -1
- package/package.json +11 -4
- package/dist/state-queue.d.ts +0 -14
- package/dist/state-queue.d.ts.map +0 -1
- package/dist/state-queue.js +0 -44
- package/dist/state-queue.js.map +0 -1
- package/dist/state-queue.test.d.ts +0 -2
- package/dist/state-queue.test.d.ts.map +0 -1
- package/dist/state-queue.test.js +0 -99
- package/dist/state-queue.test.js.map +0 -1
package/dist/trace.test.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { logger } from "@opencode-trace/core";
|
|
5
6
|
import entrypoint, { _resetForTesting } from "./trace.js";
|
|
7
|
+
vi.mock("node:os", async (importOriginal) => {
|
|
8
|
+
const original = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...original,
|
|
11
|
+
homedir: () => {
|
|
12
|
+
const testDir = process.env._TEST_DIR_;
|
|
13
|
+
if (testDir)
|
|
14
|
+
return testDir;
|
|
15
|
+
return original.homedir();
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
});
|
|
6
19
|
async function waitForFile(filePath, timeoutMs = 5000) {
|
|
7
20
|
const startTime = Date.now();
|
|
8
21
|
while (true) {
|
|
@@ -14,23 +27,22 @@ async function waitForFile(filePath, timeoutMs = 5000) {
|
|
|
14
27
|
return;
|
|
15
28
|
}
|
|
16
29
|
}
|
|
17
|
-
catch {
|
|
18
|
-
}
|
|
30
|
+
catch { }
|
|
19
31
|
}
|
|
20
32
|
if (Date.now() - startTime > timeoutMs) {
|
|
21
33
|
throw new Error(`Timeout waiting for valid file ${filePath} after ${timeoutMs}ms`);
|
|
22
34
|
}
|
|
23
|
-
await new Promise(r => setTimeout(r, 10));
|
|
35
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
let testDir;
|
|
27
39
|
beforeEach(() => {
|
|
28
40
|
testDir = mkdtempSync(join(tmpdir(), "plugin-test-"));
|
|
29
|
-
|
|
41
|
+
process.env._TEST_DIR_ = testDir;
|
|
30
42
|
});
|
|
31
43
|
afterEach(() => {
|
|
32
44
|
rmSync(testDir, { recursive: true, force: true });
|
|
33
|
-
|
|
45
|
+
delete process.env._TEST_DIR_;
|
|
34
46
|
});
|
|
35
47
|
describe("Plugin - Hooks 返回值", () => {
|
|
36
48
|
test("plugin 返回包含 event hook", async () => {
|
|
@@ -88,8 +100,8 @@ describe("Plugin - event hook 处理 session.created", () => {
|
|
|
88
100
|
const traceDir = join(testDir, ".opencode-trace");
|
|
89
101
|
const sessionDir = join(traceDir, sessionId);
|
|
90
102
|
expect(existsSync(sessionDir)).toBe(true);
|
|
91
|
-
const
|
|
92
|
-
expect(existsSync(
|
|
103
|
+
const configPath = join(traceDir, "config.json");
|
|
104
|
+
expect(existsSync(configPath)).toBe(true);
|
|
93
105
|
const metaPath = join(sessionDir, "metadata.json");
|
|
94
106
|
expect(existsSync(metaPath)).toBe(true);
|
|
95
107
|
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
@@ -150,9 +162,13 @@ describe("tracedFetch stream integration", () => {
|
|
|
150
162
|
const mockFetch = vi.fn();
|
|
151
163
|
const originalFetch = globalThis.fetch;
|
|
152
164
|
globalThis.fetch = mockFetch;
|
|
153
|
-
const chunks = [
|
|
165
|
+
const chunks = [
|
|
166
|
+
'data: {"content": "Hello"}\n',
|
|
167
|
+
'data: {"content": "World"}\n',
|
|
168
|
+
"data: [DONE]\n",
|
|
169
|
+
];
|
|
154
170
|
const encoder = new TextEncoder();
|
|
155
|
-
const streamChunks = chunks.map(c => encoder.encode(c));
|
|
171
|
+
const streamChunks = chunks.map((c) => encoder.encode(c));
|
|
156
172
|
const mockStream = new ReadableStream({
|
|
157
173
|
start(controller) {
|
|
158
174
|
for (const chunk of streamChunks) {
|
|
@@ -196,7 +212,10 @@ describe("tracedFetch stream integration", () => {
|
|
|
196
212
|
"content-type": "application/json",
|
|
197
213
|
"x-opencode-session": sessionId,
|
|
198
214
|
},
|
|
199
|
-
body: JSON.stringify({
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
stream: true,
|
|
217
|
+
messages: [{ role: "user", content: "test" }],
|
|
218
|
+
}),
|
|
200
219
|
});
|
|
201
220
|
const response = await globalThis.fetch(streamRequest);
|
|
202
221
|
expect(response.__latencyMeta).toBeDefined();
|
|
@@ -266,7 +285,10 @@ describe("tracedFetch stream integration", () => {
|
|
|
266
285
|
"content-type": "application/json",
|
|
267
286
|
"x-opencode-session": sessionId,
|
|
268
287
|
},
|
|
269
|
-
body: JSON.stringify({
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
stream: false,
|
|
290
|
+
messages: [{ role: "user", content: "test" }],
|
|
291
|
+
}),
|
|
270
292
|
});
|
|
271
293
|
const response = await globalThis.fetch(request);
|
|
272
294
|
expect(response.__latencyMeta).toBeUndefined();
|
|
@@ -312,8 +334,8 @@ describe("Plugin - tool.execute.after hook 处理 Task 工具", () => {
|
|
|
312
334
|
metadata: { session_id: subSessionId },
|
|
313
335
|
});
|
|
314
336
|
const traceDir = join(testDir, ".opencode-trace");
|
|
315
|
-
const
|
|
316
|
-
expect(existsSync(
|
|
337
|
+
const configPath = join(traceDir, "config.json");
|
|
338
|
+
expect(existsSync(configPath)).toBe(true);
|
|
317
339
|
});
|
|
318
340
|
test("tool.execute.after hook 对非 Task 工具不记录 sub session", async () => {
|
|
319
341
|
const hooks = await entrypoint.server({
|
|
@@ -352,8 +374,711 @@ describe("Plugin - tool.execute.after hook 处理 Task 工具", () => {
|
|
|
352
374
|
metadata: {},
|
|
353
375
|
});
|
|
354
376
|
const traceDir = join(testDir, ".opencode-trace");
|
|
355
|
-
const
|
|
356
|
-
expect(existsSync(
|
|
377
|
+
const configPath = join(traceDir, "config.json");
|
|
378
|
+
expect(existsSync(configPath)).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
describe("Plugin - global/local mode", () => {
|
|
382
|
+
test("trace_on tool enables session scope", async () => {
|
|
383
|
+
const hooks = await entrypoint.server({
|
|
384
|
+
client: {},
|
|
385
|
+
project: {},
|
|
386
|
+
directory: testDir,
|
|
387
|
+
worktree: testDir,
|
|
388
|
+
experimental_workspace: { register: vi.fn() },
|
|
389
|
+
serverUrl: new URL("http://localhost"),
|
|
390
|
+
$: {},
|
|
391
|
+
});
|
|
392
|
+
const sessionId = "test-session-on";
|
|
393
|
+
await hooks.event({
|
|
394
|
+
event: {
|
|
395
|
+
type: "session.created",
|
|
396
|
+
properties: {
|
|
397
|
+
info: {
|
|
398
|
+
id: sessionId,
|
|
399
|
+
projectID: "test-project",
|
|
400
|
+
directory: testDir,
|
|
401
|
+
title: "Test Session",
|
|
402
|
+
version: "1.0",
|
|
403
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
const result = await hooks.tool.trace_on.execute({}, {
|
|
409
|
+
sessionID: sessionId,
|
|
410
|
+
});
|
|
411
|
+
expect(result).toContain("Trace enabled for session");
|
|
412
|
+
const globalDir = join(testDir, ".opencode-trace");
|
|
413
|
+
const sessionDir = join(globalDir, sessionId);
|
|
414
|
+
await hooks.event({
|
|
415
|
+
event: {
|
|
416
|
+
type: "session.updated",
|
|
417
|
+
properties: {
|
|
418
|
+
info: {
|
|
419
|
+
id: sessionId,
|
|
420
|
+
projectID: "test-project",
|
|
421
|
+
directory: testDir,
|
|
422
|
+
title: "Test Session Updated",
|
|
423
|
+
version: "1.0",
|
|
424
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
430
|
+
expect(existsSync(sessionDir)).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
test("trace_off tool disables session scope", async () => {
|
|
433
|
+
const hooks = await entrypoint.server({
|
|
434
|
+
client: {},
|
|
435
|
+
project: {},
|
|
436
|
+
directory: testDir,
|
|
437
|
+
worktree: testDir,
|
|
438
|
+
experimental_workspace: { register: vi.fn() },
|
|
439
|
+
serverUrl: new URL("http://localhost"),
|
|
440
|
+
$: {},
|
|
441
|
+
});
|
|
442
|
+
const sessionId = "test-session-off";
|
|
443
|
+
await hooks.event({
|
|
444
|
+
event: {
|
|
445
|
+
type: "session.created",
|
|
446
|
+
properties: {
|
|
447
|
+
info: {
|
|
448
|
+
id: sessionId,
|
|
449
|
+
projectID: "test-project",
|
|
450
|
+
directory: testDir,
|
|
451
|
+
title: "Test Session",
|
|
452
|
+
version: "1.0",
|
|
453
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
const result = await hooks.tool.trace_off.execute({}, {
|
|
459
|
+
sessionID: sessionId,
|
|
460
|
+
});
|
|
461
|
+
expect(result).toContain("Trace disabled for session");
|
|
462
|
+
});
|
|
463
|
+
test("trace_status tool shows current status", async () => {
|
|
464
|
+
const hooks = await entrypoint.server({
|
|
465
|
+
client: {},
|
|
466
|
+
project: {},
|
|
467
|
+
directory: testDir,
|
|
468
|
+
worktree: testDir,
|
|
469
|
+
experimental_workspace: { register: vi.fn() },
|
|
470
|
+
serverUrl: new URL("http://localhost"),
|
|
471
|
+
$: {},
|
|
472
|
+
});
|
|
473
|
+
const sessionId = "test-session-status";
|
|
474
|
+
await hooks.event({
|
|
475
|
+
event: {
|
|
476
|
+
type: "session.created",
|
|
477
|
+
properties: {
|
|
478
|
+
info: {
|
|
479
|
+
id: sessionId,
|
|
480
|
+
projectID: "test-project",
|
|
481
|
+
directory: testDir,
|
|
482
|
+
title: "Test Session",
|
|
483
|
+
version: "1.0",
|
|
484
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
const result = await hooks.tool.trace_status.execute({}, {
|
|
490
|
+
sessionID: sessionId,
|
|
491
|
+
});
|
|
492
|
+
expect(result).toContain("Trace Status");
|
|
493
|
+
expect(result).toContain("Global");
|
|
494
|
+
expect(result).toContain("Local");
|
|
495
|
+
expect(result).toContain("Session");
|
|
496
|
+
expect(result).toContain("Storage");
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
async function setupPluginWithMockClient(testDir) {
|
|
500
|
+
_resetForTesting();
|
|
501
|
+
const mockPrompt = vi.fn().mockResolvedValue({});
|
|
502
|
+
const hooks = await entrypoint.server({
|
|
503
|
+
client: { session: { prompt: mockPrompt } },
|
|
504
|
+
project: {},
|
|
505
|
+
directory: testDir,
|
|
506
|
+
worktree: testDir,
|
|
507
|
+
experimental_workspace: { register: vi.fn() },
|
|
508
|
+
serverUrl: new URL("http://localhost"),
|
|
509
|
+
$: {},
|
|
510
|
+
});
|
|
511
|
+
return { hooks, mockPrompt };
|
|
512
|
+
}
|
|
513
|
+
async function runTraceCommand(hooks, mockPrompt, args, sessionId = "test-slash-session") {
|
|
514
|
+
const output = { parts: [{ type: "text", text: "original" }] };
|
|
515
|
+
let error = null;
|
|
516
|
+
try {
|
|
517
|
+
await hooks["command.execute.before"]({
|
|
518
|
+
command: "trace",
|
|
519
|
+
sessionID: sessionId,
|
|
520
|
+
arguments: args,
|
|
521
|
+
}, output);
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
error = err;
|
|
525
|
+
}
|
|
526
|
+
expect(error).toBeTruthy();
|
|
527
|
+
expect(error.message).toBe("__TRACE_HANDLED__");
|
|
528
|
+
expect(output.parts.length).toBe(0);
|
|
529
|
+
if (mockPrompt.mock.calls.length > 0) {
|
|
530
|
+
const call = mockPrompt.mock.calls[0][0];
|
|
531
|
+
return { text: call.body.parts[0].text, outputParts: output.parts };
|
|
532
|
+
}
|
|
533
|
+
return { text: null, outputParts: output.parts };
|
|
534
|
+
}
|
|
535
|
+
async function createSessionViaEvent(hooks, sessionId, testDir, title = "Slash Test Session", parentID) {
|
|
536
|
+
await hooks.event({
|
|
537
|
+
event: {
|
|
538
|
+
type: "session.created",
|
|
539
|
+
properties: {
|
|
540
|
+
info: {
|
|
541
|
+
id: sessionId,
|
|
542
|
+
projectID: "test-project",
|
|
543
|
+
directory: testDir,
|
|
544
|
+
title,
|
|
545
|
+
parentID,
|
|
546
|
+
version: "1.0",
|
|
547
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function readConfig(testDir) {
|
|
554
|
+
const configPath = join(testDir, ".opencode-trace", "config.json");
|
|
555
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
556
|
+
}
|
|
557
|
+
function readSessionMetadata(testDir, sessionId) {
|
|
558
|
+
const metaPath = join(testDir, ".opencode-trace", sessionId, "metadata.json");
|
|
559
|
+
if (!existsSync(metaPath))
|
|
560
|
+
return null;
|
|
561
|
+
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
562
|
+
}
|
|
563
|
+
describe("Plugin - /trace on (slash command)", () => {
|
|
564
|
+
test("/trace on with no flags enables global scope by default", async () => {
|
|
565
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
566
|
+
const sessionId = "slash-on-default";
|
|
567
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
568
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on", sessionId);
|
|
569
|
+
expect(text).toContain("Trace enabled");
|
|
570
|
+
expect(text).toContain("global");
|
|
571
|
+
const config = readConfig(testDir);
|
|
572
|
+
expect(config.global_trace_enabled).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
test("/trace on -g explicitly enables global scope", async () => {
|
|
575
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
576
|
+
const sessionId = "slash-on-g";
|
|
577
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
578
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -g", sessionId);
|
|
579
|
+
expect(text).toContain("Trace enabled");
|
|
580
|
+
expect(text).toContain("global");
|
|
581
|
+
const config = readConfig(testDir);
|
|
582
|
+
expect(config.global_trace_enabled).toBe(true);
|
|
583
|
+
});
|
|
584
|
+
test("/trace on --global (long form) enables global scope", async () => {
|
|
585
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
586
|
+
const sessionId = "slash-on-long-global";
|
|
587
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
588
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on --global", sessionId);
|
|
589
|
+
expect(text).toContain("Trace enabled");
|
|
590
|
+
expect(text).toContain("global");
|
|
591
|
+
const config = readConfig(testDir);
|
|
592
|
+
expect(config.global_trace_enabled).toBe(true);
|
|
593
|
+
});
|
|
594
|
+
test("/trace on -l enables local scope", async () => {
|
|
595
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
596
|
+
const sessionId = "slash-on-l";
|
|
597
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
598
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -l", sessionId);
|
|
599
|
+
expect(text).toContain("Trace enabled");
|
|
600
|
+
expect(text).toContain("local");
|
|
601
|
+
const config = readConfig(testDir);
|
|
602
|
+
expect(config.global_trace_enabled).toBe(true);
|
|
603
|
+
});
|
|
604
|
+
test("/trace on -s enables session scope", async () => {
|
|
605
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
606
|
+
const sessionId = "slash-on-s";
|
|
607
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
608
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -s", sessionId);
|
|
609
|
+
expect(text).toContain("Trace enabled");
|
|
610
|
+
expect(text).toContain("session");
|
|
611
|
+
const meta = readSessionMetadata(testDir, sessionId);
|
|
612
|
+
expect(meta).toBeTruthy();
|
|
613
|
+
expect(meta.trace_enabled).toBe(true);
|
|
614
|
+
});
|
|
615
|
+
test("/trace on -d local sets storage preference to local", async () => {
|
|
616
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
617
|
+
const sessionId = "slash-on-d-local";
|
|
618
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
619
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -d local", sessionId);
|
|
620
|
+
expect(text).toContain("Trace enabled");
|
|
621
|
+
expect(text).toContain("storage: local");
|
|
622
|
+
const config = readConfig(testDir);
|
|
623
|
+
expect(config.storage_preference).toBe("local");
|
|
624
|
+
expect(config.global_trace_enabled).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
test("/trace on -d global sets storage preference to global", async () => {
|
|
627
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
628
|
+
const sessionId = "slash-on-d-global";
|
|
629
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
630
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -d global", sessionId);
|
|
631
|
+
expect(text).toContain("Trace enabled");
|
|
632
|
+
const config = readConfig(testDir);
|
|
633
|
+
expect(config.storage_preference).toBe("global");
|
|
634
|
+
});
|
|
635
|
+
test("/trace on -g -l enables both global and local scopes", async () => {
|
|
636
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
637
|
+
const sessionId = "slash-on-gl";
|
|
638
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
639
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -g -l", sessionId);
|
|
640
|
+
expect(text).toContain("Trace enabled");
|
|
641
|
+
expect(text).toContain("global");
|
|
642
|
+
expect(text).toContain("local");
|
|
643
|
+
const config = readConfig(testDir);
|
|
644
|
+
expect(config.global_trace_enabled).toBe(true);
|
|
645
|
+
});
|
|
646
|
+
test("/trace on -g -l -s enables all three scopes", async () => {
|
|
647
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
648
|
+
const sessionId = "slash-on-gls";
|
|
649
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
650
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -g -l -s", sessionId);
|
|
651
|
+
expect(text).toContain("Trace enabled");
|
|
652
|
+
expect(text).toContain("global");
|
|
653
|
+
expect(text).toContain("local");
|
|
654
|
+
expect(text).toContain("session");
|
|
655
|
+
const meta = readSessionMetadata(testDir, sessionId);
|
|
656
|
+
expect(meta.trace_enabled).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
test("/trace on -s -d local enables session and sets local storage", async () => {
|
|
659
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
660
|
+
const sessionId = "slash-on-s-d-local";
|
|
661
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
662
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "on -s -d local", sessionId);
|
|
663
|
+
expect(text).toContain("Trace enabled");
|
|
664
|
+
expect(text).toContain("session");
|
|
665
|
+
expect(text).toContain("storage: local");
|
|
666
|
+
const meta = readSessionMetadata(testDir, sessionId);
|
|
667
|
+
expect(meta.trace_enabled).toBe(true);
|
|
668
|
+
expect(meta.storage_preference).toBe("local");
|
|
669
|
+
});
|
|
670
|
+
test("/trace enable (alias) works the same as /trace on", async () => {
|
|
671
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
672
|
+
const sessionId = "slash-enable-alias";
|
|
673
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
674
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "enable", sessionId);
|
|
675
|
+
expect(text).toContain("Trace enabled");
|
|
676
|
+
expect(text).toContain("global");
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
describe("Plugin - /trace off (slash command)", () => {
|
|
680
|
+
test("/trace off with no flags disables global scope by default", async () => {
|
|
681
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
682
|
+
const sessionId = "slash-off-default";
|
|
683
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
684
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "off", sessionId);
|
|
685
|
+
expect(text).toContain("Trace disabled");
|
|
686
|
+
expect(text).toContain("global");
|
|
687
|
+
const config = readConfig(testDir);
|
|
688
|
+
expect(config.global_trace_enabled).toBe(false);
|
|
689
|
+
});
|
|
690
|
+
test("/trace off -g explicitly disables global scope", async () => {
|
|
691
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
692
|
+
const sessionId = "slash-off-g";
|
|
693
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
694
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "off -g", sessionId);
|
|
695
|
+
expect(text).toContain("Trace disabled");
|
|
696
|
+
expect(text).toContain("global");
|
|
697
|
+
const config = readConfig(testDir);
|
|
698
|
+
expect(config.global_trace_enabled).toBe(false);
|
|
699
|
+
});
|
|
700
|
+
test("/trace off -l disables local scope", async () => {
|
|
701
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
702
|
+
const sessionId = "slash-off-l";
|
|
703
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
704
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "off -l", sessionId);
|
|
705
|
+
expect(text).toContain("Trace disabled");
|
|
706
|
+
expect(text).toContain("local");
|
|
707
|
+
const config = readConfig(testDir);
|
|
708
|
+
expect(config.global_trace_enabled).toBe(false);
|
|
709
|
+
});
|
|
710
|
+
test("/trace off -s disables session scope", async () => {
|
|
711
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
712
|
+
const sessionId = "slash-off-s";
|
|
713
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
714
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "off -s", sessionId);
|
|
715
|
+
expect(text).toContain("Trace disabled");
|
|
716
|
+
expect(text).toContain("session");
|
|
717
|
+
const meta = readSessionMetadata(testDir, sessionId);
|
|
718
|
+
expect(meta.trace_enabled).toBe(false);
|
|
719
|
+
});
|
|
720
|
+
test("/trace off -g -l -s disables all three scopes", async () => {
|
|
721
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
722
|
+
const sessionId = "slash-off-all";
|
|
723
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
724
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "off -g -l -s", sessionId);
|
|
725
|
+
expect(text).toContain("Trace disabled");
|
|
726
|
+
expect(text).toContain("global");
|
|
727
|
+
expect(text).toContain("local");
|
|
728
|
+
expect(text).toContain("session");
|
|
729
|
+
const config = readConfig(testDir);
|
|
730
|
+
expect(config.global_trace_enabled).toBe(false);
|
|
731
|
+
const meta = readSessionMetadata(testDir, sessionId);
|
|
732
|
+
expect(meta.trace_enabled).toBe(false);
|
|
733
|
+
});
|
|
734
|
+
test("/trace disable (alias) works the same as /trace off", async () => {
|
|
735
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
736
|
+
const sessionId = "slash-disable-alias";
|
|
737
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
738
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "disable", sessionId);
|
|
739
|
+
expect(text).toContain("Trace disabled");
|
|
740
|
+
expect(text).toContain("global");
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
describe("Plugin - /trace status (slash command)", () => {
|
|
744
|
+
test("/trace status with no flags shows full status", async () => {
|
|
745
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
746
|
+
const sessionId = "slash-status-default";
|
|
747
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
748
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "status", sessionId);
|
|
749
|
+
expect(text).toContain("Trace Status");
|
|
750
|
+
expect(text).toContain("Global");
|
|
751
|
+
expect(text).toContain("Local");
|
|
752
|
+
expect(text).toContain("Session");
|
|
753
|
+
expect(text).toContain("Storage");
|
|
754
|
+
expect(text).toContain("Effective");
|
|
755
|
+
});
|
|
756
|
+
test("/trace status -g shows status with global flag", async () => {
|
|
757
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
758
|
+
const sessionId = "slash-status-g";
|
|
759
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
760
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "status -g", sessionId);
|
|
761
|
+
expect(text).toContain("Trace Status");
|
|
762
|
+
});
|
|
763
|
+
test("/trace status -l shows status with local flag", async () => {
|
|
764
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
765
|
+
const sessionId = "slash-status-l";
|
|
766
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
767
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "status -l", sessionId);
|
|
768
|
+
expect(text).toContain("Trace Status");
|
|
769
|
+
});
|
|
770
|
+
test("/trace status -s shows status with session flag", async () => {
|
|
771
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
772
|
+
const sessionId = "slash-status-s";
|
|
773
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
774
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "status -s", sessionId);
|
|
775
|
+
expect(text).toContain("Trace Status");
|
|
776
|
+
expect(text).toContain(`Session : ON`);
|
|
777
|
+
});
|
|
778
|
+
test("/trace status reflects ON state after /trace on -g", async () => {
|
|
779
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
780
|
+
const sessionId = "slash-status-after-on";
|
|
781
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
782
|
+
await runTraceCommand(hooks, mockPrompt, "on -g", sessionId);
|
|
783
|
+
mockPrompt.mockClear();
|
|
784
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "status", sessionId);
|
|
785
|
+
expect(text).toContain("Trace Status");
|
|
786
|
+
expect(text).toContain("Global : ON");
|
|
787
|
+
expect(text).toContain("Effective: RECORDING");
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
describe("Plugin - /trace help and unknown commands", () => {
|
|
791
|
+
test("/trace help shows help text", async () => {
|
|
792
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
793
|
+
const sessionId = "slash-help";
|
|
794
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
795
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "help", sessionId);
|
|
796
|
+
expect(text).toContain("Usage: /trace");
|
|
797
|
+
expect(text).toContain("Commands:");
|
|
798
|
+
expect(text).toContain("on");
|
|
799
|
+
expect(text).toContain("off");
|
|
800
|
+
expect(text).toContain("status");
|
|
801
|
+
expect(text).toContain("-g");
|
|
802
|
+
expect(text).toContain("-l");
|
|
803
|
+
expect(text).toContain("-s");
|
|
804
|
+
expect(text).toContain("-d");
|
|
805
|
+
});
|
|
806
|
+
test("/trace with no arguments also shows help", async () => {
|
|
807
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
808
|
+
const sessionId = "slash-no-args";
|
|
809
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
810
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "", sessionId);
|
|
811
|
+
expect(text).toContain("Usage: /trace");
|
|
812
|
+
});
|
|
813
|
+
test("/trace foo (unknown subcommand) returns error message without crashing", async () => {
|
|
814
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
815
|
+
const sessionId = "slash-unknown";
|
|
816
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
817
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "foo", sessionId);
|
|
818
|
+
expect(text).toContain("Unknown command: foo");
|
|
819
|
+
expect(text).toContain("/trace on");
|
|
820
|
+
expect(text).toContain("/trace off");
|
|
821
|
+
expect(text).toContain("/trace status");
|
|
822
|
+
});
|
|
823
|
+
test("/trace with mixed-case ON is normalized", async () => {
|
|
824
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
825
|
+
const sessionId = "slash-mixed-case";
|
|
826
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
827
|
+
const { text } = await runTraceCommand(hooks, mockPrompt, "ON", sessionId);
|
|
828
|
+
expect(text).toContain("Trace enabled");
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
describe("Plugin - slash command guards", () => {
|
|
832
|
+
test("non-trace commands are ignored (no prompt sent, no throw)", async () => {
|
|
833
|
+
const { hooks, mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
834
|
+
const sessionId = "slash-other-cmd";
|
|
835
|
+
await createSessionViaEvent(hooks, sessionId, testDir);
|
|
836
|
+
const output = { parts: [{ type: "text", text: "original" }] };
|
|
837
|
+
let error = null;
|
|
838
|
+
try {
|
|
839
|
+
await hooks["command.execute.before"]({
|
|
840
|
+
command: "help",
|
|
841
|
+
sessionID: sessionId,
|
|
842
|
+
arguments: "",
|
|
843
|
+
}, output);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
error = err;
|
|
847
|
+
}
|
|
848
|
+
expect(error).toBeNull();
|
|
849
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
850
|
+
expect(output.parts.length).toBe(1);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
describe("Plugin - tool.execute.after parentID linking", () => {
|
|
854
|
+
test("tool.execute.after with task tool records sub-session under parent's subSessions", async () => {
|
|
855
|
+
const { hooks, mockPrompt: _mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
856
|
+
void _mockPrompt;
|
|
857
|
+
const parentSessionId = "parent-link-test";
|
|
858
|
+
const subSessionId = "sub-link-test";
|
|
859
|
+
await createSessionViaEvent(hooks, parentSessionId, testDir, "Parent");
|
|
860
|
+
await hooks["tool.execute.after"]({
|
|
861
|
+
tool: "task",
|
|
862
|
+
sessionID: parentSessionId,
|
|
863
|
+
callID: "call-789",
|
|
864
|
+
args: { description: "do work", prompt: "do work" },
|
|
865
|
+
}, {
|
|
866
|
+
title: "Task done",
|
|
867
|
+
output: "completed",
|
|
868
|
+
metadata: { session_id: subSessionId },
|
|
869
|
+
});
|
|
870
|
+
const parentMeta = readSessionMetadata(testDir, parentSessionId);
|
|
871
|
+
expect(parentMeta).toBeTruthy();
|
|
872
|
+
expect(parentMeta.subSessions).toBeDefined();
|
|
873
|
+
expect(parentMeta.subSessions).toContain(subSessionId);
|
|
874
|
+
});
|
|
875
|
+
test("tool.execute.after ignores task output without session_id metadata", async () => {
|
|
876
|
+
const { hooks, mockPrompt: _mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
877
|
+
void _mockPrompt;
|
|
878
|
+
const parentSessionId = "parent-link-no-sid";
|
|
879
|
+
await createSessionViaEvent(hooks, parentSessionId, testDir, "Parent");
|
|
880
|
+
await hooks["tool.execute.after"]({
|
|
881
|
+
tool: "task",
|
|
882
|
+
sessionID: parentSessionId,
|
|
883
|
+
callID: "call-no-sid",
|
|
884
|
+
args: { description: "test" },
|
|
885
|
+
}, {
|
|
886
|
+
title: "Task done",
|
|
887
|
+
output: "completed",
|
|
888
|
+
metadata: {},
|
|
889
|
+
});
|
|
890
|
+
const parentMeta = readSessionMetadata(testDir, parentSessionId);
|
|
891
|
+
if (parentMeta.subSessions) {
|
|
892
|
+
expect(parentMeta.subSessions).not.toContain("phantom-sub");
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
test("event hook with parentID records sub-session in parent metadata", async () => {
|
|
896
|
+
const { hooks, mockPrompt: _mockPrompt } = await setupPluginWithMockClient(testDir);
|
|
897
|
+
void _mockPrompt;
|
|
898
|
+
const parentSessionId = "parent-event-link";
|
|
899
|
+
const childSessionId = "child-event-link";
|
|
900
|
+
await createSessionViaEvent(hooks, parentSessionId, testDir, "Parent");
|
|
901
|
+
await createSessionViaEvent(hooks, childSessionId, testDir, "Child", parentSessionId);
|
|
902
|
+
const parentMeta = readSessionMetadata(testDir, parentSessionId);
|
|
903
|
+
expect(parentMeta).toBeTruthy();
|
|
904
|
+
expect(parentMeta.subSessions).toBeDefined();
|
|
905
|
+
expect(parentMeta.subSessions).toContain(childSessionId);
|
|
906
|
+
const childMeta = readSessionMetadata(testDir, childSessionId);
|
|
907
|
+
expect(childMeta).toBeTruthy();
|
|
908
|
+
expect(childMeta.parentID).toBe(parentSessionId);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
describe("Plugin - new hooks wiring (chat.message, chat.params, tool.execute.before)", () => {
|
|
912
|
+
test("plugin returns chat.message, chat.params, and tool.execute.before hooks as functions", async () => {
|
|
913
|
+
const hooks = await entrypoint.server({
|
|
914
|
+
client: {},
|
|
915
|
+
project: {},
|
|
916
|
+
directory: testDir,
|
|
917
|
+
worktree: testDir,
|
|
918
|
+
experimental_workspace: { register: vi.fn() },
|
|
919
|
+
serverUrl: new URL("http://localhost"),
|
|
920
|
+
$: {},
|
|
921
|
+
});
|
|
922
|
+
expect(typeof hooks["chat.message"]).toBe("function");
|
|
923
|
+
expect(typeof hooks["chat.params"]).toBe("function");
|
|
924
|
+
expect(typeof hooks["tool.execute.before"]).toBe("function");
|
|
925
|
+
});
|
|
926
|
+
test("chat.message hook logs via logger.info and does not throw", async () => {
|
|
927
|
+
const infoSpy = vi
|
|
928
|
+
.spyOn(logger, "info")
|
|
929
|
+
.mockImplementation((() => logger));
|
|
930
|
+
try {
|
|
931
|
+
const hooks = await entrypoint.server({
|
|
932
|
+
client: {},
|
|
933
|
+
project: {},
|
|
934
|
+
directory: testDir,
|
|
935
|
+
worktree: testDir,
|
|
936
|
+
experimental_workspace: { register: vi.fn() },
|
|
937
|
+
serverUrl: new URL("http://localhost"),
|
|
938
|
+
$: {},
|
|
939
|
+
});
|
|
940
|
+
const sessionId = "chat-message-test";
|
|
941
|
+
await createSessionViaEvent(hooks, sessionId, testDir, "ChatMsg");
|
|
942
|
+
await expect(hooks["chat.message"]({
|
|
943
|
+
sessionID: sessionId,
|
|
944
|
+
messageID: "msg-1",
|
|
945
|
+
agent: "build",
|
|
946
|
+
}, { message: {}, parts: [] })).resolves.toBeUndefined();
|
|
947
|
+
const chatMessageCalls = infoSpy.mock.calls.filter((c) => c[0] === "chat.message");
|
|
948
|
+
expect(chatMessageCalls.length).toBeGreaterThan(0);
|
|
949
|
+
const payload = chatMessageCalls[0][1];
|
|
950
|
+
expect(payload.sessionID).toBe(sessionId);
|
|
951
|
+
expect(payload.messageID).toBe("msg-1");
|
|
952
|
+
expect(payload.agent).toBe("build");
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
infoSpy.mockRestore();
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
test("chat.params hook logs via logger.info and does not throw", async () => {
|
|
959
|
+
const infoSpy = vi
|
|
960
|
+
.spyOn(logger, "info")
|
|
961
|
+
.mockImplementation((() => logger));
|
|
962
|
+
try {
|
|
963
|
+
const hooks = await entrypoint.server({
|
|
964
|
+
client: {},
|
|
965
|
+
project: {},
|
|
966
|
+
directory: testDir,
|
|
967
|
+
worktree: testDir,
|
|
968
|
+
experimental_workspace: { register: vi.fn() },
|
|
969
|
+
serverUrl: new URL("http://localhost"),
|
|
970
|
+
$: {},
|
|
971
|
+
});
|
|
972
|
+
const sessionId = "chat-params-test";
|
|
973
|
+
await createSessionViaEvent(hooks, sessionId, testDir, "ChatParams");
|
|
974
|
+
await expect(hooks["chat.params"]({
|
|
975
|
+
sessionID: sessionId,
|
|
976
|
+
agent: "build",
|
|
977
|
+
model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
|
|
978
|
+
provider: { source: "config", info: {}, options: {} },
|
|
979
|
+
message: {},
|
|
980
|
+
}, {
|
|
981
|
+
temperature: 0.7,
|
|
982
|
+
topP: 0.9,
|
|
983
|
+
topK: 40,
|
|
984
|
+
maxOutputTokens: 100,
|
|
985
|
+
options: {},
|
|
986
|
+
})).resolves.toBeUndefined();
|
|
987
|
+
const chatParamsCalls = infoSpy.mock.calls.filter((c) => c[0] === "chat.params");
|
|
988
|
+
expect(chatParamsCalls.length).toBeGreaterThan(0);
|
|
989
|
+
const payload = chatParamsCalls[0][1];
|
|
990
|
+
expect(payload.sessionID).toBe(sessionId);
|
|
991
|
+
expect(payload.agent).toBe("build");
|
|
992
|
+
}
|
|
993
|
+
finally {
|
|
994
|
+
infoSpy.mockRestore();
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
test("tool.execute.before hook logs via logger.info and does not throw", async () => {
|
|
998
|
+
const infoSpy = vi
|
|
999
|
+
.spyOn(logger, "info")
|
|
1000
|
+
.mockImplementation((() => logger));
|
|
1001
|
+
try {
|
|
1002
|
+
const hooks = await entrypoint.server({
|
|
1003
|
+
client: {},
|
|
1004
|
+
project: {},
|
|
1005
|
+
directory: testDir,
|
|
1006
|
+
worktree: testDir,
|
|
1007
|
+
experimental_workspace: { register: vi.fn() },
|
|
1008
|
+
serverUrl: new URL("http://localhost"),
|
|
1009
|
+
$: {},
|
|
1010
|
+
});
|
|
1011
|
+
const sessionId = "tool-before-test";
|
|
1012
|
+
await createSessionViaEvent(hooks, sessionId, testDir, "ToolBefore");
|
|
1013
|
+
await expect(hooks["tool.execute.before"]({ tool: "bash", sessionID: sessionId, callID: "call-1" }, { args: { command: "ls" } })).resolves.toBeUndefined();
|
|
1014
|
+
const toolBeforeCalls = infoSpy.mock.calls.filter((c) => c[0] === "tool.execute.before");
|
|
1015
|
+
expect(toolBeforeCalls.length).toBeGreaterThan(0);
|
|
1016
|
+
const payload = toolBeforeCalls[0][1];
|
|
1017
|
+
expect(payload.sessionID).toBe(sessionId);
|
|
1018
|
+
expect(payload.callID).toBe("call-1");
|
|
1019
|
+
expect(payload.tool).toBe("bash");
|
|
1020
|
+
}
|
|
1021
|
+
finally {
|
|
1022
|
+
infoSpy.mockRestore();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
describe("Plugin - tool.execute.after parentID eager propagation", () => {
|
|
1027
|
+
test("eagerly writes child session's parentID via updateSessionMetadata when task tool completes", async () => {
|
|
1028
|
+
const { hooks } = await setupPluginWithMockClient(testDir);
|
|
1029
|
+
const parentSessionId = "parent-eager-pid";
|
|
1030
|
+
const childSessionId = "child-eager-pid";
|
|
1031
|
+
await createSessionViaEvent(hooks, parentSessionId, testDir, "Parent");
|
|
1032
|
+
await hooks["tool.execute.after"]({
|
|
1033
|
+
tool: "task",
|
|
1034
|
+
sessionID: parentSessionId,
|
|
1035
|
+
callID: "call-eager",
|
|
1036
|
+
args: { description: "sub-work", prompt: "do it" },
|
|
1037
|
+
}, {
|
|
1038
|
+
title: "Task done",
|
|
1039
|
+
output: "completed",
|
|
1040
|
+
metadata: { session_id: childSessionId },
|
|
1041
|
+
});
|
|
1042
|
+
const childMeta = readSessionMetadata(testDir, childSessionId);
|
|
1043
|
+
expect(childMeta).toBeTruthy();
|
|
1044
|
+
expect(childMeta.parentID).toBe(parentSessionId);
|
|
1045
|
+
const parentMeta = readSessionMetadata(testDir, parentSessionId);
|
|
1046
|
+
expect(parentMeta.subSessions).toContain(childSessionId);
|
|
1047
|
+
});
|
|
1048
|
+
test("does not eagerly write parentID for non-task tools", async () => {
|
|
1049
|
+
const { hooks } = await setupPluginWithMockClient(testDir);
|
|
1050
|
+
const sessionId = "non-task-eager";
|
|
1051
|
+
await createSessionViaEvent(hooks, sessionId, testDir, "Parent");
|
|
1052
|
+
const ghostChild = "ghost-child-eager";
|
|
1053
|
+
await hooks["tool.execute.after"]({
|
|
1054
|
+
tool: "read",
|
|
1055
|
+
sessionID: sessionId,
|
|
1056
|
+
callID: "call-read",
|
|
1057
|
+
args: { filePath: "/foo" },
|
|
1058
|
+
}, {
|
|
1059
|
+
title: "Read done",
|
|
1060
|
+
output: "contents",
|
|
1061
|
+
metadata: { session_id: ghostChild },
|
|
1062
|
+
});
|
|
1063
|
+
const ghostMeta = readSessionMetadata(testDir, ghostChild);
|
|
1064
|
+
if (ghostMeta !== null) {
|
|
1065
|
+
expect(ghostMeta.parentID).toBeUndefined();
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
test("does not throw when task metadata has no session_id", async () => {
|
|
1069
|
+
const { hooks } = await setupPluginWithMockClient(testDir);
|
|
1070
|
+
const sessionId = "task-no-sid-eager";
|
|
1071
|
+
await createSessionViaEvent(hooks, sessionId, testDir, "Parent");
|
|
1072
|
+
await expect(hooks["tool.execute.after"]({
|
|
1073
|
+
tool: "task",
|
|
1074
|
+
sessionID: sessionId,
|
|
1075
|
+
callID: "call-no-sid-eager",
|
|
1076
|
+
args: {},
|
|
1077
|
+
}, {
|
|
1078
|
+
title: "Task done",
|
|
1079
|
+
output: "completed",
|
|
1080
|
+
metadata: {},
|
|
1081
|
+
})).resolves.toBeUndefined();
|
|
357
1082
|
});
|
|
358
1083
|
});
|
|
359
1084
|
//# sourceMappingURL=trace.test.js.map
|