aws-runtime-bridge 1.7.50 → 1.8.0
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 +427 -105
- package/dist/adapter/AcodeSdkAdapter.test.js +1093 -86
- 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 +221 -110
- package/dist/adapter/types.test.js +10 -3
- 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 +20 -5
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +420 -170
- package/dist/routes/terminal.test.js +380 -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 +10 -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,517 @@
|
|
|
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, 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);
|
|
64
90
|
});
|
|
65
91
|
});
|
|
66
|
-
describe(
|
|
67
|
-
it(
|
|
68
|
-
const
|
|
69
|
-
|
|
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
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
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");
|
|
201
|
+
});
|
|
202
|
+
it("uses in-memory runtime status when adapter status is temporarily unavailable", () => {
|
|
203
|
+
expect(resolveSdkSessionDisplayStatus(undefined, "tool_using")).toBe("tool_using");
|
|
146
204
|
});
|
|
147
|
-
it(
|
|
148
|
-
expect(resolveSdkSessionDisplayStatus(undefined,
|
|
205
|
+
it("falls back to running only when no fine-grained state exists", () => {
|
|
206
|
+
expect(resolveSdkSessionDisplayStatus(undefined, undefined)).toBe("running");
|
|
149
207
|
});
|
|
150
|
-
|
|
151
|
-
|
|
208
|
+
});
|
|
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
|
+
});
|
|
152
266
|
});
|
|
153
267
|
});
|
|
154
|
-
describe(
|
|
155
|
-
it(
|
|
156
|
-
const tempDir = await mkdtemp(path.join(os.tmpdir(),
|
|
157
|
-
const clipboardDir = path.join(tempDir,
|
|
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
|
+
});
|
|
221
379
|
});
|
|
222
380
|
});
|
|
223
|
-
describe(
|
|
224
|
-
it(
|
|
381
|
+
describe("SDK output forwarding", () => {
|
|
382
|
+
it("formats assistant text provider events for the terminal output callback path", () => {
|
|
225
383
|
const textEvent = {
|
|
226
|
-
type:
|
|
227
|
-
sessionId:
|
|
384
|
+
type: "text_delta",
|
|
385
|
+
sessionId: "session-1",
|
|
228
386
|
timestamp: new Date(0).toISOString(),
|
|
229
|
-
data: { text:
|
|
387
|
+
data: { text: "hello" },
|
|
230
388
|
};
|
|
389
|
+
expect(formatSdkOutputEvent(textEvent)).toBe("hello");
|
|
390
|
+
});
|
|
391
|
+
it("formats thinking provider events as typed realtime output chunks", () => {
|
|
231
392
|
const thinkingEvent = {
|
|
232
|
-
type:
|
|
233
|
-
sessionId:
|
|
393
|
+
type: "thinking",
|
|
394
|
+
sessionId: "session-1",
|
|
234
395
|
timestamp: new Date(0).toISOString(),
|
|
235
|
-
data: { text:
|
|
396
|
+
data: { text: "internal prompt or reasoning" },
|
|
236
397
|
};
|
|
237
|
-
expect(formatSdkOutputEvent(
|
|
238
|
-
expect(formatSdkOutputEvent(thinkingEvent)).toBe('thinking...');
|
|
398
|
+
expect(formatSdkOutputEvent(thinkingEvent)).toBe("internal prompt or reasoning");
|
|
239
399
|
});
|
|
240
|
-
it(
|
|
400
|
+
it("formats SDK errors as terminal output and ignores non-output events", () => {
|
|
241
401
|
const errorEvent = {
|
|
242
|
-
type:
|
|
243
|
-
sessionId:
|
|
402
|
+
type: "error",
|
|
403
|
+
sessionId: "session-1",
|
|
244
404
|
timestamp: new Date(0).toISOString(),
|
|
245
|
-
data: { text:
|
|
405
|
+
data: { text: "boom" },
|
|
246
406
|
};
|
|
247
407
|
const toolEvent = {
|
|
248
|
-
type:
|
|
249
|
-
sessionId:
|
|
408
|
+
type: "tool_use_start",
|
|
409
|
+
sessionId: "session-1",
|
|
250
410
|
timestamp: new Date(0).toISOString(),
|
|
251
|
-
data: { toolName:
|
|
411
|
+
data: { toolName: "read_file" },
|
|
252
412
|
};
|
|
253
|
-
expect(formatSdkOutputEvent(errorEvent)).toBe(
|
|
413
|
+
expect(formatSdkOutputEvent(errorEvent)).toBe("\r\n[SDK Error] boom\r\n");
|
|
254
414
|
expect(formatSdkOutputEvent(toolEvent)).toBeUndefined();
|
|
255
415
|
});
|
|
416
|
+
it("formats AI request events as immediate realtime ACode output", () => {
|
|
417
|
+
const requestEvent = {
|
|
418
|
+
type: "ai_request",
|
|
419
|
+
sessionId: "session-1",
|
|
420
|
+
timestamp: new Date(0).toISOString(),
|
|
421
|
+
data: {
|
|
422
|
+
model: "test-model",
|
|
423
|
+
lastUserMessagePreview: "inspect status",
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
expect(formatSdkOutputEvent(requestEvent)).toBe("\r\n[ACode] 正在请求模型 test-model:inspect status\r\n");
|
|
427
|
+
});
|
|
256
428
|
});
|
|
257
|
-
describe(
|
|
258
|
-
it(
|
|
429
|
+
describe("SDK tool result timeline action info", () => {
|
|
430
|
+
it("keeps MCP tool results on MCP timeline events", () => {
|
|
259
431
|
const event = {
|
|
260
|
-
type:
|
|
261
|
-
sessionId:
|
|
432
|
+
type: "tool_use_end",
|
|
433
|
+
sessionId: "session-1",
|
|
262
434
|
timestamp: new Date(0).toISOString(),
|
|
263
435
|
data: {
|
|
264
|
-
toolName:
|
|
265
|
-
toolUseId:
|
|
436
|
+
toolName: "mcp__aws-mcp__get_profile",
|
|
437
|
+
toolUseId: "tool-1",
|
|
266
438
|
toolResult: '{"displayName":"Agent"}',
|
|
267
439
|
},
|
|
268
440
|
};
|
|
269
441
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
270
|
-
actionType:
|
|
271
|
-
actionLabel:
|
|
272
|
-
actionDetail:
|
|
273
|
-
actionId:
|
|
442
|
+
actionType: "mcp",
|
|
443
|
+
actionLabel: "MCP返回",
|
|
444
|
+
actionDetail: "mcp__aws-mcp__get_profile",
|
|
445
|
+
actionId: "tool-1",
|
|
274
446
|
actionResult: '{"displayName":"Agent"}',
|
|
275
447
|
});
|
|
276
448
|
});
|
|
277
|
-
it(
|
|
449
|
+
it("forwards bash tool results to command timeline events", () => {
|
|
278
450
|
const event = {
|
|
279
|
-
type:
|
|
280
|
-
sessionId:
|
|
451
|
+
type: "tool_use_end",
|
|
452
|
+
sessionId: "session-1",
|
|
281
453
|
timestamp: new Date(0).toISOString(),
|
|
282
454
|
data: {
|
|
283
|
-
toolName:
|
|
284
|
-
toolInput: { command:
|
|
285
|
-
toolUseId:
|
|
286
|
-
toolResult:
|
|
455
|
+
toolName: "bash",
|
|
456
|
+
toolInput: { command: "npm run build" },
|
|
457
|
+
toolUseId: "tool-2",
|
|
458
|
+
toolResult: "build passed",
|
|
287
459
|
},
|
|
288
460
|
};
|
|
289
461
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
290
|
-
actionType:
|
|
291
|
-
actionLabel:
|
|
292
|
-
actionDetail:
|
|
293
|
-
actionId:
|
|
294
|
-
actionResult:
|
|
462
|
+
actionType: "bash",
|
|
463
|
+
actionLabel: "执行命令",
|
|
464
|
+
actionDetail: "npm run build",
|
|
465
|
+
actionId: "tool-2",
|
|
466
|
+
actionResult: "build passed",
|
|
295
467
|
});
|
|
296
468
|
});
|
|
297
|
-
it(
|
|
469
|
+
it("forwards read tool results to file-read timeline events", () => {
|
|
298
470
|
const event = {
|
|
299
|
-
type:
|
|
300
|
-
sessionId:
|
|
471
|
+
type: "tool_use_end",
|
|
472
|
+
sessionId: "session-1",
|
|
301
473
|
timestamp: new Date(0).toISOString(),
|
|
302
474
|
data: {
|
|
303
|
-
toolName:
|
|
304
|
-
toolInput: { path:
|
|
305
|
-
actionId:
|
|
306
|
-
actionResult:
|
|
475
|
+
toolName: "read_file",
|
|
476
|
+
toolInput: { path: "src/index.ts" },
|
|
477
|
+
actionId: "tool-3",
|
|
478
|
+
actionResult: "export const ok = true",
|
|
307
479
|
},
|
|
308
480
|
};
|
|
309
481
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
310
|
-
actionType:
|
|
311
|
-
actionLabel:
|
|
312
|
-
actionDetail:
|
|
313
|
-
actionId:
|
|
314
|
-
actionResult:
|
|
482
|
+
actionType: "read_file",
|
|
483
|
+
actionLabel: "读取文件",
|
|
484
|
+
actionDetail: "src/index.ts",
|
|
485
|
+
actionId: "tool-3",
|
|
486
|
+
actionResult: "export const ok = true",
|
|
315
487
|
});
|
|
316
488
|
});
|
|
317
|
-
it(
|
|
489
|
+
it("keeps MCP-named filesystem results on semantic file-read timeline events", () => {
|
|
318
490
|
const event = {
|
|
319
|
-
type:
|
|
320
|
-
sessionId:
|
|
491
|
+
type: "tool_use_end",
|
|
492
|
+
sessionId: "session-1",
|
|
321
493
|
timestamp: new Date(0).toISOString(),
|
|
322
494
|
data: {
|
|
323
|
-
toolName:
|
|
324
|
-
toolInput: { path:
|
|
325
|
-
toolUseId:
|
|
326
|
-
toolResult:
|
|
495
|
+
toolName: "mcp__filesystem__read_file",
|
|
496
|
+
toolInput: { path: "src/index.ts" },
|
|
497
|
+
toolUseId: "tool-4",
|
|
498
|
+
toolResult: "export const ok = true",
|
|
327
499
|
},
|
|
328
500
|
};
|
|
329
501
|
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
330
|
-
actionType:
|
|
331
|
-
actionLabel:
|
|
332
|
-
actionDetail:
|
|
333
|
-
actionId:
|
|
334
|
-
actionResult:
|
|
502
|
+
actionType: "read_file",
|
|
503
|
+
actionLabel: "读取文件",
|
|
504
|
+
actionDetail: "src/index.ts",
|
|
505
|
+
actionId: "tool-4",
|
|
506
|
+
actionResult: "export const ok = true",
|
|
335
507
|
});
|
|
336
508
|
});
|
|
337
|
-
it(
|
|
509
|
+
it("ignores tool result events without displayable result content", () => {
|
|
338
510
|
const event = {
|
|
339
|
-
type:
|
|
340
|
-
sessionId:
|
|
511
|
+
type: "tool_use_end",
|
|
512
|
+
sessionId: "session-1",
|
|
341
513
|
timestamp: new Date(0).toISOString(),
|
|
342
|
-
data: { toolName:
|
|
514
|
+
data: { toolName: "read_file", toolResult: " " },
|
|
343
515
|
};
|
|
344
516
|
expect(buildToolResultTimelineActionInfo(event)).toBeUndefined();
|
|
345
517
|
});
|