aws-runtime-bridge 1.7.50 → 1.8.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/dist/adapter/AcodeSdkAdapter.d.ts +30 -3
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +461 -105
- package/dist/adapter/AcodeSdkAdapter.test.js +1282 -93
- package/dist/adapter/ClaudeSdkAdapter.test.js +31 -8
- package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/CodexSdkAdapter.js +2 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +2 -0
- package/dist/adapter/SdkProviderSpi.d.ts.map +1 -1
- package/dist/adapter/adapter.test.js +2 -2
- package/dist/adapter/idle-poll-loop.d.ts +4 -4
- package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
- package/dist/adapter/idle-poll-loop.js +3 -3
- package/dist/adapter/idle-poll-loop.test.js +15 -1
- package/dist/adapter/types.d.ts +25 -6
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +226 -111
- package/dist/adapter/types.test.js +10 -3
- package/dist/routes/ai-sources.js +5 -1
- package/dist/routes/ai-sources.test.js +21 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +6 -7
- package/dist/routes/runtime-binding.js +2 -2
- package/dist/routes/terminal.d.ts +22 -5
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +522 -170
- package/dist/routes/terminal.test.js +497 -208
- package/dist/services/mcp-launch-binding-queue.d.ts +1 -0
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +8 -2
- package/dist/services/mcp-launch-binding-queue.test.js +47 -4
- package/dist/services/session-output.d.ts +14 -6
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +29 -14
- package/dist/services/session-output.test.js +93 -39
- package/package/acode/dist/built-in-file-tools.d.ts +1 -1
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/built-in-file-tools.js +388 -50
- package/package/acode/dist/runtime.d.ts +11 -3
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +902 -190
- package/package/acode/dist/types.d.ts +18 -5
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +3 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +11 -9
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +1 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +31 -3
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +2 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js +43 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,345 +1,634 @@
|
|
|
1
|
-
import { mkdtemp, mkdir, writeFile } from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { describe, expect, it } from
|
|
5
|
-
import { SDK_PROVIDER_DEFINITIONS } from
|
|
6
|
-
import { buildClaudeCodeLaunchConfig, buildRuntimeEnv, buildToolResultTimelineActionInfo, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveSdkInputMessage, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveSdkSessionDisplayStatus, resolveStatusChangeUsage, } from
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
1
|
+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { SDK_PROVIDER_DEFINITIONS } from "../adapter/SdkProviderSpi.js";
|
|
6
|
+
import { buildAcodeCommandOutputPath, buildClaudeCodeLaunchConfig, buildRuntimeEnv, buildSdkOutputMetadata, buildToolResultTimelineActionInfo, countTerminalOutputLines, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatCommandInactivityTimeoutNotice, formatCommandOutputSpilloverNotice, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveSdkInputMessage, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveSdkSessionDisplayStatus, resolveRuntimeStatusFromProviderEvent, resolveStatusChangeUsage, } from "./terminal.js";
|
|
7
|
+
describe("terminal route validation", () => {
|
|
8
|
+
it("requires agentId and workspacePath for start", () => {
|
|
9
9
|
const validateStartRequest = (body) => {
|
|
10
10
|
if (!body.agentId || !body.workspacePath)
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
valid: false,
|
|
13
|
+
error: "agentId and workspacePath are required",
|
|
14
|
+
};
|
|
12
15
|
return { valid: true };
|
|
13
16
|
};
|
|
14
17
|
expect(validateStartRequest({}).valid).toBe(false);
|
|
15
|
-
expect(validateStartRequest({ agentId:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
expect(resolveCommand(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
expect(resolveLaunchMode(
|
|
26
|
-
expect(resolveLaunchMode(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
expect(validateStartRequest({ agentId: "agent-1", workspacePath: "/path" })
|
|
19
|
+
.valid).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it("uses SDK default command when not provided", () => {
|
|
22
|
+
const resolveCommand = (command) => command || "acode";
|
|
23
|
+
expect(resolveCommand(undefined)).toBe("acode");
|
|
24
|
+
expect(resolveCommand("codex")).toBe("codex");
|
|
25
|
+
});
|
|
26
|
+
it("uses SDK launch mode for unsupported mode values", () => {
|
|
27
|
+
const resolveLaunchMode = (mode) => mode === "sdk" ? "sdk" : "sdk";
|
|
28
|
+
expect(resolveLaunchMode(undefined)).toBe("sdk");
|
|
29
|
+
expect(resolveLaunchMode("sdk")).toBe("sdk");
|
|
30
|
+
expect(resolveLaunchMode("unsupported")).toBe("sdk");
|
|
31
|
+
});
|
|
32
|
+
it("keeps shortcut mode available for manual key injection", () => {
|
|
33
|
+
const buildInputResponse = (mode) => mode === "shortcut"
|
|
34
|
+
? { ok: true, mode: "shortcut" }
|
|
35
|
+
: { ok: true, mode: "sdk" };
|
|
36
|
+
expect(buildInputResponse("shortcut")).toEqual({
|
|
37
|
+
ok: true,
|
|
38
|
+
mode: "shortcut",
|
|
39
|
+
});
|
|
40
|
+
expect(buildInputResponse(undefined)).toEqual({ ok: true, mode: "sdk" });
|
|
41
|
+
});
|
|
42
|
+
it("does not downgrade unsupported shortcut input to normal SDK messages", () => {
|
|
43
|
+
const buildShortcutFallbackResponse = (hasShortcutSupport) => hasShortcutSupport
|
|
44
|
+
? { status: 200, body: { ok: true, mode: "shortcut" } }
|
|
45
|
+
: {
|
|
46
|
+
status: 400,
|
|
47
|
+
body: {
|
|
48
|
+
error: "shortcut input is not supported by this SDK provider",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
37
51
|
expect(buildShortcutFallbackResponse(false)).toEqual({
|
|
38
52
|
status: 400,
|
|
39
|
-
body: { error:
|
|
53
|
+
body: { error: "shortcut input is not supported by this SDK provider" },
|
|
40
54
|
});
|
|
41
55
|
});
|
|
42
|
-
it(
|
|
56
|
+
it("requires sessionId and input for SDK terminal input", () => {
|
|
43
57
|
const validateInputRequest = (body) => {
|
|
44
58
|
if (!body.sessionId || body.input === undefined || body.input === null) {
|
|
45
|
-
return { valid: false, error:
|
|
59
|
+
return { valid: false, error: "sessionId and input are required" };
|
|
46
60
|
}
|
|
47
61
|
return { valid: true };
|
|
48
62
|
};
|
|
49
63
|
expect(validateInputRequest({}).valid).toBe(false);
|
|
50
|
-
expect(validateInputRequest({ sessionId:
|
|
51
|
-
});
|
|
52
|
-
it(
|
|
53
|
-
const buildResizeResponse = (cols, rows) => ({
|
|
54
|
-
|
|
64
|
+
expect(validateInputRequest({ sessionId: "session-1", input: "continue" }).valid).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it("keeps resize contract as SDK no-op compatibility endpoint", () => {
|
|
67
|
+
const buildResizeResponse = (cols, rows) => ({
|
|
68
|
+
ok: true,
|
|
69
|
+
mode: "sdk",
|
|
70
|
+
noop: true,
|
|
71
|
+
cols,
|
|
72
|
+
rows,
|
|
73
|
+
});
|
|
74
|
+
expect(buildResizeResponse(120, 30)).toEqual({
|
|
75
|
+
ok: true,
|
|
76
|
+
mode: "sdk",
|
|
77
|
+
noop: true,
|
|
78
|
+
cols: 120,
|
|
79
|
+
rows: 30,
|
|
80
|
+
});
|
|
55
81
|
});
|
|
56
|
-
it(
|
|
82
|
+
it("requires sessionId for stop", () => {
|
|
57
83
|
const validateStopRequest = (body) => {
|
|
58
84
|
if (!body.sessionId)
|
|
59
|
-
return { valid: false, error:
|
|
85
|
+
return { valid: false, error: "sessionId is required" };
|
|
60
86
|
return { valid: true };
|
|
61
87
|
};
|
|
62
88
|
expect(validateStopRequest({}).valid).toBe(false);
|
|
63
|
-
expect(validateStopRequest({ sessionId:
|
|
89
|
+
expect(validateStopRequest({ sessionId: "session-1" }).valid).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("SDK startup idle command ordering", () => {
|
|
93
|
+
it("sets idle commands immediately after adapter startup and before PID probing or persistence", async () => {
|
|
94
|
+
const calls = [];
|
|
95
|
+
const adapter = {
|
|
96
|
+
async startSession() {
|
|
97
|
+
calls.push("startSession");
|
|
98
|
+
},
|
|
99
|
+
setIdleCommands() {
|
|
100
|
+
calls.push("setIdleCommands");
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const startFlow = async () => {
|
|
104
|
+
await adapter.startSession();
|
|
105
|
+
adapter.setIdleCommands();
|
|
106
|
+
calls.push("probePid");
|
|
107
|
+
calls.push("persistSession");
|
|
108
|
+
};
|
|
109
|
+
await startFlow();
|
|
110
|
+
expect(calls).toEqual([
|
|
111
|
+
"startSession",
|
|
112
|
+
"setIdleCommands",
|
|
113
|
+
"probePid",
|
|
114
|
+
"persistSession",
|
|
115
|
+
]);
|
|
64
116
|
});
|
|
65
117
|
});
|
|
66
|
-
describe(
|
|
67
|
-
it(
|
|
68
|
-
const env = buildRuntimeEnv(
|
|
69
|
-
expect(env.AWS_AGENT_ID).toBe(
|
|
118
|
+
describe("terminal configuration", () => {
|
|
119
|
+
it("builds correct SDK runtime environment", () => {
|
|
120
|
+
const env = buildRuntimeEnv("agent-123", undefined, { PATH: "/usr/bin" });
|
|
121
|
+
expect(env.AWS_AGENT_ID).toBe("agent-123");
|
|
70
122
|
expect(env.AWS_MCP_LAUNCH_BINDING_ID).toBeUndefined();
|
|
71
|
-
expect(env.AWS_MCP_CLAIM_LAUNCH_BINDING).toBe(
|
|
72
|
-
});
|
|
73
|
-
it(
|
|
74
|
-
const env = buildRuntimeEnv(
|
|
75
|
-
ANTHROPIC_BASE_URL:
|
|
76
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
77
|
-
ANTHROPIC_MODEL:
|
|
78
|
-
AWS_AGENT_ID:
|
|
79
|
-
EMPTY_VALUE:
|
|
123
|
+
expect(env.AWS_MCP_CLAIM_LAUNCH_BINDING).toBe("true");
|
|
124
|
+
});
|
|
125
|
+
it("merges launch env overrides without allowing agent identity override", () => {
|
|
126
|
+
const env = buildRuntimeEnv("agent-123", "/workspace", { PATH: "/usr/bin" }, {
|
|
127
|
+
ANTHROPIC_BASE_URL: " https://api.example.com ",
|
|
128
|
+
ANTHROPIC_AUTH_TOKEN: "secret-token",
|
|
129
|
+
ANTHROPIC_MODEL: "claude-sonnet-4-5",
|
|
130
|
+
AWS_AGENT_ID: "malicious-agent",
|
|
131
|
+
EMPTY_VALUE: " ",
|
|
80
132
|
});
|
|
81
|
-
expect(env.ANTHROPIC_BASE_URL).toBe(
|
|
82
|
-
expect(env.ANTHROPIC_AUTH_TOKEN).toBe(
|
|
83
|
-
expect(env.ANTHROPIC_MODEL).toBe(
|
|
133
|
+
expect(env.ANTHROPIC_BASE_URL).toBe("https://api.example.com");
|
|
134
|
+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe("secret-token");
|
|
135
|
+
expect(env.ANTHROPIC_MODEL).toBe("claude-sonnet-4-5");
|
|
84
136
|
expect(env.EMPTY_VALUE).toBeUndefined();
|
|
85
|
-
expect(env.AWS_AGENT_ID).toBe(
|
|
86
|
-
expect(env.AWS_WORKSPACE_PATH).toBe(
|
|
137
|
+
expect(env.AWS_AGENT_ID).toBe("agent-123");
|
|
138
|
+
expect(env.AWS_WORKSPACE_PATH).toBe("/workspace");
|
|
87
139
|
});
|
|
88
|
-
it(
|
|
140
|
+
it("merges saved ClaudeCode AI settings into SDK launch env with request overrides taking precedence", () => {
|
|
89
141
|
const launchConfig = buildClaudeCodeLaunchConfig({
|
|
90
142
|
env: {
|
|
91
|
-
ANTHROPIC_BASE_URL:
|
|
92
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
93
|
-
ANTHROPIC_MODEL:
|
|
94
|
-
EMPTY_SAVED:
|
|
143
|
+
ANTHROPIC_BASE_URL: "https://saved.example.com",
|
|
144
|
+
ANTHROPIC_AUTH_TOKEN: "sk-saved",
|
|
145
|
+
ANTHROPIC_MODEL: "claude-saved",
|
|
146
|
+
EMPTY_SAVED: " ",
|
|
95
147
|
},
|
|
96
148
|
}, {
|
|
97
|
-
ANTHROPIC_MODEL:
|
|
98
|
-
EXTRA_VALUE:
|
|
149
|
+
ANTHROPIC_MODEL: " claude-request ",
|
|
150
|
+
EXTRA_VALUE: "enabled",
|
|
99
151
|
});
|
|
100
152
|
expect(launchConfig).toEqual({
|
|
101
153
|
envOverrides: {
|
|
102
|
-
ANTHROPIC_BASE_URL:
|
|
103
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
104
|
-
ANTHROPIC_MODEL:
|
|
105
|
-
EXTRA_VALUE:
|
|
154
|
+
ANTHROPIC_BASE_URL: "https://saved.example.com",
|
|
155
|
+
ANTHROPIC_AUTH_TOKEN: "sk-saved",
|
|
156
|
+
ANTHROPIC_MODEL: "claude-request",
|
|
157
|
+
EXTRA_VALUE: "enabled",
|
|
106
158
|
},
|
|
107
|
-
model:
|
|
159
|
+
model: "claude-request",
|
|
108
160
|
});
|
|
109
161
|
});
|
|
110
|
-
it(
|
|
162
|
+
it("uses CODEX_MODEL from request env overrides as SDK launch model", () => {
|
|
111
163
|
const launchConfig = buildClaudeCodeLaunchConfig(null, {
|
|
112
|
-
OPENAI_API_KEY:
|
|
113
|
-
CODEX_MODEL:
|
|
164
|
+
OPENAI_API_KEY: "sk-codex",
|
|
165
|
+
CODEX_MODEL: " gpt-5.3-codex ",
|
|
114
166
|
});
|
|
115
167
|
expect(launchConfig).toEqual({
|
|
116
168
|
envOverrides: {
|
|
117
|
-
OPENAI_API_KEY:
|
|
118
|
-
CODEX_MODEL:
|
|
169
|
+
OPENAI_API_KEY: "sk-codex",
|
|
170
|
+
CODEX_MODEL: "gpt-5.3-codex",
|
|
119
171
|
},
|
|
120
|
-
model:
|
|
172
|
+
model: "gpt-5.3-codex",
|
|
121
173
|
});
|
|
122
174
|
});
|
|
123
175
|
});
|
|
124
|
-
describe(
|
|
125
|
-
it(
|
|
176
|
+
describe("persisted SDK session reuse", () => {
|
|
177
|
+
it("does not reuse a persisted session without a PID", () => {
|
|
126
178
|
const decision = evaluatePersistedSessionReuse({}, () => true);
|
|
127
|
-
expect(decision).toEqual({ reusable: false, reason:
|
|
179
|
+
expect(decision).toEqual({ reusable: false, reason: "missing-pid" });
|
|
128
180
|
});
|
|
129
|
-
it(
|
|
181
|
+
it("does not reuse a persisted session when its PID is no longer running", () => {
|
|
130
182
|
const decision = evaluatePersistedSessionReuse({ pid: 1234 }, () => false);
|
|
131
|
-
expect(decision).toEqual({ reusable: false, reason:
|
|
183
|
+
expect(decision).toEqual({ reusable: false, reason: "dead-pid" });
|
|
132
184
|
});
|
|
133
|
-
it(
|
|
134
|
-
const decision = evaluatePersistedSessionReuse({ pid: 1234, command:
|
|
135
|
-
expect(decision).toEqual({ reusable: true, reason:
|
|
185
|
+
it("reuses a persisted session only when its PID is still running for externally controllable providers", () => {
|
|
186
|
+
const decision = evaluatePersistedSessionReuse({ pid: 1234, command: "claude" }, () => true);
|
|
187
|
+
expect(decision).toEqual({ reusable: true, reason: "active-pid" });
|
|
136
188
|
});
|
|
137
|
-
it(
|
|
138
|
-
const decision = evaluatePersistedSessionReuse({ pid: 1234, command:
|
|
139
|
-
expect(decision).toEqual({
|
|
189
|
+
it("does not reuse ACode persisted sessions because their control channel is in-process only", () => {
|
|
190
|
+
const decision = evaluatePersistedSessionReuse({ pid: 1234, command: "acode" }, () => true);
|
|
191
|
+
expect(decision).toEqual({
|
|
192
|
+
reusable: false,
|
|
193
|
+
reason: "in-process-provider",
|
|
194
|
+
});
|
|
140
195
|
});
|
|
141
196
|
});
|
|
142
|
-
describe(
|
|
143
|
-
it(
|
|
144
|
-
expect(resolveSdkSessionDisplayStatus(
|
|
145
|
-
expect(resolveSdkSessionDisplayStatus(
|
|
197
|
+
describe("SDK session display status", () => {
|
|
198
|
+
it("prefers adapter fine-grained status over coarse running fallback", () => {
|
|
199
|
+
expect(resolveSdkSessionDisplayStatus("waiting_input", "running")).toBe("waiting_input");
|
|
200
|
+
expect(resolveSdkSessionDisplayStatus("thinking", "running")).toBe("thinking");
|
|
146
201
|
});
|
|
147
|
-
it(
|
|
148
|
-
expect(resolveSdkSessionDisplayStatus(undefined,
|
|
202
|
+
it("uses in-memory runtime status when adapter status is temporarily unavailable", () => {
|
|
203
|
+
expect(resolveSdkSessionDisplayStatus(undefined, "tool_using")).toBe("tool_using");
|
|
149
204
|
});
|
|
150
|
-
it(
|
|
151
|
-
expect(resolveSdkSessionDisplayStatus(undefined, undefined)).toBe(
|
|
205
|
+
it("falls back to running only when no fine-grained state exists", () => {
|
|
206
|
+
expect(resolveSdkSessionDisplayStatus(undefined, undefined)).toBe("running");
|
|
152
207
|
});
|
|
153
208
|
});
|
|
154
|
-
describe(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
209
|
+
describe("SDK provider event runtime status mapping", () => {
|
|
210
|
+
const event = (type, data = {}) => ({
|
|
211
|
+
type,
|
|
212
|
+
sessionId: "session-1",
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
data,
|
|
215
|
+
});
|
|
216
|
+
it("maps text and thinking stream events to thinking status", () => {
|
|
217
|
+
expect(resolveRuntimeStatusFromProviderEvent(event("thinking"))?.status).toBe("thinking");
|
|
218
|
+
expect(resolveRuntimeStatusFromProviderEvent(event("text_delta"))?.status).toBe("thinking");
|
|
219
|
+
});
|
|
220
|
+
it("maps AI request events to thinking status with request details", () => {
|
|
221
|
+
const state = resolveRuntimeStatusFromProviderEvent(event("ai_request", {
|
|
222
|
+
model: "test-model",
|
|
223
|
+
endpoint: "https://example.invalid/v1/chat/completions",
|
|
224
|
+
stream: true,
|
|
225
|
+
messageCount: 3,
|
|
226
|
+
toolCount: 2,
|
|
227
|
+
lastUserMessagePreview: "inspect status",
|
|
228
|
+
requestBody: '{"model":"test-model"}',
|
|
229
|
+
}));
|
|
230
|
+
expect(state?.status).toBe("thinking");
|
|
231
|
+
expect(state?.actionInfo).toMatchObject({
|
|
232
|
+
actionType: "ai_request",
|
|
233
|
+
actionLabel: "AI请求",
|
|
234
|
+
});
|
|
235
|
+
expect(state?.actionInfo?.actionDetail).toContain("模型: test-model");
|
|
236
|
+
expect(state?.actionInfo?.actionDetail).toContain("接口: https://example.invalid/v1/chat/completions");
|
|
237
|
+
expect(state?.actionInfo?.actionDetail).toContain("最后请求: inspect status");
|
|
238
|
+
expect(state?.actionInfo?.actionDetail).toContain('请求体:\n{"model":"test-model"}');
|
|
239
|
+
});
|
|
240
|
+
it("maps tool start events to tool_using with action info", () => {
|
|
241
|
+
const state = resolveRuntimeStatusFromProviderEvent(event("tool_use_start", {
|
|
242
|
+
toolName: "mcp__aws__my_task",
|
|
243
|
+
toolUseId: "tool-1",
|
|
244
|
+
}));
|
|
245
|
+
expect(state?.status).toBe("tool_using");
|
|
246
|
+
expect(state?.actionInfo?.actionType).toBe("mcp");
|
|
247
|
+
expect(state?.actionInfo?.actionId).toBe("tool-1");
|
|
248
|
+
});
|
|
249
|
+
it("maps turn completion to idle waiting_input status", () => {
|
|
250
|
+
expect(resolveRuntimeStatusFromProviderEvent(event("turn_complete"))?.status).toBe("waiting_input");
|
|
251
|
+
});
|
|
252
|
+
it("keeps idle tool action info with waiting_input status for display", () => {
|
|
253
|
+
const state = resolveRuntimeStatusFromProviderEvent(event("tool_use_start", {
|
|
254
|
+
actionType: "idle",
|
|
255
|
+
actionLabel: "监听消息",
|
|
256
|
+
actionDetail: "poll_message",
|
|
257
|
+
toolUseId: "tool-idle-1",
|
|
258
|
+
}));
|
|
259
|
+
expect(state?.status).toBe("waiting_input");
|
|
260
|
+
expect(state?.actionInfo).toMatchObject({
|
|
261
|
+
actionType: "idle",
|
|
262
|
+
actionLabel: "监听消息",
|
|
263
|
+
actionDetail: "poll_message",
|
|
264
|
+
actionId: "tool-idle-1",
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe("terminal command helpers", () => {
|
|
269
|
+
it("expands a whole mini-term clipboard paste file path into SDK input content", async () => {
|
|
270
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "terminal-input-"));
|
|
271
|
+
const clipboardDir = path.join(tempDir, "mini-term-clipboard");
|
|
158
272
|
await mkdir(clipboardDir, { recursive: true });
|
|
159
|
-
const pasteFile = path.join(clipboardDir,
|
|
160
|
-
await writeFile(pasteFile,
|
|
161
|
-
await expect(resolveSdkInputMessage(`"${pasteFile}"`)).resolves.toBe(
|
|
273
|
+
const pasteFile = path.join(clipboardDir, "paste-1780491671137.txt");
|
|
274
|
+
await writeFile(pasteFile, "real pasted prompt", "utf-8");
|
|
275
|
+
await expect(resolveSdkInputMessage(`"${pasteFile}"`)).resolves.toBe("real pasted prompt");
|
|
162
276
|
});
|
|
163
|
-
it(
|
|
277
|
+
it("keeps explicit commands that mention mini-term clipboard files unchanged", async () => {
|
|
164
278
|
const command = 'type "C:\\Users\\ZHUANG~1\\AppData\\Local\\Temp\\mini-term-clipboard\\paste-1780491671137.txt"';
|
|
165
279
|
await expect(resolveSdkInputMessage(command)).resolves.toBe(command);
|
|
166
280
|
});
|
|
167
|
-
it(
|
|
168
|
-
expect(normalizeTerminalCommandInput(
|
|
169
|
-
expect(normalizeTerminalCommandInput(
|
|
281
|
+
it("normalizes carriage-return terminal input into executable command text", () => {
|
|
282
|
+
expect(normalizeTerminalCommandInput("npm test\r")).toBe("npm test");
|
|
283
|
+
expect(normalizeTerminalCommandInput(" pwd \n")).toBe("pwd");
|
|
170
284
|
});
|
|
171
|
-
it(
|
|
172
|
-
expect(parseTerminalDirectoryChangeTarget(
|
|
173
|
-
expect(parseTerminalDirectoryChangeTarget('cd /d "D:\\code\\repo"')).toBe(
|
|
174
|
-
expect(parseTerminalDirectoryChangeTarget(
|
|
285
|
+
it("parses cd commands without treating other shell commands as directory changes", () => {
|
|
286
|
+
expect(parseTerminalDirectoryChangeTarget("cd")).toBeNull();
|
|
287
|
+
expect(parseTerminalDirectoryChangeTarget('cd /d "D:\\code\\repo"')).toBe("D:\\code\\repo");
|
|
288
|
+
expect(parseTerminalDirectoryChangeTarget("npm run build")).toBeUndefined();
|
|
175
289
|
});
|
|
176
|
-
it(
|
|
177
|
-
expect(formatTerminalPrompt(
|
|
290
|
+
it("formats the working directory prompt as terminal output text", () => {
|
|
291
|
+
expect(formatTerminalPrompt("D:\\code\\repo")).toBe("\x1b[36mD:\\code\\repo>\x1b[0m ");
|
|
178
292
|
});
|
|
179
|
-
it(
|
|
180
|
-
expect(resolveTerminalOutputEncoding(
|
|
181
|
-
expect(resolveTerminalOutputEncoding(
|
|
182
|
-
expect(resolveTerminalOutputEncoding(
|
|
293
|
+
it("defaults Windows terminal command output to GB18030 decoding", () => {
|
|
294
|
+
expect(resolveTerminalOutputEncoding("win32", {})).toBe("gb18030");
|
|
295
|
+
expect(resolveTerminalOutputEncoding("linux", {})).toBe("utf-8");
|
|
296
|
+
expect(resolveTerminalOutputEncoding("win32", {
|
|
297
|
+
AWS_TERMINAL_OUTPUT_ENCODING: "utf-8",
|
|
298
|
+
})).toBe("utf-8");
|
|
183
299
|
});
|
|
184
|
-
it(
|
|
185
|
-
const decoder = createTerminalOutputDecoder(
|
|
300
|
+
it("decodes GB18030 Chinese output across chunk boundaries", () => {
|
|
301
|
+
const decoder = createTerminalOutputDecoder("gb18030");
|
|
186
302
|
const first = decodeTerminalOutputChunk(decoder, Buffer.from([0xd6]));
|
|
187
303
|
const second = decodeTerminalOutputChunk(decoder, Buffer.from([0xd0, 0xce, 0xc4]));
|
|
188
304
|
const flushed = decodeTerminalOutputChunk(decoder, undefined, true);
|
|
189
|
-
expect(`${first}${second}${flushed}`).toBe(
|
|
305
|
+
expect(`${first}${second}${flushed}`).toBe("中文");
|
|
306
|
+
});
|
|
307
|
+
it("counts terminal output lines without treating a trailing line break as an extra line", () => {
|
|
308
|
+
expect(countTerminalOutputLines("")).toBe(0);
|
|
309
|
+
expect(countTerminalOutputLines("one")).toBe(1);
|
|
310
|
+
expect(countTerminalOutputLines("one\ntwo\n")).toBe(2);
|
|
311
|
+
expect(countTerminalOutputLines("one\r\ntwo\rthree")).toBe(3);
|
|
312
|
+
});
|
|
313
|
+
it("builds ACode command output spillover paths under the workspace cmd-dist directory", () => {
|
|
314
|
+
const outputPath = buildAcodeCommandOutputPath(path.join("D:", "code", "repo"), new Date(2026, 1, 3, 4, 5, 6, 7));
|
|
315
|
+
expect(outputPath).toBe(path.join("D:", "code", "repo", ".agentswork", "acode", "cmd-dist", "2026-02-03", "04", "05", "20260203-040506-007-cmd.out"));
|
|
316
|
+
});
|
|
317
|
+
it("formats ACode long-output and inactivity notices for terminal users", () => {
|
|
318
|
+
expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 100)).toContain("命令输出超过 100 行");
|
|
319
|
+
expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 100)).toContain("/repo/out.txt");
|
|
320
|
+
expect(formatCommandInactivityTimeoutNotice(60_000)).toContain("超过 60 秒没有新的输出");
|
|
190
321
|
});
|
|
191
322
|
});
|
|
192
|
-
describe(
|
|
193
|
-
it(
|
|
194
|
-
expect(SDK_PROVIDER_DEFINITIONS.map((definition) => definition.runtimeMode)).toContain(
|
|
195
|
-
expect(SDK_PROVIDER_DEFINITIONS.find((definition) => definition.runtimeMode ===
|
|
196
|
-
providerId:
|
|
197
|
-
defaultCommand:
|
|
323
|
+
describe("resolveSdkProviderId", () => {
|
|
324
|
+
it("exposes codex through the SDK provider SPI", () => {
|
|
325
|
+
expect(SDK_PROVIDER_DEFINITIONS.map((definition) => definition.runtimeMode)).toContain("codex");
|
|
326
|
+
expect(SDK_PROVIDER_DEFINITIONS.find((definition) => definition.runtimeMode === "codex")).toMatchObject({
|
|
327
|
+
providerId: "codex",
|
|
328
|
+
defaultCommand: "codex",
|
|
198
329
|
});
|
|
199
330
|
});
|
|
200
|
-
it(
|
|
201
|
-
expect(resolveSdkProviderId(
|
|
202
|
-
expect(resolveSdkProviderId(
|
|
331
|
+
it("resolves codex commands to codex provider", () => {
|
|
332
|
+
expect(resolveSdkProviderId("codex")).toBe("codex");
|
|
333
|
+
expect(resolveSdkProviderId("npx codex --model gpt-5-codex")).toBe("codex");
|
|
203
334
|
});
|
|
204
|
-
it(
|
|
205
|
-
expect(resolveSdkProviderId(
|
|
335
|
+
it("resolves opencode commands to opencode provider", () => {
|
|
336
|
+
expect(resolveSdkProviderId("opencode")).toBe("opencode");
|
|
206
337
|
});
|
|
207
|
-
it(
|
|
208
|
-
expect(resolveSdkProviderId(
|
|
209
|
-
expect(resolveSdkProviderId(
|
|
210
|
-
expect(resolveSdkProviderId(
|
|
338
|
+
it("falls back to acode for unknown or empty commands while keeping claude mapped to claude-code", () => {
|
|
339
|
+
expect(resolveSdkProviderId("claude")).toBe("claude-code");
|
|
340
|
+
expect(resolveSdkProviderId("acode")).toBe("acode");
|
|
341
|
+
expect(resolveSdkProviderId("custom-binary")).toBe("acode");
|
|
342
|
+
expect(resolveSdkProviderId(undefined)).toBe("acode");
|
|
211
343
|
});
|
|
212
344
|
});
|
|
213
|
-
describe(
|
|
214
|
-
it(
|
|
215
|
-
expect(resolveStatusChangeUsage({
|
|
345
|
+
describe("SDK status usage forwarding", () => {
|
|
346
|
+
it("keeps accumulated usage from status-change action info", () => {
|
|
347
|
+
expect(resolveStatusChangeUsage({
|
|
348
|
+
usage: { inputTokens: 12000, outputTokens: 345 },
|
|
349
|
+
})).toEqual({
|
|
216
350
|
inputTokens: 12000,
|
|
217
351
|
outputTokens: 345,
|
|
218
352
|
});
|
|
219
353
|
expect(resolveStatusChangeUsage({ usage: { inputTokens: 0, outputTokens: 0 } })).toBeUndefined();
|
|
220
|
-
expect(resolveStatusChangeUsage({
|
|
354
|
+
expect(resolveStatusChangeUsage({
|
|
355
|
+
actionType: "mcp",
|
|
356
|
+
actionLabel: "Calling MCP",
|
|
357
|
+
})).toBeUndefined();
|
|
358
|
+
});
|
|
359
|
+
it("forwards structured AI response body through action result", () => {
|
|
360
|
+
const runtimeStatus = resolveRuntimeStatusFromProviderEvent({
|
|
361
|
+
type: "turn_complete",
|
|
362
|
+
sessionId: "session-1",
|
|
363
|
+
timestamp: new Date(0).toISOString(),
|
|
364
|
+
data: {
|
|
365
|
+
usage: { inputTokens: 12, outputTokens: 4 },
|
|
366
|
+
responseBody: '{"choices":[{"message":{"content":"done"}}]}',
|
|
367
|
+
finishReason: "stop",
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
expect(runtimeStatus).toEqual({
|
|
371
|
+
status: "waiting_input",
|
|
372
|
+
actionInfo: {
|
|
373
|
+
actionType: "ai_request",
|
|
374
|
+
actionLabel: "AI响应",
|
|
375
|
+
actionResult: '{"choices":[{"message":{"content":"done"}}]}',
|
|
376
|
+
usage: { inputTokens: 12, outputTokens: 4 },
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
it("includes file paths and commands in tool status action details", () => {
|
|
381
|
+
expect(resolveRuntimeStatusFromProviderEvent({
|
|
382
|
+
type: "tool_use_start",
|
|
383
|
+
sessionId: "session-1",
|
|
384
|
+
timestamp: new Date(0).toISOString(),
|
|
385
|
+
data: {
|
|
386
|
+
toolName: "builtin/read_file",
|
|
387
|
+
toolInput: { path: "src/views/App.vue" },
|
|
388
|
+
toolUseId: "tool-read",
|
|
389
|
+
},
|
|
390
|
+
})).toEqual({
|
|
391
|
+
status: "tool_using",
|
|
392
|
+
actionInfo: {
|
|
393
|
+
actionType: "read_file",
|
|
394
|
+
actionLabel: "读取文件",
|
|
395
|
+
actionDetail: "src/views/App.vue",
|
|
396
|
+
actionId: "tool-read",
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
expect(resolveRuntimeStatusFromProviderEvent({
|
|
400
|
+
type: "tool_use_start",
|
|
401
|
+
sessionId: "session-1",
|
|
402
|
+
timestamp: new Date(0).toISOString(),
|
|
403
|
+
data: {
|
|
404
|
+
toolName: "bash",
|
|
405
|
+
toolInput: { command: "npm run test:agent-timeline-panel" },
|
|
406
|
+
toolUseId: "tool-bash",
|
|
407
|
+
},
|
|
408
|
+
})).toEqual({
|
|
409
|
+
status: "tool_using",
|
|
410
|
+
actionInfo: {
|
|
411
|
+
actionType: "bash",
|
|
412
|
+
actionLabel: "执行命令",
|
|
413
|
+
actionDetail: "npm run test:agent-timeline-panel",
|
|
414
|
+
actionId: "tool-bash",
|
|
415
|
+
},
|
|
416
|
+
});
|
|
221
417
|
});
|
|
222
418
|
});
|
|
223
|
-
describe(
|
|
224
|
-
it(
|
|
419
|
+
describe("SDK output forwarding", () => {
|
|
420
|
+
it("formats assistant text provider events for the terminal output callback path", () => {
|
|
225
421
|
const textEvent = {
|
|
226
|
-
type:
|
|
227
|
-
sessionId:
|
|
422
|
+
type: "text_delta",
|
|
423
|
+
sessionId: "session-1",
|
|
228
424
|
timestamp: new Date(0).toISOString(),
|
|
229
|
-
data: { text:
|
|
425
|
+
data: { text: "hello" },
|
|
230
426
|
};
|
|
427
|
+
expect(formatSdkOutputEvent(textEvent)).toBe("hello");
|
|
428
|
+
});
|
|
429
|
+
it("formats thinking provider events as typed realtime output chunks", () => {
|
|
231
430
|
const thinkingEvent = {
|
|
232
|
-
type:
|
|
233
|
-
sessionId:
|
|
431
|
+
type: "thinking",
|
|
432
|
+
sessionId: "session-1",
|
|
234
433
|
timestamp: new Date(0).toISOString(),
|
|
235
|
-
data: { text:
|
|
434
|
+
data: { text: "internal prompt or reasoning" },
|
|
236
435
|
};
|
|
237
|
-
expect(formatSdkOutputEvent(
|
|
238
|
-
expect(formatSdkOutputEvent(thinkingEvent)).toBe('thinking...');
|
|
436
|
+
expect(formatSdkOutputEvent(thinkingEvent)).toBe("internal prompt or reasoning");
|
|
239
437
|
});
|
|
240
|
-
it(
|
|
438
|
+
it("formats SDK errors as terminal output and ignores non-output events", () => {
|
|
241
439
|
const errorEvent = {
|
|
242
|
-
type:
|
|
243
|
-
sessionId:
|
|
440
|
+
type: "error",
|
|
441
|
+
sessionId: "session-1",
|
|
244
442
|
timestamp: new Date(0).toISOString(),
|
|
245
|
-
data: { text:
|
|
443
|
+
data: { text: "boom" },
|
|
246
444
|
};
|
|
247
445
|
const toolEvent = {
|
|
248
|
-
type:
|
|
249
|
-
sessionId:
|
|
446
|
+
type: "tool_use_start",
|
|
447
|
+
sessionId: "session-1",
|
|
250
448
|
timestamp: new Date(0).toISOString(),
|
|
251
|
-
data: { toolName:
|
|
449
|
+
data: { toolName: "read_file" },
|
|
252
450
|
};
|
|
253
|
-
expect(formatSdkOutputEvent(errorEvent)).toBe(
|
|
451
|
+
expect(formatSdkOutputEvent(errorEvent)).toBe("\r\n[SDK Error] boom\r\n");
|
|
254
452
|
expect(formatSdkOutputEvent(toolEvent)).toBeUndefined();
|
|
255
453
|
});
|
|
454
|
+
it("formats AI request events as immediate realtime ACode output", () => {
|
|
455
|
+
const requestEvent = {
|
|
456
|
+
type: "ai_request",
|
|
457
|
+
sessionId: "session-1",
|
|
458
|
+
timestamp: new Date(0).toISOString(),
|
|
459
|
+
data: {
|
|
460
|
+
model: "test-model",
|
|
461
|
+
lastUserMessagePreview: "inspect status",
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
expect(formatSdkOutputEvent(requestEvent)).toBe("\r\n[ACode] 正在请求模型 test-model:inspect status\r\n");
|
|
465
|
+
});
|
|
466
|
+
it("builds ACode log metadata for output callbacks", () => {
|
|
467
|
+
const requestEvent = {
|
|
468
|
+
type: "ai_request",
|
|
469
|
+
sessionId: "session-1",
|
|
470
|
+
timestamp: new Date(0).toISOString(),
|
|
471
|
+
data: { model: "test-model", endpoint: "openai-compatible", toolCount: 2 },
|
|
472
|
+
};
|
|
473
|
+
const errorEvent = {
|
|
474
|
+
type: "error",
|
|
475
|
+
sessionId: "session-1",
|
|
476
|
+
timestamp: new Date(0).toISOString(),
|
|
477
|
+
data: { text: "provider failed" },
|
|
478
|
+
};
|
|
479
|
+
expect(buildSdkOutputMetadata(requestEvent)).toEqual({
|
|
480
|
+
sourceEventType: "ai_request",
|
|
481
|
+
logCategory: "acode",
|
|
482
|
+
logLevel: "info",
|
|
483
|
+
logMessage: "AI request started: test-model @ openai-compatible, tools=2",
|
|
484
|
+
});
|
|
485
|
+
expect(buildSdkOutputMetadata(errorEvent)).toEqual({
|
|
486
|
+
sourceEventType: "error",
|
|
487
|
+
logCategory: "acode",
|
|
488
|
+
logLevel: "error",
|
|
489
|
+
logMessage: "provider failed",
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
it("does not mark streaming text chunks as standalone ACode log rows", () => {
|
|
493
|
+
const metadata = buildSdkOutputMetadata({
|
|
494
|
+
type: "text_delta",
|
|
495
|
+
sessionId: "session-1",
|
|
496
|
+
data: { text: "word" },
|
|
497
|
+
});
|
|
498
|
+
expect(metadata).toEqual({
|
|
499
|
+
sourceEventType: "text_delta",
|
|
500
|
+
logCategory: "acode",
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
it("includes file paths and commands in ACode tool log metadata", () => {
|
|
504
|
+
const readEvent = {
|
|
505
|
+
type: "tool_use_start",
|
|
506
|
+
sessionId: "session-1",
|
|
507
|
+
timestamp: new Date(0).toISOString(),
|
|
508
|
+
data: {
|
|
509
|
+
toolName: "read_file",
|
|
510
|
+
toolInput: { path: "src/index.ts" },
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
const editEvent = {
|
|
514
|
+
type: "tool_use_start",
|
|
515
|
+
sessionId: "session-1",
|
|
516
|
+
timestamp: new Date(0).toISOString(),
|
|
517
|
+
data: {
|
|
518
|
+
toolName: "edit",
|
|
519
|
+
toolInput: { file_path: "src/app.ts" },
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
const writeEvent = {
|
|
523
|
+
type: "tool_use_end",
|
|
524
|
+
sessionId: "session-1",
|
|
525
|
+
timestamp: new Date(0).toISOString(),
|
|
526
|
+
data: {
|
|
527
|
+
toolName: "write_file",
|
|
528
|
+
toolInput: { path: "src/new-file.ts" },
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
const bashEvent = {
|
|
532
|
+
type: "tool_use_start",
|
|
533
|
+
sessionId: "session-1",
|
|
534
|
+
timestamp: new Date(0).toISOString(),
|
|
535
|
+
data: {
|
|
536
|
+
toolName: "bash",
|
|
537
|
+
toolInput: { command: "npm run build -- --mode production" },
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
expect(buildSdkOutputMetadata(readEvent).logMessage).toBe("开始读取文件: src/index.ts");
|
|
541
|
+
expect(buildSdkOutputMetadata(editEvent).logMessage).toBe("开始编辑文件: src/app.ts");
|
|
542
|
+
expect(buildSdkOutputMetadata(writeEvent).logMessage).toBe("完成写入文件: src/new-file.ts");
|
|
543
|
+
expect(buildSdkOutputMetadata(bashEvent).logMessage).toBe("开始执行命令: npm run build -- --mode production");
|
|
544
|
+
});
|
|
256
545
|
});
|
|
257
|
-
describe(
|
|
258
|
-
it(
|
|
546
|
+
describe("SDK tool result timeline action info", () => {
|
|
547
|
+
it("keeps MCP tool results on MCP timeline events", () => {
|
|
259
548
|
const event = {
|
|
260
|
-
type:
|
|
261
|
-
sessionId:
|
|
549
|
+
type: "tool_use_end",
|
|
550
|
+
sessionId: "session-1",
|
|
262
551
|
timestamp: new Date(0).toISOString(),
|
|
263
552
|
data: {
|
|
264
|
-
toolName:
|
|
265
|
-
toolUseId:
|
|
553
|
+
toolName: "mcp__aws-mcp__get_profile",
|
|
554
|
+
toolUseId: "tool-1",
|
|
266
555
|
toolResult: '{"displayName":"Agent"}',
|
|
267
556
|
},
|
|
268
557
|
};
|
|
269
558
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
270
|
-
actionType:
|
|
271
|
-
actionLabel:
|
|
272
|
-
actionDetail:
|
|
273
|
-
actionId:
|
|
559
|
+
actionType: "mcp",
|
|
560
|
+
actionLabel: "MCP返回",
|
|
561
|
+
actionDetail: "mcp__aws-mcp__get_profile",
|
|
562
|
+
actionId: "tool-1",
|
|
274
563
|
actionResult: '{"displayName":"Agent"}',
|
|
275
564
|
});
|
|
276
565
|
});
|
|
277
|
-
it(
|
|
566
|
+
it("forwards bash tool results to command timeline events", () => {
|
|
278
567
|
const event = {
|
|
279
|
-
type:
|
|
280
|
-
sessionId:
|
|
568
|
+
type: "tool_use_end",
|
|
569
|
+
sessionId: "session-1",
|
|
281
570
|
timestamp: new Date(0).toISOString(),
|
|
282
571
|
data: {
|
|
283
|
-
toolName:
|
|
284
|
-
toolInput: { command:
|
|
285
|
-
toolUseId:
|
|
286
|
-
toolResult:
|
|
572
|
+
toolName: "bash",
|
|
573
|
+
toolInput: { command: "npm run build" },
|
|
574
|
+
toolUseId: "tool-2",
|
|
575
|
+
toolResult: "build passed",
|
|
287
576
|
},
|
|
288
577
|
};
|
|
289
578
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
290
|
-
actionType:
|
|
291
|
-
actionLabel:
|
|
292
|
-
actionDetail:
|
|
293
|
-
actionId:
|
|
294
|
-
actionResult:
|
|
579
|
+
actionType: "bash",
|
|
580
|
+
actionLabel: "执行命令",
|
|
581
|
+
actionDetail: "npm run build",
|
|
582
|
+
actionId: "tool-2",
|
|
583
|
+
actionResult: "build passed",
|
|
295
584
|
});
|
|
296
585
|
});
|
|
297
|
-
it(
|
|
586
|
+
it("forwards read tool results to file-read timeline events", () => {
|
|
298
587
|
const event = {
|
|
299
|
-
type:
|
|
300
|
-
sessionId:
|
|
588
|
+
type: "tool_use_end",
|
|
589
|
+
sessionId: "session-1",
|
|
301
590
|
timestamp: new Date(0).toISOString(),
|
|
302
591
|
data: {
|
|
303
|
-
toolName:
|
|
304
|
-
toolInput: { path:
|
|
305
|
-
actionId:
|
|
306
|
-
actionResult:
|
|
592
|
+
toolName: "read_file",
|
|
593
|
+
toolInput: { path: "src/index.ts" },
|
|
594
|
+
actionId: "tool-3",
|
|
595
|
+
actionResult: "export const ok = true",
|
|
307
596
|
},
|
|
308
597
|
};
|
|
309
598
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
310
|
-
actionType:
|
|
311
|
-
actionLabel:
|
|
312
|
-
actionDetail:
|
|
313
|
-
actionId:
|
|
314
|
-
actionResult:
|
|
599
|
+
actionType: "read_file",
|
|
600
|
+
actionLabel: "读取文件",
|
|
601
|
+
actionDetail: "src/index.ts",
|
|
602
|
+
actionId: "tool-3",
|
|
603
|
+
actionResult: "export const ok = true",
|
|
315
604
|
});
|
|
316
605
|
});
|
|
317
|
-
it(
|
|
606
|
+
it("keeps MCP-named filesystem results on semantic file-read timeline events", () => {
|
|
318
607
|
const event = {
|
|
319
|
-
type:
|
|
320
|
-
sessionId:
|
|
608
|
+
type: "tool_use_end",
|
|
609
|
+
sessionId: "session-1",
|
|
321
610
|
timestamp: new Date(0).toISOString(),
|
|
322
611
|
data: {
|
|
323
|
-
toolName:
|
|
324
|
-
toolInput: { path:
|
|
325
|
-
toolUseId:
|
|
326
|
-
toolResult:
|
|
612
|
+
toolName: "mcp__filesystem__read_file",
|
|
613
|
+
toolInput: { path: "src/index.ts" },
|
|
614
|
+
toolUseId: "tool-4",
|
|
615
|
+
toolResult: "export const ok = true",
|
|
327
616
|
},
|
|
328
617
|
};
|
|
329
618
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
330
|
-
actionType:
|
|
331
|
-
actionLabel:
|
|
332
|
-
actionDetail:
|
|
333
|
-
actionId:
|
|
334
|
-
actionResult:
|
|
619
|
+
actionType: "read_file",
|
|
620
|
+
actionLabel: "读取文件",
|
|
621
|
+
actionDetail: "src/index.ts",
|
|
622
|
+
actionId: "tool-4",
|
|
623
|
+
actionResult: "export const ok = true",
|
|
335
624
|
});
|
|
336
625
|
});
|
|
337
|
-
it(
|
|
626
|
+
it("ignores tool result events without displayable result content", () => {
|
|
338
627
|
const event = {
|
|
339
|
-
type:
|
|
340
|
-
sessionId:
|
|
628
|
+
type: "tool_use_end",
|
|
629
|
+
sessionId: "session-1",
|
|
341
630
|
timestamp: new Date(0).toISOString(),
|
|
342
|
-
data: { toolName:
|
|
631
|
+
data: { toolName: "read_file", toolResult: " " },
|
|
343
632
|
};
|
|
344
633
|
expect(buildToolResultTimelineActionInfo(event)).toBeUndefined();
|
|
345
634
|
});
|