claude-code-rust 0.12.3 → 0.13.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/README.md +13 -7
- package/agent-sdk/dist/bridge/commands.js +20 -0
- package/agent-sdk/dist/bridge/events.js +15 -1
- package/agent-sdk/dist/bridge/logger.js +10 -0
- package/agent-sdk/dist/bridge/mcp_metadata.js +35 -0
- package/agent-sdk/dist/bridge/message_handlers.js +172 -1
- package/agent-sdk/dist/bridge/model_metadata.js +30 -20
- package/agent-sdk/dist/bridge/session_lifecycle.js +40 -2
- package/agent-sdk/dist/bridge/state_parsing.js +9 -0
- package/agent-sdk/dist/bridge/tooling.js +55 -8
- package/agent-sdk/dist/bridge/user_interaction.js +52 -1
- package/agent-sdk/dist/bridge.js +409 -1
- package/agent-sdk/dist/bridge.test.js +755 -7
- package/package.json +10 -5
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { AsyncQueue, CACHE_SPLIT_POLICY, buildApiRetryUpdate, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, applySessionAgent, applySessionEffort, emitAgentConfigOptionUpdate, emitEffortConfigOptionUpdate, handleTaskSystemMessage, handleSdkMessage, isShellToolName, mapSdkAccountInfo, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRuntimeSessionState, parseRateLimitStatus, bridgeMcpConfigToSdk, mapMcpServerStatus, mapMcpServerStatusConfig, normalizeSettingsParseError, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, updateAvailableCommands, handleReloadPluginsCommand, } from "./bridge.js";
|
|
3
|
+
import { AsyncQueue, CACHE_SPLIT_POLICY, buildApiRetryUpdate, buildRateLimitUpdate, buildRewindConversationPlan, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, applySessionAgent, applySessionEffort, emitAgentConfigOptionUpdate, emitEffortConfigOptionUpdate, handleTaskSystemMessage, handleSdkMessage, isShellToolName, mapSdkAccountInfo, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRuntimeSessionState, parseRateLimitStatus, bridgeMcpConfigToSdk, mapMcpServerStatus, mapMcpServerStatusConfig, normalizeSettingsParseError, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, rewindTargetsFromSessionMessages, unwrapToolUseResult, updateAvailableCommands, handleReloadPluginsCommand, } from "./bridge.js";
|
|
4
4
|
import { availableModesForSession, buildModeState, markModeUnavailableForSession, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, } from "./bridge/commands.js";
|
|
5
5
|
import { handleMcpSetServersCommand } from "./bridge/mcp.js";
|
|
6
|
-
import { emitCurrentModelUpdate, handleUserDialogResponse, refreshCurrentModel, resolveCurrentModel, sessions, shouldInvalidateResolvedRuntimeModel, shouldEmitStartupAuthRequiredForAccount, } from "./bridge/session_lifecycle.js";
|
|
6
|
+
import { emitCurrentModelUpdate, handleUserDialogResponse, closeSessionsBeforeRegister, refreshCurrentModel, resolveCurrentModel, sessions, shouldInvalidateResolvedRuntimeModel, shouldEmitStartupAuthRequiredForAccount, } from "./bridge/session_lifecycle.js";
|
|
7
7
|
import { classifyTurnErrorKind } from "./bridge/error_classification.js";
|
|
8
8
|
import { emitToolCall, emitToolProgressUpdate, emitToolResultUpdate } from "./bridge/tool_calls.js";
|
|
9
9
|
import { linkTaskToolUse } from "./bridge/task_links.js";
|
|
10
10
|
import { requestAskUserQuestionAnswers } from "./bridge/user_interaction.js";
|
|
11
|
-
import { handleResultMessage } from "./bridge/message_handlers.js";
|
|
11
|
+
import { flushPendingWorkerShutdown, handleResultMessage } from "./bridge/message_handlers.js";
|
|
12
12
|
const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
|
|
13
13
|
const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
|
|
14
14
|
"when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
|
|
@@ -39,11 +39,34 @@ function makeSessionState() {
|
|
|
39
39
|
pendingQuestions: new Map(),
|
|
40
40
|
pendingUserDialogs: new Map(),
|
|
41
41
|
pendingElicitations: new Map(),
|
|
42
|
+
informationalDedupKeys: new Set(),
|
|
42
43
|
mcpStatusRevalidatedAt: new Map(),
|
|
43
44
|
hiddenToolUseIds: new Set(),
|
|
44
45
|
authHintSent: false,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
test("closeSessionsBeforeRegister closes same-key stale session before replacement registration", async () => {
|
|
49
|
+
sessions.clear();
|
|
50
|
+
let staleClosed = 0;
|
|
51
|
+
const stale = makeSessionState();
|
|
52
|
+
stale.sessionId = "session-1";
|
|
53
|
+
stale.query = {
|
|
54
|
+
close: () => {
|
|
55
|
+
staleClosed += 1;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const replacement = makeSessionState();
|
|
59
|
+
replacement.sessionId = "session-1";
|
|
60
|
+
sessions.set(stale.sessionId, stale);
|
|
61
|
+
await closeSessionsBeforeRegister(replacement, [stale], "req-1");
|
|
62
|
+
assert.equal(staleClosed, 1);
|
|
63
|
+
assert.equal(sessions.has("session-1"), false);
|
|
64
|
+
sessions.set(replacement.sessionId, replacement);
|
|
65
|
+
await closeSessionsBeforeRegister(replacement, [stale], "req-2");
|
|
66
|
+
assert.equal(staleClosed, 2);
|
|
67
|
+
assert.equal(sessions.get("session-1"), replacement);
|
|
68
|
+
sessions.clear();
|
|
69
|
+
});
|
|
47
70
|
test("availableModesForSession omits conditional modes when unsupported", () => {
|
|
48
71
|
const session = makeSessionState();
|
|
49
72
|
refreshSupportedModesForSession(session);
|
|
@@ -296,6 +319,7 @@ test("parseCommandEnvelope validates mcp_set_servers command", () => {
|
|
|
296
319
|
"X-Test": "1",
|
|
297
320
|
},
|
|
298
321
|
timeout: 5000,
|
|
322
|
+
request_timeout_ms: 30000,
|
|
299
323
|
always_load: true,
|
|
300
324
|
tools: [
|
|
301
325
|
{
|
|
@@ -328,6 +352,7 @@ test("parseCommandEnvelope validates mcp_set_servers command", () => {
|
|
|
328
352
|
"X-Test": "1",
|
|
329
353
|
},
|
|
330
354
|
timeout: 5000,
|
|
355
|
+
request_timeout_ms: 30000,
|
|
331
356
|
always_load: true,
|
|
332
357
|
tools: [
|
|
333
358
|
{
|
|
@@ -363,6 +388,17 @@ test("parseCommandEnvelope rejects invalid latest MCP config fields", () => {
|
|
|
363
388
|
session_id: "session-123",
|
|
364
389
|
servers: { bad: { type: "http", url: "https://mcp.example.com", timeout: 999 } },
|
|
365
390
|
})), /mcp_set_servers\.servers\.bad\.timeout must be an integer >= 1000/);
|
|
391
|
+
assert.throws(() => parseCommandEnvelope(JSON.stringify({
|
|
392
|
+
command: "mcp_set_servers",
|
|
393
|
+
session_id: "session-123",
|
|
394
|
+
servers: {
|
|
395
|
+
bad: {
|
|
396
|
+
type: "http",
|
|
397
|
+
url: "https://mcp.example.com",
|
|
398
|
+
request_timeout_ms: 999,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
})), /mcp_set_servers\.servers\.bad\.request_timeout_ms must be an integer >= 1000/);
|
|
366
402
|
assert.throws(() => parseCommandEnvelope(JSON.stringify({
|
|
367
403
|
command: "mcp_set_servers",
|
|
368
404
|
session_id: "session-123",
|
|
@@ -430,6 +466,7 @@ test("handleMcpSetServersCommand emits SDK result", async () => {
|
|
|
430
466
|
type: "http",
|
|
431
467
|
url: "https://example.test/mcp",
|
|
432
468
|
always_load: true,
|
|
469
|
+
request_timeout_ms: 30000,
|
|
433
470
|
},
|
|
434
471
|
},
|
|
435
472
|
}, "req-mcp-set");
|
|
@@ -439,6 +476,7 @@ test("handleMcpSetServersCommand emits SDK result", async () => {
|
|
|
439
476
|
type: "http",
|
|
440
477
|
url: "https://example.test/mcp",
|
|
441
478
|
alwaysLoad: true,
|
|
479
|
+
requestTimeoutMs: 30000,
|
|
442
480
|
},
|
|
443
481
|
});
|
|
444
482
|
assert.deepEqual(events, [
|
|
@@ -514,6 +552,7 @@ test("bridgeMcpConfigToSdk maps latest MCP fields to SDK casing", () => {
|
|
|
514
552
|
type: "sse",
|
|
515
553
|
url: "https://mcp.example.com/sse",
|
|
516
554
|
timeout: 2500,
|
|
555
|
+
request_timeout_ms: 30000,
|
|
517
556
|
always_load: true,
|
|
518
557
|
tools: [
|
|
519
558
|
{ name: "search" },
|
|
@@ -523,6 +562,7 @@ test("bridgeMcpConfigToSdk maps latest MCP fields to SDK casing", () => {
|
|
|
523
562
|
type: "sse",
|
|
524
563
|
url: "https://mcp.example.com/sse",
|
|
525
564
|
timeout: 2500,
|
|
565
|
+
requestTimeoutMs: 30000,
|
|
526
566
|
alwaysLoad: true,
|
|
527
567
|
tools: [
|
|
528
568
|
{ name: "search" },
|
|
@@ -539,6 +579,7 @@ test("mapMcpServerStatus preserves latest MCP status config fields", () => {
|
|
|
539
579
|
url: "https://mcp.notion.com/mcp",
|
|
540
580
|
headers: { Authorization: "Bearer token" },
|
|
541
581
|
timeout: 5000,
|
|
582
|
+
requestTimeoutMs: 30000,
|
|
542
583
|
alwaysLoad: true,
|
|
543
584
|
tools: [
|
|
544
585
|
{ name: "search" },
|
|
@@ -552,6 +593,7 @@ test("mapMcpServerStatus preserves latest MCP status config fields", () => {
|
|
|
552
593
|
url: "https://mcp.notion.com/mcp",
|
|
553
594
|
headers: { Authorization: "Bearer token" },
|
|
554
595
|
timeout: 5000,
|
|
596
|
+
request_timeout_ms: 30000,
|
|
555
597
|
always_load: true,
|
|
556
598
|
tools: [
|
|
557
599
|
{ name: "search" },
|
|
@@ -706,6 +748,150 @@ test("parseCommandEnvelope validates get_context_usage command", () => {
|
|
|
706
748
|
session_id: "session-123",
|
|
707
749
|
});
|
|
708
750
|
});
|
|
751
|
+
test("parseCommandEnvelope validates get_rewind_targets command", () => {
|
|
752
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
753
|
+
request_id: "req-rewind-targets",
|
|
754
|
+
command: "get_rewind_targets",
|
|
755
|
+
session_id: "session-123",
|
|
756
|
+
}));
|
|
757
|
+
assert.equal(parsed.requestId, "req-rewind-targets");
|
|
758
|
+
assert.deepEqual(parsed.command, {
|
|
759
|
+
command: "get_rewind_targets",
|
|
760
|
+
session_id: "session-123",
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
test("parseCommandEnvelope validates rewind command modes", () => {
|
|
764
|
+
for (const restoreMode of ["both", "conversation", "code"]) {
|
|
765
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
766
|
+
request_id: "req-rewind",
|
|
767
|
+
command: "rewind",
|
|
768
|
+
session_id: "session-123",
|
|
769
|
+
target_user_message_id: "user-1",
|
|
770
|
+
restore_mode: restoreMode,
|
|
771
|
+
launch_settings: {
|
|
772
|
+
language: "German",
|
|
773
|
+
},
|
|
774
|
+
}));
|
|
775
|
+
assert.equal(parsed.requestId, "req-rewind");
|
|
776
|
+
assert.deepEqual(parsed.command, {
|
|
777
|
+
command: "rewind",
|
|
778
|
+
session_id: "session-123",
|
|
779
|
+
target_user_message_id: "user-1",
|
|
780
|
+
restore_mode: restoreMode,
|
|
781
|
+
launch_settings: { language: "German" },
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
test("parseCommandEnvelope rejects invalid rewind mode", () => {
|
|
786
|
+
assert.throws(() => parseCommandEnvelope(JSON.stringify({
|
|
787
|
+
command: "rewind",
|
|
788
|
+
session_id: "session-123",
|
|
789
|
+
target_user_message_id: "user-1",
|
|
790
|
+
restore_mode: "files",
|
|
791
|
+
})), /rewind\.restore_mode must be one of both, conversation, code/);
|
|
792
|
+
});
|
|
793
|
+
test("rewindTargetsFromSessionMessages filters user text messages with UUIDs", () => {
|
|
794
|
+
const messages = [
|
|
795
|
+
{
|
|
796
|
+
type: "user",
|
|
797
|
+
uuid: "user-1",
|
|
798
|
+
message: { role: "user", content: [{ type: "text", text: " first prompt\nline " }] },
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
type: "assistant",
|
|
802
|
+
uuid: "assistant-1",
|
|
803
|
+
message: { role: "assistant", content: [{ type: "text", text: "ignored" }] },
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
type: "user",
|
|
807
|
+
uuid: "tool-result",
|
|
808
|
+
message: { role: "user", content: [{ type: "tool_result", content: "ignored" }] },
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
type: "user",
|
|
812
|
+
uuid: "user-2",
|
|
813
|
+
message: { role: "user", content: [{ type: "text", text: "second prompt" }] },
|
|
814
|
+
},
|
|
815
|
+
];
|
|
816
|
+
assert.deepEqual(rewindTargetsFromSessionMessages(messages), [
|
|
817
|
+
{
|
|
818
|
+
uuid: "user-2",
|
|
819
|
+
first_text: "second prompt",
|
|
820
|
+
input_text: "second prompt",
|
|
821
|
+
index: 3,
|
|
822
|
+
previous_assistant_uuid: "assistant-1",
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
uuid: "user-1",
|
|
826
|
+
first_text: "first prompt line",
|
|
827
|
+
input_text: "first prompt\nline",
|
|
828
|
+
index: 0,
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
831
|
+
});
|
|
832
|
+
test("buildRewindConversationPlan anchors at previous assistant message", () => {
|
|
833
|
+
const messages = [
|
|
834
|
+
{
|
|
835
|
+
type: "user",
|
|
836
|
+
uuid: "user-1",
|
|
837
|
+
message: { role: "user", content: "first prompt" },
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
type: "assistant",
|
|
841
|
+
uuid: "assistant-1",
|
|
842
|
+
message: { role: "assistant", content: [{ type: "text", text: "reply" }] },
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
type: "user",
|
|
846
|
+
uuid: "user-2",
|
|
847
|
+
message: { role: "user", content: "second prompt" },
|
|
848
|
+
},
|
|
849
|
+
];
|
|
850
|
+
const plan = buildRewindConversationPlan(messages, "user-2");
|
|
851
|
+
assert.ok(plan);
|
|
852
|
+
assert.equal(plan.inputText, "second prompt");
|
|
853
|
+
assert.equal(plan.previousAssistantUuid, "assistant-1");
|
|
854
|
+
assert.equal(plan.targetIndex, 2);
|
|
855
|
+
assert.deepEqual(plan.retainedMessages.map((message) => message.uuid), ["user-1", "assistant-1"]);
|
|
856
|
+
assert.ok(plan.resumeUpdates.length > 0);
|
|
857
|
+
});
|
|
858
|
+
test("buildRewindConversationPlan treats first user message as fresh replacement", () => {
|
|
859
|
+
const messages = [
|
|
860
|
+
{
|
|
861
|
+
type: "user",
|
|
862
|
+
uuid: "user-1",
|
|
863
|
+
message: { role: "user", content: " first prompt\nline " },
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
type: "assistant",
|
|
867
|
+
uuid: "assistant-1",
|
|
868
|
+
message: { role: "assistant", content: [{ type: "text", text: "reply" }] },
|
|
869
|
+
},
|
|
870
|
+
];
|
|
871
|
+
const plan = buildRewindConversationPlan(messages, "user-1");
|
|
872
|
+
assert.ok(plan);
|
|
873
|
+
assert.equal(plan.inputText, " first prompt\nline ");
|
|
874
|
+
assert.equal(plan.previousAssistantUuid, undefined);
|
|
875
|
+
assert.equal(plan.targetIndex, 0);
|
|
876
|
+
assert.deepEqual(plan.retainedMessages, []);
|
|
877
|
+
assert.deepEqual(plan.resumeUpdates, []);
|
|
878
|
+
});
|
|
879
|
+
test("buildRewindConversationPlan rejects stale or inconsistent targets", () => {
|
|
880
|
+
const messages = [
|
|
881
|
+
{
|
|
882
|
+
type: "user",
|
|
883
|
+
uuid: "user-1",
|
|
884
|
+
message: { role: "user", content: "first prompt" },
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
type: "user",
|
|
888
|
+
uuid: "user-2",
|
|
889
|
+
message: { role: "user", content: "second prompt" },
|
|
890
|
+
},
|
|
891
|
+
];
|
|
892
|
+
assert.equal(buildRewindConversationPlan(messages, "missing-user"), null);
|
|
893
|
+
assert.equal(buildRewindConversationPlan(messages, "user-2"), null);
|
|
894
|
+
});
|
|
709
895
|
test("staleMcpAuthCandidates selects previously connected servers that regressed to needs-auth", () => {
|
|
710
896
|
const candidates = staleMcpAuthCandidates([
|
|
711
897
|
{
|
|
@@ -817,12 +1003,31 @@ test("buildQueryOptions maps launch settings into sdk query options", () => {
|
|
|
817
1003
|
assert.equal("effort" in options, false);
|
|
818
1004
|
assert.equal(options.agentProgressSummaries, true);
|
|
819
1005
|
assert.equal(options.promptSuggestions, true);
|
|
1006
|
+
assert.equal(options.enableFileCheckpointing, true);
|
|
820
1007
|
assert.equal(options.sessionId, "session-1");
|
|
821
1008
|
assert.deepEqual(options.settingSources, ["user", "project", "local"]);
|
|
822
1009
|
assert.deepEqual(options.toolConfig, {
|
|
823
1010
|
askUserQuestion: { previewFormat: "markdown" },
|
|
824
1011
|
});
|
|
825
1012
|
});
|
|
1013
|
+
test("buildQueryOptions includes resumeSessionAt when provided", () => {
|
|
1014
|
+
const input = new AsyncQueue();
|
|
1015
|
+
const options = buildQueryOptions({
|
|
1016
|
+
cwd: "C:/work",
|
|
1017
|
+
resume: "session-1",
|
|
1018
|
+
resumeSessionAt: "assistant-1",
|
|
1019
|
+
launchSettings: {},
|
|
1020
|
+
provisionalSessionId: "session-1",
|
|
1021
|
+
input,
|
|
1022
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
1023
|
+
enableSdkDebug: false,
|
|
1024
|
+
enableSpawnDebug: false,
|
|
1025
|
+
sessionIdForLogs: () => "session-1",
|
|
1026
|
+
});
|
|
1027
|
+
assert.equal(options.resume, "session-1");
|
|
1028
|
+
assert.equal(options.resumeSessionAt, "assistant-1");
|
|
1029
|
+
assert.equal("sessionId" in options, false);
|
|
1030
|
+
});
|
|
826
1031
|
test("buildQueryOptions forwards settings and maps startup model and permission mode", () => {
|
|
827
1032
|
const input = new AsyncQueue();
|
|
828
1033
|
const options = buildQueryOptions({
|
|
@@ -917,6 +1122,7 @@ test("buildQueryOptions enables dangerous skip flag for bypass permissions start
|
|
|
917
1122
|
});
|
|
918
1123
|
assert.equal(options.permissionMode, "bypassPermissions");
|
|
919
1124
|
assert.equal(options.allowDangerouslySkipPermissions, true);
|
|
1125
|
+
assert.equal("canUseTool" in options, false);
|
|
920
1126
|
});
|
|
921
1127
|
test("buildQueryOptions omits optional startup overrides but keeps bridge guard prompt", () => {
|
|
922
1128
|
const input = new AsyncQueue();
|
|
@@ -1389,6 +1595,96 @@ test("handleSdkMessage emits transcript retraction for model_refusal_fallback",
|
|
|
1389
1595
|
]);
|
|
1390
1596
|
assert.equal(events.some((event) => event.update?.type === "system_notice_update"), false);
|
|
1391
1597
|
});
|
|
1598
|
+
test("handleSdkMessage emits warning notice for model_refusal_no_fallback with explanation", () => {
|
|
1599
|
+
const session = makeSessionState();
|
|
1600
|
+
const events = captureBridgeEvents(() => {
|
|
1601
|
+
handleSdkMessage(session, {
|
|
1602
|
+
type: "system",
|
|
1603
|
+
subtype: "model_refusal_no_fallback",
|
|
1604
|
+
original_model: "claude-opus-4-1",
|
|
1605
|
+
request_id: "req-1",
|
|
1606
|
+
api_refusal_category: "cyber",
|
|
1607
|
+
api_refusal_explanation: "policy text",
|
|
1608
|
+
refused_user_message_uuid: "user-1",
|
|
1609
|
+
content: "raw content",
|
|
1610
|
+
uuid: "refusal-notice",
|
|
1611
|
+
session_id: "session-1",
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
1615
|
+
{
|
|
1616
|
+
type: "system_notice_update",
|
|
1617
|
+
severity: "warning",
|
|
1618
|
+
message: "Could not continue with claude-opus-4-1: model refused the request and no fallback model is configured. Reason: policy text.",
|
|
1619
|
+
},
|
|
1620
|
+
]);
|
|
1621
|
+
});
|
|
1622
|
+
test("handleSdkMessage emits warning notice for model_refusal_no_fallback with category only", () => {
|
|
1623
|
+
const session = makeSessionState();
|
|
1624
|
+
const events = captureBridgeEvents(() => {
|
|
1625
|
+
handleSdkMessage(session, {
|
|
1626
|
+
type: "system",
|
|
1627
|
+
subtype: "model_refusal_no_fallback",
|
|
1628
|
+
original_model: "claude-opus-4-1",
|
|
1629
|
+
request_id: "req-1",
|
|
1630
|
+
api_refusal_category: "cyber",
|
|
1631
|
+
uuid: "refusal-notice",
|
|
1632
|
+
session_id: "session-1",
|
|
1633
|
+
});
|
|
1634
|
+
});
|
|
1635
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
1636
|
+
{
|
|
1637
|
+
type: "system_notice_update",
|
|
1638
|
+
severity: "warning",
|
|
1639
|
+
message: "Could not continue with claude-opus-4-1: model refused the request and no fallback model is configured. Refusal category: cyber.",
|
|
1640
|
+
},
|
|
1641
|
+
]);
|
|
1642
|
+
});
|
|
1643
|
+
test("handleSdkMessage emits warning notice for model_refusal_no_fallback with content detail", () => {
|
|
1644
|
+
const session = makeSessionState();
|
|
1645
|
+
const events = captureBridgeEvents(() => {
|
|
1646
|
+
handleSdkMessage(session, {
|
|
1647
|
+
type: "system",
|
|
1648
|
+
subtype: "model_refusal_no_fallback",
|
|
1649
|
+
original_model: "claude-opus-4-1",
|
|
1650
|
+
request_id: "req-1",
|
|
1651
|
+
content: "Refused by policy!",
|
|
1652
|
+
uuid: "refusal-notice",
|
|
1653
|
+
session_id: "session-1",
|
|
1654
|
+
});
|
|
1655
|
+
});
|
|
1656
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
1657
|
+
{
|
|
1658
|
+
type: "system_notice_update",
|
|
1659
|
+
severity: "warning",
|
|
1660
|
+
message: "Could not continue with claude-opus-4-1: model refused the request and no fallback model is configured. Refused by policy!",
|
|
1661
|
+
},
|
|
1662
|
+
]);
|
|
1663
|
+
});
|
|
1664
|
+
test("handleSdkMessage emits readable model_refusal_no_fallback notice for empty metadata", () => {
|
|
1665
|
+
const session = makeSessionState();
|
|
1666
|
+
const events = captureBridgeEvents(() => {
|
|
1667
|
+
handleSdkMessage(session, {
|
|
1668
|
+
type: "system",
|
|
1669
|
+
subtype: "model_refusal_no_fallback",
|
|
1670
|
+
original_model: " ",
|
|
1671
|
+
request_id: null,
|
|
1672
|
+
api_refusal_category: " ",
|
|
1673
|
+
api_refusal_explanation: null,
|
|
1674
|
+
refused_user_message_uuid: null,
|
|
1675
|
+
content: " ",
|
|
1676
|
+
uuid: "refusal-notice",
|
|
1677
|
+
session_id: "session-1",
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
1681
|
+
{
|
|
1682
|
+
type: "system_notice_update",
|
|
1683
|
+
severity: "warning",
|
|
1684
|
+
message: "Could not continue with the selected model: model refused the request and no fallback model is configured.",
|
|
1685
|
+
},
|
|
1686
|
+
]);
|
|
1687
|
+
});
|
|
1392
1688
|
test("handleSdkMessage emits tolerant transcript retraction for model_fallback", () => {
|
|
1393
1689
|
const session = makeSessionState();
|
|
1394
1690
|
const events = captureBridgeEvents(() => {
|
|
@@ -2886,6 +3182,70 @@ test("requestAskUserQuestionAnswers preserves previews and annotations in update
|
|
|
2886
3182
|
question_index: 0,
|
|
2887
3183
|
total_questions: 1,
|
|
2888
3184
|
});
|
|
3185
|
+
const completedQuestionUpdate = events
|
|
3186
|
+
.map((event) => (event.event === "session_update" ? event.update : undefined))
|
|
3187
|
+
.find((update) => {
|
|
3188
|
+
const toolCallUpdate = update?.tool_call_update;
|
|
3189
|
+
const fields = toolCallUpdate?.fields;
|
|
3190
|
+
return toolCallUpdate?.tool_call_id === "tool-question" && fields?.status === "completed";
|
|
3191
|
+
})?.tool_call_update;
|
|
3192
|
+
const completedFields = completedQuestionUpdate?.fields;
|
|
3193
|
+
assert.deepEqual(completedFields?.raw_input, {
|
|
3194
|
+
questions: [
|
|
3195
|
+
{
|
|
3196
|
+
question: "Pick deployment target",
|
|
3197
|
+
header: "Target",
|
|
3198
|
+
multiSelect: true,
|
|
3199
|
+
options: [
|
|
3200
|
+
{
|
|
3201
|
+
label: "Staging",
|
|
3202
|
+
description: "Low-risk validation",
|
|
3203
|
+
preview: "Deploy to staging first.",
|
|
3204
|
+
},
|
|
3205
|
+
{
|
|
3206
|
+
label: "Production",
|
|
3207
|
+
description: "Customer-facing rollout",
|
|
3208
|
+
preview: "Deploy to production after approval.",
|
|
3209
|
+
},
|
|
3210
|
+
],
|
|
3211
|
+
},
|
|
3212
|
+
],
|
|
3213
|
+
answers: {
|
|
3214
|
+
"Pick deployment target": "Staging, Production",
|
|
3215
|
+
},
|
|
3216
|
+
annotations: {
|
|
3217
|
+
"Pick deployment target": {
|
|
3218
|
+
preview: "Deploy to staging first.\n\nDeploy to production after approval.",
|
|
3219
|
+
notes: "Roll out in both environments",
|
|
3220
|
+
},
|
|
3221
|
+
},
|
|
3222
|
+
question_results: [
|
|
3223
|
+
{
|
|
3224
|
+
question: "Pick deployment target",
|
|
3225
|
+
header: "Target",
|
|
3226
|
+
question_index: 0,
|
|
3227
|
+
total_questions: 1,
|
|
3228
|
+
selected_options: [
|
|
3229
|
+
{
|
|
3230
|
+
option_id: "question_0",
|
|
3231
|
+
label: "Staging",
|
|
3232
|
+
description: "Low-risk validation",
|
|
3233
|
+
preview: "Deploy to staging first.",
|
|
3234
|
+
},
|
|
3235
|
+
{
|
|
3236
|
+
option_id: "question_1",
|
|
3237
|
+
label: "Production",
|
|
3238
|
+
description: "Customer-facing rollout",
|
|
3239
|
+
preview: "Deploy to production after approval.",
|
|
3240
|
+
},
|
|
3241
|
+
],
|
|
3242
|
+
annotation: {
|
|
3243
|
+
preview: "Deploy to staging first.\n\nDeploy to production after approval.",
|
|
3244
|
+
notes: "Roll out in both environments",
|
|
3245
|
+
},
|
|
3246
|
+
},
|
|
3247
|
+
],
|
|
3248
|
+
});
|
|
2889
3249
|
});
|
|
2890
3250
|
test("normalizeToolKind maps known tool names", () => {
|
|
2891
3251
|
assert.equal(normalizeToolKind("Bash"), "execute");
|
|
@@ -2908,6 +3268,7 @@ test("normalizeToolKind maps known tool names", () => {
|
|
|
2908
3268
|
assert.equal(normalizeToolKind("ShowOnboardingRolePicker"), "other");
|
|
2909
3269
|
assert.equal(normalizeToolKind("TaskOutput"), "other");
|
|
2910
3270
|
assert.equal(normalizeToolKind("TaskStop"), "other");
|
|
3271
|
+
assert.equal(normalizeToolKind("ReadMcpResourceDir"), "read");
|
|
2911
3272
|
assert.equal(normalizeToolKind("Task"), "think");
|
|
2912
3273
|
assert.equal(normalizeToolKind("Agent"), "think");
|
|
2913
3274
|
assert.equal(normalizeToolKind("EnterPlanMode"), "switch_mode");
|
|
@@ -2925,6 +3286,16 @@ test("shell tool titles use input command", () => {
|
|
|
2925
3286
|
assert.equal(createToolCall("tc-powershell-title", "PowerShell", { command: "Get-ChildItem" }).title, "Get-ChildItem");
|
|
2926
3287
|
assert.equal(createToolCall("tc-powershell-empty", "PowerShell", {}).title, "Terminal");
|
|
2927
3288
|
});
|
|
3289
|
+
test("ReadMcpResourceDir titles include server and URI context", () => {
|
|
3290
|
+
assert.equal(createToolCall("tc-mcp-dir-title", "ReadMcpResourceDir", {
|
|
3291
|
+
server: "docs",
|
|
3292
|
+
uri: "file://manuals/",
|
|
3293
|
+
}).title, "ReadMcpResourceDir docs file://manuals/");
|
|
3294
|
+
assert.equal(createToolCall("tc-mcp-dir-uri-title", "ReadMcpResourceDir", {
|
|
3295
|
+
uri: "file://manuals/",
|
|
3296
|
+
}).title, "ReadMcpResourceDir file://manuals/");
|
|
3297
|
+
assert.equal(createToolCall("tc-mcp-dir-fallback-title", "ReadMcpResourceDir", {}).title, "ReadMcpResourceDir");
|
|
3298
|
+
});
|
|
2928
3299
|
test("parseFastModeState accepts known values and rejects unknown values", () => {
|
|
2929
3300
|
assert.equal(parseFastModeState("off"), "off");
|
|
2930
3301
|
assert.equal(parseFastModeState("cooldown"), "cooldown");
|
|
@@ -2957,10 +3328,14 @@ test("buildRateLimitUpdate maps SDK fields to wire shape", () => {
|
|
|
2957
3328
|
overageDisabledReason: "out_of_credits",
|
|
2958
3329
|
isUsingOverage: false,
|
|
2959
3330
|
surpassedThreshold: 0.9,
|
|
3331
|
+
errorCode: "credits_required",
|
|
3332
|
+
canUserPurchaseCredits: true,
|
|
3333
|
+
hasChargeableSavedPaymentMethod: false,
|
|
2960
3334
|
});
|
|
2961
3335
|
assert.deepEqual(update, {
|
|
2962
3336
|
type: "rate_limit_update",
|
|
2963
3337
|
status: "allowed_warning",
|
|
3338
|
+
error_code: "credits_required",
|
|
2964
3339
|
resets_at: 1_741_280_000,
|
|
2965
3340
|
utilization: 0.92,
|
|
2966
3341
|
rate_limit_type: "five_hour",
|
|
@@ -2969,6 +3344,8 @@ test("buildRateLimitUpdate maps SDK fields to wire shape", () => {
|
|
|
2969
3344
|
overage_disabled_reason: "out_of_credits",
|
|
2970
3345
|
is_using_overage: false,
|
|
2971
3346
|
surpassed_threshold: 0.9,
|
|
3347
|
+
can_user_purchase_credits: true,
|
|
3348
|
+
has_chargeable_saved_payment_method: false,
|
|
2972
3349
|
});
|
|
2973
3350
|
});
|
|
2974
3351
|
test("buildRateLimitUpdate normalizes SDK overage boolean spellings", () => {
|
|
@@ -2998,6 +3375,10 @@ test("buildRateLimitUpdate rejects invalid payloads", () => {
|
|
|
2998
3375
|
status: "rejected",
|
|
2999
3376
|
overageStatus: "bad_status",
|
|
3000
3377
|
}), { type: "rate_limit_update", status: "rejected" });
|
|
3378
|
+
assert.deepEqual(buildRateLimitUpdate({ status: "rejected", errorCode: "other" }), {
|
|
3379
|
+
type: "rate_limit_update",
|
|
3380
|
+
status: "rejected",
|
|
3381
|
+
});
|
|
3001
3382
|
});
|
|
3002
3383
|
test("buildApiRetryUpdate maps SDK api_retry messages to wire shape", () => {
|
|
3003
3384
|
assert.deepEqual(buildApiRetryUpdate({
|
|
@@ -3283,6 +3664,197 @@ test("handleSdkMessage emits system notices for notifications and plugin failure
|
|
|
3283
3664
|
{ type: "system_notice_update", severity: "warning", message: "Plugin install failed acme: download failed" },
|
|
3284
3665
|
]);
|
|
3285
3666
|
});
|
|
3667
|
+
test("handleSdkMessage maps informational system messages to notices by level", () => {
|
|
3668
|
+
const session = makeSessionState();
|
|
3669
|
+
const events = captureBridgeEvents(() => {
|
|
3670
|
+
handleSdkMessage(session, {
|
|
3671
|
+
type: "system",
|
|
3672
|
+
subtype: "informational",
|
|
3673
|
+
content: " Sync ready ",
|
|
3674
|
+
level: "notice",
|
|
3675
|
+
uuid: "message-info-notice",
|
|
3676
|
+
session_id: "session-1",
|
|
3677
|
+
});
|
|
3678
|
+
handleSdkMessage(session, {
|
|
3679
|
+
type: "system",
|
|
3680
|
+
subtype: "informational",
|
|
3681
|
+
content: "Try /compact",
|
|
3682
|
+
level: "suggestion",
|
|
3683
|
+
uuid: "message-info-suggestion",
|
|
3684
|
+
session_id: "session-1",
|
|
3685
|
+
});
|
|
3686
|
+
handleSdkMessage(session, {
|
|
3687
|
+
type: "system",
|
|
3688
|
+
subtype: "informational",
|
|
3689
|
+
content: "Hook blocked continuation",
|
|
3690
|
+
level: "warning",
|
|
3691
|
+
uuid: "message-info-warning",
|
|
3692
|
+
session_id: "session-1",
|
|
3693
|
+
});
|
|
3694
|
+
});
|
|
3695
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
3696
|
+
{ type: "system_notice_update", severity: "info", message: "Sync ready" },
|
|
3697
|
+
{ type: "system_notice_update", severity: "info", message: "Suggestion: Try /compact" },
|
|
3698
|
+
{ type: "system_notice_update", severity: "warning", message: "Hook blocked continuation" },
|
|
3699
|
+
]);
|
|
3700
|
+
});
|
|
3701
|
+
test("handleSdkMessage keeps informational info log-only unless continuation is prevented", () => {
|
|
3702
|
+
const session = makeSessionState();
|
|
3703
|
+
const events = captureBridgeEvents(() => {
|
|
3704
|
+
handleSdkMessage(session, {
|
|
3705
|
+
type: "system",
|
|
3706
|
+
subtype: "informational",
|
|
3707
|
+
content: "Transcript-only progress",
|
|
3708
|
+
level: "info",
|
|
3709
|
+
uuid: "message-info-log-only",
|
|
3710
|
+
session_id: "session-1",
|
|
3711
|
+
});
|
|
3712
|
+
handleSdkMessage(session, {
|
|
3713
|
+
type: "system",
|
|
3714
|
+
subtype: "informational",
|
|
3715
|
+
content: "Stop hook denied continuation",
|
|
3716
|
+
level: "info",
|
|
3717
|
+
prevent_continuation: true,
|
|
3718
|
+
uuid: "message-info-prevented",
|
|
3719
|
+
session_id: "session-1",
|
|
3720
|
+
});
|
|
3721
|
+
});
|
|
3722
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
3723
|
+
{
|
|
3724
|
+
type: "system_notice_update",
|
|
3725
|
+
severity: "warning",
|
|
3726
|
+
message: "Stop hook denied continuation",
|
|
3727
|
+
},
|
|
3728
|
+
]);
|
|
3729
|
+
});
|
|
3730
|
+
test("handleSdkMessage deduplicates informational messages by tool use, level, and content", () => {
|
|
3731
|
+
const session = makeSessionState();
|
|
3732
|
+
const events = captureBridgeEvents(() => {
|
|
3733
|
+
handleSdkMessage(session, {
|
|
3734
|
+
type: "system",
|
|
3735
|
+
subtype: "informational",
|
|
3736
|
+
content: "Progress",
|
|
3737
|
+
level: "notice",
|
|
3738
|
+
tool_use_id: "tool-1",
|
|
3739
|
+
uuid: "message-info-1",
|
|
3740
|
+
session_id: "session-1",
|
|
3741
|
+
});
|
|
3742
|
+
handleSdkMessage(session, {
|
|
3743
|
+
type: "system",
|
|
3744
|
+
subtype: "informational",
|
|
3745
|
+
content: " Progress ",
|
|
3746
|
+
level: "notice",
|
|
3747
|
+
tool_use_id: "tool-1",
|
|
3748
|
+
uuid: "message-info-2",
|
|
3749
|
+
session_id: "session-1",
|
|
3750
|
+
});
|
|
3751
|
+
handleSdkMessage(session, {
|
|
3752
|
+
type: "system",
|
|
3753
|
+
subtype: "informational",
|
|
3754
|
+
content: "Progress updated",
|
|
3755
|
+
level: "notice",
|
|
3756
|
+
tool_use_id: "tool-1",
|
|
3757
|
+
uuid: "message-info-3",
|
|
3758
|
+
session_id: "session-1",
|
|
3759
|
+
});
|
|
3760
|
+
});
|
|
3761
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
3762
|
+
{ type: "system_notice_update", severity: "info", message: "Progress" },
|
|
3763
|
+
{ type: "system_notice_update", severity: "info", message: "Progress updated" },
|
|
3764
|
+
]);
|
|
3765
|
+
});
|
|
3766
|
+
test("handleSdkMessage does not deduplicate informational messages without a tool use id", () => {
|
|
3767
|
+
const session = makeSessionState();
|
|
3768
|
+
const events = captureBridgeEvents(() => {
|
|
3769
|
+
for (const uuid of ["message-info-1", "message-info-2"]) {
|
|
3770
|
+
handleSdkMessage(session, {
|
|
3771
|
+
type: "system",
|
|
3772
|
+
subtype: "informational",
|
|
3773
|
+
content: "Repeated global notice",
|
|
3774
|
+
level: "notice",
|
|
3775
|
+
uuid,
|
|
3776
|
+
session_id: "session-1",
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
3781
|
+
{ type: "system_notice_update", severity: "info", message: "Repeated global notice" },
|
|
3782
|
+
{ type: "system_notice_update", severity: "info", message: "Repeated global notice" },
|
|
3783
|
+
]);
|
|
3784
|
+
});
|
|
3785
|
+
test("handleSdkMessage treats worker shutdown before connect as log-only", () => {
|
|
3786
|
+
const session = makeSessionState();
|
|
3787
|
+
session.connected = false;
|
|
3788
|
+
const events = captureBridgeEvents(() => {
|
|
3789
|
+
handleSdkMessage(session, {
|
|
3790
|
+
type: "system",
|
|
3791
|
+
subtype: "worker_shutting_down",
|
|
3792
|
+
reason: "host_exit",
|
|
3793
|
+
uuid: "message-worker-shutdown",
|
|
3794
|
+
session_id: "session-1",
|
|
3795
|
+
});
|
|
3796
|
+
flushPendingWorkerShutdown(session);
|
|
3797
|
+
});
|
|
3798
|
+
assert.deepEqual(events, []);
|
|
3799
|
+
assert.equal(session.pendingWorkerShutdown, undefined);
|
|
3800
|
+
});
|
|
3801
|
+
test("handleSdkMessage flushes connected worker shutdown only when stream ends", () => {
|
|
3802
|
+
const session = makeSessionState();
|
|
3803
|
+
const events = captureBridgeEvents(() => {
|
|
3804
|
+
handleSdkMessage(session, {
|
|
3805
|
+
type: "system",
|
|
3806
|
+
subtype: "worker_shutting_down",
|
|
3807
|
+
reason: "host_exit",
|
|
3808
|
+
uuid: "message-worker-shutdown",
|
|
3809
|
+
session_id: "session-1",
|
|
3810
|
+
});
|
|
3811
|
+
flushPendingWorkerShutdown(session);
|
|
3812
|
+
});
|
|
3813
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
3814
|
+
{
|
|
3815
|
+
type: "system_notice_update",
|
|
3816
|
+
severity: "warning",
|
|
3817
|
+
message: "Claude worker is shutting down: host_exit",
|
|
3818
|
+
},
|
|
3819
|
+
]);
|
|
3820
|
+
});
|
|
3821
|
+
test("handleSdkMessage cancels pending worker shutdown after later SDK activity", () => {
|
|
3822
|
+
const session = makeSessionState();
|
|
3823
|
+
const events = captureBridgeEvents(() => {
|
|
3824
|
+
handleSdkMessage(session, {
|
|
3825
|
+
type: "system",
|
|
3826
|
+
subtype: "worker_shutting_down",
|
|
3827
|
+
reason: "host_exit",
|
|
3828
|
+
uuid: "message-worker-shutdown",
|
|
3829
|
+
session_id: "session-1",
|
|
3830
|
+
});
|
|
3831
|
+
handleSdkMessage(session, {
|
|
3832
|
+
type: "system",
|
|
3833
|
+
subtype: "notification",
|
|
3834
|
+
text: "Still running",
|
|
3835
|
+
priority: "low",
|
|
3836
|
+
uuid: "message-notification",
|
|
3837
|
+
session_id: "session-1",
|
|
3838
|
+
});
|
|
3839
|
+
flushPendingWorkerShutdown(session);
|
|
3840
|
+
});
|
|
3841
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
3842
|
+
{ type: "system_notice_update", severity: "info", message: "Still running" },
|
|
3843
|
+
]);
|
|
3844
|
+
});
|
|
3845
|
+
test("handleSdkMessage ignores unknown future system subtypes", () => {
|
|
3846
|
+
const session = makeSessionState();
|
|
3847
|
+
const events = captureBridgeEvents(() => {
|
|
3848
|
+
handleSdkMessage(session, {
|
|
3849
|
+
type: "system",
|
|
3850
|
+
subtype: "future_subtype",
|
|
3851
|
+
content: "Unknown",
|
|
3852
|
+
uuid: "message-future",
|
|
3853
|
+
session_id: "session-1",
|
|
3854
|
+
});
|
|
3855
|
+
});
|
|
3856
|
+
assert.deepEqual(events, []);
|
|
3857
|
+
});
|
|
3286
3858
|
test("handleSdkMessage treats mirror errors as log-only diagnostics", () => {
|
|
3287
3859
|
const session = makeSessionState();
|
|
3288
3860
|
const events = captureBridgeEvents(() => {
|
|
@@ -3841,6 +4413,7 @@ test("createToolCall maps project and artifact tools to compact titles", () => {
|
|
|
3841
4413
|
file_path: "C:/work/report.html",
|
|
3842
4414
|
favicon: "R",
|
|
3843
4415
|
label: "report-v2",
|
|
4416
|
+
description: "Quarterly report",
|
|
3844
4417
|
});
|
|
3845
4418
|
const artifactFallback = createToolCall("tc-artifact-path", "Artifact", {
|
|
3846
4419
|
file_path: "C:/work/report.html",
|
|
@@ -3853,6 +4426,7 @@ test("createToolCall maps project and artifact tools to compact titles", () => {
|
|
|
3853
4426
|
assert.equal(projectSearch.title, "Projects: search migration");
|
|
3854
4427
|
assert.equal(artifactWithLabel.kind, "other");
|
|
3855
4428
|
assert.equal(artifactWithLabel.title, "Artifact: report-v2");
|
|
4429
|
+
assert.equal(artifactWithLabel.raw_input.description, "Quarterly report");
|
|
3856
4430
|
assert.equal(artifactFallback.title, "Artifact: C:/work/report.html");
|
|
3857
4431
|
assert.equal(rolePicker.kind, "other");
|
|
3858
4432
|
assert.equal(rolePicker.title, "ShowOnboardingRolePicker");
|
|
@@ -4185,6 +4759,133 @@ test("buildToolResultFields marks ReadMcpResource error output as failed", () =>
|
|
|
4185
4759
|
},
|
|
4186
4760
|
]);
|
|
4187
4761
|
});
|
|
4762
|
+
test("buildToolResultFields renders structured ReadMcpResourceDir listings", () => {
|
|
4763
|
+
const base = createToolCall("tc-mcp-dir", "ReadMcpResourceDir", {
|
|
4764
|
+
server: "docs",
|
|
4765
|
+
uri: "file://manuals/",
|
|
4766
|
+
});
|
|
4767
|
+
const fields = buildToolResultFields(false, {
|
|
4768
|
+
resources: [
|
|
4769
|
+
{
|
|
4770
|
+
name: "guide.md",
|
|
4771
|
+
uri: "file://manuals/guide.md",
|
|
4772
|
+
mimeType: "text/markdown",
|
|
4773
|
+
},
|
|
4774
|
+
{
|
|
4775
|
+
name: "images",
|
|
4776
|
+
uri: "file://manuals/images",
|
|
4777
|
+
mimeType: "inode/directory",
|
|
4778
|
+
},
|
|
4779
|
+
{
|
|
4780
|
+
name: "readme",
|
|
4781
|
+
uri: "file://manuals/readme",
|
|
4782
|
+
},
|
|
4783
|
+
],
|
|
4784
|
+
}, base);
|
|
4785
|
+
const expected = "guide.md - file://manuals/guide.md (text/markdown)\n" +
|
|
4786
|
+
"images - file://manuals/images (directory)\n" +
|
|
4787
|
+
"readme - file://manuals/readme";
|
|
4788
|
+
assert.equal(fields.status, "completed");
|
|
4789
|
+
assert.equal(fields.raw_output, expected);
|
|
4790
|
+
assert.deepEqual(fields.content, [
|
|
4791
|
+
{
|
|
4792
|
+
type: "content",
|
|
4793
|
+
content: { type: "text", text: expected },
|
|
4794
|
+
},
|
|
4795
|
+
]);
|
|
4796
|
+
});
|
|
4797
|
+
test("buildToolResultFields renders empty ReadMcpResourceDir listings", () => {
|
|
4798
|
+
const base = createToolCall("tc-mcp-dir-empty", "ReadMcpResourceDir", {
|
|
4799
|
+
server: "docs",
|
|
4800
|
+
uri: "file://empty/",
|
|
4801
|
+
});
|
|
4802
|
+
const fields = buildToolResultFields(false, { resources: [] }, base);
|
|
4803
|
+
assert.equal(fields.status, "completed");
|
|
4804
|
+
assert.equal(fields.raw_output, "No resources found.");
|
|
4805
|
+
assert.deepEqual(fields.content, [
|
|
4806
|
+
{
|
|
4807
|
+
type: "content",
|
|
4808
|
+
content: { type: "text", text: "No resources found." },
|
|
4809
|
+
},
|
|
4810
|
+
]);
|
|
4811
|
+
});
|
|
4812
|
+
test("buildToolResultFields marks ReadMcpResourceDir error output as failed", () => {
|
|
4813
|
+
const base = createToolCall("tc-mcp-dir-error", "ReadMcpResourceDir", {
|
|
4814
|
+
server: "docs",
|
|
4815
|
+
uri: "file://missing/",
|
|
4816
|
+
});
|
|
4817
|
+
const fields = buildToolResultFields(false, {
|
|
4818
|
+
resources: [],
|
|
4819
|
+
error: "directory not found",
|
|
4820
|
+
}, base);
|
|
4821
|
+
assert.equal(fields.status, "failed");
|
|
4822
|
+
assert.equal(fields.raw_output, "Error: directory not found");
|
|
4823
|
+
assert.deepEqual(fields.content, [
|
|
4824
|
+
{
|
|
4825
|
+
type: "content",
|
|
4826
|
+
content: { type: "text", text: "Error: directory not found" },
|
|
4827
|
+
},
|
|
4828
|
+
]);
|
|
4829
|
+
});
|
|
4830
|
+
test("buildToolResultFields parses ReadMcpResourceDir transcript JSON", () => {
|
|
4831
|
+
const base = createToolCall("tc-mcp-dir-history", "ReadMcpResourceDir", {
|
|
4832
|
+
server: "docs",
|
|
4833
|
+
uri: "file://manuals/",
|
|
4834
|
+
});
|
|
4835
|
+
const transcriptJson = JSON.stringify({
|
|
4836
|
+
resources: [
|
|
4837
|
+
{
|
|
4838
|
+
name: "api.json",
|
|
4839
|
+
uri: "file://manuals/api.json",
|
|
4840
|
+
mimeType: "application/json",
|
|
4841
|
+
},
|
|
4842
|
+
],
|
|
4843
|
+
});
|
|
4844
|
+
const fields = buildToolResultFields(false, transcriptJson, base, {
|
|
4845
|
+
type: "tool_result",
|
|
4846
|
+
tool_use_id: "tc-mcp-dir-history",
|
|
4847
|
+
content: transcriptJson,
|
|
4848
|
+
});
|
|
4849
|
+
assert.equal(fields.raw_output, "api.json - file://manuals/api.json (application/json)");
|
|
4850
|
+
assert.deepEqual(fields.content, [
|
|
4851
|
+
{
|
|
4852
|
+
type: "content",
|
|
4853
|
+
content: {
|
|
4854
|
+
type: "text",
|
|
4855
|
+
text: "api.json - file://manuals/api.json (application/json)",
|
|
4856
|
+
},
|
|
4857
|
+
},
|
|
4858
|
+
]);
|
|
4859
|
+
});
|
|
4860
|
+
test("buildToolResultFields skips invalid ReadMcpResourceDir entries", () => {
|
|
4861
|
+
const base = createToolCall("tc-mcp-dir-invalid", "ReadMcpResourceDir", {
|
|
4862
|
+
server: "docs",
|
|
4863
|
+
uri: "file://manuals/",
|
|
4864
|
+
});
|
|
4865
|
+
const fields = buildToolResultFields(false, {
|
|
4866
|
+
resources: [
|
|
4867
|
+
{ name: "missing-uri" },
|
|
4868
|
+
{ uri: "file://manuals/missing-name" },
|
|
4869
|
+
null,
|
|
4870
|
+
{
|
|
4871
|
+
name: "valid.txt",
|
|
4872
|
+
uri: "file://manuals/valid.txt",
|
|
4873
|
+
mimeType: "text/plain",
|
|
4874
|
+
},
|
|
4875
|
+
],
|
|
4876
|
+
}, base);
|
|
4877
|
+
assert.equal(fields.status, "completed");
|
|
4878
|
+
assert.equal(fields.raw_output, "valid.txt - file://manuals/valid.txt (text/plain)");
|
|
4879
|
+
assert.deepEqual(fields.content, [
|
|
4880
|
+
{
|
|
4881
|
+
type: "content",
|
|
4882
|
+
content: {
|
|
4883
|
+
type: "text",
|
|
4884
|
+
text: "valid.txt - file://manuals/valid.txt (text/plain)",
|
|
4885
|
+
},
|
|
4886
|
+
},
|
|
4887
|
+
]);
|
|
4888
|
+
});
|
|
4188
4889
|
test("buildToolResultFields preserves WebFetch artifactRead only as metadata", () => {
|
|
4189
4890
|
const base = createToolCall("tc-web-fetch-artifact", "WebFetch", {
|
|
4190
4891
|
url: "https://artifact.local/dashboard",
|
|
@@ -4405,7 +5106,7 @@ test("looksLikeAuthRequired detects login hints", () => {
|
|
|
4405
5106
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
4406
5107
|
});
|
|
4407
5108
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
4408
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.3.
|
|
5109
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.3.198");
|
|
4409
5110
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
4410
5111
|
});
|
|
4411
5112
|
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
@@ -4919,13 +5620,15 @@ test("mapSdkSessions normalizes and sorts sessions", () => {
|
|
|
4919
5620
|
},
|
|
4920
5621
|
]);
|
|
4921
5622
|
});
|
|
4922
|
-
test("buildSessionListOptions
|
|
5623
|
+
test("buildSessionListOptions includes SDK-created sessions for resume listings", () => {
|
|
4923
5624
|
assert.deepEqual(buildSessionListOptions("C:/repo"), {
|
|
4924
5625
|
dir: "C:/repo",
|
|
5626
|
+
includeProgrammatic: true,
|
|
4925
5627
|
includeWorktrees: true,
|
|
4926
5628
|
limit: 50,
|
|
4927
5629
|
});
|
|
4928
5630
|
assert.deepEqual(buildSessionListOptions(undefined), {
|
|
5631
|
+
includeProgrammatic: true,
|
|
4929
5632
|
limit: 50,
|
|
4930
5633
|
});
|
|
4931
5634
|
});
|
|
@@ -5471,6 +6174,7 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
5471
6174
|
const mapped = mapAvailableModels([
|
|
5472
6175
|
{
|
|
5473
6176
|
value: "sonnet",
|
|
6177
|
+
resolvedModel: "claude-sonnet-5",
|
|
5474
6178
|
displayName: "Claude Sonnet",
|
|
5475
6179
|
description: "Balanced model",
|
|
5476
6180
|
supportsEffort: true,
|
|
@@ -5489,6 +6193,7 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
5489
6193
|
assert.deepEqual(mapped, [
|
|
5490
6194
|
{
|
|
5491
6195
|
id: "sonnet",
|
|
6196
|
+
resolved_model: "claude-sonnet-5",
|
|
5492
6197
|
display_name: "Claude Sonnet",
|
|
5493
6198
|
description: "Balanced model",
|
|
5494
6199
|
supports_effort: true,
|
|
@@ -5506,12 +6211,13 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
5506
6211
|
},
|
|
5507
6212
|
]);
|
|
5508
6213
|
});
|
|
5509
|
-
test("mapAvailableModels
|
|
6214
|
+
test("mapAvailableModels preserves Fable models and unknown ids", () => {
|
|
5510
6215
|
const mapped = mapAvailableModels([
|
|
5511
6216
|
{
|
|
5512
6217
|
value: "fable",
|
|
6218
|
+
resolvedModel: "claude-fable-5",
|
|
5513
6219
|
displayName: "Claude Fable",
|
|
5514
|
-
description: "
|
|
6220
|
+
description: "Default model alias",
|
|
5515
6221
|
supportsEffort: true,
|
|
5516
6222
|
},
|
|
5517
6223
|
{
|
|
@@ -5534,6 +6240,28 @@ test("mapAvailableModels filters unavailable Fable models while preserving unkno
|
|
|
5534
6240
|
},
|
|
5535
6241
|
]);
|
|
5536
6242
|
assert.deepEqual(mapped, [
|
|
6243
|
+
{
|
|
6244
|
+
id: "fable",
|
|
6245
|
+
resolved_model: "claude-fable-5",
|
|
6246
|
+
display_name: "Claude Fable",
|
|
6247
|
+
description: "Default model alias",
|
|
6248
|
+
supports_effort: true,
|
|
6249
|
+
supported_effort_levels: [],
|
|
6250
|
+
},
|
|
6251
|
+
{
|
|
6252
|
+
id: "claude-fable-5",
|
|
6253
|
+
display_name: "Claude Fable 5",
|
|
6254
|
+
description: "Unavailable model",
|
|
6255
|
+
supports_effort: true,
|
|
6256
|
+
supported_effort_levels: [],
|
|
6257
|
+
},
|
|
6258
|
+
{
|
|
6259
|
+
id: "claude-fable-5-20260612",
|
|
6260
|
+
display_name: "Claude Fable 5 dated",
|
|
6261
|
+
description: "Unavailable dated model",
|
|
6262
|
+
supports_effort: true,
|
|
6263
|
+
supported_effort_levels: [],
|
|
6264
|
+
},
|
|
5537
6265
|
{
|
|
5538
6266
|
id: "claude-unknown-1",
|
|
5539
6267
|
display_name: "Claude Unknown",
|
|
@@ -5543,6 +6271,26 @@ test("mapAvailableModels filters unavailable Fable models while preserving unkno
|
|
|
5543
6271
|
},
|
|
5544
6272
|
]);
|
|
5545
6273
|
});
|
|
6274
|
+
test("resolveCurrentModel matches full Fable runtime ids to the fable alias", () => {
|
|
6275
|
+
const session = makeSessionState();
|
|
6276
|
+
session.model = "fable";
|
|
6277
|
+
session.requestedModelId = "fable";
|
|
6278
|
+
session.resolvedRuntimeModelId = "claude-fable-5-20260612";
|
|
6279
|
+
session.availableModels = [
|
|
6280
|
+
{
|
|
6281
|
+
id: "fable",
|
|
6282
|
+
resolved_model: "claude-fable-5",
|
|
6283
|
+
display_name: "Claude Fable 5",
|
|
6284
|
+
supports_effort: true,
|
|
6285
|
+
supported_effort_levels: ["low", "medium", "high", "xhigh", "max"],
|
|
6286
|
+
},
|
|
6287
|
+
];
|
|
6288
|
+
const currentModel = resolveCurrentModel(session);
|
|
6289
|
+
assert.equal(currentModel.display_name_short, "Fable 5");
|
|
6290
|
+
assert.equal(currentModel.display_name_long, "Claude Fable 5");
|
|
6291
|
+
assert.equal(currentModel.catalog_id, "fable");
|
|
6292
|
+
assert.equal(currentModel.supports_effort, true);
|
|
6293
|
+
});
|
|
5546
6294
|
test("resolveCurrentModel keeps 1M context suffix in short and long display names", () => {
|
|
5547
6295
|
const session = makeSessionState();
|
|
5548
6296
|
session.resolvedRuntimeModelId = "claude-opus-4-7[1m]";
|