@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.3
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/docs/desktop-browser-cli.md +33 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +50 -4
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +33 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/credential-routes.test.ts +38 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +6 -2
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +185 -30
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/oauth-commands-routes.test.ts +55 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +69 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +132 -0
- package/src/calls/voice-session-bridge.ts +56 -26
- package/src/calls/voice-triage-escalate.ts +3 -3
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +43 -4
- package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +11 -3
- package/src/config/loader.ts +1 -0
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/services.ts +10 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-surfaces.ts +6 -1
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/host-cu-proxy.ts +94 -31
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +143 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
- package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +47 -0
- package/src/live-voice/__tests__/session-controls.test.ts +113 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +88 -7
- package/src/live-voice/protocol.ts +69 -0
- package/src/live-voice/session-controls.ts +139 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
- package/src/oauth/connection-resolver.test.ts +27 -0
- package/src/oauth/connection-resolver.ts +25 -1
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/seed-providers.ts +121 -2
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/app-management-routes.ts +17 -2
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/credential-routes.ts +1 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/oauth-commands-routes.ts +14 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +3 -4
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/watch/watch-retro.ts +6 -8
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -107,6 +107,51 @@ describe("testInferenceConnection", () => {
|
|
|
107
107
|
expect(calls[0].body).not.toHaveProperty(OPENCODE_SESSION_HEADER);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
test("probes /responses for an OpenCode responses-only model", async () => {
|
|
111
|
+
const calls: ProbeCall[] = [];
|
|
112
|
+
const result = await testInferenceConnection(
|
|
113
|
+
{
|
|
114
|
+
provider: "opencode",
|
|
115
|
+
auth: { type: "none" },
|
|
116
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
117
|
+
models: [{ id: "muse-spark-1.2-contributor-free" }],
|
|
118
|
+
},
|
|
119
|
+
stubFetch(200, calls),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(result).toMatchObject({
|
|
123
|
+
ok: true,
|
|
124
|
+
resolved_url: "https://opencode.ai/zen/v1/responses",
|
|
125
|
+
});
|
|
126
|
+
expect(calls[0].body).toMatchObject({
|
|
127
|
+
model: "muse-spark-1.2-contributor-free",
|
|
128
|
+
input: "ping",
|
|
129
|
+
max_output_tokens: 16,
|
|
130
|
+
});
|
|
131
|
+
expect(calls[0].headers[OPENCODE_SESSION_HEADER]).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("keeps other OpenCode models on /chat/completions", async () => {
|
|
135
|
+
const calls: ProbeCall[] = [];
|
|
136
|
+
const result = await testInferenceConnection(
|
|
137
|
+
{
|
|
138
|
+
provider: "opencode",
|
|
139
|
+
auth: { type: "none" },
|
|
140
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
141
|
+
models: [{ id: "mimo-v2.5-free" }],
|
|
142
|
+
},
|
|
143
|
+
stubFetch(200, calls),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(result?.resolved_url).toBe(
|
|
147
|
+
"https://opencode.ai/zen/v1/chat/completions",
|
|
148
|
+
);
|
|
149
|
+
expect(calls[0].body).toMatchObject({
|
|
150
|
+
model: "mimo-v2.5-free",
|
|
151
|
+
max_tokens: 1,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
110
155
|
test("does not send OpenCode headers to other providers", async () => {
|
|
111
156
|
const calls: ProbeCall[] = [];
|
|
112
157
|
await testInferenceConnection(CONNECTION, stubFetch(200, calls));
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
12
|
import { getLogger } from "../../util/logger.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
OPENCODE_RESPONSES_ONLY_MODELS,
|
|
15
|
+
resolveOpenCodeRequestHeaders,
|
|
16
|
+
} from "../opencode/client.js";
|
|
14
17
|
import type { Auth, ConnectionModel } from "./auth.js";
|
|
15
18
|
import { resolveAuth } from "./resolve-auth.js";
|
|
16
19
|
|
|
@@ -46,10 +49,12 @@ function hintForStatus(status: number): string {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
|
-
* Probe a connection's custom endpoint with a minimal
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
52
|
+
* Probe a connection's custom endpoint with a minimal request on the wire
|
|
53
|
+
* its first model is served over: chat completions (`max_tokens: 1`), or
|
|
54
|
+
* the Responses API for OpenCode's responses-only models, which answer
|
|
55
|
+
* `/chat/completions` with HTTP 500. Returns `null` when there is nothing
|
|
56
|
+
* to probe: no custom base URL, no model id to send, or auth that cannot be
|
|
57
|
+
* resolved (a missing credential surfaces through its own error path).
|
|
53
58
|
*/
|
|
54
59
|
export async function testInferenceConnection(
|
|
55
60
|
connection: {
|
|
@@ -78,7 +83,12 @@ export async function testInferenceConnection(
|
|
|
78
83
|
const providerHeaders =
|
|
79
84
|
connection.provider === "opencode" ? resolveOpenCodeRequestHeaders() : {};
|
|
80
85
|
|
|
81
|
-
const
|
|
86
|
+
const useResponses =
|
|
87
|
+
connection.provider === "opencode" &&
|
|
88
|
+
OPENCODE_RESPONSES_ONLY_MODELS.has(model);
|
|
89
|
+
const url = `${connection.baseUrl.replace(/\/+$/, "")}${
|
|
90
|
+
useResponses ? "/responses" : "/chat/completions"
|
|
91
|
+
}`;
|
|
82
92
|
try {
|
|
83
93
|
const res = await fetchImpl(url, {
|
|
84
94
|
method: "POST",
|
|
@@ -87,12 +97,17 @@ export async function testInferenceConnection(
|
|
|
87
97
|
...providerHeaders,
|
|
88
98
|
"Content-Type": "application/json",
|
|
89
99
|
},
|
|
90
|
-
body: JSON.stringify(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
body: JSON.stringify(
|
|
101
|
+
useResponses
|
|
102
|
+
? // The Responses API rejects output budgets below 16 tokens.
|
|
103
|
+
{ model, input: "ping", max_output_tokens: 16, stream: false }
|
|
104
|
+
: {
|
|
105
|
+
model,
|
|
106
|
+
messages: [{ role: "user", content: "ping" }],
|
|
107
|
+
max_tokens: 1,
|
|
108
|
+
stream: false,
|
|
109
|
+
},
|
|
110
|
+
),
|
|
96
111
|
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
97
112
|
});
|
|
98
113
|
void res.body?.cancel();
|
|
@@ -109,7 +109,9 @@ function mapResponsesReasoningEffort(
|
|
|
109
109
|
return "none";
|
|
110
110
|
}
|
|
111
111
|
const clamped = clampReasoningEffort(raw, effortCeilingForModel(model));
|
|
112
|
-
return supported
|
|
112
|
+
return supported
|
|
113
|
+
? snapReasoningEffortToSupported(clamped, supported)
|
|
114
|
+
: clamped;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/** Values accepted by the Responses API `text.verbosity` parameter. */
|
|
@@ -280,7 +282,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
280
282
|
const effort = configObj?.effort as string | undefined;
|
|
281
283
|
const verbosity = configObj?.verbosity as string | undefined;
|
|
282
284
|
const usageAttributionHeaders = configObj?.usageAttributionHeaders as
|
|
283
|
-
Record<string, string>
|
|
285
|
+
| Record<string, string>
|
|
286
|
+
| undefined;
|
|
287
|
+
const perRequestHeaders = configObj?.requestHeaders as
|
|
288
|
+
| Record<string, string>
|
|
289
|
+
| undefined;
|
|
284
290
|
const disableCache = configObj?.disableCache === true;
|
|
285
291
|
const disableTurnStartCache = configObj?.disableTurnStartCache === true;
|
|
286
292
|
const promptCacheKey =
|
|
@@ -463,6 +469,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
463
469
|
const requestHeaders = {
|
|
464
470
|
...this.requestHeaders,
|
|
465
471
|
...(usageAttributionHeaders ?? {}),
|
|
472
|
+
...(perRequestHeaders ?? {}),
|
|
466
473
|
};
|
|
467
474
|
const stream = await responsesApi.create(
|
|
468
475
|
{ ...params, stream: true },
|
|
@@ -9,18 +9,72 @@ import {
|
|
|
9
9
|
EMPTY_ASSISTANT_TURN_PLACEHOLDER,
|
|
10
10
|
OpenAIChatCompletionsProvider,
|
|
11
11
|
} from "../openai/chat-completions-provider.js";
|
|
12
|
+
import { OpenAIResponsesProvider } from "../openai/responses-provider.js";
|
|
13
|
+
import type { Message } from "../types.js";
|
|
12
14
|
import {
|
|
13
15
|
buildOpenCodeRequestHeaders,
|
|
14
16
|
OPENCODE_GO_BASE_URL,
|
|
15
17
|
OPENCODE_REQUEST_HEADER,
|
|
18
|
+
OPENCODE_RESPONSES_ONLY_MODELS,
|
|
16
19
|
OPENCODE_SESSION_HEADER,
|
|
17
20
|
OPENCODE_ZEN_BASE_URL,
|
|
18
21
|
OpenCodeProvider,
|
|
22
|
+
OpenCodeResponsesProvider,
|
|
19
23
|
resetOpenCodeFallbackSessionForTests,
|
|
20
24
|
resolveOpenCodeBaseURL,
|
|
21
25
|
resolveOpenCodeRequestHeaders,
|
|
22
26
|
} from "./client.js";
|
|
23
27
|
|
|
28
|
+
const USER_TURN: Message[] = [
|
|
29
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Stub both wires on an OpenCodeProvider and record which one was called. */
|
|
33
|
+
function stubTransports(provider: OpenCodeProvider) {
|
|
34
|
+
const chat: unknown[] = [];
|
|
35
|
+
const responses: Array<{ params: unknown; options: unknown }> = [];
|
|
36
|
+
(provider as unknown as { client: unknown }).client = {
|
|
37
|
+
chat: {
|
|
38
|
+
completions: {
|
|
39
|
+
create: async (params: unknown) => {
|
|
40
|
+
chat.push(params);
|
|
41
|
+
return {
|
|
42
|
+
async *[Symbol.asyncIterator]() {
|
|
43
|
+
yield {
|
|
44
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
45
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const inner = (
|
|
54
|
+
provider as unknown as { getResponsesInner(): OpenCodeResponsesProvider }
|
|
55
|
+
).getResponsesInner();
|
|
56
|
+
(inner as unknown as { client: unknown }).client = {
|
|
57
|
+
responses: {
|
|
58
|
+
create: async (params: unknown, options: unknown) => {
|
|
59
|
+
responses.push({ params, options });
|
|
60
|
+
return {
|
|
61
|
+
async *[Symbol.asyncIterator]() {
|
|
62
|
+
yield { type: "response.output_text.delta", delta: "ok" };
|
|
63
|
+
yield {
|
|
64
|
+
type: "response.completed",
|
|
65
|
+
response: {
|
|
66
|
+
status: "completed",
|
|
67
|
+
usage: { input_tokens: 2, output_tokens: 1 },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return { chat, responses };
|
|
76
|
+
}
|
|
77
|
+
|
|
24
78
|
describe("resolveOpenCodeBaseURL", () => {
|
|
25
79
|
test("defaults to OpenCode Zen", () => {
|
|
26
80
|
expect(resolveOpenCodeBaseURL()).toBe(OPENCODE_ZEN_BASE_URL);
|
|
@@ -241,3 +295,116 @@ describe("OpenCodeProvider", () => {
|
|
|
241
295
|
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
242
296
|
});
|
|
243
297
|
});
|
|
298
|
+
|
|
299
|
+
describe("OPENCODE_RESPONSES_ONLY_MODELS", () => {
|
|
300
|
+
test("lists the verified responses-only models and nothing else", () => {
|
|
301
|
+
expect([...OPENCODE_RESPONSES_ONLY_MODELS].sort()).toEqual([
|
|
302
|
+
"muse-spark-1.2-contributor-free",
|
|
303
|
+
"muse-spark-1.3-contributor-free",
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("OpenCodeResponsesProvider", () => {
|
|
309
|
+
test("is the Responses transport under the opencode name on OpenCode Zen", () => {
|
|
310
|
+
const provider = new OpenCodeResponsesProvider(
|
|
311
|
+
"sk-test",
|
|
312
|
+
"muse-spark-1.2-contributor-free",
|
|
313
|
+
);
|
|
314
|
+
expect(provider).toBeInstanceOf(OpenAIResponsesProvider);
|
|
315
|
+
expect(provider.name).toBe("opencode");
|
|
316
|
+
expect(
|
|
317
|
+
(provider as unknown as { client: { baseURL?: string } }).client.baseURL,
|
|
318
|
+
).toBe(OPENCODE_ZEN_BASE_URL);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("keeps a configured OpenCode Go origin", () => {
|
|
322
|
+
const provider = new OpenCodeResponsesProvider(
|
|
323
|
+
"sk-test",
|
|
324
|
+
"muse-spark-1.2-contributor-free",
|
|
325
|
+
{ baseURL: OPENCODE_GO_BASE_URL },
|
|
326
|
+
);
|
|
327
|
+
expect(
|
|
328
|
+
(provider as unknown as { client: { baseURL?: string } }).client.baseURL,
|
|
329
|
+
).toBe(OPENCODE_GO_BASE_URL);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("OpenCodeProvider transport selection", () => {
|
|
334
|
+
beforeEach(() => {
|
|
335
|
+
mockDeviceId = null;
|
|
336
|
+
resetOpenCodeFallbackSessionForTests();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("sends a responses-only model through the Responses API with the caller's OpenCode headers", async () => {
|
|
340
|
+
const provider = new OpenCodeProvider(
|
|
341
|
+
"sk-test",
|
|
342
|
+
"muse-spark-1.2-contributor-free",
|
|
343
|
+
);
|
|
344
|
+
const { chat, responses } = stubTransports(provider);
|
|
345
|
+
|
|
346
|
+
const result = await provider.sendMessage(USER_TURN, {
|
|
347
|
+
config: {
|
|
348
|
+
requestHeaders: {
|
|
349
|
+
[OPENCODE_SESSION_HEADER]: "conv-xyz",
|
|
350
|
+
[OPENCODE_REQUEST_HEADER]: "req-123",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(chat).toHaveLength(0);
|
|
356
|
+
expect(responses).toHaveLength(1);
|
|
357
|
+
expect((responses[0]!.params as { model: string }).model).toBe(
|
|
358
|
+
"muse-spark-1.2-contributor-free",
|
|
359
|
+
);
|
|
360
|
+
const options = responses[0]!.options as {
|
|
361
|
+
headers?: Record<string, string>;
|
|
362
|
+
};
|
|
363
|
+
expect(options.headers?.[OPENCODE_SESSION_HEADER]).toBe("conv-xyz");
|
|
364
|
+
expect(options.headers?.[OPENCODE_REQUEST_HEADER]).toBe("req-123");
|
|
365
|
+
expect(options.headers).not.toHaveProperty("session_id");
|
|
366
|
+
expect(result.content).toEqual([{ type: "text", text: "ok" }]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("stamps the session backstop on a configless Responses call", async () => {
|
|
370
|
+
mockDeviceId = "device-abc";
|
|
371
|
+
const provider = new OpenCodeProvider(
|
|
372
|
+
"sk-test",
|
|
373
|
+
"muse-spark-1.2-contributor-free",
|
|
374
|
+
);
|
|
375
|
+
const { responses } = stubTransports(provider);
|
|
376
|
+
|
|
377
|
+
await provider.sendMessage(USER_TURN);
|
|
378
|
+
|
|
379
|
+
const options = responses[0]!.options as {
|
|
380
|
+
headers?: Record<string, string>;
|
|
381
|
+
};
|
|
382
|
+
expect(options.headers?.[OPENCODE_SESSION_HEADER]).toBe("device-abc");
|
|
383
|
+
expect(options.headers?.[OPENCODE_REQUEST_HEADER]).toBeTruthy();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("keeps a chat-completions model on the chat wire", async () => {
|
|
387
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
388
|
+
const { chat, responses } = stubTransports(provider);
|
|
389
|
+
|
|
390
|
+
await provider.sendMessage(USER_TURN);
|
|
391
|
+
|
|
392
|
+
expect(chat).toHaveLength(1);
|
|
393
|
+
expect(responses).toHaveLength(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("a per-call model override picks the transport that model needs", async () => {
|
|
397
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
398
|
+
const { chat, responses } = stubTransports(provider);
|
|
399
|
+
|
|
400
|
+
await provider.sendMessage(USER_TURN, {
|
|
401
|
+
config: { model: "muse-spark-1.2-contributor-free" },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(chat).toHaveLength(0);
|
|
405
|
+
expect(responses).toHaveLength(1);
|
|
406
|
+
expect((responses[0]!.params as { model: string }).model).toBe(
|
|
407
|
+
"muse-spark-1.2-contributor-free",
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { getExistingDeviceId } from "../../util/device-id.js";
|
|
4
4
|
import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
|
|
5
|
+
import { OpenAIResponsesProvider } from "../openai/responses-provider.js";
|
|
5
6
|
import type {
|
|
6
7
|
Message,
|
|
7
8
|
ProviderResponse,
|
|
@@ -18,6 +19,18 @@ export interface OpenCodeProviderOptions {
|
|
|
18
19
|
streamTimeoutMs?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* OpenCode Zen models served only by the Responses API: Zen answers
|
|
24
|
+
* `/chat/completions` for them with HTTP 500 and completes the same request
|
|
25
|
+
* on `/responses`. OpenCode's own client picks the transport per model from
|
|
26
|
+
* its catalog's `api.endpoint`; the assistant has no such feed, so the
|
|
27
|
+
* verified ids live here. Everything else stays on chat completions.
|
|
28
|
+
*/
|
|
29
|
+
export const OPENCODE_RESPONSES_ONLY_MODELS: ReadonlySet<string> = new Set([
|
|
30
|
+
"muse-spark-1.2-contributor-free",
|
|
31
|
+
"muse-spark-1.3-contributor-free",
|
|
32
|
+
]);
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Resolve the OpenCode chat-completions origin. A stored connection URL
|
|
23
36
|
* wins; otherwise the request goes to OpenCode Zen.
|
|
@@ -82,7 +95,38 @@ export function resolveOpenCodeRequestHeaders(
|
|
|
82
95
|
});
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Responses-API transport for OpenCode. Shares the chat client's origin
|
|
100
|
+
* resolution and provider name, so the same `x-opencode-session` /
|
|
101
|
+
* `x-opencode-request` headers reach its requests.
|
|
102
|
+
*/
|
|
103
|
+
export class OpenCodeResponsesProvider extends OpenAIResponsesProvider {
|
|
104
|
+
constructor(
|
|
105
|
+
apiKey: string,
|
|
106
|
+
model: string,
|
|
107
|
+
options: OpenCodeProviderOptions = {},
|
|
108
|
+
) {
|
|
109
|
+
super(apiKey, model, {
|
|
110
|
+
baseURL: resolveOpenCodeBaseURL(options.baseURL),
|
|
111
|
+
providerName: "opencode",
|
|
112
|
+
providerLabel: "OpenCode",
|
|
113
|
+
streamTimeoutMs: options.streamTimeoutMs,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* OpenCode adapter. Chat completions is the default wire; models in
|
|
120
|
+
* {@link OPENCODE_RESPONSES_ONLY_MODELS} are delegated to an
|
|
121
|
+
* {@link OpenCodeResponsesProvider} on the same origin. The choice is made
|
|
122
|
+
* per send from the effective model, so a per-call model override lands on
|
|
123
|
+
* the transport that model needs.
|
|
124
|
+
*/
|
|
85
125
|
export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
126
|
+
private readonly openCodeApiKey: string;
|
|
127
|
+
private readonly openCodeOptions: OpenCodeProviderOptions;
|
|
128
|
+
private responsesInner: OpenCodeResponsesProvider | undefined;
|
|
129
|
+
|
|
86
130
|
constructor(
|
|
87
131
|
apiKey: string,
|
|
88
132
|
model: string,
|
|
@@ -96,20 +140,22 @@ export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
|
96
140
|
assistantReasoningField: "reasoning_content",
|
|
97
141
|
omitToolChoiceWhenReasoning: true,
|
|
98
142
|
});
|
|
143
|
+
this.openCodeApiKey = apiKey;
|
|
144
|
+
this.openCodeOptions = options;
|
|
99
145
|
}
|
|
100
146
|
|
|
101
147
|
/**
|
|
102
148
|
* Transport-level backstop so a call that reaches this provider without
|
|
103
149
|
* going through `RetryProvider` (or with no config at all) still carries
|
|
104
|
-
* the session and request headers zen/go requires. Headers
|
|
105
|
-
* already resolved (conversation-derived) win.
|
|
150
|
+
* the session and request headers zen/go requires on either wire. Headers
|
|
151
|
+
* the caller already resolved (conversation-derived) win.
|
|
106
152
|
*/
|
|
107
153
|
override async sendMessage(
|
|
108
154
|
messages: Message[],
|
|
109
155
|
options?: SendMessageOptions,
|
|
110
156
|
): Promise<ProviderResponse> {
|
|
111
157
|
const config = options?.config ?? {};
|
|
112
|
-
|
|
158
|
+
const stamped: SendMessageOptions = {
|
|
113
159
|
...options,
|
|
114
160
|
config: {
|
|
115
161
|
...config,
|
|
@@ -118,6 +164,29 @@ export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
|
118
164
|
...config.requestHeaders,
|
|
119
165
|
},
|
|
120
166
|
},
|
|
121
|
-
}
|
|
167
|
+
};
|
|
168
|
+
if (
|
|
169
|
+
OPENCODE_RESPONSES_ONLY_MODELS.has(this.resolveEffectiveModel(config))
|
|
170
|
+
) {
|
|
171
|
+
return this.getResponsesInner().sendMessage(messages, stamped);
|
|
172
|
+
}
|
|
173
|
+
return super.sendMessage(messages, stamped);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private resolveEffectiveModel(config: Record<string, unknown>): string {
|
|
177
|
+
const override =
|
|
178
|
+
typeof config.model === "string" && config.model.trim().length > 0
|
|
179
|
+
? config.model.trim()
|
|
180
|
+
: undefined;
|
|
181
|
+
return override ?? this.defaultModel;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private getResponsesInner(): OpenCodeResponsesProvider {
|
|
185
|
+
this.responsesInner ??= new OpenCodeResponsesProvider(
|
|
186
|
+
this.openCodeApiKey,
|
|
187
|
+
this.defaultModel,
|
|
188
|
+
this.openCodeOptions,
|
|
189
|
+
);
|
|
190
|
+
return this.responsesInner;
|
|
122
191
|
}
|
|
123
192
|
}
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -161,6 +161,10 @@ In the CDP factory the bridge is the internal `"host-bridge"` candidate kind (`I
|
|
|
161
161
|
|
|
162
162
|
**After the first successful CDP command**, the selected backend becomes **sticky** for the remainder of the tool invocation. Subsequent commands always route through the same backend so multi-command tool flows do not hop transports mid-step.
|
|
163
163
|
|
|
164
|
+
### Browser CLI surface defaults
|
|
165
|
+
|
|
166
|
+
The browser execute and tab routes share `browser/virtual-desktop-target.ts`. Platform-hosted web guardian conversations use installed, enabled virtual desktop Chrome by default. The shared `isVirtualDesktopEnabled` gate requires both `IS_PLATFORM` and `IS_CONTAINERIZED` alongside the `assistant-desktop` flag. Native renderer turns also use the `web` transport, so the frozen turn `clientOs` excludes native apps from automatic streamed-browser selection. Native apps retain their existing backend selection and fallback behavior. Explicit desktop/backend/client targets and existing personal-browser sessions override the surface default. Disabled or uninstalled desktop support retains the existing browser path; selection never triggers installation.
|
|
167
|
+
|
|
164
168
|
### Per-tool `browser_mode` override
|
|
165
169
|
|
|
166
170
|
All CDP-backed browser tools (`browser_navigate`, `browser_snapshot`, `browser_screenshot`, `browser_click`, `browser_type`, `browser_hover`, `browser_scroll`, `browser_press_key`, `browser_select_option`, `browser_wait_for`, `browser_extract`, `browser_fill_credential`, `browser_attach`, `browser_detach`, `browser_close`, `browser_status`) accept an optional `browser_mode` input parameter that overrides the automatic backend selection for that invocation.
|
|
@@ -15,6 +15,7 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
15
15
|
let baseUrl: string;
|
|
16
16
|
let restoreAuthEnv: () => void;
|
|
17
17
|
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
18
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
18
19
|
|
|
19
20
|
beforeEach(async () => {
|
|
20
21
|
restoreAuthEnv = requireHttpAuth();
|
|
@@ -22,6 +23,7 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
22
23
|
server = new RuntimeHttpServer({ port, hostname: "127.0.0.1" });
|
|
23
24
|
await server.start();
|
|
24
25
|
baseUrl = `127.0.0.1:${server.actualPort}`;
|
|
26
|
+
process.env.IS_PLATFORM = "true";
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
afterEach(async () => {
|
|
@@ -32,6 +34,11 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
32
34
|
} else {
|
|
33
35
|
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
34
36
|
}
|
|
37
|
+
if (originalPlatform === undefined) {
|
|
38
|
+
delete process.env.IS_PLATFORM;
|
|
39
|
+
} else {
|
|
40
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
41
|
+
}
|
|
35
42
|
setOverridesForTesting({});
|
|
36
43
|
});
|
|
37
44
|
|
|
@@ -70,7 +77,9 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
70
77
|
);
|
|
71
78
|
const closed = await waitForClose(ws);
|
|
72
79
|
expect(closed.code).toBe(4008);
|
|
73
|
-
expect(closed.reason).toBe(
|
|
80
|
+
expect(closed.reason).toBe(
|
|
81
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
82
|
+
);
|
|
74
83
|
});
|
|
75
84
|
for (const flag of [false, undefined]) {
|
|
76
85
|
test(`refuses a containerized stream with a disabled or missing flag (${flag})`, async () => {
|
|
@@ -83,7 +92,41 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
83
92
|
);
|
|
84
93
|
const closed = await waitForClose(ws);
|
|
85
94
|
expect(closed.code).toBe(4008);
|
|
86
|
-
expect(closed.reason).toBe(
|
|
95
|
+
expect(closed.reason).toBe(
|
|
96
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
97
|
+
);
|
|
87
98
|
});
|
|
88
99
|
}
|
|
89
100
|
});
|
|
101
|
+
|
|
102
|
+
test("self-hosted containers cannot stream with the feature flag enabled", async () => {
|
|
103
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
104
|
+
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
105
|
+
const restoreAuth = requireHttpAuth();
|
|
106
|
+
const server = new RuntimeHttpServer({ port: 0, hostname: "127.0.0.1" });
|
|
107
|
+
try {
|
|
108
|
+
await server.start();
|
|
109
|
+
process.env.IS_PLATFORM = "false";
|
|
110
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
111
|
+
setOverridesForTesting({ "assistant-desktop": true });
|
|
112
|
+
const baseUrl = `127.0.0.1:${server.actualPort}`;
|
|
113
|
+
const ws = new WebSocket(
|
|
114
|
+
`ws://${baseUrl}/v1/desktop/stream?token=${encodeURIComponent(mintGatewayToken())}`,
|
|
115
|
+
);
|
|
116
|
+
expect((await waitForClose(ws)).code).toBe(4008);
|
|
117
|
+
} finally {
|
|
118
|
+
await server.stop();
|
|
119
|
+
restoreAuth();
|
|
120
|
+
if (originalPlatform === undefined) {
|
|
121
|
+
delete process.env.IS_PLATFORM;
|
|
122
|
+
} else {
|
|
123
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
124
|
+
}
|
|
125
|
+
if (originalContainerized === undefined) {
|
|
126
|
+
delete process.env.IS_CONTAINERIZED;
|
|
127
|
+
} else {
|
|
128
|
+
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
129
|
+
}
|
|
130
|
+
setOverridesForTesting({});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -32,12 +32,12 @@ import {
|
|
|
32
32
|
} from "../daemon/daemon-readiness.js";
|
|
33
33
|
import { processMessage } from "../daemon/process-message.js";
|
|
34
34
|
import { makeAddrInUseError } from "../daemon/startup-error.js";
|
|
35
|
-
import { isAssistantDesktopEnabled } from "../desktop/desktop-feature.js";
|
|
36
35
|
import {
|
|
37
36
|
DESKTOP_CLOSE,
|
|
38
37
|
destroyDesktopSessionManager,
|
|
39
38
|
} from "../desktop/desktop-session-manager.js";
|
|
40
39
|
import { DesktopStreamBridge } from "../desktop/desktop-stream-bridge.js";
|
|
40
|
+
import { isVirtualDesktopEnabled } from "../desktop/virtual-desktop-feature.js";
|
|
41
41
|
import {
|
|
42
42
|
createLiveVoiceConnection,
|
|
43
43
|
type LiveVoiceConnection,
|
|
@@ -252,9 +252,9 @@ type AllWebSocketData =
|
|
|
252
252
|
| WatchStreamWebSocketData
|
|
253
253
|
| DesktopStreamWebSocketData;
|
|
254
254
|
|
|
255
|
-
function
|
|
255
|
+
function virtualDesktopEnabled(): boolean {
|
|
256
256
|
try {
|
|
257
|
-
return
|
|
257
|
+
return isVirtualDesktopEnabled(getConfig());
|
|
258
258
|
} catch (err) {
|
|
259
259
|
log.warn({ err }, "Failed to read config for desktop stream gate");
|
|
260
260
|
return false;
|
|
@@ -453,15 +453,15 @@ export class RuntimeHttpServer {
|
|
|
453
453
|
}
|
|
454
454
|
if (data.wsType === "desktop-stream") {
|
|
455
455
|
log.info("Desktop stream WebSocket opened");
|
|
456
|
-
if (!
|
|
456
|
+
if (!virtualDesktopEnabled()) {
|
|
457
457
|
ws.close(
|
|
458
458
|
DESKTOP_CLOSE.unavailable,
|
|
459
|
-
"
|
|
459
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
460
460
|
);
|
|
461
461
|
return;
|
|
462
462
|
}
|
|
463
463
|
const bridge = new DesktopStreamBridge(ws, {
|
|
464
|
-
isEnabled:
|
|
464
|
+
isEnabled: virtualDesktopEnabled,
|
|
465
465
|
});
|
|
466
466
|
data.bridge = bridge;
|
|
467
467
|
void bridge.start();
|
|
@@ -48,7 +48,7 @@ mock.module("../../../security/oauth2.js", () => ({
|
|
|
48
48
|
|
|
49
49
|
const actualClaudeOauth = await import("../../../acp/acp-claude-oauth.js");
|
|
50
50
|
const { CLAUDE_MANUAL_REDIRECT_URI, CLAUDE_OAUTH_CONFIG } = actualClaudeOauth;
|
|
51
|
-
const storeAcpClaudeTokenMock = mock(async (
|
|
51
|
+
const storeAcpClaudeTokenMock = mock(async (_tokens: unknown) => {});
|
|
52
52
|
// The connect-status route reads token presence; mock it so the route test
|
|
53
53
|
// doesn't reach real secure storage.
|
|
54
54
|
const hasAcpClaudeTokenMock = mock(async () => false);
|
|
@@ -228,9 +228,12 @@ describe("loopback capture", () => {
|
|
|
228
228
|
const status = await waitForStatus(state, "connected");
|
|
229
229
|
expect(status).toEqual({ status: "connected" });
|
|
230
230
|
|
|
231
|
-
// Access token persisted via storeAcpClaudeToken.
|
|
232
231
|
expect(storeAcpClaudeTokenMock).toHaveBeenCalledTimes(1);
|
|
233
|
-
expect(storeAcpClaudeTokenMock).toHaveBeenCalledWith(
|
|
232
|
+
expect(storeAcpClaudeTokenMock).toHaveBeenCalledWith({
|
|
233
|
+
accessToken: "sk-ant-oat-access",
|
|
234
|
+
refreshToken: "refresh-xyz",
|
|
235
|
+
expiresIn: 3600,
|
|
236
|
+
});
|
|
234
237
|
});
|
|
235
238
|
|
|
236
239
|
test("flips status to error when the exchange fails", async () => {
|
|
@@ -339,7 +342,11 @@ describe("acp_claude_auth_exchange", () => {
|
|
|
339
342
|
expect(call[1]).toBe("auth-code-123");
|
|
340
343
|
expect(call[2]).toBe(CLAUDE_MANUAL_REDIRECT_URI);
|
|
341
344
|
expect(call[3]).toBeTruthy(); // PKCE verifier from the start call
|
|
342
|
-
expect(storeAcpClaudeTokenMock).toHaveBeenCalledWith(
|
|
345
|
+
expect(storeAcpClaudeTokenMock).toHaveBeenCalledWith({
|
|
346
|
+
accessToken: "sk-ant-oat-manual",
|
|
347
|
+
refreshToken: "refresh-manual",
|
|
348
|
+
expiresIn: 3600,
|
|
349
|
+
});
|
|
343
350
|
|
|
344
351
|
// The pending entry is consumed — a second exchange fails.
|
|
345
352
|
await expect(
|
|
@@ -360,7 +367,11 @@ describe("acp_claude_auth_exchange", () => {
|
|
|
360
367
|
string,
|
|
361
368
|
];
|
|
362
369
|
expect(call[1]).toBe("raw-code-xyz");
|
|
363
|
-
expect(storeAcpClaudeTokenMock).toHaveBeenCalledWith(
|
|
370
|
+
expect(storeAcpClaudeTokenMock).toHaveBeenCalledWith({
|
|
371
|
+
accessToken: "sk-ant-oat-manual",
|
|
372
|
+
refreshToken: "refresh-manual",
|
|
373
|
+
expiresIn: 3600,
|
|
374
|
+
});
|
|
364
375
|
});
|
|
365
376
|
|
|
366
377
|
test("malformed paste (no `#`, no state) is rejected", async () => {
|