@zhushanwen/pi-session-manager 0.1.16 → 0.2.1
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/package.json +10 -3
- package/skills/session-manager-ext-config/SKILL.md +82 -0
- package/src/__tests__/helpers/extension-harness.ts +137 -0
- package/src/__tests__/helpers/logger-mock.ts +39 -0
- package/src/__tests__/notify-content.test.ts +108 -0
- package/src/__tests__/notify-ledger-discipline.test.ts +260 -0
- package/src/__tests__/session-start-recovery.test.ts +145 -0
- package/src/__tests__/tool-error-handling.test.ts +57 -63
- package/src/__tests__/tool-non-json-response.test.ts +34 -49
- package/src/__tests__/watch-orchestration.test.ts +563 -0
- package/src/index.ts +120 -25
- package/src/notify-content.ts +77 -0
- package/src/notify-ledger.ts +121 -0
- package/src/watch-coordinator.ts +336 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// session-start-recovery.test.ts — 重启收口腿(notify-once D7③ / U4)。
|
|
2
|
+
// 断言面:活跃 type 'session' register 重开 watch(入参 = entry id 即 notifyId)/
|
|
3
|
+
// 类型过滤(workflow 与已注销条目不触碰)/ 覆盖序(W1 悬置被 W2 覆盖后该 entry 恰一次
|
|
4
|
+
// unregister、后续 entry 不受阻塞——每 entry 独立 handler 禁串行 await)/
|
|
5
|
+
// 收口腿 label 降级源 = register 三键 name。
|
|
6
|
+
// pi/ctx 装配、respondWatch、flushMicrotasks 走共享 harness(helpers/extension-harness.ts);
|
|
7
|
+
// watch-only select(非 watch action 即抛)为本文件域行为,留在本地。
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
flushMicrotasks,
|
|
12
|
+
makeRespondWatch,
|
|
13
|
+
mountExtension,
|
|
14
|
+
type ExtensionHarness,
|
|
15
|
+
type WatchInvocation,
|
|
16
|
+
} from "./helpers/extension-harness.ts";
|
|
17
|
+
|
|
18
|
+
const LEDGER_SLOT_KEY = Symbol.for("@zhushanwen/subagent-core.notifyLedger");
|
|
19
|
+
|
|
20
|
+
interface Harness extends ExtensionHarness {
|
|
21
|
+
watches: WatchInvocation[];
|
|
22
|
+
respondWatch(index: number, value: unknown): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const E1 = "sm-aaaaaaaa-0000-4000-8000-000000000001";
|
|
26
|
+
const E2 = "sm-bbbbbbbb-0000-4000-8000-000000000002";
|
|
27
|
+
const E3 = "sm-cccccccc-0000-4000-8000-000000000003";
|
|
28
|
+
const WF = "wf-run-1";
|
|
29
|
+
|
|
30
|
+
function staleEntries(): unknown[] {
|
|
31
|
+
return [
|
|
32
|
+
{ customType: "pending:register", data: { id: E1, type: "session", name: "Alpha", registeredAt: 1, sessionId: "parent-1" } },
|
|
33
|
+
{ customType: "pending:register", data: { id: E2, type: "session", name: "Beta", registeredAt: 2, sessionId: "parent-1" } },
|
|
34
|
+
// workflow 类型归 core 对账 sweep,收口腿不得触碰
|
|
35
|
+
{ customType: "pending:register", data: { id: WF, type: "workflow", name: "run-1" } },
|
|
36
|
+
// 已注销(差集净空)→ 不重开
|
|
37
|
+
{ customType: "pending:register", data: { id: E3, type: "session", name: "Gamma", registeredAt: 3, sessionId: "parent-1" } },
|
|
38
|
+
{ customType: "pending:unregister", data: { id: E3, reason: "completed", status: "completed" } },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createHarness(entries: unknown[]): Harness {
|
|
43
|
+
const watches: WatchInvocation[] = [];
|
|
44
|
+
const selectMock = vi.fn((_marker: unknown, args: unknown[], _opts?: unknown) => {
|
|
45
|
+
const payload = JSON.parse((args as string[])[0]) as {
|
|
46
|
+
action: string;
|
|
47
|
+
params: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
if (payload.action !== "watch") throw new Error(`unexpected action ${payload.action}`);
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
watches.push({ notifyId: String(payload.params.notifyId), resolve });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
...mountExtension(selectMock, entries),
|
|
56
|
+
watches,
|
|
57
|
+
respondWatch: makeRespondWatch(watches),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function unregistersOf(h: Harness): Array<[string, unknown]> {
|
|
62
|
+
return h.emitted
|
|
63
|
+
.filter((e) => e.event === "pending:unregister")
|
|
64
|
+
.map((e) => [String(e.data.id), e.data.reason]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 单条 session register 的收口腿驱动(fail-closed / label 降级两用例共用前置):
|
|
68
|
+
* 开表(session_start)→ 排干微任务 → 应答首个 watch → 再排干 */
|
|
69
|
+
async function startSingleEntrySession(respondValue: unknown): Promise<Harness> {
|
|
70
|
+
const h = createHarness([
|
|
71
|
+
{ customType: "pending:register", data: { id: E1, type: "session", name: "Alpha", registeredAt: 1, sessionId: "parent-1" } },
|
|
72
|
+
]);
|
|
73
|
+
h.handlers.session_start({ type: "session_start" }, h.ctx);
|
|
74
|
+
await flushMicrotasks();
|
|
75
|
+
h.respondWatch(0, respondValue);
|
|
76
|
+
await flushMicrotasks();
|
|
77
|
+
return h;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.useFakeTimers();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
vi.useRealTimers();
|
|
86
|
+
Reflect.deleteProperty(globalThis, LEDGER_SLOT_KEY);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("session_start 重启收口腿(D7③)", () => {
|
|
90
|
+
it("重开 watch 覆盖序:W1 悬置被 W2 覆盖后该 entry 恰一次 unregister,且后续 entry 不受阻塞", async () => {
|
|
91
|
+
const h = createHarness(staleEntries());
|
|
92
|
+
expect(h.handlers.session_start).toBeTypeOf("function");
|
|
93
|
+
|
|
94
|
+
// 第一次 session_start:E1 的 watch 永挂(模拟被后续 W2 覆盖 / runtime 只应答最新槽),
|
|
95
|
+
// E2 仍须立刻开表——串行 await 链(先等 E1 再开 E2)在此必挂死
|
|
96
|
+
h.handlers.session_start({ type: "session_start" }, h.ctx);
|
|
97
|
+
await flushMicrotasks();
|
|
98
|
+
expect(h.watches.map((w) => w.notifyId)).toEqual([E1, E2]);
|
|
99
|
+
|
|
100
|
+
// E2 先应答 orphaned(杀父腿,触发器②批量转移先行)→ 即时静默 unregister
|
|
101
|
+
h.respondWatch(1, JSON.stringify({ reason: "orphaned", sessionId: "child-b" }));
|
|
102
|
+
await flushMicrotasks();
|
|
103
|
+
expect(unregistersOf(h)).toEqual([[E2, "orphaned"]]);
|
|
104
|
+
|
|
105
|
+
// 第二次 session_start(如 /resume 再触发):E1 的 W2 到达并获应答——
|
|
106
|
+
// W1 悬置全程未 resolve,恰一次 unregister 由「单槽只应答最新 + 写侧差集幂等」保证
|
|
107
|
+
h.handlers.session_start({ type: "session_start" }, h.ctx);
|
|
108
|
+
await flushMicrotasks();
|
|
109
|
+
expect(h.watches).toHaveLength(4);
|
|
110
|
+
expect(h.watches[2].notifyId).toBe(E1);
|
|
111
|
+
h.respondWatch(2, JSON.stringify({ reason: "cancelled" }));
|
|
112
|
+
await flushMicrotasks();
|
|
113
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
114
|
+
|
|
115
|
+
// E1 恰一次、E2 恰一次(第二次的 watch 不再应答 → 不重复注销)
|
|
116
|
+
expect(unregistersOf(h).sort()).toEqual(
|
|
117
|
+
[
|
|
118
|
+
[E1, "cancelled"],
|
|
119
|
+
[E2, "orphaned"],
|
|
120
|
+
].sort(),
|
|
121
|
+
);
|
|
122
|
+
// workflow / 已注销条目全程未触碰
|
|
123
|
+
const watchedIds = h.watches.map((w) => w.notifyId);
|
|
124
|
+
expect(watchedIds).not.toContain(WF);
|
|
125
|
+
expect(watchedIds).not.toContain(E3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("runtime 已重启查无 claim → fail-closed 'cancelled' 腿:静默注销(活跃集回归基线)", async () => {
|
|
129
|
+
const h = await startSingleEntrySession(JSON.stringify({ reason: "cancelled" }));
|
|
130
|
+
expect(unregistersOf(h)).toEqual([[E1, "cancelled"]]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("收口腿 label 降级源 = register 三键 name(D9 label 两路径的恢复路径)", async () => {
|
|
134
|
+
Reflect.set(globalThis, LEDGER_SLOT_KEY, {
|
|
135
|
+
current: { record: vi.fn(() => true), compactionCheck: () => 0 },
|
|
136
|
+
});
|
|
137
|
+
const record = Reflect.get(globalThis, LEDGER_SLOT_KEY).current.record;
|
|
138
|
+
const h = await startSingleEntrySession(
|
|
139
|
+
JSON.stringify({ reason: "completed", sessionId: "child-x", settleSeq: 1, fulfillsN: 1 }),
|
|
140
|
+
);
|
|
141
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
142
|
+
expect(record).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(String(record.mock.calls[0][1])).toContain('Managed session "Alpha" (child-x)');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -1,81 +1,75 @@
|
|
|
1
|
-
// tool-error-handling.test.ts — U5-A4: null/undefined returns → cancelled
|
|
1
|
+
// tool-error-handling.test.ts — U5-A4: null/undefined returns → cancelled throw; exceptions → propagate;
|
|
2
|
+
// {error} respond → throw(W4 throw 范式:pi agent-loop 仅在 execute throw 时置 isError:true——
|
|
3
|
+
// pi-agent-core dist/agent-loop.js:453-483 executePreparedToolCall 正常 return 硬编码 isError:false;
|
|
4
|
+
// 语义登记 PS-56)
|
|
2
5
|
|
|
3
6
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
function createHarness(selectImpl: (...args: unknown[]) => Promise<unknown>) {
|
|
7
|
-
const registered: Array<{ name: string; execute: Function }> = [];
|
|
8
|
-
const selectMock = vi.fn(selectImpl);
|
|
9
|
-
const pi = {
|
|
10
|
-
registerTool: (tool: { name: string; execute: Function }) => registered.push(tool),
|
|
11
|
-
on: vi.fn(),
|
|
12
|
-
getAllTools: vi.fn(() => []),
|
|
13
|
-
setActiveTools: vi.fn(),
|
|
14
|
-
};
|
|
15
|
-
const ctx = {
|
|
16
|
-
mode: "rpc" as const,
|
|
17
|
-
hasUI: true,
|
|
18
|
-
ui: { select: selectMock },
|
|
19
|
-
};
|
|
20
|
-
registerExtension(pi as never);
|
|
21
|
-
return { registered, selectMock, ctx };
|
|
22
|
-
}
|
|
7
|
+
import { createToolHarness, expectToolRejects, runToolForText } from "./helpers/extension-harness.ts";
|
|
23
8
|
|
|
24
9
|
describe("U5-A4 tool-error-handling", () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const tool = registered.find((t) => t.name === "create_managed_session")!;
|
|
28
|
-
const result = await tool.execute("call-1", { cwd: "/tmp" }, undefined, undefined, ctx);
|
|
29
|
-
expect(result.content).toHaveLength(1);
|
|
30
|
-
expect(result.content[0].text).toContain("cancelled");
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
31
12
|
});
|
|
32
13
|
|
|
33
|
-
it("select returning
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
it("select returning undefined (user cancel/timeout) → cancelled throw(W4 范式:错误路径 throw)", async () => {
|
|
15
|
+
await expectToolRejects(
|
|
16
|
+
createToolHarness(vi.fn().mockResolvedValue(undefined)),
|
|
17
|
+
"create_managed_session",
|
|
18
|
+
{ cwd: "/tmp" },
|
|
19
|
+
/cancelled/,
|
|
20
|
+
);
|
|
39
21
|
});
|
|
40
22
|
|
|
41
|
-
it("select
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
23
|
+
it("select returning null → cancelled throw", async () => {
|
|
24
|
+
await expectToolRejects(
|
|
25
|
+
createToolHarness(vi.fn().mockResolvedValue(null)),
|
|
26
|
+
"send_to_session",
|
|
27
|
+
{ sessionId: "s1", prompt: "hi" },
|
|
28
|
+
/cancelled/,
|
|
29
|
+
);
|
|
47
30
|
});
|
|
48
31
|
|
|
49
|
-
it("select throwing
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
it("select throwing exception → cancelled throw(select 通道异常同折叠)", async () => {
|
|
33
|
+
await expectToolRejects(
|
|
34
|
+
createToolHarness(vi.fn().mockRejectedValue(new Error("channel closed"))),
|
|
35
|
+
"get_session_status",
|
|
36
|
+
{ sessionId: "s1" },
|
|
37
|
+
/cancelled/,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("select throwing non-Error → cancelled throw", async () => {
|
|
42
|
+
await expectToolRejects(
|
|
43
|
+
createToolHarness(vi.fn().mockRejectedValue("string error")),
|
|
44
|
+
"abort_session",
|
|
45
|
+
{ sessionId: "s1" },
|
|
46
|
+
/cancelled/,
|
|
47
|
+
);
|
|
55
48
|
});
|
|
56
49
|
|
|
57
|
-
it("runtime respond 携带 {error} JSON → isError:
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
it("runtime respond 携带 {error} JSON → throw(禁止错误成功模式;pi 置 isError:true)", async () => {
|
|
51
|
+
await expectToolRejects(
|
|
52
|
+
createToolHarness(
|
|
53
|
+
vi.fn().mockResolvedValue(JSON.stringify({ error: "session unreachable", hint: "check get_session_status" })),
|
|
54
|
+
),
|
|
55
|
+
"send_to_session",
|
|
56
|
+
{ sessionId: "s1", prompt: "hi" },
|
|
57
|
+
/session unreachable[\s\S]*hint/,
|
|
60
58
|
);
|
|
61
|
-
const tool = registered.find((t) => t.name === "send_to_session")!;
|
|
62
|
-
const result = await tool.execute("call-1", { sessionId: "s1", prompt: "hi" }, undefined, undefined, ctx);
|
|
63
|
-
expect(result.isError).toBe(true);
|
|
64
|
-
expect(result.content[0].text).toContain("session unreachable");
|
|
65
|
-
expect(result.content[0].text).toContain("hint");
|
|
66
59
|
});
|
|
67
60
|
|
|
68
|
-
it("runtime respond 正常 JSON →
|
|
69
|
-
const
|
|
70
|
-
vi.fn().mockResolvedValue(JSON.stringify({ queued: true })),
|
|
61
|
+
it("runtime respond 正常 JSON → 成功返回(不含 isError 字段)", async () => {
|
|
62
|
+
const result = await runToolForText(
|
|
63
|
+
createToolHarness(vi.fn().mockResolvedValue(JSON.stringify({ queued: true }))),
|
|
64
|
+
"send_to_session",
|
|
65
|
+
{ sessionId: "s1", prompt: "hi" },
|
|
71
66
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
expect(result.isError).toBeUndefined();
|
|
67
|
+
expect(Object.hasOwn(result, "isError")).toBe(false);
|
|
68
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ queued: true });
|
|
75
69
|
});
|
|
76
70
|
|
|
77
|
-
it("all 6 tools handle null gracefully", async () => {
|
|
78
|
-
const { registered, ctx } =
|
|
71
|
+
it("all 6 tools handle null gracefully(cancelled throw)", async () => {
|
|
72
|
+
const { registered, ctx } = createToolHarness(vi.fn().mockResolvedValue(null));
|
|
79
73
|
for (const tool of registered) {
|
|
80
74
|
const params = tool.name === "create_managed_session"
|
|
81
75
|
? { cwd: "/tmp" }
|
|
@@ -86,9 +80,9 @@ describe("U5-A4 tool-error-handling", () => {
|
|
|
86
80
|
: tool.name === "list_my_sessions"
|
|
87
81
|
? {}
|
|
88
82
|
: { sessionId: "s1" };
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
await expect(tool.execute("call-1", params, undefined, undefined, ctx)).rejects.toThrow(
|
|
84
|
+
/cancelled/,
|
|
85
|
+
);
|
|
92
86
|
}
|
|
93
87
|
});
|
|
94
88
|
});
|
|
@@ -1,64 +1,46 @@
|
|
|
1
|
-
// tool-non-json-response.test.ts — 行为微变①(D8):非 JSON 回包 →
|
|
1
|
+
// tool-non-json-response.test.ts — 行为微变①(D8):非 JSON 回包 → throw + 留痕
|
|
2
2
|
//
|
|
3
3
|
// [HISTORICAL] 改前行为(D8 前):executeTool 对回包 try JSON.parse、catch 后
|
|
4
4
|
// parsed=undefined,非 JSON 字符串静默当成功文本返回(content[0].text = raw、无
|
|
5
5
|
// isError)——runtime 协议漂移对 agent 不可辨。D8
|
|
6
|
-
|
|
6
|
+
//(docs/architecture/ext-simplify-17-shared-extraction.md §3.3,有意微变)统一为
|
|
7
7
|
// 对齐 plugin-bridge 形态:logger.error 留痕(callMarkerRpc 原语经注入的 log 承担)
|
|
8
|
-
// + isError:true
|
|
8
|
+
// + 错误路径 throw(W4 范式:pi agent-loop 仅在 execute throw 时置 isError:true——
|
|
9
|
+
// pi-agent-core dist/agent-loop.js:453-483,语义登记 PS-56)。
|
|
9
10
|
|
|
10
11
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
12
|
+
// logger mock helper 必须先于 extension-harness(其 import 链触发被 mock 模块加载)
|
|
13
|
+
import { createLoggerModuleMock, getLoggerMock } from "./helpers/logger-mock.ts";
|
|
14
|
+
import { createToolHarness, expectToolRejects, runToolForText } from "./helpers/extension-harness.ts";
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
error: vi.fn(),
|
|
14
|
-
warn: vi.fn(),
|
|
15
|
-
debug: vi.fn(),
|
|
16
|
-
}));
|
|
17
|
-
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
18
|
-
getLogger: () => loggerMock,
|
|
19
|
-
setPiHandle: vi.fn(),
|
|
20
|
-
}));
|
|
16
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => createLoggerModuleMock());
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
const loggerMock = getLoggerMock();
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
const registered: Array<{ name: string; execute: Function }> = [];
|
|
26
|
-
const selectMock = vi.fn(selectImpl);
|
|
27
|
-
const pi = {
|
|
28
|
-
registerTool: (tool: { name: string; execute: Function }) => registered.push(tool),
|
|
29
|
-
on: vi.fn(),
|
|
30
|
-
getAllTools: vi.fn(() => []),
|
|
31
|
-
setActiveTools: vi.fn(),
|
|
32
|
-
};
|
|
33
|
-
const ctx = {
|
|
34
|
-
mode: "rpc" as const,
|
|
35
|
-
hasUI: true,
|
|
36
|
-
ui: { select: selectMock },
|
|
37
|
-
};
|
|
38
|
-
registerExtension(pi as never);
|
|
39
|
-
return { registered, selectMock, ctx };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("D8 行为微变①:非 JSON 回包 → isError + 留痕", () => {
|
|
20
|
+
describe("D8 行为微变①:非 JSON 回包 → throw + 留痕", () => {
|
|
43
21
|
beforeEach(() => {
|
|
44
22
|
vi.clearAllMocks();
|
|
45
23
|
});
|
|
46
24
|
|
|
47
|
-
it("非 JSON 回包 → isError:true
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
25
|
+
it("非 JSON 回包 → throw 提示文本(不再静默当成功文本返回;pi 侧置 isError:true)", async () => {
|
|
26
|
+
await expectToolRejects(
|
|
27
|
+
createToolHarness(vi.fn().mockResolvedValue("plain text response")),
|
|
28
|
+
"send_to_session",
|
|
29
|
+
{ sessionId: "s1", prompt: "hi" },
|
|
30
|
+
/non-JSON/,
|
|
31
|
+
"call-1",
|
|
32
|
+
);
|
|
33
|
+
// 改前对照:text 曾是 raw 本身('plain text response')静默当成功
|
|
56
34
|
});
|
|
57
35
|
|
|
58
36
|
it("留痕:logger.error 收到 non-JSON 消息 + responseHead(原语经注入 log 承担)", async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
37
|
+
await expectToolRejects(
|
|
38
|
+
createToolHarness(vi.fn().mockResolvedValue("plain text response")),
|
|
39
|
+
"list_my_sessions",
|
|
40
|
+
{},
|
|
41
|
+
/non-JSON/,
|
|
42
|
+
"call-2",
|
|
43
|
+
);
|
|
62
44
|
expect(loggerMock.error).toHaveBeenCalled();
|
|
63
45
|
const [msg, detail] = loggerMock.error.mock.calls.at(-1)!;
|
|
64
46
|
expect(msg).toContain("[session-manager]");
|
|
@@ -66,11 +48,14 @@ describe("D8 行为微变①:非 JSON 回包 → isError + 留痕", () => {
|
|
|
66
48
|
expect(detail).toMatchObject({ responseHead: "plain text response" });
|
|
67
49
|
});
|
|
68
50
|
|
|
69
|
-
it("对照:合法 JSON 回包仍走成功路径(raw 透传、无 isError
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
it("对照:合法 JSON 回包仍走成功路径(raw 透传、无 isError 字段、无留痕)", async () => {
|
|
52
|
+
const result = await runToolForText(
|
|
53
|
+
createToolHarness(vi.fn().mockResolvedValue('{"queued":true}')),
|
|
54
|
+
"send_to_session",
|
|
55
|
+
{ sessionId: "s1", prompt: "hi" },
|
|
56
|
+
"call-3",
|
|
57
|
+
);
|
|
58
|
+
expect(Object.hasOwn(result, "isError")).toBe(false);
|
|
74
59
|
expect(result.content[0].text).toBe('{"queued":true}');
|
|
75
60
|
expect(loggerMock.error).not.toHaveBeenCalled();
|
|
76
61
|
});
|