claude-code-rust 0.9.0 → 0.11.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 +16 -9
- package/agent-sdk/README.md +1 -1
- package/agent-sdk/dist/bridge/commands.js +71 -2
- package/agent-sdk/dist/bridge/events.js +127 -12
- package/agent-sdk/dist/bridge/history.js +9 -4
- package/agent-sdk/dist/bridge/logger.js +253 -0
- package/agent-sdk/dist/bridge/mcp.js +81 -5
- package/agent-sdk/dist/bridge/message_handlers.js +318 -67
- package/agent-sdk/dist/bridge/session_lifecycle.js +774 -58
- package/agent-sdk/dist/bridge/shared.js +0 -7
- package/agent-sdk/dist/bridge/state_parsing.js +61 -0
- package/agent-sdk/dist/bridge/tool_calls.js +269 -66
- package/agent-sdk/dist/bridge/tooling.js +60 -27
- package/agent-sdk/dist/bridge/user_interaction.js +34 -23
- package/agent-sdk/dist/bridge.js +406 -42
- package/agent-sdk/dist/bridge.test.js +765 -23
- package/package.json +2 -2
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { AsyncQueue, CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, handleTaskSystemMessage, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
3
|
+
import { AsyncQueue, CACHE_SPLIT_POLICY, buildApiRetryUpdate, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, handleTaskSystemMessage, handleSdkMessage, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, attachRequestUserDialogInterceptor, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRuntimeSessionState, parseRateLimitStatus, normalizeSettingsParseError, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
4
|
+
import { availableModesForSession, buildModeState, markModeUnavailableForSession, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, } from "./bridge/commands.js";
|
|
5
|
+
import { emitCurrentModelUpdate, refreshCurrentModel, resolveCurrentModel, shouldInvalidateResolvedRuntimeModel, shouldEmitStartupAuthRequiredForAccount, } from "./bridge/session_lifecycle.js";
|
|
4
6
|
import { emitToolProgressUpdate } from "./bridge/tool_calls.js";
|
|
5
7
|
import { requestAskUserQuestionAnswers } from "./bridge/user_interaction.js";
|
|
8
|
+
import { handleResultMessage } from "./bridge/message_handlers.js";
|
|
6
9
|
function makeSessionState() {
|
|
7
10
|
const input = new AsyncQueue();
|
|
8
11
|
return {
|
|
@@ -11,6 +14,9 @@ function makeSessionState() {
|
|
|
11
14
|
model: "haiku",
|
|
12
15
|
availableModels: [],
|
|
13
16
|
mode: null,
|
|
17
|
+
supportedModeIds: [],
|
|
18
|
+
runtimeUnavailableModeIds: [],
|
|
19
|
+
supportsBypassPermissionsMode: false,
|
|
14
20
|
fastModeState: "off",
|
|
15
21
|
query: {},
|
|
16
22
|
input,
|
|
@@ -25,6 +31,80 @@ function makeSessionState() {
|
|
|
25
31
|
authHintSent: false,
|
|
26
32
|
};
|
|
27
33
|
}
|
|
34
|
+
test("availableModesForSession omits conditional modes when unsupported", () => {
|
|
35
|
+
const session = makeSessionState();
|
|
36
|
+
refreshSupportedModesForSession(session);
|
|
37
|
+
assert.deepEqual(availableModesForSession(session).map((entry) => entry.id), ["default", "acceptEdits", "plan", "dontAsk"]);
|
|
38
|
+
});
|
|
39
|
+
test("buildModeState includes auto and bypassPermissions when supported", () => {
|
|
40
|
+
const session = makeSessionState();
|
|
41
|
+
session.mode = "default";
|
|
42
|
+
session.model = "sonnet";
|
|
43
|
+
session.supportsBypassPermissionsMode = true;
|
|
44
|
+
session.availableModels = [
|
|
45
|
+
{
|
|
46
|
+
id: "sonnet",
|
|
47
|
+
display_name: "Sonnet",
|
|
48
|
+
supports_effort: true,
|
|
49
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
50
|
+
supports_auto_mode: true,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
refreshSupportedModesForSession(session);
|
|
54
|
+
const mode = buildModeState(session, "default");
|
|
55
|
+
assert.deepEqual(mode.available_modes.map((entry) => entry.id), ["default", "auto", "acceptEdits", "plan", "dontAsk", "bypassPermissions"]);
|
|
56
|
+
});
|
|
57
|
+
test("refreshSupportedModesForSession uses resolved current model for auto-mode eligibility", () => {
|
|
58
|
+
const session = makeSessionState();
|
|
59
|
+
session.model = "sonnet";
|
|
60
|
+
session.availableModels = [
|
|
61
|
+
{
|
|
62
|
+
id: "sonnet",
|
|
63
|
+
display_name: "Claude Sonnet",
|
|
64
|
+
supports_effort: true,
|
|
65
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
66
|
+
supports_auto_mode: true,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
session.currentModel = {
|
|
70
|
+
resolved_id: "claude-sonnet-4-6[1m]",
|
|
71
|
+
display_name_short: "Sonnet [1M]",
|
|
72
|
+
display_name_long: "Sonnet 4.6 [1M]",
|
|
73
|
+
supports_effort: true,
|
|
74
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
75
|
+
supports_auto_mode: false,
|
|
76
|
+
is_authoritative: true,
|
|
77
|
+
};
|
|
78
|
+
refreshSupportedModesForSession(session);
|
|
79
|
+
assert.deepEqual(availableModesForSession(session).map((entry) => entry.id), ["default", "acceptEdits", "plan", "dontAsk"]);
|
|
80
|
+
});
|
|
81
|
+
test("refreshSupportedModesForSession retains current mode before capability data arrives", () => {
|
|
82
|
+
const session = makeSessionState();
|
|
83
|
+
session.mode = "auto";
|
|
84
|
+
refreshSupportedModesForSession(session);
|
|
85
|
+
assert.deepEqual(session.supportedModeIds, ["default", "auto", "acceptEdits", "plan", "dontAsk"]);
|
|
86
|
+
});
|
|
87
|
+
test("markModeUnavailableForSession prunes rejected runtime mode from session list", () => {
|
|
88
|
+
const session = makeSessionState();
|
|
89
|
+
session.model = "sonnet";
|
|
90
|
+
session.availableModels = [
|
|
91
|
+
{
|
|
92
|
+
id: "sonnet",
|
|
93
|
+
display_name: "Sonnet",
|
|
94
|
+
supports_effort: true,
|
|
95
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
96
|
+
supports_auto_mode: true,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
refreshSupportedModesForSession(session);
|
|
100
|
+
assert.equal(markModeUnavailableForSession(session, "auto"), true);
|
|
101
|
+
assert.deepEqual(availableModesForSession(session).map((entry) => entry.id), ["default", "acceptEdits", "plan", "dontAsk"]);
|
|
102
|
+
});
|
|
103
|
+
test("permissionModeFailureLooksUnsupported detects SDK capability rejections", () => {
|
|
104
|
+
assert.equal(permissionModeFailureLooksUnsupported("auto", "Cannot set permission mode to auto: not available in my plan"), true);
|
|
105
|
+
assert.equal(permissionModeFailureLooksUnsupported("bypassPermissions", "Cannot set permission mode to bypassPermissions because the session was not launched with --dangerously-skip-permissions"), true);
|
|
106
|
+
assert.equal(permissionModeFailureLooksUnsupported("auto", "bridge disconnected before request completed"), false);
|
|
107
|
+
});
|
|
28
108
|
function captureBridgeEvents(run) {
|
|
29
109
|
const writes = [];
|
|
30
110
|
const originalWrite = process.stdout.write;
|
|
@@ -221,6 +301,30 @@ test("parseCommandEnvelope validates mcp_set_servers command", () => {
|
|
|
221
301
|
},
|
|
222
302
|
});
|
|
223
303
|
});
|
|
304
|
+
test("parseCommandEnvelope validates reload_plugins command", () => {
|
|
305
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
306
|
+
request_id: "req-reload",
|
|
307
|
+
command: "reload_plugins",
|
|
308
|
+
session_id: "session-123",
|
|
309
|
+
}));
|
|
310
|
+
assert.equal(parsed.requestId, "req-reload");
|
|
311
|
+
assert.deepEqual(parsed.command, {
|
|
312
|
+
command: "reload_plugins",
|
|
313
|
+
session_id: "session-123",
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
test("parseCommandEnvelope validates get_context_usage command", () => {
|
|
317
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
318
|
+
request_id: "req-usage",
|
|
319
|
+
command: "get_context_usage",
|
|
320
|
+
session_id: "session-123",
|
|
321
|
+
}));
|
|
322
|
+
assert.equal(parsed.requestId, "req-usage");
|
|
323
|
+
assert.deepEqual(parsed.command, {
|
|
324
|
+
command: "get_context_usage",
|
|
325
|
+
session_id: "session-123",
|
|
326
|
+
});
|
|
327
|
+
});
|
|
224
328
|
test("staleMcpAuthCandidates selects previously connected servers that regressed to needs-auth", () => {
|
|
225
329
|
const candidates = staleMcpAuthCandidates([
|
|
226
330
|
{
|
|
@@ -330,6 +434,7 @@ test("buildQueryOptions maps launch settings into sdk query options", () => {
|
|
|
330
434
|
assert.equal("thinking" in options, false);
|
|
331
435
|
assert.equal("effort" in options, false);
|
|
332
436
|
assert.equal(options.agentProgressSummaries, true);
|
|
437
|
+
assert.equal(options.promptSuggestions, true);
|
|
333
438
|
assert.equal(options.sessionId, "session-1");
|
|
334
439
|
assert.deepEqual(options.settingSources, ["user", "project", "local"]);
|
|
335
440
|
assert.deepEqual(options.toolConfig, {
|
|
@@ -393,6 +498,25 @@ test("buildQueryOptions trims startup model before passing sdk option", () => {
|
|
|
393
498
|
assert.equal(options.model, "claude-opus-4-6");
|
|
394
499
|
assert.equal(options.permissionMode, "plan");
|
|
395
500
|
});
|
|
501
|
+
test("buildQueryOptions maps auto startup permission mode", () => {
|
|
502
|
+
const input = new AsyncQueue();
|
|
503
|
+
const options = buildQueryOptions({
|
|
504
|
+
cwd: "C:/work",
|
|
505
|
+
launchSettings: {
|
|
506
|
+
settings: {
|
|
507
|
+
permissions: { defaultMode: "auto" },
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
provisionalSessionId: "session-auto",
|
|
511
|
+
input,
|
|
512
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
513
|
+
enableSdkDebug: false,
|
|
514
|
+
enableSpawnDebug: false,
|
|
515
|
+
sessionIdForLogs: () => "session-auto",
|
|
516
|
+
});
|
|
517
|
+
assert.equal(options.permissionMode, "auto");
|
|
518
|
+
assert.equal("allowDangerouslySkipPermissions" in options, false);
|
|
519
|
+
});
|
|
396
520
|
test("buildQueryOptions enables dangerous skip flag for bypass permissions startup mode", () => {
|
|
397
521
|
const input = new AsyncQueue();
|
|
398
522
|
const options = buildQueryOptions({
|
|
@@ -430,6 +554,57 @@ test("buildQueryOptions omits startup overrides for default logout path", () =>
|
|
|
430
554
|
assert.equal("systemPrompt" in options, false);
|
|
431
555
|
assert.equal("agentProgressSummaries" in options, false);
|
|
432
556
|
});
|
|
557
|
+
test("buildQueryOptions makes sandbox fallback explicit when enabled", () => {
|
|
558
|
+
const input = new AsyncQueue();
|
|
559
|
+
const options = buildQueryOptions({
|
|
560
|
+
cwd: "C:/work",
|
|
561
|
+
launchSettings: {
|
|
562
|
+
settings: {
|
|
563
|
+
sandbox: {
|
|
564
|
+
enabled: true,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
provisionalSessionId: "session-sandbox",
|
|
569
|
+
input,
|
|
570
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
571
|
+
enableSdkDebug: false,
|
|
572
|
+
enableSpawnDebug: false,
|
|
573
|
+
sessionIdForLogs: () => "session-sandbox",
|
|
574
|
+
});
|
|
575
|
+
assert.deepEqual(options.settings, {
|
|
576
|
+
sandbox: {
|
|
577
|
+
enabled: true,
|
|
578
|
+
failIfUnavailable: false,
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
test("buildQueryOptions preserves explicit sandbox failIfUnavailable setting", () => {
|
|
583
|
+
const input = new AsyncQueue();
|
|
584
|
+
const options = buildQueryOptions({
|
|
585
|
+
cwd: "C:/work",
|
|
586
|
+
launchSettings: {
|
|
587
|
+
settings: {
|
|
588
|
+
sandbox: {
|
|
589
|
+
enabled: true,
|
|
590
|
+
failIfUnavailable: true,
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
provisionalSessionId: "session-sandbox-explicit",
|
|
595
|
+
input,
|
|
596
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
597
|
+
enableSdkDebug: false,
|
|
598
|
+
enableSpawnDebug: false,
|
|
599
|
+
sessionIdForLogs: () => "session-sandbox-explicit",
|
|
600
|
+
});
|
|
601
|
+
assert.deepEqual(options.settings, {
|
|
602
|
+
sandbox: {
|
|
603
|
+
enabled: true,
|
|
604
|
+
failIfUnavailable: true,
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
});
|
|
433
608
|
test("handleTaskSystemMessage prefers task_progress summary over fallback text", () => {
|
|
434
609
|
const session = makeSessionState();
|
|
435
610
|
const events = captureBridgeEvents(() => {
|
|
@@ -539,6 +714,131 @@ test("handleTaskSystemMessage final summary replaces prior task content and fina
|
|
|
539
714
|
});
|
|
540
715
|
assert.equal(session.taskToolUseIds.has("task-1"), false);
|
|
541
716
|
});
|
|
717
|
+
test("handleTaskSystemMessage applies task_updated description patches to the linked task", () => {
|
|
718
|
+
const session = makeSessionState();
|
|
719
|
+
const events = captureBridgeEvents(() => {
|
|
720
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
721
|
+
task_id: "task-1",
|
|
722
|
+
tool_use_id: "tool-1",
|
|
723
|
+
description: "Initial task description",
|
|
724
|
+
});
|
|
725
|
+
handleTaskSystemMessage(session, "task_updated", {
|
|
726
|
+
task_id: "task-1",
|
|
727
|
+
patch: {
|
|
728
|
+
status: "running",
|
|
729
|
+
description: "Refining the migration plan",
|
|
730
|
+
is_backgrounded: true,
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
const lastEvent = events.at(-1);
|
|
735
|
+
assert.ok(lastEvent);
|
|
736
|
+
assert.equal(lastEvent.event, "session_update");
|
|
737
|
+
assert.deepEqual(lastEvent.update, {
|
|
738
|
+
type: "tool_call_update",
|
|
739
|
+
tool_call_update: {
|
|
740
|
+
tool_call_id: "tool-1",
|
|
741
|
+
fields: {
|
|
742
|
+
status: "in_progress",
|
|
743
|
+
raw_output: "Refining the migration plan",
|
|
744
|
+
content: [
|
|
745
|
+
{
|
|
746
|
+
type: "content",
|
|
747
|
+
content: { type: "text", text: "Refining the migration plan" },
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
task_metadata: {
|
|
751
|
+
is_backgrounded: true,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
test("handleTaskSystemMessage uses task_updated terminal error text when description is absent", () => {
|
|
758
|
+
const session = makeSessionState();
|
|
759
|
+
const events = captureBridgeEvents(() => {
|
|
760
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
761
|
+
task_id: "task-1",
|
|
762
|
+
tool_use_id: "tool-1",
|
|
763
|
+
description: "Initial task description",
|
|
764
|
+
});
|
|
765
|
+
handleTaskSystemMessage(session, "task_updated", {
|
|
766
|
+
task_id: "task-1",
|
|
767
|
+
patch: {
|
|
768
|
+
status: "killed",
|
|
769
|
+
error: "Task stopped by parent agent",
|
|
770
|
+
end_time: 1234,
|
|
771
|
+
total_paused_ms: 250,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
const lastEvent = events.at(-1);
|
|
776
|
+
assert.ok(lastEvent);
|
|
777
|
+
assert.equal(lastEvent.event, "session_update");
|
|
778
|
+
assert.deepEqual(lastEvent.update, {
|
|
779
|
+
type: "tool_call_update",
|
|
780
|
+
tool_call_update: {
|
|
781
|
+
tool_call_id: "tool-1",
|
|
782
|
+
fields: {
|
|
783
|
+
status: "killed",
|
|
784
|
+
raw_output: "Task stopped by parent agent",
|
|
785
|
+
content: [
|
|
786
|
+
{
|
|
787
|
+
type: "content",
|
|
788
|
+
content: { type: "text", text: "Task stopped by parent agent" },
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
task_metadata: {
|
|
792
|
+
error: "Task stopped by parent agent",
|
|
793
|
+
end_time: 1234,
|
|
794
|
+
total_paused_ms: 250,
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
test("handleTaskSystemMessage merges task metadata patches into the linked task state", () => {
|
|
801
|
+
const session = makeSessionState();
|
|
802
|
+
captureBridgeEvents(() => {
|
|
803
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
804
|
+
task_id: "task-1",
|
|
805
|
+
tool_use_id: "tool-1",
|
|
806
|
+
description: "Initial task description",
|
|
807
|
+
});
|
|
808
|
+
handleTaskSystemMessage(session, "task_updated", {
|
|
809
|
+
task_id: "task-1",
|
|
810
|
+
patch: {
|
|
811
|
+
status: "running",
|
|
812
|
+
is_backgrounded: true,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
handleTaskSystemMessage(session, "task_updated", {
|
|
816
|
+
task_id: "task-1",
|
|
817
|
+
patch: {
|
|
818
|
+
error: "Task stopped by parent agent",
|
|
819
|
+
end_time: 1234,
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
assert.deepEqual(session.toolCalls.get("tool-1")?.task_metadata, {
|
|
824
|
+
is_backgrounded: true,
|
|
825
|
+
error: "Task stopped by parent agent",
|
|
826
|
+
end_time: 1234,
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
test("handleTaskSystemMessage skips unlinked task_updated messages", () => {
|
|
830
|
+
const session = makeSessionState();
|
|
831
|
+
const events = captureBridgeEvents(() => {
|
|
832
|
+
handleTaskSystemMessage(session, "task_updated", {
|
|
833
|
+
task_id: "task-missing",
|
|
834
|
+
patch: {
|
|
835
|
+
status: "running",
|
|
836
|
+
description: "This should not be emitted",
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
assert.equal(events.length, 0);
|
|
841
|
+
});
|
|
542
842
|
test("emitToolProgressUpdate does not reopen completed tools", () => {
|
|
543
843
|
const session = makeSessionState();
|
|
544
844
|
session.toolCalls.set("tool-1", {
|
|
@@ -548,7 +848,7 @@ test("emitToolProgressUpdate does not reopen completed tools", () => {
|
|
|
548
848
|
status: "completed",
|
|
549
849
|
content: [],
|
|
550
850
|
locations: [],
|
|
551
|
-
meta: { claudeCode: { toolName: "Bash" } },
|
|
851
|
+
meta: { claudeCode: { toolName: "Bash", parentToolUseId: null } },
|
|
552
852
|
});
|
|
553
853
|
const events = captureBridgeEvents(() => {
|
|
554
854
|
emitToolProgressUpdate(session, "tool-1", "Bash");
|
|
@@ -618,7 +918,7 @@ test("requestAskUserQuestionAnswers preserves previews and annotations in update
|
|
|
618
918
|
status: "in_progress",
|
|
619
919
|
content: [],
|
|
620
920
|
locations: [],
|
|
621
|
-
meta: { claudeCode: { toolName: "AskUserQuestion" } },
|
|
921
|
+
meta: { claudeCode: { toolName: "AskUserQuestion", parentToolUseId: null } },
|
|
622
922
|
};
|
|
623
923
|
const events = await captureBridgeEventsAsync(async () => {
|
|
624
924
|
const resultPromise = requestAskUserQuestionAnswers(session, "tool-question", {
|
|
@@ -698,7 +998,7 @@ test("requestAskUserQuestionAnswers preserves previews and annotations in update
|
|
|
698
998
|
status: "in_progress",
|
|
699
999
|
content: [],
|
|
700
1000
|
locations: [],
|
|
701
|
-
meta: { claudeCode: { toolName: "AskUserQuestion" } },
|
|
1001
|
+
meta: { claudeCode: { toolName: "AskUserQuestion", parentToolUseId: null } },
|
|
702
1002
|
raw_input: {
|
|
703
1003
|
prompt: {
|
|
704
1004
|
question: "Pick deployment target",
|
|
@@ -769,6 +1069,13 @@ test("parseRateLimitStatus accepts known values and rejects unknown values", ()
|
|
|
769
1069
|
assert.equal(parseRateLimitStatus("warn"), null);
|
|
770
1070
|
assert.equal(parseRateLimitStatus(undefined), null);
|
|
771
1071
|
});
|
|
1072
|
+
test("parseRuntimeSessionState accepts known values and rejects unknown values", () => {
|
|
1073
|
+
assert.equal(parseRuntimeSessionState("idle"), "idle");
|
|
1074
|
+
assert.equal(parseRuntimeSessionState("running"), "running");
|
|
1075
|
+
assert.equal(parseRuntimeSessionState("requires_action"), "requires_action");
|
|
1076
|
+
assert.equal(parseRuntimeSessionState("blocked"), null);
|
|
1077
|
+
assert.equal(parseRuntimeSessionState(undefined), null);
|
|
1078
|
+
});
|
|
772
1079
|
test("buildRateLimitUpdate maps SDK fields to wire shape", () => {
|
|
773
1080
|
const update = buildRateLimitUpdate({
|
|
774
1081
|
status: "allowed_warning",
|
|
@@ -803,6 +1110,135 @@ test("buildRateLimitUpdate rejects invalid payloads", () => {
|
|
|
803
1110
|
overageStatus: "bad_status",
|
|
804
1111
|
}), { type: "rate_limit_update", status: "rejected" });
|
|
805
1112
|
});
|
|
1113
|
+
test("buildApiRetryUpdate maps SDK api_retry messages to wire shape", () => {
|
|
1114
|
+
assert.deepEqual(buildApiRetryUpdate({
|
|
1115
|
+
attempt: 2,
|
|
1116
|
+
max_retries: 4,
|
|
1117
|
+
retry_delay_ms: 1500,
|
|
1118
|
+
error_status: 529,
|
|
1119
|
+
error: "server_error",
|
|
1120
|
+
}), {
|
|
1121
|
+
type: "api_retry_update",
|
|
1122
|
+
attempt: 2,
|
|
1123
|
+
max_retries: 4,
|
|
1124
|
+
retry_delay_ms: 1500,
|
|
1125
|
+
error_status: 529,
|
|
1126
|
+
error: "server_error",
|
|
1127
|
+
});
|
|
1128
|
+
assert.deepEqual(buildApiRetryUpdate({
|
|
1129
|
+
attempt: 1,
|
|
1130
|
+
maxRetries: 4,
|
|
1131
|
+
retryDelayMs: 1000,
|
|
1132
|
+
errorStatus: null,
|
|
1133
|
+
error: "unexpected",
|
|
1134
|
+
}), {
|
|
1135
|
+
type: "api_retry_update",
|
|
1136
|
+
attempt: 1,
|
|
1137
|
+
max_retries: 4,
|
|
1138
|
+
retry_delay_ms: 1000,
|
|
1139
|
+
error_status: null,
|
|
1140
|
+
error: "unknown",
|
|
1141
|
+
});
|
|
1142
|
+
assert.equal(buildApiRetryUpdate({ attempt: 1 }), null);
|
|
1143
|
+
});
|
|
1144
|
+
test("normalizeSettingsParseError accepts only SDK-shaped errors", () => {
|
|
1145
|
+
assert.deepEqual(normalizeSettingsParseError({
|
|
1146
|
+
file: "C:/work/.claude/settings.json",
|
|
1147
|
+
path: "permissions.allow",
|
|
1148
|
+
message: "Expected array",
|
|
1149
|
+
}), {
|
|
1150
|
+
file: "C:/work/.claude/settings.json",
|
|
1151
|
+
path: "permissions.allow",
|
|
1152
|
+
message: "Expected array",
|
|
1153
|
+
});
|
|
1154
|
+
assert.deepEqual(normalizeSettingsParseError({ path: "", message: "Invalid JSON" }), {
|
|
1155
|
+
path: "",
|
|
1156
|
+
message: "Invalid JSON",
|
|
1157
|
+
});
|
|
1158
|
+
assert.equal(normalizeSettingsParseError({ path: "", message: "" }), null);
|
|
1159
|
+
assert.equal(normalizeSettingsParseError("Invalid JSON"), null);
|
|
1160
|
+
});
|
|
1161
|
+
test("handleSdkMessage emits lifecycle compatibility session updates", () => {
|
|
1162
|
+
const session = makeSessionState();
|
|
1163
|
+
const events = captureBridgeEvents(() => {
|
|
1164
|
+
handleSdkMessage(session, {
|
|
1165
|
+
type: "prompt_suggestion",
|
|
1166
|
+
suggestion: "Write tests for this change",
|
|
1167
|
+
uuid: "message-1",
|
|
1168
|
+
session_id: "session-1",
|
|
1169
|
+
});
|
|
1170
|
+
handleSdkMessage(session, {
|
|
1171
|
+
type: "system",
|
|
1172
|
+
subtype: "api_retry",
|
|
1173
|
+
attempt: 1,
|
|
1174
|
+
max_retries: 4,
|
|
1175
|
+
retry_delay_ms: 1000,
|
|
1176
|
+
error_status: null,
|
|
1177
|
+
error: "server_error",
|
|
1178
|
+
uuid: "message-2",
|
|
1179
|
+
session_id: "session-1",
|
|
1180
|
+
});
|
|
1181
|
+
handleSdkMessage(session, {
|
|
1182
|
+
type: "system",
|
|
1183
|
+
subtype: "session_state_changed",
|
|
1184
|
+
state: "idle",
|
|
1185
|
+
uuid: "message-3",
|
|
1186
|
+
session_id: "session-1",
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
assert.deepEqual(events.map((event) => event.update), [
|
|
1190
|
+
{ type: "prompt_suggestion_update", suggestion: "Write tests for this change" },
|
|
1191
|
+
{
|
|
1192
|
+
type: "api_retry_update",
|
|
1193
|
+
attempt: 1,
|
|
1194
|
+
max_retries: 4,
|
|
1195
|
+
retry_delay_ms: 1000,
|
|
1196
|
+
error_status: null,
|
|
1197
|
+
error: "server_error",
|
|
1198
|
+
},
|
|
1199
|
+
{ type: "runtime_session_state_update", state: "idle" },
|
|
1200
|
+
]);
|
|
1201
|
+
});
|
|
1202
|
+
test("shouldEmitStartupAuthRequiredForAccount keeps legacy first-party behavior", () => {
|
|
1203
|
+
assert.equal(shouldEmitStartupAuthRequiredForAccount({}), true);
|
|
1204
|
+
assert.equal(shouldEmitStartupAuthRequiredForAccount({ apiProvider: "firstParty" }), true);
|
|
1205
|
+
assert.equal(shouldEmitStartupAuthRequiredForAccount({
|
|
1206
|
+
apiProvider: "firstParty",
|
|
1207
|
+
apiKeySource: "oauth",
|
|
1208
|
+
}), false);
|
|
1209
|
+
assert.equal(shouldEmitStartupAuthRequiredForAccount({
|
|
1210
|
+
apiProvider: "firstParty",
|
|
1211
|
+
email: "user@example.com",
|
|
1212
|
+
}), false);
|
|
1213
|
+
});
|
|
1214
|
+
test("shouldEmitStartupAuthRequiredForAccount skips Claude OAuth hint for external providers", () => {
|
|
1215
|
+
for (const apiProvider of [
|
|
1216
|
+
"bedrock",
|
|
1217
|
+
"vertex",
|
|
1218
|
+
"foundry",
|
|
1219
|
+
"anthropicAws",
|
|
1220
|
+
"mantle",
|
|
1221
|
+
]) {
|
|
1222
|
+
assert.equal(shouldEmitStartupAuthRequiredForAccount({ apiProvider }), false);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
test("handleSdkMessage emits settings parse errors from defensive payloads", () => {
|
|
1226
|
+
const session = makeSessionState();
|
|
1227
|
+
const events = captureBridgeEvents(() => {
|
|
1228
|
+
handleSdkMessage(session, {
|
|
1229
|
+
type: "settings_parse_error",
|
|
1230
|
+
file: "C:/work/.claude/settings.json",
|
|
1231
|
+
path: "permissions.allow",
|
|
1232
|
+
message: "Expected array",
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
assert.deepEqual(events.at(-1)?.update, {
|
|
1236
|
+
type: "settings_parse_error",
|
|
1237
|
+
file: "C:/work/.claude/settings.json",
|
|
1238
|
+
path: "permissions.allow",
|
|
1239
|
+
message: "Expected array",
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
806
1242
|
test("mapAvailableAgents normalizes and deduplicates agents", () => {
|
|
807
1243
|
const agents = mapAvailableAgents([
|
|
808
1244
|
{ name: "reviewer", description: "", model: "" },
|
|
@@ -835,7 +1271,13 @@ test("createToolCall builds edit diff content", () => {
|
|
|
835
1271
|
old: "old",
|
|
836
1272
|
new: "new",
|
|
837
1273
|
});
|
|
838
|
-
assert.deepEqual(toolCall.meta, { claudeCode: { toolName: "Edit" } });
|
|
1274
|
+
assert.deepEqual(toolCall.meta, { claudeCode: { toolName: "Edit", parentToolUseId: null } });
|
|
1275
|
+
});
|
|
1276
|
+
test("createToolCall preserves parent tool linkage metadata", () => {
|
|
1277
|
+
const toolCall = createToolCall("tc-child", "Bash", { command: "echo hi" }, "tc-parent");
|
|
1278
|
+
assert.deepEqual(toolCall.meta, {
|
|
1279
|
+
claudeCode: { toolName: "Bash", parentToolUseId: "tc-parent" },
|
|
1280
|
+
});
|
|
839
1281
|
});
|
|
840
1282
|
test("createToolCall builds write preview diff content", () => {
|
|
841
1283
|
const toolCall = createToolCall("tc-w", "Write", {
|
|
@@ -984,27 +1426,23 @@ test("buildToolResultFields preserves Edit diff content from input and structure
|
|
|
984
1426
|
},
|
|
985
1427
|
]);
|
|
986
1428
|
});
|
|
987
|
-
test("buildToolResultFields
|
|
1429
|
+
test("buildToolResultFields ignores model-facing Bash stale read hints", () => {
|
|
988
1430
|
const base = createToolCall("tc-bash", "Bash", { command: "npm test" });
|
|
989
1431
|
const fields = buildToolResultFields(false, {
|
|
990
1432
|
stdout: "real stdout",
|
|
991
1433
|
stderr: "",
|
|
992
1434
|
interrupted: false,
|
|
993
|
-
|
|
1435
|
+
staleReadFileStateHint: "src/main.rs changed while command ran",
|
|
994
1436
|
}, base, {
|
|
995
1437
|
result: {
|
|
996
1438
|
stdout: "real stdout",
|
|
997
1439
|
stderr: "",
|
|
998
1440
|
interrupted: false,
|
|
999
|
-
|
|
1441
|
+
staleReadFileStateHint: "src/main.rs changed while command ran",
|
|
1000
1442
|
},
|
|
1001
1443
|
});
|
|
1002
1444
|
assert.equal(fields.raw_output, "real stdout");
|
|
1003
|
-
assert.
|
|
1004
|
-
bash: {
|
|
1005
|
-
token_saver_active: true,
|
|
1006
|
-
},
|
|
1007
|
-
});
|
|
1445
|
+
assert.equal(fields.output_metadata, undefined);
|
|
1008
1446
|
});
|
|
1009
1447
|
test("buildToolResultFields adds Bash auto-backgrounded metadata and message", () => {
|
|
1010
1448
|
const base = createToolCall("tc-bash-bg", "Bash", { command: "npm run watch" });
|
|
@@ -1235,7 +1673,7 @@ test("looksLikeAuthRequired detects login hints", () => {
|
|
|
1235
1673
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
1236
1674
|
});
|
|
1237
1675
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
1238
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.
|
|
1676
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.104");
|
|
1239
1677
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
1240
1678
|
});
|
|
1241
1679
|
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
@@ -1297,6 +1735,87 @@ test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
|
1297
1735
|
assert.equal(variantCounts.get("tool_call"), 1);
|
|
1298
1736
|
assert.equal(variantCounts.get("tool_call_update"), 1);
|
|
1299
1737
|
});
|
|
1738
|
+
test("mapSessionMessagesToUpdates preserves parallel tool results", () => {
|
|
1739
|
+
const updates = mapSessionMessagesToUpdates([
|
|
1740
|
+
{
|
|
1741
|
+
type: "assistant",
|
|
1742
|
+
uuid: "a1",
|
|
1743
|
+
session_id: "s1",
|
|
1744
|
+
parent_tool_use_id: null,
|
|
1745
|
+
message: {
|
|
1746
|
+
role: "assistant",
|
|
1747
|
+
content: [
|
|
1748
|
+
{ type: "tool_use", id: "tool-a", name: "Bash", input: { command: "echo a" } },
|
|
1749
|
+
{ type: "tool_use", id: "tool-b", name: "Bash", input: { command: "echo b" } },
|
|
1750
|
+
],
|
|
1751
|
+
},
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
type: "user",
|
|
1755
|
+
uuid: "u1",
|
|
1756
|
+
session_id: "s1",
|
|
1757
|
+
parent_tool_use_id: null,
|
|
1758
|
+
message: {
|
|
1759
|
+
role: "user",
|
|
1760
|
+
content: [
|
|
1761
|
+
{
|
|
1762
|
+
type: "tool_result",
|
|
1763
|
+
tool_use_id: "tool-b",
|
|
1764
|
+
content: "result b",
|
|
1765
|
+
is_error: false,
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
type: "tool_result",
|
|
1769
|
+
tool_use_id: "tool-a",
|
|
1770
|
+
content: "result a",
|
|
1771
|
+
is_error: false,
|
|
1772
|
+
},
|
|
1773
|
+
],
|
|
1774
|
+
},
|
|
1775
|
+
},
|
|
1776
|
+
]);
|
|
1777
|
+
const toolCalls = updates.filter((update) => update.type === "tool_call");
|
|
1778
|
+
const toolUpdates = updates.filter((update) => update.type === "tool_call_update");
|
|
1779
|
+
assert.deepEqual(toolCalls.map((update) => update.tool_call.tool_call_id), ["tool-a", "tool-b"]);
|
|
1780
|
+
assert.deepEqual(toolUpdates.map((update) => update.tool_call_update.tool_call_id), ["tool-b", "tool-a"]);
|
|
1781
|
+
assert.deepEqual(toolUpdates.map((update) => update.tool_call_update.fields.raw_output), ["result b", "result a"]);
|
|
1782
|
+
});
|
|
1783
|
+
test("handleResultMessage emits terminal reason on successful turn completion", () => {
|
|
1784
|
+
const session = makeSessionState();
|
|
1785
|
+
const events = captureBridgeEvents(() => {
|
|
1786
|
+
handleResultMessage(session, {
|
|
1787
|
+
type: "result",
|
|
1788
|
+
subtype: "success",
|
|
1789
|
+
terminal_reason: "completed",
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
const lastEvent = events.at(-1);
|
|
1793
|
+
assert.deepEqual(lastEvent, {
|
|
1794
|
+
event: "turn_complete",
|
|
1795
|
+
session_id: "session-1",
|
|
1796
|
+
terminal_reason: "completed",
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
1799
|
+
test("handleResultMessage emits terminal reason on turn errors", () => {
|
|
1800
|
+
const session = makeSessionState();
|
|
1801
|
+
const events = captureBridgeEvents(() => {
|
|
1802
|
+
handleResultMessage(session, {
|
|
1803
|
+
type: "result",
|
|
1804
|
+
subtype: "error_max_turns",
|
|
1805
|
+
terminal_reason: "max_turns",
|
|
1806
|
+
errors: ["max turns exceeded"],
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
const lastEvent = events.at(-1);
|
|
1810
|
+
assert.deepEqual(lastEvent, {
|
|
1811
|
+
event: "turn_error",
|
|
1812
|
+
session_id: "session-1",
|
|
1813
|
+
message: "max turns exceeded",
|
|
1814
|
+
error_kind: "plan_limit",
|
|
1815
|
+
sdk_result_subtype: "error_max_turns",
|
|
1816
|
+
terminal_reason: "max_turns",
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1300
1819
|
test("mapSessionMessagesToUpdates ignores unsupported records", () => {
|
|
1301
1820
|
const updates = mapSessionMessagesToUpdates([
|
|
1302
1821
|
{
|
|
@@ -1360,19 +1879,62 @@ test("buildSessionListOptions scopes repo-local listings to worktrees", () => {
|
|
|
1360
1879
|
limit: 50,
|
|
1361
1880
|
});
|
|
1362
1881
|
});
|
|
1363
|
-
test("buildToolResultFields
|
|
1364
|
-
const base = createToolCall("tc-
|
|
1365
|
-
const fields = buildToolResultFields(false,
|
|
1882
|
+
test("buildToolResultFields renders file_unchanged Read results compactly", () => {
|
|
1883
|
+
const base = createToolCall("tc-read", "Read", { file_path: "src/main.rs" });
|
|
1884
|
+
const fields = buildToolResultFields(false, {
|
|
1885
|
+
type: "file_unchanged",
|
|
1886
|
+
file: { filePath: "src/main.rs" },
|
|
1887
|
+
}, base, {
|
|
1366
1888
|
result: {
|
|
1367
|
-
|
|
1368
|
-
|
|
1889
|
+
type: "file_unchanged",
|
|
1890
|
+
file: { filePath: "src/main.rs" },
|
|
1369
1891
|
},
|
|
1370
1892
|
});
|
|
1371
|
-
assert.
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1893
|
+
assert.equal(fields.raw_output, "File unchanged: src/main.rs");
|
|
1894
|
+
assert.deepEqual(fields.content, [
|
|
1895
|
+
{ type: "content", content: { type: "text", text: "File unchanged: src/main.rs" } },
|
|
1896
|
+
]);
|
|
1897
|
+
});
|
|
1898
|
+
test("buildToolResultFields renders array-wrapped file_unchanged Read results compactly", () => {
|
|
1899
|
+
const base = createToolCall("tc-read", "Read", { file_path: "src/lib.rs" });
|
|
1900
|
+
const fields = buildToolResultFields(false, [], base, {
|
|
1901
|
+
result: [
|
|
1902
|
+
{
|
|
1903
|
+
type: "file_unchanged",
|
|
1904
|
+
file: { filePath: "src/lib.rs" },
|
|
1905
|
+
},
|
|
1906
|
+
],
|
|
1375
1907
|
});
|
|
1908
|
+
assert.equal(fields.raw_output, "File unchanged: src/lib.rs");
|
|
1909
|
+
});
|
|
1910
|
+
test("buildToolResultFields uses Agent output agentType as task title", () => {
|
|
1911
|
+
const base = createToolCall("tc-agent", "Agent", { prompt: "Review tests" });
|
|
1912
|
+
const fields = buildToolResultFields(false, {
|
|
1913
|
+
agentId: "agent-1",
|
|
1914
|
+
agentType: "reviewer",
|
|
1915
|
+
content: [{ type: "text", text: "Done" }],
|
|
1916
|
+
totalToolUseCount: 0,
|
|
1917
|
+
totalDurationMs: 10,
|
|
1918
|
+
totalTokens: 20,
|
|
1919
|
+
usage: {},
|
|
1920
|
+
status: "completed",
|
|
1921
|
+
prompt: "Review tests",
|
|
1922
|
+
}, base);
|
|
1923
|
+
assert.equal(fields.title, "reviewer");
|
|
1924
|
+
});
|
|
1925
|
+
test("buildToolResultFields reads array-wrapped Agent output agentType", () => {
|
|
1926
|
+
const base = createToolCall("tc-agent", "Agent", { prompt: "Review tests" });
|
|
1927
|
+
const fields = buildToolResultFields(false, [], base, {
|
|
1928
|
+
result: [
|
|
1929
|
+
{
|
|
1930
|
+
agentId: "agent-1",
|
|
1931
|
+
agentType: "planner",
|
|
1932
|
+
content: [{ type: "text", text: "Done" }],
|
|
1933
|
+
status: "completed",
|
|
1934
|
+
},
|
|
1935
|
+
],
|
|
1936
|
+
});
|
|
1937
|
+
assert.equal(fields.title, "planner");
|
|
1376
1938
|
});
|
|
1377
1939
|
test("buildToolResultFields extracts TodoWrite verification metadata from structured results", () => {
|
|
1378
1940
|
const base = createToolCall("tc-todo", "TodoWrite", {
|
|
@@ -1430,3 +1992,183 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
1430
1992
|
},
|
|
1431
1993
|
]);
|
|
1432
1994
|
});
|
|
1995
|
+
test("resolveCurrentModel keeps 1M context suffix in short and long display names", () => {
|
|
1996
|
+
const session = makeSessionState();
|
|
1997
|
+
session.resolvedRuntimeModelId = "claude-opus-4-6[1m]";
|
|
1998
|
+
const currentModel = resolveCurrentModel(session);
|
|
1999
|
+
assert.equal(currentModel.display_name_short, "Opus [1M]");
|
|
2000
|
+
assert.equal(currentModel.display_name_long, "Opus 4.6 [1M]");
|
|
2001
|
+
});
|
|
2002
|
+
test("resolveCurrentModel does not inherit standard Opus capabilities for 1M when sibling variants exist", () => {
|
|
2003
|
+
const session = makeSessionState();
|
|
2004
|
+
session.requestedModelId = "claude-opus-4-6";
|
|
2005
|
+
session.resolvedRuntimeModelId = "claude-opus-4-6[1m]";
|
|
2006
|
+
session.availableModels = [
|
|
2007
|
+
{
|
|
2008
|
+
id: "claude-opus-4-6",
|
|
2009
|
+
display_name: "Claude Opus",
|
|
2010
|
+
supports_effort: true,
|
|
2011
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
id: "claude-opus-4-6[1m]",
|
|
2015
|
+
display_name: "Claude Opus 1M",
|
|
2016
|
+
supports_effort: false,
|
|
2017
|
+
supported_effort_levels: [],
|
|
2018
|
+
},
|
|
2019
|
+
];
|
|
2020
|
+
const currentModel = resolveCurrentModel(session);
|
|
2021
|
+
assert.equal(currentModel.catalog_id, "claude-opus-4-6[1m]");
|
|
2022
|
+
assert.equal(currentModel.supports_effort, false);
|
|
2023
|
+
});
|
|
2024
|
+
test("resolveCurrentModel avoids suffix-insensitive fallback when sibling variants make it ambiguous", () => {
|
|
2025
|
+
const session = makeSessionState();
|
|
2026
|
+
session.requestedModelId = "claude-opus-4-6";
|
|
2027
|
+
session.resolvedRuntimeModelId = "claude-opus-4-6[1m]";
|
|
2028
|
+
session.availableModels = [
|
|
2029
|
+
{
|
|
2030
|
+
id: "claude-opus-4-6",
|
|
2031
|
+
display_name: "Claude Opus",
|
|
2032
|
+
supports_effort: true,
|
|
2033
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
id: "claude-opus-4-6-alt[1m]",
|
|
2037
|
+
display_name: "Claude Opus Alt 1M",
|
|
2038
|
+
supports_effort: false,
|
|
2039
|
+
supported_effort_levels: [],
|
|
2040
|
+
},
|
|
2041
|
+
];
|
|
2042
|
+
const currentModel = resolveCurrentModel(session);
|
|
2043
|
+
assert.equal(currentModel.catalog_id, undefined);
|
|
2044
|
+
assert.equal(currentModel.supports_effort, false);
|
|
2045
|
+
});
|
|
2046
|
+
test("emitCurrentModelUpdate can acknowledge a successful no-op set_model", () => {
|
|
2047
|
+
const session = makeSessionState();
|
|
2048
|
+
session.model = "default";
|
|
2049
|
+
session.requestedModelId = "default";
|
|
2050
|
+
session.resolvedRuntimeModelId = "claude-opus-4-6[1m]";
|
|
2051
|
+
refreshCurrentModel(session);
|
|
2052
|
+
const events = captureBridgeEvents(() => {
|
|
2053
|
+
const changed = refreshCurrentModel(session, true);
|
|
2054
|
+
const forced = !changed && emitCurrentModelUpdate(session);
|
|
2055
|
+
assert.equal(changed, false);
|
|
2056
|
+
assert.equal(forced, true);
|
|
2057
|
+
});
|
|
2058
|
+
const lastEvent = events.at(-1);
|
|
2059
|
+
assert.ok(lastEvent);
|
|
2060
|
+
assert.equal(lastEvent.event, "session_update");
|
|
2061
|
+
assert.deepEqual(lastEvent.update, {
|
|
2062
|
+
type: "current_model_update",
|
|
2063
|
+
current_model: {
|
|
2064
|
+
requested_id: "default",
|
|
2065
|
+
resolved_id: "claude-opus-4-6[1m]",
|
|
2066
|
+
display_name_short: "Opus [1M]",
|
|
2067
|
+
display_name_long: "Opus 4.6 [1M]",
|
|
2068
|
+
supports_effort: false,
|
|
2069
|
+
supported_effort_levels: [],
|
|
2070
|
+
is_authoritative: true,
|
|
2071
|
+
},
|
|
2072
|
+
});
|
|
2073
|
+
});
|
|
2074
|
+
test("emitCurrentModelUpdate can publish catalog-enriched current model metadata after connect", () => {
|
|
2075
|
+
const session = makeSessionState();
|
|
2076
|
+
session.model = "sonnet";
|
|
2077
|
+
refreshCurrentModel(session);
|
|
2078
|
+
session.availableModels = [
|
|
2079
|
+
{
|
|
2080
|
+
id: "sonnet",
|
|
2081
|
+
display_name: "Claude Sonnet",
|
|
2082
|
+
supports_effort: true,
|
|
2083
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
2084
|
+
supports_auto_mode: true,
|
|
2085
|
+
},
|
|
2086
|
+
];
|
|
2087
|
+
const events = captureBridgeEvents(() => {
|
|
2088
|
+
const changed = refreshCurrentModel(session, false);
|
|
2089
|
+
assert.equal(changed, true);
|
|
2090
|
+
assert.equal(emitCurrentModelUpdate(session), true);
|
|
2091
|
+
});
|
|
2092
|
+
const lastEvent = events.at(-1);
|
|
2093
|
+
assert.ok(lastEvent);
|
|
2094
|
+
assert.equal(lastEvent.event, "session_update");
|
|
2095
|
+
assert.deepEqual(lastEvent.update, {
|
|
2096
|
+
type: "current_model_update",
|
|
2097
|
+
current_model: {
|
|
2098
|
+
resolved_id: "sonnet",
|
|
2099
|
+
display_name_short: "Sonnet",
|
|
2100
|
+
display_name_long: "Sonnet",
|
|
2101
|
+
catalog_id: "sonnet",
|
|
2102
|
+
supports_effort: true,
|
|
2103
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
2104
|
+
supports_auto_mode: true,
|
|
2105
|
+
is_authoritative: true,
|
|
2106
|
+
},
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
test("shouldInvalidateResolvedRuntimeModel invalidates stale runtime identity only when the request changes", () => {
|
|
2110
|
+
assert.equal(shouldInvalidateResolvedRuntimeModel("default", "default", "sonnet"), true);
|
|
2111
|
+
assert.equal(shouldInvalidateResolvedRuntimeModel("sonnet", "sonnet", "haiku"), true);
|
|
2112
|
+
assert.equal(shouldInvalidateResolvedRuntimeModel("default", "default", "default"), false);
|
|
2113
|
+
});
|
|
2114
|
+
test("resolveCurrentModel falls back to the requested model immediately after stale runtime identity is cleared", () => {
|
|
2115
|
+
const session = makeSessionState();
|
|
2116
|
+
session.requestedModelId = "sonnet";
|
|
2117
|
+
session.model = "sonnet";
|
|
2118
|
+
session.availableModels = [
|
|
2119
|
+
{
|
|
2120
|
+
id: "sonnet",
|
|
2121
|
+
display_name: "Claude Sonnet",
|
|
2122
|
+
supports_effort: true,
|
|
2123
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
2124
|
+
},
|
|
2125
|
+
];
|
|
2126
|
+
const currentModel = resolveCurrentModel(session);
|
|
2127
|
+
assert.equal(currentModel.resolved_id, "sonnet");
|
|
2128
|
+
assert.equal(currentModel.display_name_short, "Sonnet");
|
|
2129
|
+
assert.equal(currentModel.display_name_long, "Sonnet");
|
|
2130
|
+
assert.equal(currentModel.catalog_id, "sonnet");
|
|
2131
|
+
assert.equal(currentModel.supports_effort, true);
|
|
2132
|
+
});
|
|
2133
|
+
test("attachRequestUserDialogInterceptor rejects request_user_dialog with a stable error", async () => {
|
|
2134
|
+
const calls = [];
|
|
2135
|
+
const fakeQuery = {
|
|
2136
|
+
async processControlRequest(request, _signal) {
|
|
2137
|
+
calls.push(request);
|
|
2138
|
+
return { ok: true };
|
|
2139
|
+
},
|
|
2140
|
+
};
|
|
2141
|
+
assert.equal(attachRequestUserDialogInterceptor(fakeQuery, () => "session-test"), true);
|
|
2142
|
+
await assert.rejects(fakeQuery.processControlRequest({
|
|
2143
|
+
request_id: "dialog-1",
|
|
2144
|
+
request: {
|
|
2145
|
+
subtype: "request_user_dialog",
|
|
2146
|
+
dialog_kind: "computer_use_approval",
|
|
2147
|
+
payload: { title: "Need approval", kind: "computer_use_approval" },
|
|
2148
|
+
tool_use_id: "tool-1",
|
|
2149
|
+
},
|
|
2150
|
+
}, new AbortController().signal), /request_user_dialog is not supported by claude-rs yet \(dialog_kind: computer_use_approval\)/);
|
|
2151
|
+
assert.equal(calls.length, 0);
|
|
2152
|
+
});
|
|
2153
|
+
test("attachRequestUserDialogInterceptor preserves non-dialog control requests", async () => {
|
|
2154
|
+
const calls = [];
|
|
2155
|
+
const fakeQuery = {
|
|
2156
|
+
async processControlRequest(request, _signal) {
|
|
2157
|
+
calls.push(request);
|
|
2158
|
+
return { ok: true };
|
|
2159
|
+
},
|
|
2160
|
+
};
|
|
2161
|
+
attachRequestUserDialogInterceptor(fakeQuery, () => "session-test");
|
|
2162
|
+
const result = await fakeQuery.processControlRequest({
|
|
2163
|
+
request_id: "permission-1",
|
|
2164
|
+
request: {
|
|
2165
|
+
subtype: "can_use_tool",
|
|
2166
|
+
tool_name: "Bash",
|
|
2167
|
+
input: { command: "dir" },
|
|
2168
|
+
tool_use_id: "tool-1",
|
|
2169
|
+
},
|
|
2170
|
+
}, new AbortController().signal);
|
|
2171
|
+
assert.deepEqual(result, { ok: true });
|
|
2172
|
+
assert.equal(calls.length, 1);
|
|
2173
|
+
assert.equal(calls[0]?.request.subtype, "can_use_tool");
|
|
2174
|
+
});
|