@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
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const mockDisconnect = jest.fn();
|
|
5
|
-
let mockIsConnected = true;
|
|
6
|
-
let mockLastError: Error | null = null;
|
|
3
|
+
const getServerState = jest.fn();
|
|
7
4
|
|
|
8
5
|
mock.module("../mcp/client.js", () => ({
|
|
9
6
|
McpClient: class {
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
constructor() {
|
|
8
|
+
throw new Error("list route must not construct an MCP client");
|
|
12
9
|
}
|
|
13
|
-
get lastError() {
|
|
14
|
-
return mockLastError;
|
|
15
|
-
}
|
|
16
|
-
connect = mockConnect;
|
|
17
|
-
disconnect = mockDisconnect;
|
|
18
10
|
},
|
|
19
11
|
}));
|
|
20
12
|
|
|
13
|
+
mock.module("../mcp/manager.js", () => ({
|
|
14
|
+
getMcpServerManager: () => ({ getServerState }),
|
|
15
|
+
}));
|
|
16
|
+
|
|
21
17
|
import { setWorkspaceMcp } from "./helpers/set-workspace-mcp.js";
|
|
22
18
|
|
|
23
19
|
// Seed the MCP server the list route reads from workspace mcp.json.
|
|
@@ -56,28 +52,22 @@ const listHandler = ROUTES.find(
|
|
|
56
52
|
(r: { operationId: string }) => r.operationId === "internal_mcp_list",
|
|
57
53
|
)!.handler;
|
|
58
54
|
|
|
59
|
-
describe("
|
|
55
|
+
describe("internal_mcp_list runtime status", () => {
|
|
60
56
|
beforeEach(() => {
|
|
61
|
-
|
|
62
|
-
mockDisconnect.mockReset();
|
|
63
|
-
mockIsConnected = true;
|
|
64
|
-
mockLastError = null;
|
|
57
|
+
getServerState.mockReset();
|
|
65
58
|
});
|
|
66
59
|
|
|
67
|
-
test("returns connected
|
|
68
|
-
|
|
69
|
-
mockDisconnect.mockResolvedValue(undefined);
|
|
60
|
+
test("returns connected from the manager's recorded state", async () => {
|
|
61
|
+
getServerState.mockReturnValue("connected");
|
|
70
62
|
|
|
71
63
|
const result = (await listHandler({})) as {
|
|
72
64
|
servers: { status: string }[];
|
|
73
65
|
};
|
|
74
66
|
expect(result.servers[0].status).toBe("connected");
|
|
75
|
-
expect(mockDisconnect).toHaveBeenCalled();
|
|
76
67
|
});
|
|
77
68
|
|
|
78
|
-
test("returns needs-auth
|
|
79
|
-
|
|
80
|
-
mockIsConnected = false;
|
|
69
|
+
test("returns needs-auth from the manager's recorded state", async () => {
|
|
70
|
+
getServerState.mockReturnValue("needs-auth");
|
|
81
71
|
|
|
82
72
|
const result = (await listHandler({})) as {
|
|
83
73
|
servers: { status: string }[];
|
|
@@ -85,15 +75,25 @@ describe("checkServerHealth (via internal_mcp_list route)", () => {
|
|
|
85
75
|
expect(result.servers[0].status).toBe("needs-auth");
|
|
86
76
|
});
|
|
87
77
|
|
|
88
|
-
test("
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
mockDisconnect.mockResolvedValue(undefined);
|
|
78
|
+
test.each([undefined, "connecting", "error"])(
|
|
79
|
+
"returns the legacy error status for runtime state %s",
|
|
80
|
+
async (runtimeState) => {
|
|
81
|
+
getServerState.mockReturnValue(runtimeState);
|
|
93
82
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
83
|
+
const result = (await listHandler({})) as {
|
|
84
|
+
servers: { status: string }[];
|
|
85
|
+
};
|
|
86
|
+
expect(result.servers[0].status).toBe("error");
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
test("repeated reads do not connect or spawn a configured server", async () => {
|
|
91
|
+
getServerState.mockReturnValue("connected");
|
|
92
|
+
|
|
93
|
+
await listHandler({});
|
|
94
|
+
await listHandler({});
|
|
95
|
+
|
|
96
|
+
expect(getServerState).toHaveBeenCalledTimes(2);
|
|
97
|
+
expect(getServerState).toHaveBeenCalledWith("test", "workspace");
|
|
98
98
|
});
|
|
99
99
|
});
|
|
@@ -8,38 +8,35 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 1. A workspace entry of the same id wins. Getting precedence backwards
|
|
10
10
|
* would let a plugin redirect a server the user configured by hand.
|
|
11
|
-
* 2. A plugin server is never health-checked
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* URL, so probing one would send a workspace credential to an endpoint
|
|
15
|
-
* the plugin chose whenever an id happens to match a stored key.
|
|
11
|
+
* 2. A plugin server is never health-checked as a side effect of listing.
|
|
12
|
+
* OAuth status is read from the plugin and endpoint-scoped credential
|
|
13
|
+
* identity, while workspace static headers remain isolated.
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
16
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
19
17
|
import { join } from "node:path";
|
|
20
18
|
import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
|
|
21
19
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
const getServerState = jest.fn();
|
|
21
|
+
const hasMcpOAuthTokens = jest.fn(async (target: any) => {
|
|
22
|
+
if (target.source === "plugin" && target.url === "not a url") {
|
|
23
|
+
throw new TypeError("invalid URL");
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
});
|
|
26
27
|
|
|
27
28
|
mock.module("../mcp/client.js", () => ({
|
|
28
29
|
McpClient: class {
|
|
29
|
-
constructor(
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
get isConnected() {
|
|
33
|
-
return true;
|
|
30
|
+
constructor() {
|
|
31
|
+
throw new Error("list route must not construct an MCP client");
|
|
34
32
|
}
|
|
35
|
-
get lastError() {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
connect = mockConnect;
|
|
39
|
-
disconnect = mockDisconnect;
|
|
40
33
|
},
|
|
41
34
|
}));
|
|
42
35
|
|
|
36
|
+
mock.module("../mcp/manager.js", () => ({
|
|
37
|
+
getMcpServerManager: () => ({ getServerState }),
|
|
38
|
+
}));
|
|
39
|
+
|
|
43
40
|
mock.module("../mcp/mcp-auth-orchestrator.js", () => ({
|
|
44
41
|
orchestrateMcpOAuthConnect: async () => ({
|
|
45
42
|
auth_url: "",
|
|
@@ -52,9 +49,7 @@ mock.module("../mcp/mcp-auth-state.js", () => ({
|
|
|
52
49
|
}));
|
|
53
50
|
|
|
54
51
|
mock.module("../mcp/mcp-oauth-provider.js", () => ({
|
|
55
|
-
|
|
56
|
-
// is the condition under which a leak would be observable.
|
|
57
|
-
hasMcpOAuthTokens: async () => true,
|
|
52
|
+
hasMcpOAuthTokens,
|
|
58
53
|
deleteMcpOAuthCredentials: async () => ({ ok: true, failedKeys: [] }),
|
|
59
54
|
}));
|
|
60
55
|
|
|
@@ -108,6 +103,19 @@ function writePlugin(name: string, mcpJson: unknown): void {
|
|
|
108
103
|
writeFileSync(join(dir, "mcp.json"), JSON.stringify(mcpJson));
|
|
109
104
|
}
|
|
110
105
|
|
|
106
|
+
function writeStandardPlugin(name: string, mcpJson: unknown): void {
|
|
107
|
+
const dir = join(getWorkspacePluginsDir(), name);
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
writeFileSync(
|
|
110
|
+
join(dir, "plugin.json"),
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
113
|
+
name,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
writeFileSync(join(dir, "mcp.json"), JSON.stringify(mcpJson));
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
function unabyssManifest(): unknown {
|
|
112
120
|
return {
|
|
113
121
|
mcpServers: {
|
|
@@ -123,9 +131,8 @@ async function listServers(): Promise<ListedServer[]> {
|
|
|
123
131
|
|
|
124
132
|
describe("internal_mcp_list, plugin-declared servers", () => {
|
|
125
133
|
beforeEach(() => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
connectedServerIds.length = 0;
|
|
134
|
+
getServerState.mockReset();
|
|
135
|
+
hasMcpOAuthTokens.mockClear();
|
|
129
136
|
rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
|
|
130
137
|
mkdirSync(getWorkspacePluginsDir(), { recursive: true });
|
|
131
138
|
});
|
|
@@ -138,6 +145,14 @@ describe("internal_mcp_list, plugin-declared servers", () => {
|
|
|
138
145
|
expect(ids).toContain("unabyss");
|
|
139
146
|
});
|
|
140
147
|
|
|
148
|
+
test("a standard-only plugin keeps its server identity", async () => {
|
|
149
|
+
writeStandardPlugin("unabyss", unabyssManifest());
|
|
150
|
+
|
|
151
|
+
const plugin = (await listServers()).find((s) => s.id === "unabyss")!;
|
|
152
|
+
expect(plugin.source).toBe("plugin");
|
|
153
|
+
expect(plugin.pluginName).toBe("unabyss");
|
|
154
|
+
});
|
|
155
|
+
|
|
141
156
|
test("plugin servers are labelled with their origin, workspace servers are not", async () => {
|
|
142
157
|
writePlugin("unabyss", unabyssManifest());
|
|
143
158
|
|
|
@@ -171,20 +186,36 @@ describe("internal_mcp_list, plugin-declared servers", () => {
|
|
|
171
186
|
const servers = await listServers();
|
|
172
187
|
|
|
173
188
|
expect(servers.find((s) => s.id === "unabyss")!.status).toEqual("declared");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// would ship them to the plugin-declared URL.
|
|
177
|
-
expect(connectedServerIds).toContain("from-workspace");
|
|
178
|
-
expect(connectedServerIds).not.toContain("unabyss");
|
|
189
|
+
expect(getServerState).toHaveBeenCalledWith("from-workspace", "workspace");
|
|
190
|
+
expect(getServerState).toHaveBeenCalledWith("unabyss", "plugin");
|
|
179
191
|
});
|
|
180
192
|
|
|
181
|
-
test("plugin
|
|
193
|
+
test("plugin status uses only state recorded for the plugin source", async () => {
|
|
194
|
+
writePlugin("unabyss", unabyssManifest());
|
|
195
|
+
getServerState.mockImplementation(
|
|
196
|
+
(serverId: string, source: "workspace" | "plugin") =>
|
|
197
|
+
serverId === "unabyss" && source === "plugin" ? "connected" : undefined,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const plugin = (await listServers()).find((s) => s.id === "unabyss")!;
|
|
201
|
+
expect(plugin.status).toBe("connected");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("plugin servers report endpoint-scoped OAuth without static auth", async () => {
|
|
182
205
|
writePlugin("unabyss", unabyssManifest());
|
|
183
206
|
|
|
184
207
|
const plugin = (await listServers()).find((s) => s.id === "unabyss")!;
|
|
185
|
-
expect(plugin.hasOAuth).toBe(
|
|
208
|
+
expect(plugin.hasOAuth).toBe(true);
|
|
186
209
|
expect(plugin.hasStaticAuth).toBe(false);
|
|
187
210
|
expect(plugin.authType).toEqual("none");
|
|
211
|
+
expect(hasMcpOAuthTokens).toHaveBeenCalledWith(
|
|
212
|
+
expect.objectContaining({
|
|
213
|
+
source: "plugin",
|
|
214
|
+
pluginName: "unabyss",
|
|
215
|
+
serverKey: "unabyss",
|
|
216
|
+
url: "https://mcp.unabyss.com",
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
188
219
|
});
|
|
189
220
|
|
|
190
221
|
test("workspace servers keep reporting their auth state", async () => {
|
|
@@ -225,6 +256,20 @@ describe("internal_mcp_list, plugin-declared servers", () => {
|
|
|
225
256
|
expect(ids).toContain("from-workspace");
|
|
226
257
|
});
|
|
227
258
|
|
|
259
|
+
test("an invalid plugin transport URL does not break the listing", async () => {
|
|
260
|
+
writePlugin("bad-url", {
|
|
261
|
+
mcpServers: {
|
|
262
|
+
remote: { type: "streamable-http", url: "not a url" },
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const servers = await listServers();
|
|
267
|
+
expect(servers.find((s) => s.id === "bad-url__remote")?.hasOAuth).toBe(
|
|
268
|
+
false,
|
|
269
|
+
);
|
|
270
|
+
expect(servers.some((s) => s.id === "from-workspace")).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
228
273
|
test("no plugins installed leaves the listing unchanged", async () => {
|
|
229
274
|
const servers = await listServers();
|
|
230
275
|
expect(servers.every((s) => s.source === "workspace")).toBe(true);
|
|
@@ -32,10 +32,12 @@ const { RiskLevel } = await import("../permissions/types.js");
|
|
|
32
32
|
type ServerSource = "workspace" | "plugin";
|
|
33
33
|
|
|
34
34
|
function serverConfig(source: ServerSource) {
|
|
35
|
-
|
|
35
|
+
const base = {
|
|
36
36
|
transport: { type: "stdio" as const, command: "echo", args: [] },
|
|
37
|
-
source,
|
|
38
37
|
};
|
|
38
|
+
return source === "workspace"
|
|
39
|
+
? { ...base, source }
|
|
40
|
+
: { ...base, source, pluginName: "plugin", serverKey: "server" };
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
interface RiskAnnotations {
|
|
@@ -1165,6 +1165,33 @@ describe("POST oauth/managed-connect/start", () => {
|
|
|
1165
1165
|
expect(result.connect_url).toBe("https://app.vellum.ai/connect/abc");
|
|
1166
1166
|
});
|
|
1167
1167
|
|
|
1168
|
+
test("forwards tenant_host to the platform only when one is supplied", async () => {
|
|
1169
|
+
// Shopify's endpoints live on the merchant's host, which the platform
|
|
1170
|
+
// substitutes into its templates; other providers must not see the key.
|
|
1171
|
+
const bodies: Array<Record<string, unknown>> = [];
|
|
1172
|
+
mockFetchImpl = async (_path, init) => {
|
|
1173
|
+
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
1174
|
+
return {
|
|
1175
|
+
ok: true,
|
|
1176
|
+
status: 200,
|
|
1177
|
+
json: async () => ({
|
|
1178
|
+
connect_url: "https://app.vellum.ai/connect/abc",
|
|
1179
|
+
}),
|
|
1180
|
+
text: async () => "",
|
|
1181
|
+
};
|
|
1182
|
+
};
|
|
1183
|
+
await getRoute("POST", "oauth/managed-connect/start").handler(
|
|
1184
|
+
makeArgs({
|
|
1185
|
+
body: { provider: "shopify", tenant_host: " my-store.myshopify.com " },
|
|
1186
|
+
}),
|
|
1187
|
+
);
|
|
1188
|
+
await getRoute("POST", "oauth/managed-connect/start").handler(
|
|
1189
|
+
makeArgs({ body: { provider: "google", tenant_host: " " } }),
|
|
1190
|
+
);
|
|
1191
|
+
expect(bodies[0]?.tenant_host).toBe("my-store.myshopify.com");
|
|
1192
|
+
expect(bodies[1]).not.toHaveProperty("tenant_host");
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1168
1195
|
test("raises InternalError when platform returns 401", async () => {
|
|
1169
1196
|
mockFetchImpl = async () => ({
|
|
1170
1197
|
ok: false,
|
|
@@ -1228,6 +1255,7 @@ describe("GET oauth/managed-connect/poll", () => {
|
|
|
1228
1255
|
id: string;
|
|
1229
1256
|
account_label: string | null;
|
|
1230
1257
|
scopes_granted: string[];
|
|
1258
|
+
provider_params: Record<string, string>;
|
|
1231
1259
|
}>;
|
|
1232
1260
|
};
|
|
1233
1261
|
expect(result.ok).toBe(true);
|
|
@@ -1236,6 +1264,7 @@ describe("GET oauth/managed-connect/poll", () => {
|
|
|
1236
1264
|
id: "conn-1",
|
|
1237
1265
|
account_label: "alice@example.com",
|
|
1238
1266
|
scopes_granted: ["email"],
|
|
1267
|
+
provider_params: {},
|
|
1239
1268
|
},
|
|
1240
1269
|
]);
|
|
1241
1270
|
});
|
|
@@ -1248,4 +1277,30 @@ describe("GET oauth/managed-connect/poll", () => {
|
|
|
1248
1277
|
),
|
|
1249
1278
|
).rejects.toBeInstanceOf(BadRequestError);
|
|
1250
1279
|
});
|
|
1280
|
+
|
|
1281
|
+
test("passes through the provider params a connection is scoped by", async () => {
|
|
1282
|
+
// QuickBooks pins the company (realm) the user picked to the connection;
|
|
1283
|
+
// a caller addressing /companyinfo/<realmId> needs it back.
|
|
1284
|
+
mockFetchImpl = async () => ({
|
|
1285
|
+
ok: true,
|
|
1286
|
+
status: 200,
|
|
1287
|
+
json: async () => [
|
|
1288
|
+
{
|
|
1289
|
+
id: "conn-qb",
|
|
1290
|
+
account_label: "Acme Widgets",
|
|
1291
|
+
scopes_granted: ["com.intuit.quickbooks.accounting"],
|
|
1292
|
+
provider_params: { realm_id: "9130357849012345" },
|
|
1293
|
+
},
|
|
1294
|
+
],
|
|
1295
|
+
text: async () => "",
|
|
1296
|
+
});
|
|
1297
|
+
const result = (await getRoute("GET", "oauth/managed-connect/poll").handler(
|
|
1298
|
+
makeArgs({ queryParams: { provider: "quickbooks" } }),
|
|
1299
|
+
)) as {
|
|
1300
|
+
connections: Array<{ provider_params: Record<string, string> }>;
|
|
1301
|
+
};
|
|
1302
|
+
expect(result.connections[0]?.provider_params).toEqual({
|
|
1303
|
+
realm_id: "9130357849012345",
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1251
1306
|
});
|
|
@@ -291,10 +291,32 @@ describe("serializeProviderSummary", () => {
|
|
|
291
291
|
supports_managed_mode: true,
|
|
292
292
|
managed_service_is_paid: false,
|
|
293
293
|
feature_flag: null,
|
|
294
|
+
tenant_host: null,
|
|
294
295
|
acts_as: "user",
|
|
295
296
|
});
|
|
296
297
|
});
|
|
297
298
|
|
|
299
|
+
test("exposes the tenant host a per-tenant provider needs at connect", () => {
|
|
300
|
+
// Shopify's OAuth endpoints live on the merchant's own host, so a client
|
|
301
|
+
// has to collect it before the managed flow can start. The summary is the
|
|
302
|
+
// only provider metadata the web client reads, so it carries the ask.
|
|
303
|
+
const shopify = serializeProviderSummary(makeRow({ provider: "shopify" }))!;
|
|
304
|
+
expect(shopify.tenant_host).toEqual({
|
|
305
|
+
pattern: "^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$",
|
|
306
|
+
label: "Shop domain",
|
|
307
|
+
placeholder: "your-store.myshopify.com",
|
|
308
|
+
});
|
|
309
|
+
expect(
|
|
310
|
+
new RegExp(shopify.tenant_host!.pattern).test("my-store.myshopify.com"),
|
|
311
|
+
).toBe(true);
|
|
312
|
+
expect(new RegExp(shopify.tenant_host!.pattern).test("evil.com")).toBe(
|
|
313
|
+
false,
|
|
314
|
+
);
|
|
315
|
+
expect(
|
|
316
|
+
serializeProviderSummary(makeRow({ provider: "github" }))!.tenant_host,
|
|
317
|
+
).toBeNull();
|
|
318
|
+
});
|
|
319
|
+
|
|
298
320
|
test("names which sense of connected a provider represents", () => {
|
|
299
321
|
// The provider key does not say, and the naming actively misleads:
|
|
300
322
|
// `slack` is the user integration while its bot is `slack_channel`, but
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ACP_OAUTH_EXPIRES_AT_FIELD,
|
|
5
|
+
ACP_OAUTH_REFRESH_TOKEN_FIELD,
|
|
6
|
+
ACP_OAUTH_TOKEN_FIELD,
|
|
7
|
+
ACP_SERVICE,
|
|
8
|
+
} from "../acp/acp-credentials.js";
|
|
4
9
|
import { credentialKey } from "../security/credential-key.js";
|
|
5
10
|
|
|
6
11
|
let secureKeyStore: Record<string, string | undefined> = {};
|
|
@@ -32,6 +37,11 @@ mock.module("../oauth/manual-token-connection.js", () => ({
|
|
|
32
37
|
syncManualTokenConnection: async () => {},
|
|
33
38
|
}));
|
|
34
39
|
|
|
40
|
+
mock.module("../runtime/routes/credential-in-use.js", () => ({
|
|
41
|
+
assertCredentialNotInUse: () => [],
|
|
42
|
+
invalidateConnectionsAfterCredentialDelete: () => {},
|
|
43
|
+
}));
|
|
44
|
+
|
|
35
45
|
afterAll(() => {
|
|
36
46
|
mock.restore();
|
|
37
47
|
});
|
|
@@ -42,6 +52,9 @@ import { ROUTES } from "../runtime/routes/secret-routes.js";
|
|
|
42
52
|
const addRoute = ROUTES.find(
|
|
43
53
|
(r) => r.method === "POST" && r.endpoint === "secrets",
|
|
44
54
|
)!;
|
|
55
|
+
const deleteRoute = ROUTES.find(
|
|
56
|
+
(r) => r.method === "DELETE" && r.endpoint === "secrets",
|
|
57
|
+
)!;
|
|
45
58
|
|
|
46
59
|
function addAcpOauthToken(value: string) {
|
|
47
60
|
return addRoute.handler({
|
|
@@ -74,4 +87,49 @@ describe("secret routes ACP OAuth-token format guard", () => {
|
|
|
74
87
|
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD)],
|
|
75
88
|
).toBe("sk-ant-oat01-valid-oauth-token");
|
|
76
89
|
});
|
|
90
|
+
|
|
91
|
+
test("stores a pasted Claude token without touching leftover refresh material", async () => {
|
|
92
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_REFRESH_TOKEN_FIELD)] =
|
|
93
|
+
"stale-refresh";
|
|
94
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_EXPIRES_AT_FIELD)] =
|
|
95
|
+
"111";
|
|
96
|
+
|
|
97
|
+
await addAcpOauthToken("sk-ant-oat01-pasted-token");
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD)],
|
|
101
|
+
).toBe("sk-ant-oat01-pasted-token");
|
|
102
|
+
expect(
|
|
103
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_REFRESH_TOKEN_FIELD)],
|
|
104
|
+
).toBe("stale-refresh");
|
|
105
|
+
expect(
|
|
106
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_EXPIRES_AT_FIELD)],
|
|
107
|
+
).toBe("111");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("deletes only the requested Claude access-token field", async () => {
|
|
111
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD)] =
|
|
112
|
+
"sk-ant-oat01-connected";
|
|
113
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_REFRESH_TOKEN_FIELD)] =
|
|
114
|
+
"refresh-leftover";
|
|
115
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_EXPIRES_AT_FIELD)] =
|
|
116
|
+
"111";
|
|
117
|
+
|
|
118
|
+
await deleteRoute.handler({
|
|
119
|
+
body: {
|
|
120
|
+
type: "credential",
|
|
121
|
+
name: `${ACP_SERVICE}:${ACP_OAUTH_TOKEN_FIELD}`,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD)],
|
|
127
|
+
).toBeUndefined();
|
|
128
|
+
expect(
|
|
129
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_REFRESH_TOKEN_FIELD)],
|
|
130
|
+
).toBe("refresh-leftover");
|
|
131
|
+
expect(
|
|
132
|
+
secureKeyStore[credentialKey(ACP_SERVICE, ACP_OAUTH_EXPIRES_AT_FIELD)],
|
|
133
|
+
).toBe("111");
|
|
134
|
+
});
|
|
77
135
|
});
|
|
@@ -1054,6 +1054,16 @@ describe("ingress-dependent setup skills declare public-ingress intentionally",
|
|
|
1054
1054
|
expect(includes ?? []).not.toContain("public-ingress");
|
|
1055
1055
|
});
|
|
1056
1056
|
|
|
1057
|
+
test("telegram-setup documents the private-chat limitation", () => {
|
|
1058
|
+
const content = readFileSync(
|
|
1059
|
+
join(FIRST_PARTY_SKILLS_DIR, "telegram-setup", "SKILL.md"),
|
|
1060
|
+
"utf-8",
|
|
1061
|
+
);
|
|
1062
|
+
expect(content).toMatch(/private-chat only/i);
|
|
1063
|
+
expect(content).toMatch(/group, supergroup, and channel/i);
|
|
1064
|
+
expect(content).toMatch(/button taps/i);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1057
1067
|
test("twilio-setup includes public-ingress", () => {
|
|
1058
1068
|
const includes = readSkillIncludes(FIRST_PARTY_SKILLS_DIR, "twilio-setup");
|
|
1059
1069
|
expect(includes).toBeDefined();
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
} from "../tools/registry.js";
|
|
89
89
|
import { RiskLevel } from "../tools/tool-types.js";
|
|
90
90
|
import type { Tool } from "../tools/types.js";
|
|
91
|
+
import { uiShowTool } from "../tools/ui-surface/definitions.js";
|
|
91
92
|
import { asConversation } from "./helpers/mock-conversation.js";
|
|
92
93
|
|
|
93
94
|
// ---------------------------------------------------------------------------
|
|
@@ -249,6 +250,56 @@ describe("createResolveToolsCallback — toolContextPin", () => {
|
|
|
249
250
|
});
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
test("Slack and desktop turns keep the same full ui_show schema while surfaces persist", () => {
|
|
254
|
+
projectedSkillToolNames = [];
|
|
255
|
+
const slackCaps = {
|
|
256
|
+
channel: "slack" as const,
|
|
257
|
+
dashboardCapable: false,
|
|
258
|
+
supportsDynamicUi: false,
|
|
259
|
+
supportsVoiceInput: false,
|
|
260
|
+
};
|
|
261
|
+
const desktopCaps = {
|
|
262
|
+
channel: "macos" as const,
|
|
263
|
+
dashboardCapable: true,
|
|
264
|
+
supportsDynamicUi: true,
|
|
265
|
+
supportsVoiceInput: true,
|
|
266
|
+
};
|
|
267
|
+
const liveSlackResolve = createResolveToolsCallback(
|
|
268
|
+
[uiShowTool],
|
|
269
|
+
makeProjectionCtx({
|
|
270
|
+
hasNoClient: false,
|
|
271
|
+
channelCapabilities: slackCaps,
|
|
272
|
+
}),
|
|
273
|
+
)!;
|
|
274
|
+
const clientlessSlackResolve = createResolveToolsCallback(
|
|
275
|
+
[uiShowTool],
|
|
276
|
+
clientlessExecutionCtx({
|
|
277
|
+
channelCapabilities: slackCaps,
|
|
278
|
+
}),
|
|
279
|
+
)!;
|
|
280
|
+
const liveDesktopResolve = createResolveToolsCallback(
|
|
281
|
+
[uiShowTool],
|
|
282
|
+
makeProjectionCtx({
|
|
283
|
+
hasNoClient: false,
|
|
284
|
+
channelCapabilities: desktopCaps,
|
|
285
|
+
}),
|
|
286
|
+
)!;
|
|
287
|
+
|
|
288
|
+
const [liveSlackUiShow] = liveSlackResolve(EMPTY_HISTORY);
|
|
289
|
+
const [clientlessSlackUiShow] = clientlessSlackResolve(EMPTY_HISTORY);
|
|
290
|
+
const [liveDesktopUiShow] = liveDesktopResolve(EMPTY_HISTORY);
|
|
291
|
+
expect(liveSlackUiShow).toEqual(clientlessSlackUiShow);
|
|
292
|
+
expect(liveSlackUiShow).toEqual(liveDesktopUiShow);
|
|
293
|
+
expect(
|
|
294
|
+
(
|
|
295
|
+
liveSlackUiShow.input_schema as {
|
|
296
|
+
properties: { surface_type: { enum: string[] } };
|
|
297
|
+
}
|
|
298
|
+
).properties.surface_type.enum,
|
|
299
|
+
).toContain("choice");
|
|
300
|
+
expect(liveSlackUiShow.description).toContain("dynamic_page");
|
|
301
|
+
});
|
|
302
|
+
|
|
252
303
|
test("control: without a pin, a clientless fork drops client-gated tools but keeps host and UI tools", () => {
|
|
253
304
|
projectedSkillToolNames = [];
|
|
254
305
|
const resolve = createResolveToolsCallback(
|
|
@@ -2,10 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import type { ToolDefinition } from "../providers/types.js";
|
|
4
4
|
import { ACTIVATION_MOMENT_PARAMS } from "../telemetry/activation-funnel.js";
|
|
5
|
-
import {
|
|
6
|
-
injectActivationMomentParam,
|
|
7
|
-
projectUiToolsForChannel,
|
|
8
|
-
} from "../tools/ui-surface/channel-variants.js";
|
|
5
|
+
import { injectActivationMomentParam } from "../tools/ui-surface/channel-variants.js";
|
|
9
6
|
import {
|
|
10
7
|
uiDismissTool,
|
|
11
8
|
uiShowTool,
|
|
@@ -23,58 +20,6 @@ const defs: ToolDefinition[] = [
|
|
|
23
20
|
},
|
|
24
21
|
];
|
|
25
22
|
|
|
26
|
-
function surfaceTypeEnum(def: ToolDefinition): string[] {
|
|
27
|
-
return (
|
|
28
|
-
def.input_schema as { properties: { surface_type: { enum: string[] } } }
|
|
29
|
-
).properties.surface_type.enum;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("projectUiToolsForChannel", () => {
|
|
33
|
-
test("returns the same array for channels without a variant", () => {
|
|
34
|
-
for (const channel of [undefined, "macos", "telegram", "phone"]) {
|
|
35
|
-
expect(projectUiToolsForChannel(defs, channel)).toBe(defs);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("slack gets a task_progress-only ui_show", () => {
|
|
40
|
-
const projected = projectUiToolsForChannel(defs, "slack");
|
|
41
|
-
const uiShow = projected.find((d) => d.name === "ui_show")!;
|
|
42
|
-
|
|
43
|
-
expect(surfaceTypeEnum(uiShow)).toEqual(["card"]);
|
|
44
|
-
expect(uiShow.description).toContain("task_progress");
|
|
45
|
-
expect(uiShow.description).toContain(
|
|
46
|
-
"the only surface this channel renders",
|
|
47
|
-
);
|
|
48
|
-
expect(uiShow.description).not.toContain("oauth_connect");
|
|
49
|
-
expect(uiShow.description).not.toContain("dynamic_page");
|
|
50
|
-
expect(uiShow.description!.length).toBeLessThan(
|
|
51
|
-
uiShowTool.description.length / 2,
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("slack projection keeps execute and leaves other tools untouched", () => {
|
|
56
|
-
const projected = projectUiToolsForChannel(defs, "slack");
|
|
57
|
-
const uiShow = projected.find((d) => d.name === "ui_show") as {
|
|
58
|
-
execute?: unknown;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
expect(uiShow.execute).toBe(uiShowTool.execute);
|
|
62
|
-
expect(projected.find((d) => d.name === "ui_update")).toBe(uiUpdateTool);
|
|
63
|
-
expect(projected.find((d) => d.name === "ui_dismiss")).toBe(uiDismissTool);
|
|
64
|
-
expect(projected.find((d) => d.name === "bash")).toBe(defs[3]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("projection never mutates the shared definitions", () => {
|
|
68
|
-
const descriptionBefore = uiShowTool.description;
|
|
69
|
-
const schemaBefore = JSON.stringify(uiShowTool.input_schema);
|
|
70
|
-
|
|
71
|
-
projectUiToolsForChannel(defs, "slack");
|
|
72
|
-
|
|
73
|
-
expect(uiShowTool.description).toBe(descriptionBefore);
|
|
74
|
-
expect(JSON.stringify(uiShowTool.input_schema)).toBe(schemaBefore);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
23
|
describe("injectActivationMomentParam", () => {
|
|
79
24
|
function properties(def: ToolDefinition): Record<string, unknown> {
|
|
80
25
|
return (def.input_schema as { properties: Record<string, unknown> })
|