@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Regression tests for app surface refresh and eventing side effects in
|
|
3
3
|
* createToolExecutor (conversation-tool-setup.ts).
|
|
4
4
|
*
|
|
5
|
-
* Tests verify that app_refresh, app_create, and app_delete hooks
|
|
6
|
-
* correctly, and that
|
|
7
|
-
*
|
|
5
|
+
* Tests verify that app_refresh, app_update, app_create, and app_delete hooks
|
|
6
|
+
* fire correctly, and that non-hooked tools (app_file_edit, app_file_write) do
|
|
7
|
+
* not trigger side effects.
|
|
8
8
|
*
|
|
9
9
|
* File-change detection for file_write/file_edit is handled by
|
|
10
10
|
* AppSourceWatcher (see app-source-watcher.test.ts).
|
|
@@ -248,6 +248,76 @@ describe("session-tool-setup app refresh side effects", () => {
|
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
// ── app_update ──────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
describe("app_update", () => {
|
|
254
|
+
test("triggers refreshSurfacesForApp and broadcast on success", async () => {
|
|
255
|
+
const ctx = makeCtx();
|
|
256
|
+
const executor = makeFakeExecutor({
|
|
257
|
+
content: '{"updated":true,"appId":"app-7"}',
|
|
258
|
+
isError: false,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const toolFn = createToolExecutor(
|
|
262
|
+
executor as unknown as ToolExecutor,
|
|
263
|
+
noopPrompter,
|
|
264
|
+
noopSecretPrompter,
|
|
265
|
+
ctx,
|
|
266
|
+
noopLifecycleHandler,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await toolFn("app_update", { app_id: "app-7" });
|
|
270
|
+
|
|
271
|
+
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect((refreshSpy.mock.calls as unknown[][])[0][1]).toBe("app-7");
|
|
273
|
+
expectAppChangeBroadcast("app-7");
|
|
274
|
+
expect(updatePublishedSpy).toHaveBeenCalledTimes(1);
|
|
275
|
+
expect((updatePublishedSpy.mock.calls as unknown[][])[0][0]).toBe(
|
|
276
|
+
"app-7",
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("skips side effects when result is an error", async () => {
|
|
281
|
+
const ctx = makeCtx();
|
|
282
|
+
const executor = makeFakeExecutor({
|
|
283
|
+
content: "Error: not found",
|
|
284
|
+
isError: true,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const toolFn = createToolExecutor(
|
|
288
|
+
executor as unknown as ToolExecutor,
|
|
289
|
+
noopPrompter,
|
|
290
|
+
noopSecretPrompter,
|
|
291
|
+
ctx,
|
|
292
|
+
noopLifecycleHandler,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
await toolFn("app_update", { app_id: "app-err" });
|
|
296
|
+
|
|
297
|
+
expect(refreshSpy).not.toHaveBeenCalled();
|
|
298
|
+
expect(broadcastSpy).not.toHaveBeenCalled();
|
|
299
|
+
expect(updatePublishedSpy).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("skips side effects when app_id is missing", async () => {
|
|
303
|
+
const ctx = makeCtx();
|
|
304
|
+
const executor = makeFakeExecutor({ content: "{}", isError: false });
|
|
305
|
+
|
|
306
|
+
const toolFn = createToolExecutor(
|
|
307
|
+
executor as unknown as ToolExecutor,
|
|
308
|
+
noopPrompter,
|
|
309
|
+
noopSecretPrompter,
|
|
310
|
+
ctx,
|
|
311
|
+
noopLifecycleHandler,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await toolFn("app_update", {});
|
|
315
|
+
|
|
316
|
+
expect(refreshSpy).not.toHaveBeenCalled();
|
|
317
|
+
expect(broadcastSpy).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
251
321
|
// ── app_create side effects ─────────────────────────────────────────
|
|
252
322
|
|
|
253
323
|
describe("app_create side effects", () => {
|
|
@@ -518,7 +588,6 @@ describe("session-tool-setup app refresh side effects", () => {
|
|
|
518
588
|
"write_file",
|
|
519
589
|
"shell",
|
|
520
590
|
"app_list",
|
|
521
|
-
"app_update",
|
|
522
591
|
"app_file_edit",
|
|
523
592
|
"app_file_write",
|
|
524
593
|
]) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { findLastUndoableUserMessageIndex } from "../daemon/conversation.js";
|
|
3
4
|
import {
|
|
4
5
|
CONTEXT_SUMMARY_MARKER,
|
|
5
6
|
createContextSummaryMessage,
|
|
6
|
-
} from "../
|
|
7
|
-
import { findLastUndoableUserMessageIndex } from "../daemon/conversation.js";
|
|
7
|
+
} from "../plugins/defaults/compaction/window-manager.js";
|
|
8
8
|
import type { Message } from "../providers/types.js";
|
|
9
9
|
|
|
10
10
|
function textMessage(role: "user" | "assistant", text: string): Message {
|
|
@@ -137,7 +137,7 @@ mock.module("../memory/retrieval-budget.js", () => ({
|
|
|
137
137
|
computeRecallBudget: () => 0,
|
|
138
138
|
}));
|
|
139
139
|
|
|
140
|
-
mock.module("../
|
|
140
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
141
141
|
ContextWindowManager: class {
|
|
142
142
|
constructor() {}
|
|
143
143
|
updateConfig() {}
|
|
@@ -181,7 +181,7 @@ mock.module("../memory/query-builder.js", () => ({
|
|
|
181
181
|
mock.module("../memory/retrieval-budget.js", () => ({
|
|
182
182
|
computeRecallBudget: () => 0,
|
|
183
183
|
}));
|
|
184
|
-
mock.module("../
|
|
184
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
185
185
|
ContextWindowManager: class {
|
|
186
186
|
updateConfig() {}
|
|
187
187
|
shouldCompact() {
|
|
@@ -178,7 +178,7 @@ mock.module("../memory/query-builder.js", () => ({
|
|
|
178
178
|
mock.module("../memory/retrieval-budget.js", () => ({
|
|
179
179
|
computeRecallBudget: () => 0,
|
|
180
180
|
}));
|
|
181
|
-
mock.module("../
|
|
181
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
182
182
|
ContextWindowManager: class {
|
|
183
183
|
updateConfig() {}
|
|
184
184
|
shouldCompact() {
|
|
@@ -230,6 +230,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
230
230
|
"runtime/routes/platform-routes.ts", // CLI platform connect/disconnect/status routes (CLI-migrated to IPC)
|
|
231
231
|
"ipc/skill-routes/providers.ts", // host.providers.secureKeys.getProviderKey IPC route (out-of-process SkillHost companion)
|
|
232
232
|
"daemon/external-plugins-bootstrap.ts", // reads credentials at plugin init (manifest.requiresCredential) via the CES-mediated getSecureKeyAsync path
|
|
233
|
+
"plugins/external-api.ts", // globalThis runtime bridge that exposes getSecureKeyAsync to dynamically-imported workspace plugins (compiled-binary plugin loading)
|
|
233
234
|
"inbound/platform-callback-registration.ts", // managed credential lookup for platform base URL, assistant ID, and API key
|
|
234
235
|
"tts/providers/elevenlabs-provider.ts", // ElevenLabs TTS API key lookup
|
|
235
236
|
"tts/providers/deepgram-provider.ts", // Deepgram TTS API key lookup
|
|
@@ -69,6 +69,7 @@ type SurfaceConversationContext =
|
|
|
69
69
|
function buildMockContext(
|
|
70
70
|
hostCuProxy?: InstanceType<typeof HostCuProxy>,
|
|
71
71
|
trustGuardianPrincipalId: string | null = DEFAULT_PRINCIPAL,
|
|
72
|
+
actorPrincipalId: string | null = trustGuardianPrincipalId,
|
|
72
73
|
): SurfaceConversationContext {
|
|
73
74
|
return {
|
|
74
75
|
conversationId: "test-session",
|
|
@@ -80,6 +81,16 @@ function buildMockContext(
|
|
|
80
81
|
guardianPrincipalId: trustGuardianPrincipalId,
|
|
81
82
|
}
|
|
82
83
|
: undefined,
|
|
84
|
+
authContext:
|
|
85
|
+
actorPrincipalId != null
|
|
86
|
+
? ({ actorPrincipalId } as SurfaceConversationContext["authContext"])
|
|
87
|
+
: undefined,
|
|
88
|
+
currentTurnAuthContext:
|
|
89
|
+
actorPrincipalId != null
|
|
90
|
+
? ({
|
|
91
|
+
actorPrincipalId,
|
|
92
|
+
} as SurfaceConversationContext["currentTurnAuthContext"])
|
|
93
|
+
: undefined,
|
|
83
94
|
traceEmitter: { emit: () => {} },
|
|
84
95
|
sendToClient: () => {},
|
|
85
96
|
pendingSurfaceActions: new Map(),
|
|
@@ -665,6 +676,31 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
665
676
|
expect(sentMessages).toHaveLength(0);
|
|
666
677
|
});
|
|
667
678
|
|
|
679
|
+
test("rejects untargeted CU dispatch to owner client when current requester is a trusted contact", async () => {
|
|
680
|
+
sentMessages.length = 0;
|
|
681
|
+
mockHasClient = true;
|
|
682
|
+
mockCuClients = [
|
|
683
|
+
{
|
|
684
|
+
clientId: "owner-cu-client",
|
|
685
|
+
capabilities: ["host_cu"],
|
|
686
|
+
actorPrincipalId: "owner-user",
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
proxy = new HostCuProxy();
|
|
690
|
+
const ctx = buildMockContext(proxy, "owner-user", "trusted-contact-user");
|
|
691
|
+
|
|
692
|
+
const result = await surfaceProxyResolver(ctx, "computer_use_click", {
|
|
693
|
+
element_id: 1,
|
|
694
|
+
reasoning: "click",
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
expect(result.isError).toBe(true);
|
|
698
|
+
expect(result.content).toContain("current actor");
|
|
699
|
+
expect(proxy.stepCount).toBe(0);
|
|
700
|
+
expect(proxy.actionHistory).toHaveLength(0);
|
|
701
|
+
expect(sentMessages).toHaveLength(0);
|
|
702
|
+
});
|
|
703
|
+
|
|
668
704
|
test("auto-resolves to the unique same-user CU client when cross-user clients are present", async () => {
|
|
669
705
|
// Regression: previously the dispatch counted only same-user clients
|
|
670
706
|
// for the multi-client guard, so 1 same-user + 1 cross-user passed the
|
|
@@ -40,6 +40,8 @@ const noopLogger: PluginLogger = {
|
|
|
40
40
|
function makeCtx(messages: Message[]): UserPromptSubmitContext {
|
|
41
41
|
return {
|
|
42
42
|
conversationId: "conv-test",
|
|
43
|
+
userMessageId: "msg-test",
|
|
44
|
+
requestId: "req-test",
|
|
43
45
|
prompt: "",
|
|
44
46
|
originalMessages: messages,
|
|
45
47
|
latestMessages: messages,
|
|
@@ -1249,3 +1249,76 @@ describe("resolveDefaultProfileKey", () => {
|
|
|
1249
1249
|
expect(resolveDefaultProfileKey("mainAgent", llm)).toBe("balanced");
|
|
1250
1250
|
});
|
|
1251
1251
|
});
|
|
1252
|
+
|
|
1253
|
+
describe("resolveCallSiteConfig logitBias provenance", () => {
|
|
1254
|
+
const kimi = "accounts/fireworks/models/kimi-k2p6";
|
|
1255
|
+
|
|
1256
|
+
test("forwards logitBias from the active profile that opted in", () => {
|
|
1257
|
+
const llm = LLMSchema.parse({
|
|
1258
|
+
default: fullDefault,
|
|
1259
|
+
profiles: {
|
|
1260
|
+
"balanced-economy": {
|
|
1261
|
+
provider: "fireworks",
|
|
1262
|
+
model: kimi,
|
|
1263
|
+
logitBias: "suppress-cjk",
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
activeProfile: "balanced-economy",
|
|
1267
|
+
});
|
|
1268
|
+
expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBe(
|
|
1269
|
+
"suppress-cjk",
|
|
1270
|
+
);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("a higher-precedence override profile that omits logitBias clears it", () => {
|
|
1274
|
+
// Active profile opts in, but a pinned (override) Kimi profile did not —
|
|
1275
|
+
// the override must not inherit suppress-cjk just because it resolves to
|
|
1276
|
+
// Fireworks.
|
|
1277
|
+
const llm = LLMSchema.parse({
|
|
1278
|
+
default: fullDefault,
|
|
1279
|
+
profiles: {
|
|
1280
|
+
"balanced-economy": {
|
|
1281
|
+
provider: "fireworks",
|
|
1282
|
+
model: kimi,
|
|
1283
|
+
logitBias: "suppress-cjk",
|
|
1284
|
+
},
|
|
1285
|
+
"my-kimi": { provider: "fireworks", model: kimi },
|
|
1286
|
+
},
|
|
1287
|
+
activeProfile: "balanced-economy",
|
|
1288
|
+
});
|
|
1289
|
+
const resolved = resolveCallSiteConfig("mainAgent", llm, {
|
|
1290
|
+
overrideProfile: "my-kimi",
|
|
1291
|
+
});
|
|
1292
|
+
expect(resolved.logitBias).toBeUndefined();
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
test("does not leak the active profile's logitBias into a call site's own profile", () => {
|
|
1296
|
+
// For non-main call sites the call-site profile wins; since it didn't opt
|
|
1297
|
+
// in, the active profile's preset must not bleed through.
|
|
1298
|
+
const llm = LLMSchema.parse({
|
|
1299
|
+
default: fullDefault,
|
|
1300
|
+
profiles: {
|
|
1301
|
+
"balanced-economy": {
|
|
1302
|
+
provider: "fireworks",
|
|
1303
|
+
model: kimi,
|
|
1304
|
+
logitBias: "suppress-cjk",
|
|
1305
|
+
},
|
|
1306
|
+
plain: { provider: "anthropic", model: "claude-opus-4-7" },
|
|
1307
|
+
},
|
|
1308
|
+
activeProfile: "balanced-economy",
|
|
1309
|
+
callSites: { memoryExtraction: { profile: "plain" } },
|
|
1310
|
+
});
|
|
1311
|
+
expect(
|
|
1312
|
+
resolveCallSiteConfig("memoryExtraction", llm).logitBias,
|
|
1313
|
+
).toBeUndefined();
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
test("a logitBias on a non-profile layer (llm.default) does not apply when the winning profile omits it", () => {
|
|
1317
|
+
const llm = LLMSchema.parse({
|
|
1318
|
+
default: { ...fullDefault, logitBias: "suppress-cjk" },
|
|
1319
|
+
profiles: { plain: { provider: "anthropic", model: "claude-opus-4-7" } },
|
|
1320
|
+
activeProfile: "plain",
|
|
1321
|
+
});
|
|
1322
|
+
expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBeUndefined();
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
@@ -36,6 +36,7 @@ const applyRuntimeInjectionsMock = mock(async (messages: unknown) => ({
|
|
|
36
36
|
}));
|
|
37
37
|
mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
38
38
|
applyRuntimeInjections: applyRuntimeInjectionsMock,
|
|
39
|
+
resolveTurnInboundActorContext: () => null,
|
|
39
40
|
}));
|
|
40
41
|
|
|
41
42
|
// The hook self-resolves the live conversation and its trust class; both are
|
|
@@ -146,10 +147,8 @@ function makeHookCtx(
|
|
|
146
147
|
} as unknown as MemoryRetrievalHookContext["logger"],
|
|
147
148
|
latestMessages: [],
|
|
148
149
|
requestId: "req-test",
|
|
149
|
-
mode: "full",
|
|
150
150
|
isNonInteractive: false,
|
|
151
151
|
modelProfile: null,
|
|
152
|
-
actorContext: null,
|
|
153
152
|
...overrides,
|
|
154
153
|
};
|
|
155
154
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the durable-downscale path of the image-too-large
|
|
3
|
+
* recovery (JARVIS-1041 review follow-up).
|
|
4
|
+
*
|
|
5
|
+
* When an oversized stored image *can* be shrunk on this host (the common macOS
|
|
6
|
+
* path where `sips` is available), `persistUnsendableImageDowngrades` must write
|
|
7
|
+
* the downscaled bytes back to the DB — not leave the original in place. The
|
|
8
|
+
* latest tool-result media is intentionally kept in context, so leaving the
|
|
9
|
+
* full-size block would rehydrate and re-reject on every later turn instead of
|
|
10
|
+
* durably self-healing the conversation.
|
|
11
|
+
*
|
|
12
|
+
* `optimizeImageForTransport` needs `sips` and a decodable image to actually
|
|
13
|
+
* downscale, which is not portable to CI, so it is mocked here to simulate a
|
|
14
|
+
* successful shrink. The mock is process-global, so this case lives in its own
|
|
15
|
+
* file (the test runner isolates each file in its own process) to avoid
|
|
16
|
+
* disturbing the no-op-resize cases in persist-unsendable-image.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
20
|
+
|
|
21
|
+
// ── Module mock (must precede the import of the module under test) ────
|
|
22
|
+
// Simulate a host where resizing succeeds: any oversized image is shrunk to a
|
|
23
|
+
// small, distinct JPEG payload. durableImageReplacement checks the provider
|
|
24
|
+
// caps before calling this, so in-limit images never reach the mock.
|
|
25
|
+
const SHRUNK_DATA = "c2hydW5r"; // base64 for "shrunk"
|
|
26
|
+
mock.module("../agent/image-optimize.js", () => ({
|
|
27
|
+
optimizeImageForTransport: () => ({
|
|
28
|
+
data: SHRUNK_DATA,
|
|
29
|
+
mediaType: "image/jpeg",
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import { persistUnsendableImageDowngrades } from "../daemon/persist-unsendable-image.js";
|
|
34
|
+
import {
|
|
35
|
+
addMessage,
|
|
36
|
+
createConversation,
|
|
37
|
+
getMessages,
|
|
38
|
+
} from "../memory/conversation-crud.js";
|
|
39
|
+
import { getDb } from "../memory/db-connection.js";
|
|
40
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
41
|
+
import type { ContentBlock } from "../providers/types.js";
|
|
42
|
+
|
|
43
|
+
initializeDb();
|
|
44
|
+
|
|
45
|
+
function resetTables(): void {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
db.run("DELETE FROM message_attachments");
|
|
48
|
+
db.run("DELETE FROM attachments");
|
|
49
|
+
db.run("DELETE FROM memory_segments");
|
|
50
|
+
db.run("DELETE FROM memory_embeddings");
|
|
51
|
+
db.run("DELETE FROM messages");
|
|
52
|
+
db.run("DELETE FROM conversations");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Minimal PNG whose IHDR declares dimensions past the 8000px provider cap. */
|
|
56
|
+
function oversizedPngBase64(): string {
|
|
57
|
+
const width = 12000;
|
|
58
|
+
const height = 9000;
|
|
59
|
+
return Buffer.from(
|
|
60
|
+
Uint8Array.from([
|
|
61
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
|
|
62
|
+
0x00, 0x00, 0x00, 0x0d, // IHDR length (13)
|
|
63
|
+
0x49, 0x48, 0x44, 0x52, // "IHDR"
|
|
64
|
+
(width >>> 24) & 0xff,
|
|
65
|
+
(width >>> 16) & 0xff,
|
|
66
|
+
(width >>> 8) & 0xff,
|
|
67
|
+
width & 0xff,
|
|
68
|
+
(height >>> 24) & 0xff,
|
|
69
|
+
(height >>> 16) & 0xff,
|
|
70
|
+
(height >>> 8) & 0xff,
|
|
71
|
+
height & 0xff,
|
|
72
|
+
0x08, 0x06, 0x00, 0x00, 0x00,
|
|
73
|
+
]),
|
|
74
|
+
).toString("base64");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolResultWithImage(data: string): ContentBlock {
|
|
78
|
+
return {
|
|
79
|
+
type: "tool_result",
|
|
80
|
+
tool_use_id: "toolu_123",
|
|
81
|
+
content: "Screenshot captured",
|
|
82
|
+
contentBlocks: [
|
|
83
|
+
{ type: "image", source: { type: "base64", media_type: "image/png", data } },
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function storedContent(conversationId: string): ContentBlock[][] {
|
|
89
|
+
return getMessages(conversationId).map(
|
|
90
|
+
(row) => JSON.parse(row.content) as ContentBlock[],
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("persistUnsendableImageDowngrades (downscalable host)", () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
resetTables();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** JARVIS-1041: an oversized screenshot that CAN be shrunk must persist the
|
|
100
|
+
* downscaled bytes, not the note and not the original. */
|
|
101
|
+
test("persists the downscaled image for a shrinkable tool_result screenshot", async () => {
|
|
102
|
+
// GIVEN a tool_result holding an oversized but shrinkable screenshot
|
|
103
|
+
const conv = createConversation();
|
|
104
|
+
await addMessage(
|
|
105
|
+
conv.id,
|
|
106
|
+
"user",
|
|
107
|
+
JSON.stringify([toolResultWithImage(oversizedPngBase64())]),
|
|
108
|
+
{ skipIndexing: true },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// WHEN the downgrade is persisted
|
|
112
|
+
const rewritten = persistUnsendableImageDowngrades(conv.id);
|
|
113
|
+
|
|
114
|
+
// THEN the nested block stays an image, rewritten to the downscaled payload
|
|
115
|
+
expect(rewritten).toBe(1);
|
|
116
|
+
const [content] = storedContent(conv.id);
|
|
117
|
+
const toolResult = content.find((b) => b.type === "tool_result") as {
|
|
118
|
+
contentBlocks?: ContentBlock[];
|
|
119
|
+
};
|
|
120
|
+
const nested = toolResult.contentBlocks?.[0];
|
|
121
|
+
expect(nested?.type).toBe("image");
|
|
122
|
+
expect((nested as Extract<ContentBlock, { type: "image" }>).source.data).toBe(
|
|
123
|
+
SHRUNK_DATA,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** Re-running is a no-op: the downscaled payload is within limits. */
|
|
128
|
+
test("is idempotent after a downscale rewrite", async () => {
|
|
129
|
+
// GIVEN a conversation whose oversized screenshot was already downscaled
|
|
130
|
+
const conv = createConversation();
|
|
131
|
+
await addMessage(
|
|
132
|
+
conv.id,
|
|
133
|
+
"user",
|
|
134
|
+
JSON.stringify([toolResultWithImage(oversizedPngBase64())]),
|
|
135
|
+
{ skipIndexing: true },
|
|
136
|
+
);
|
|
137
|
+
expect(persistUnsendableImageDowngrades(conv.id)).toBe(1);
|
|
138
|
+
|
|
139
|
+
// WHEN the downgrade runs again
|
|
140
|
+
const secondRun = persistUnsendableImageDowngrades(conv.id);
|
|
141
|
+
|
|
142
|
+
// THEN nothing further is rewritten
|
|
143
|
+
expect(secondRun).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
oversizedImageReplacement,
|
|
19
|
+
persistUnsendableImageDowngrades,
|
|
20
|
+
} from "../daemon/persist-unsendable-image.js";
|
|
18
21
|
import {
|
|
19
22
|
addMessage,
|
|
20
23
|
createConversation,
|
|
@@ -87,6 +90,20 @@ function imageBlock(data: string): ContentBlock {
|
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
/**
|
|
94
|
+
* A tool_result carrying a nested image in its contentBlocks, mirroring what a
|
|
95
|
+
* browser screenshot produces. This is the JARVIS-1041 shape: the oversized
|
|
96
|
+
* image lives at tool_result.contentBlocks, never as a top-level block.
|
|
97
|
+
*/
|
|
98
|
+
function toolResultWithImage(data: string): ContentBlock {
|
|
99
|
+
return {
|
|
100
|
+
type: "tool_result",
|
|
101
|
+
tool_use_id: "toolu_123",
|
|
102
|
+
content: "Screenshot captured",
|
|
103
|
+
contentBlocks: [imageBlock(data)],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
function storedContent(conversationId: string): ContentBlock[][] {
|
|
91
108
|
return getMessages(conversationId).map(
|
|
92
109
|
(row) => JSON.parse(row.content) as ContentBlock[],
|
|
@@ -194,6 +211,61 @@ describe("persistUnsendableImageDowngrades", () => {
|
|
|
194
211
|
expect(content.some((b) => b.type === "image")).toBe(false);
|
|
195
212
|
});
|
|
196
213
|
|
|
214
|
+
/** JARVIS-1041: the oversized image is nested inside a tool_result (e.g. a
|
|
215
|
+
* browser screenshot), not a top-level block. The downgrade must descend
|
|
216
|
+
* into tool_result.contentBlocks and swap the nested image for a note, while
|
|
217
|
+
* keeping the tool_result itself intact so tool_use/tool_result pairing
|
|
218
|
+
* survives. */
|
|
219
|
+
test("downgrades an oversized image nested in tool_result.contentBlocks", async () => {
|
|
220
|
+
// GIVEN an assistant turn whose tool_result holds an oversized screenshot
|
|
221
|
+
const conv = createConversation();
|
|
222
|
+
await addMessage(
|
|
223
|
+
conv.id,
|
|
224
|
+
"user",
|
|
225
|
+
JSON.stringify([toolResultWithImage(makePngBase64(12000, 9000))]),
|
|
226
|
+
{ skipIndexing: true },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// WHEN the downgrade is persisted
|
|
230
|
+
const rewritten = persistUnsendableImageDowngrades(conv.id);
|
|
231
|
+
|
|
232
|
+
// THEN the message is rewritten with the nested image swapped for a note
|
|
233
|
+
expect(rewritten).toBe(1);
|
|
234
|
+
const [content] = storedContent(conv.id);
|
|
235
|
+
const toolResult = content.find((b) => b.type === "tool_result");
|
|
236
|
+
expect(toolResult).toBeDefined();
|
|
237
|
+
// AND the tool_result is preserved (pairing intact) with no image left
|
|
238
|
+
const nested = (toolResult as { contentBlocks?: ContentBlock[] })
|
|
239
|
+
.contentBlocks;
|
|
240
|
+
expect(nested?.some((b) => b.type === "image")).toBe(false);
|
|
241
|
+
expect(nested?.some((b) => b.type === "text")).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/** A sendable nested screenshot is never disturbed. */
|
|
245
|
+
test("leaves a normally-sized tool_result image untouched", async () => {
|
|
246
|
+
// GIVEN a tool_result with an image well within provider limits
|
|
247
|
+
const conv = createConversation();
|
|
248
|
+
await addMessage(
|
|
249
|
+
conv.id,
|
|
250
|
+
"user",
|
|
251
|
+
JSON.stringify([toolResultWithImage(makePngBase64(1024, 768))]),
|
|
252
|
+
{ skipIndexing: true },
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// WHEN the downgrade is persisted
|
|
256
|
+
const rewritten = persistUnsendableImageDowngrades(conv.id);
|
|
257
|
+
|
|
258
|
+
// THEN nothing is rewritten and the nested image remains
|
|
259
|
+
expect(rewritten).toBe(0);
|
|
260
|
+
const [content] = storedContent(conv.id);
|
|
261
|
+
const toolResult = content.find((b) => b.type === "tool_result") as {
|
|
262
|
+
contentBlocks?: ContentBlock[];
|
|
263
|
+
};
|
|
264
|
+
expect(toolResult.contentBlocks?.some((b) => b.type === "image")).toBe(
|
|
265
|
+
true,
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
197
269
|
/** Re-running after a rewrite is a safe no-op (no image blocks remain). */
|
|
198
270
|
test("is idempotent — a second run rewrites nothing", async () => {
|
|
199
271
|
// GIVEN a conversation whose oversized image has already been downgraded
|
|
@@ -213,3 +285,27 @@ describe("persistUnsendableImageDowngrades", () => {
|
|
|
213
285
|
expect(secondRun).toBe(0);
|
|
214
286
|
});
|
|
215
287
|
});
|
|
288
|
+
|
|
289
|
+
describe("oversizedImageReplacement", () => {
|
|
290
|
+
/** A still-sendable image must be left alone — never replaced with a note.
|
|
291
|
+
* This is the gate that keeps the in-memory recovery from discarding valid
|
|
292
|
+
* screenshots when only one image in the turn was actually oversized. */
|
|
293
|
+
test("returns null for an image within the provider caps", () => {
|
|
294
|
+
const sendable = imageBlock(makePngBase64(1024, 768)) as Extract<
|
|
295
|
+
ContentBlock,
|
|
296
|
+
{ type: "image" }
|
|
297
|
+
>;
|
|
298
|
+
expect(oversizedImageReplacement(sendable)).toBeNull();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/** An image past the provider caps that cannot be shrunk on this host (fake
|
|
302
|
+
* PNG that sips cannot decode) collapses to the unsendable note. */
|
|
303
|
+
test("returns the unsendable note when an oversized image cannot be shrunk", () => {
|
|
304
|
+
const oversized = imageBlock(makePngBase64(12000, 9000)) as Extract<
|
|
305
|
+
ContentBlock,
|
|
306
|
+
{ type: "image" }
|
|
307
|
+
>;
|
|
308
|
+
const replacement = oversizedImageReplacement(oversized);
|
|
309
|
+
expect(replacement?.type).toBe("text");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `globalThis.__vellumPluginRuntime` bridge.
|
|
3
|
+
*
|
|
4
|
+
* The bridge exists so workspace-local plugins (`<workspaceDir>/plugins/*`)
|
|
5
|
+
* can register with the daemon's bundled module instances even when the
|
|
6
|
+
* daemon is a `bun --compile` binary. Absolute-path imports against a
|
|
7
|
+
* compiled binary load fresh disk copies into a disjoint module graph; the
|
|
8
|
+
* bridge sidesteps that by attaching a stable handle to globalThis.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getPluginRuntime,
|
|
15
|
+
installPluginRuntime,
|
|
16
|
+
uninstallPluginRuntimeForTests,
|
|
17
|
+
} from "../plugins/external-api.js";
|
|
18
|
+
import { registerPlugin } from "../plugins/registry.js";
|
|
19
|
+
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
20
|
+
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
21
|
+
|
|
22
|
+
describe("plugin external-api bridge", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
uninstallPluginRuntimeForTests();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
uninstallPluginRuntimeForTests();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("installs a runtime handle on globalThis", () => {
|
|
32
|
+
expect(getPluginRuntime()).toBeUndefined();
|
|
33
|
+
installPluginRuntime();
|
|
34
|
+
const runtime = getPluginRuntime();
|
|
35
|
+
expect(runtime).toBeDefined();
|
|
36
|
+
expect(runtime?.version).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("exposes the canonical registerPlugin / hub / secrets references", () => {
|
|
40
|
+
installPluginRuntime();
|
|
41
|
+
const runtime = getPluginRuntime();
|
|
42
|
+
expect(runtime?.registerPlugin).toBe(registerPlugin);
|
|
43
|
+
expect(runtime?.assistantEventHub).toBe(assistantEventHub);
|
|
44
|
+
expect(runtime?.getSecureKeyAsync).toBe(getSecureKeyAsync);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("plugins can read the runtime via the documented globalThis key", () => {
|
|
48
|
+
installPluginRuntime();
|
|
49
|
+
// Mirror the access pattern documented for plugin authors.
|
|
50
|
+
const runtime = (
|
|
51
|
+
globalThis as { __vellumPluginRuntime?: { version: number } }
|
|
52
|
+
).__vellumPluginRuntime;
|
|
53
|
+
expect(runtime).toBeDefined();
|
|
54
|
+
expect(runtime?.version).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("install is idempotent — repeat calls preserve the same handle", () => {
|
|
58
|
+
installPluginRuntime();
|
|
59
|
+
const first = getPluginRuntime();
|
|
60
|
+
installPluginRuntime();
|
|
61
|
+
installPluginRuntime();
|
|
62
|
+
expect(getPluginRuntime()).toBe(first);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("getPluginRuntime returns undefined when the bridge is not installed", () => {
|
|
66
|
+
expect(getPluginRuntime()).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|