@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa
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/.env.example +2 -0
- package/ARCHITECTURE.md +26 -3
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +38 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +161 -17
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +54 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +9 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +14 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/process-message.ts +5 -0
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/tool-approval-handler.ts +9 -1
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { saveRawConfig } from "../config/loader.js";
|
|
4
|
+
import type { NotificationSignal } from "../notifications/signal.js";
|
|
5
|
+
import type { NotificationChannel } from "../notifications/types.js";
|
|
6
|
+
|
|
7
|
+
let assistantInitiatedCandidate = false;
|
|
8
|
+
const resolveAssistantInitiatedThreadMock = mock(
|
|
9
|
+
(signal: NotificationSignal) => ({
|
|
10
|
+
isCandidate: assistantInitiatedCandidate,
|
|
11
|
+
vellumSignal: assistantInitiatedCandidate
|
|
12
|
+
? {
|
|
13
|
+
...signal,
|
|
14
|
+
requiresConversation: true,
|
|
15
|
+
conversationMetadata: { source: "assistant_initiated" },
|
|
16
|
+
}
|
|
17
|
+
: signal,
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
mock.module("../notifications/assistant-initiated-thread.js", () => ({
|
|
21
|
+
resolveAssistantInitiatedThread: resolveAssistantInitiatedThreadMock,
|
|
22
|
+
}));
|
|
23
|
+
|
|
3
24
|
const evaluateSignalMock = mock();
|
|
4
25
|
const enforceRoutingIntentMock = mock();
|
|
5
26
|
const updateDecisionMock = mock();
|
|
@@ -8,6 +29,14 @@ const createEventMock = mock();
|
|
|
8
29
|
const setEventDedupeKeyMock = mock();
|
|
9
30
|
const dispatchDecisionMock = mock();
|
|
10
31
|
const isPlatformClientConfiguredMock = mock();
|
|
32
|
+
const writeHomeFeedItemForSignalMock = mock(
|
|
33
|
+
async (..._args: unknown[]) => null,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
mock.module("../notifications/home-feed-side-effect.js", () => ({
|
|
37
|
+
writeHomeFeedItemForSignal: (...args: unknown[]) =>
|
|
38
|
+
writeHomeFeedItemForSignalMock(...args),
|
|
39
|
+
}));
|
|
11
40
|
|
|
12
41
|
mock.module("../channels/config.js", () => ({
|
|
13
42
|
getDeliverableChannels: () => ["vellum", "telegram"],
|
|
@@ -101,6 +130,10 @@ import {
|
|
|
101
130
|
} from "../notifications/emit-signal.js";
|
|
102
131
|
|
|
103
132
|
beforeEach(() => {
|
|
133
|
+
saveRawConfig({});
|
|
134
|
+
assistantInitiatedCandidate = false;
|
|
135
|
+
resolveAssistantInitiatedThreadMock.mockClear();
|
|
136
|
+
writeHomeFeedItemForSignalMock.mockClear();
|
|
104
137
|
evaluateSignalMock.mockReset();
|
|
105
138
|
enforceRoutingIntentMock.mockReset();
|
|
106
139
|
updateDecisionMock.mockReset();
|
|
@@ -121,6 +154,104 @@ beforeEach(() => {
|
|
|
121
154
|
enforceRoutingIntentMock.mockImplementation((decision: unknown) => decision);
|
|
122
155
|
});
|
|
123
156
|
|
|
157
|
+
describe("emitNotificationSignal chat reply preference", () => {
|
|
158
|
+
test.each([
|
|
159
|
+
[false, false],
|
|
160
|
+
[true, true],
|
|
161
|
+
[false, true],
|
|
162
|
+
[true, false],
|
|
163
|
+
])(
|
|
164
|
+
"preserves routing candidates when the setting changes %s -> %s",
|
|
165
|
+
async (initial, enabled) => {
|
|
166
|
+
saveRawConfig({ notifications: { newMessageEnabled: initial } });
|
|
167
|
+
const selectedChannels: NotificationChannel[] = [
|
|
168
|
+
"vellum",
|
|
169
|
+
"platform",
|
|
170
|
+
"telegram",
|
|
171
|
+
];
|
|
172
|
+
const decision = {
|
|
173
|
+
shouldNotify: true,
|
|
174
|
+
selectedChannels,
|
|
175
|
+
reasoningSummary: "Selected reply delivery",
|
|
176
|
+
renderedCopy: {
|
|
177
|
+
vellum: { title: "Answer", body: "The result is ready." },
|
|
178
|
+
},
|
|
179
|
+
dedupeKey: "reply-1",
|
|
180
|
+
confidence: 1,
|
|
181
|
+
fallbackUsed: false,
|
|
182
|
+
persistedDecisionId: "decision-1",
|
|
183
|
+
};
|
|
184
|
+
evaluateSignalMock.mockResolvedValue(decision);
|
|
185
|
+
enforceRoutingIntentMock.mockReturnValue(decision);
|
|
186
|
+
runDeterministicChecksMock.mockImplementation(async () => {
|
|
187
|
+
await Promise.resolve();
|
|
188
|
+
saveRawConfig({ notifications: { newMessageEnabled: enabled } });
|
|
189
|
+
return { passed: true };
|
|
190
|
+
});
|
|
191
|
+
const result = await emitNotificationSignal({
|
|
192
|
+
sourceEventName: "chat.assistant_reply",
|
|
193
|
+
sourceChannel: "vellum",
|
|
194
|
+
sourceContextId: "conv-1",
|
|
195
|
+
attentionHints: {
|
|
196
|
+
urgency: "high",
|
|
197
|
+
requiresAction: false,
|
|
198
|
+
isAsyncBackground: false,
|
|
199
|
+
visibleInSourceNow: false,
|
|
200
|
+
},
|
|
201
|
+
routingIntent: "all_channels",
|
|
202
|
+
});
|
|
203
|
+
expect(runDeterministicChecksMock.mock.calls[0]?.[1]).toMatchObject({
|
|
204
|
+
selectedChannels: decision.selectedChannels,
|
|
205
|
+
});
|
|
206
|
+
expect(result.selectedChannels).toEqual(selectedChannels);
|
|
207
|
+
expect(result.dispatched).toBe(true);
|
|
208
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[1]).toMatchObject({
|
|
209
|
+
shouldNotify: true,
|
|
210
|
+
selectedChannels,
|
|
211
|
+
});
|
|
212
|
+
expect(writeHomeFeedItemForSignalMock).toHaveBeenCalledTimes(1);
|
|
213
|
+
expect(writeHomeFeedItemForSignalMock.mock.calls[0]?.[1]).toMatchObject({
|
|
214
|
+
shouldNotify: true,
|
|
215
|
+
selectedChannels,
|
|
216
|
+
});
|
|
217
|
+
expect(updateDecisionMock).not.toHaveBeenCalled();
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
test.each([
|
|
222
|
+
"schedule.result",
|
|
223
|
+
"guardian.question",
|
|
224
|
+
"ingress.access_request",
|
|
225
|
+
"activity.failed",
|
|
226
|
+
"chat.assistant_reply.extra",
|
|
227
|
+
])("preserves platform selection for %s", async (sourceEventName) => {
|
|
228
|
+
saveRawConfig({ notifications: { newMessageEnabled: false } });
|
|
229
|
+
evaluateSignalMock.mockResolvedValue({
|
|
230
|
+
shouldNotify: true,
|
|
231
|
+
selectedChannels: ["vellum", "platform"],
|
|
232
|
+
reasoningSummary: "Urgent delivery",
|
|
233
|
+
renderedCopy: { vellum: { title: "Alert", body: "Action required." } },
|
|
234
|
+
dedupeKey: "alert-1",
|
|
235
|
+
confidence: 1,
|
|
236
|
+
fallbackUsed: false,
|
|
237
|
+
persistedDecisionId: "decision-1",
|
|
238
|
+
});
|
|
239
|
+
const result = await emitNotificationSignal({
|
|
240
|
+
sourceEventName,
|
|
241
|
+
sourceChannel: "vellum",
|
|
242
|
+
sourceContextId: "conv-1",
|
|
243
|
+
attentionHints: {
|
|
244
|
+
urgency: "critical",
|
|
245
|
+
requiresAction: true,
|
|
246
|
+
isAsyncBackground: false,
|
|
247
|
+
visibleInSourceNow: false,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
expect(result.selectedChannels).toEqual(["vellum", "platform"]);
|
|
251
|
+
expect(updateDecisionMock).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
124
255
|
describe("emitNotificationSignal routing intent re-persistence", () => {
|
|
125
256
|
test("re-persists selectedChannels/reasoningSummary when enforcement changes the decision", async () => {
|
|
126
257
|
const preDecision = {
|
|
@@ -909,3 +1040,142 @@ describe("classifyNotificationReceipt", () => {
|
|
|
909
1040
|
).toBe("unknown");
|
|
910
1041
|
});
|
|
911
1042
|
});
|
|
1043
|
+
|
|
1044
|
+
describe("emitNotificationSignal From me push defaults", () => {
|
|
1045
|
+
const params = {
|
|
1046
|
+
sourceEventName: "assistant.share",
|
|
1047
|
+
sourceChannel: "assistant_tool" as const,
|
|
1048
|
+
sourceContextId: "conv-background",
|
|
1049
|
+
attentionHints: {
|
|
1050
|
+
urgency: "low" as const,
|
|
1051
|
+
requiresAction: false,
|
|
1052
|
+
isAsyncBackground: true,
|
|
1053
|
+
visibleInSourceNow: false,
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
const decision = (selectedChannels: NotificationChannel[] = ["vellum"]) => ({
|
|
1057
|
+
shouldNotify: true,
|
|
1058
|
+
selectedChannels,
|
|
1059
|
+
reasoningSummary: "Assistant-authored share",
|
|
1060
|
+
renderedCopy: { vellum: { title: "An update", body: "A useful result." } },
|
|
1061
|
+
dedupeKey: "share-1",
|
|
1062
|
+
confidence: 1,
|
|
1063
|
+
fallbackUsed: false,
|
|
1064
|
+
persistedDecisionId: "decision-share",
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
beforeEach(() => {
|
|
1068
|
+
assistantInitiatedCandidate = true;
|
|
1069
|
+
evaluateSignalMock.mockResolvedValue(decision());
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test("adds configured platform after routing and persists the final decision", async () => {
|
|
1073
|
+
const result = await emitNotificationSignal(params);
|
|
1074
|
+
expect(result.selectedChannels).toEqual(["vellum", "platform"]);
|
|
1075
|
+
expect(resolveAssistantInitiatedThreadMock).toHaveBeenCalledTimes(1);
|
|
1076
|
+
expect(updateDecisionMock).toHaveBeenCalledWith(
|
|
1077
|
+
"decision-share",
|
|
1078
|
+
expect.objectContaining({
|
|
1079
|
+
selectedChannels: ["vellum", "platform"],
|
|
1080
|
+
}),
|
|
1081
|
+
);
|
|
1082
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[0]).toMatchObject({
|
|
1083
|
+
attentionHints: { urgency: "low" },
|
|
1084
|
+
});
|
|
1085
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[3]).toMatchObject({
|
|
1086
|
+
assistantInitiatedThread: {
|
|
1087
|
+
platformAdded: true,
|
|
1088
|
+
resolution: {
|
|
1089
|
+
isCandidate: true,
|
|
1090
|
+
vellumSignal: {
|
|
1091
|
+
requiresConversation: true,
|
|
1092
|
+
conversationMetadata: { source: "assistant_initiated" },
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
test.each([
|
|
1100
|
+
"quiet",
|
|
1101
|
+
"unconfigured",
|
|
1102
|
+
"noncandidate",
|
|
1103
|
+
"suppressed",
|
|
1104
|
+
"single_channel",
|
|
1105
|
+
"external_only",
|
|
1106
|
+
"vellum_allowlist",
|
|
1107
|
+
])("respects %s policy without adding platform", async (scenario) => {
|
|
1108
|
+
assistantInitiatedCandidate = scenario !== "noncandidate";
|
|
1109
|
+
isPlatformClientConfiguredMock.mockResolvedValue(
|
|
1110
|
+
scenario !== "unconfigured",
|
|
1111
|
+
);
|
|
1112
|
+
evaluateSignalMock.mockResolvedValue({
|
|
1113
|
+
...decision(scenario === "external_only" ? ["telegram"] : ["vellum"]),
|
|
1114
|
+
shouldNotify: scenario !== "suppressed",
|
|
1115
|
+
});
|
|
1116
|
+
const result = await emitNotificationSignal({
|
|
1117
|
+
...params,
|
|
1118
|
+
routingIntent:
|
|
1119
|
+
scenario === "single_channel" ? "single_channel" : undefined,
|
|
1120
|
+
contextPayload: {
|
|
1121
|
+
quiet: scenario === "quiet",
|
|
1122
|
+
...(scenario === "vellum_allowlist"
|
|
1123
|
+
? { channelAllowlist: ["vellum"] }
|
|
1124
|
+
: {}),
|
|
1125
|
+
},
|
|
1126
|
+
});
|
|
1127
|
+
expect(result.selectedChannels).not.toContain("platform");
|
|
1128
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[3]).toMatchObject({
|
|
1129
|
+
assistantInitiatedThread: { platformAdded: false },
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test("keeps existing platform and external selections independent of this feature", async () => {
|
|
1134
|
+
evaluateSignalMock.mockResolvedValue(
|
|
1135
|
+
decision(["vellum", "platform", "telegram"]),
|
|
1136
|
+
);
|
|
1137
|
+
const result = await emitNotificationSignal({
|
|
1138
|
+
...params,
|
|
1139
|
+
contextPayload: { quiet: true },
|
|
1140
|
+
});
|
|
1141
|
+
expect(result.selectedChannels).toEqual(["vellum", "platform", "telegram"]);
|
|
1142
|
+
expect(isPlatformClientConfiguredMock).not.toHaveBeenCalled();
|
|
1143
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[3]).toMatchObject({
|
|
1144
|
+
assistantInitiatedThread: { platformAdded: false },
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test("probes an unbound platform once across urgency and From me policy", async () => {
|
|
1149
|
+
isPlatformClientConfiguredMock.mockResolvedValue(false);
|
|
1150
|
+
const result = await emitNotificationSignal({
|
|
1151
|
+
...params,
|
|
1152
|
+
attentionHints: { ...params.attentionHints, urgency: "high" },
|
|
1153
|
+
});
|
|
1154
|
+
expect(result.selectedChannels).toEqual(["vellum"]);
|
|
1155
|
+
expect(isPlatformClientConfiguredMock).toHaveBeenCalledTimes(1);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("preserves preferred external selections while adding the platform default", async () => {
|
|
1159
|
+
evaluateSignalMock.mockResolvedValue(decision(["vellum", "telegram"]));
|
|
1160
|
+
const result = await emitNotificationSignal(params);
|
|
1161
|
+
expect(result.selectedChannels).toEqual(["vellum", "telegram", "platform"]);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
test.each(["deduplicated", "source_active"])(
|
|
1165
|
+
"does not resolve candidates for %s signals",
|
|
1166
|
+
async (scenario) => {
|
|
1167
|
+
if (scenario === "deduplicated") {
|
|
1168
|
+
createEventMock.mockReturnValue(null);
|
|
1169
|
+
}
|
|
1170
|
+
await emitNotificationSignal({
|
|
1171
|
+
...params,
|
|
1172
|
+
attentionHints: {
|
|
1173
|
+
...params.attentionHints,
|
|
1174
|
+
visibleInSourceNow: scenario === "source_active",
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
expect(resolveAssistantInitiatedThreadMock).not.toHaveBeenCalled();
|
|
1178
|
+
expect(dispatchDecisionMock).not.toHaveBeenCalled();
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
});
|
|
@@ -26,11 +26,12 @@ mock.module("../ipc/socket-path.js", () => ({
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
let refreshCallCount = 0;
|
|
28
28
|
let refreshReturnsLoaded = true;
|
|
29
|
+
let pendingRefresh: Promise<boolean> | undefined;
|
|
29
30
|
|
|
30
31
|
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
31
32
|
refreshOverridesFromGateway: async () => {
|
|
32
33
|
refreshCallCount++;
|
|
33
|
-
return refreshReturnsLoaded;
|
|
34
|
+
return pendingRefresh ?? refreshReturnsLoaded;
|
|
34
35
|
},
|
|
35
36
|
initFeatureFlagOverrides: async () => true,
|
|
36
37
|
clearFeatureFlagOverridesCache: () => {},
|
|
@@ -136,6 +137,7 @@ describe("gateway-flag-listener", () => {
|
|
|
136
137
|
mkdirSync(testRoot, { recursive: true });
|
|
137
138
|
refreshCallCount = 0;
|
|
138
139
|
refreshReturnsLoaded = true;
|
|
140
|
+
pendingRefresh = undefined;
|
|
139
141
|
publishedTagSets = [];
|
|
140
142
|
reconcileCallCount = 0;
|
|
141
143
|
reconcileReturns = false;
|
|
@@ -177,7 +179,7 @@ describe("gateway-flag-listener", () => {
|
|
|
177
179
|
expect(refreshCallCount).toBe(2);
|
|
178
180
|
});
|
|
179
181
|
|
|
180
|
-
test("broadcasts feature-flags sync_changed
|
|
182
|
+
test("broadcasts feature-flags sync_changed after connect and flag changes", async () => {
|
|
181
183
|
await new Promise<void>((resolve) => {
|
|
182
184
|
testServer.server.listen(socketPath, resolve);
|
|
183
185
|
});
|
|
@@ -186,19 +188,59 @@ describe("gateway-flag-listener", () => {
|
|
|
186
188
|
await testServer.waitForClient();
|
|
187
189
|
await new Promise((r) => setTimeout(r, 100));
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
expect(publishedTagSets.length).toBe(0);
|
|
191
|
+
expect(publishedTagSets.length).toBe(1);
|
|
191
192
|
|
|
192
193
|
testServer.emit("feature_flags_changed");
|
|
193
194
|
await new Promise((r) => setTimeout(r, 100));
|
|
194
195
|
|
|
195
|
-
expect(publishedTagSets.length).toBe(
|
|
196
|
-
expect(publishedTagSets[
|
|
196
|
+
expect(publishedTagSets.length).toBe(2);
|
|
197
|
+
expect(publishedTagSets[1]).toEqual([
|
|
197
198
|
"feature-flags:client",
|
|
198
199
|
"feature-flags:assistant",
|
|
199
200
|
]);
|
|
200
201
|
});
|
|
201
202
|
|
|
203
|
+
test("defers invalidation until the flag-change refresh settles", async () => {
|
|
204
|
+
await new Promise<void>((resolve) => {
|
|
205
|
+
testServer.server.listen(socketPath, resolve);
|
|
206
|
+
});
|
|
207
|
+
startGatewayFlagListener();
|
|
208
|
+
await testServer.waitForClient();
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
210
|
+
publishedTagSets = [];
|
|
211
|
+
const refresh = Promise.withResolvers<boolean>();
|
|
212
|
+
pendingRefresh = refresh.promise;
|
|
213
|
+
testServer.emit("feature_flags_changed");
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
215
|
+
expect(refreshCallCount).toBe(2);
|
|
216
|
+
expect(publishedTagSets).toEqual([]);
|
|
217
|
+
expect(reconcileCallCount).toBe(1);
|
|
218
|
+
refresh.resolve(true);
|
|
219
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
220
|
+
expect(publishedTagSets).toEqual([
|
|
221
|
+
["feature-flags:client", "feature-flags:assistant"],
|
|
222
|
+
]);
|
|
223
|
+
expect(reconcileCallCount).toBe(2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("defers connection invalidation until the refreshed cache is available", async () => {
|
|
227
|
+
const refresh = Promise.withResolvers<boolean>();
|
|
228
|
+
pendingRefresh = refresh.promise;
|
|
229
|
+
await new Promise<void>((resolve) => {
|
|
230
|
+
testServer.server.listen(socketPath, resolve);
|
|
231
|
+
});
|
|
232
|
+
startGatewayFlagListener();
|
|
233
|
+
await testServer.waitForClient();
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
235
|
+
expect(refreshCallCount).toBe(1);
|
|
236
|
+
expect(publishedTagSets).toEqual([]);
|
|
237
|
+
refresh.resolve(true);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
239
|
+
expect(publishedTagSets).toEqual([
|
|
240
|
+
["feature-flags:client", "feature-flags:assistant"],
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
202
244
|
test("reconciles flag-gated profiles and broadcasts config_changed when the profile set changes", async () => {
|
|
203
245
|
reconcileReturns = true;
|
|
204
246
|
await new Promise<void>((resolve) => {
|
|
@@ -251,12 +293,22 @@ describe("gateway-flag-listener", () => {
|
|
|
251
293
|
// The profile reconcile/broadcast are gated on flags actually loading.
|
|
252
294
|
expect(reconcileCallCount).toBe(0);
|
|
253
295
|
expect(configChangedCount).toBe(0);
|
|
296
|
+
expect(publishedTagSets).toEqual([]);
|
|
254
297
|
|
|
255
298
|
testServer.emit("feature_flags_changed");
|
|
256
299
|
await new Promise((r) => setTimeout(r, 100));
|
|
257
300
|
|
|
258
301
|
expect(reconcileCallCount).toBe(0);
|
|
259
302
|
expect(configChangedCount).toBe(0);
|
|
303
|
+
expect(publishedTagSets).toEqual([]);
|
|
304
|
+
refreshReturnsLoaded = true;
|
|
305
|
+
testServer.emit("feature_flags_changed");
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
307
|
+
expect(reconcileCallCount).toBe(1);
|
|
308
|
+
expect(configChangedCount).toBe(1);
|
|
309
|
+
expect(publishedTagSets).toEqual([
|
|
310
|
+
["feature-flags:client", "feature-flags:assistant"],
|
|
311
|
+
]);
|
|
260
312
|
});
|
|
261
313
|
|
|
262
314
|
test("reconciles profiles when the refresh reports flags loaded", async () => {
|
|
@@ -318,6 +370,7 @@ describe("gateway-flag-listener", () => {
|
|
|
318
370
|
await new Promise((r) => setTimeout(r, 100));
|
|
319
371
|
const countAfterReconnect = refreshCallCount;
|
|
320
372
|
expect(countAfterReconnect).toBeGreaterThan(0);
|
|
373
|
+
expect(publishedTagSets).toHaveLength(countAfterReconnect);
|
|
321
374
|
|
|
322
375
|
const payload = JSON.stringify({ event: "feature_flags_changed" }) + "\n";
|
|
323
376
|
secondClient!.write(payload);
|
|
@@ -233,6 +233,7 @@ describe("executeBrowserNavigate", () => {
|
|
|
233
233
|
resetMockPage();
|
|
234
234
|
resetCdp();
|
|
235
235
|
__resetPinnedTabsForTests();
|
|
236
|
+
mockExtensionAvailable = false;
|
|
236
237
|
});
|
|
237
238
|
|
|
238
239
|
// ── Input validation ───────────────────────────────────────────
|
|
@@ -606,6 +607,13 @@ describe("executeBrowserNavigate", () => {
|
|
|
606
607
|
const routeCallsBefore = mockPage.route.mock.calls.length;
|
|
607
608
|
const unrouteCallsBefore = mockPage.unroute.mock.calls.length;
|
|
608
609
|
|
|
610
|
+
cdpSendHandler = (method, params) => {
|
|
611
|
+
if (method === "Vellum.createTab") {
|
|
612
|
+
return { tabId: "42" };
|
|
613
|
+
}
|
|
614
|
+
return defaultCdpHandler(method, params);
|
|
615
|
+
};
|
|
616
|
+
|
|
609
617
|
const result = await executeBrowserNavigate(
|
|
610
618
|
{ url: "https://example.com/page" },
|
|
611
619
|
extensionCtx,
|
|
@@ -643,6 +651,9 @@ describe("executeBrowserNavigate", () => {
|
|
|
643
651
|
// navigateAndWait returns a private final URL (simulating a
|
|
644
652
|
// server-side redirect).
|
|
645
653
|
cdpSendHandler = (method, params) => {
|
|
654
|
+
if (method === "Vellum.createTab") {
|
|
655
|
+
return { tabId: "42" };
|
|
656
|
+
}
|
|
646
657
|
if (method === "Page.navigate") {
|
|
647
658
|
return { frameId: "f1" };
|
|
648
659
|
}
|
|
@@ -745,13 +756,7 @@ describe("executeBrowserNavigate", () => {
|
|
|
745
756
|
expect(cdpDisposed).toBe(true);
|
|
746
757
|
});
|
|
747
758
|
|
|
748
|
-
test("new_tab: true on extension path with no tabId in response
|
|
749
|
-
// Defensive: dispatcher returns success but no tabId. The
|
|
750
|
-
// executor logs a warn, resets the live cdp session to undefined
|
|
751
|
-
// (so the follow-on Page.navigate routes to the active tab
|
|
752
|
-
// instead of any stale pin the cdp instance was constructed
|
|
753
|
-
// with), and proceeds. The navigate still runs (degraded
|
|
754
|
-
// behaviour but not a hard failure).
|
|
759
|
+
test("new_tab: true on extension path with no tabId in response fails closed", async () => {
|
|
755
760
|
parseUrlResult = new URL("https://example.com/page");
|
|
756
761
|
mockExtensionAvailable = true;
|
|
757
762
|
|
|
@@ -767,25 +772,13 @@ describe("executeBrowserNavigate", () => {
|
|
|
767
772
|
{ ...ctx },
|
|
768
773
|
);
|
|
769
774
|
|
|
770
|
-
expect(result.isError).toBe(
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
expect(cdpSetSessionIdCalls).toEqual([undefined]);
|
|
775
|
-
// Page.navigate still ran.
|
|
776
|
-
expect(cdpSendCalls.some((c) => c.method === "Page.navigate")).toBe(true);
|
|
775
|
+
expect(result.isError).toBe(true);
|
|
776
|
+
expect(String(result.content)).toContain("returned no tabId");
|
|
777
|
+
expect(cdpSetSessionIdCalls).toEqual([]);
|
|
778
|
+
expect(cdpSendCalls.some((c) => c.method === "Page.navigate")).toBe(false);
|
|
777
779
|
});
|
|
778
780
|
|
|
779
|
-
test("new_tab: true with no tabId in response clears a pre-existing stale pin
|
|
780
|
-
// Regression for the Codex round-2 findings (P2 + round-3 P1):
|
|
781
|
-
// when Vellum.createTab returns a malformed response with no
|
|
782
|
-
// tabId, the executor falls back to active-tab routing — but
|
|
783
|
-
// (a) the pin store still held the stale pin (round-2 P2 fix
|
|
784
|
-
// added clearPinnedTab), and (b) the LIVE cdp instance was
|
|
785
|
-
// already constructed with that stale cdpSessionId, so the
|
|
786
|
-
// follow-on Page.navigate would still route to the dead tab
|
|
787
|
-
// unless we reset the session on the cdp instance too (round-3
|
|
788
|
-
// P1 fix added cdp.setCdpSessionId(undefined)).
|
|
781
|
+
test("new_tab: true with no tabId in response clears a pre-existing stale pin and fails closed", async () => {
|
|
789
782
|
const { setPinnedTab, getPinnedTab } =
|
|
790
783
|
await import("../tools/browser/pinned-tabs.js");
|
|
791
784
|
setPinnedTab(ctx.conversationId, "stale-pinned-tab-id");
|
|
@@ -806,12 +799,9 @@ describe("executeBrowserNavigate", () => {
|
|
|
806
799
|
{ ...ctx },
|
|
807
800
|
);
|
|
808
801
|
|
|
809
|
-
expect(result.isError).toBe(
|
|
810
|
-
// (a) Pin store cleared.
|
|
802
|
+
expect(result.isError).toBe(true);
|
|
811
803
|
expect(getPinnedTab(ctx.conversationId)).toBeUndefined();
|
|
812
|
-
|
|
813
|
-
// setCdpSessionId arg; expect exactly one call with undefined).
|
|
814
|
-
expect(cdpSetSessionIdCalls).toEqual([undefined]);
|
|
804
|
+
expect(cdpSetSessionIdCalls).toEqual([]);
|
|
815
805
|
});
|
|
816
806
|
|
|
817
807
|
test("new_tab: true on LOCAL path is a no-op (Playwright manages its own isolated browser)", async () => {
|
|
@@ -935,7 +925,7 @@ describe("executeBrowserNavigate", () => {
|
|
|
935
925
|
false,
|
|
936
926
|
);
|
|
937
927
|
expect(getPinnedTab(ctx.conversationId)).toBeUndefined();
|
|
938
|
-
expect(cdpSetSessionIdCalls).toEqual([
|
|
928
|
+
expect(cdpSetSessionIdCalls).toEqual(["active"]);
|
|
939
929
|
expect(cdpSendCalls.some((c) => c.method === "Page.navigate")).toBe(true);
|
|
940
930
|
});
|
|
941
931
|
|
|
@@ -1186,6 +1186,85 @@ describe("HostCuProxy", () => {
|
|
|
1186
1186
|
|
|
1187
1187
|
expect(result.content).toContain("2560x1440 px");
|
|
1188
1188
|
expect(result.content).toContain("1280x720 pt");
|
|
1189
|
+
expect(result.content).toContain("x = round(image_x * 0.500000)");
|
|
1190
|
+
expect(result.content).toContain("y = round(image_y * 0.500000)");
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
test("derives separate screen-point scales for a downscaled screenshot", () => {
|
|
1194
|
+
setup();
|
|
1195
|
+
|
|
1196
|
+
const result = proxy.formatObservation({
|
|
1197
|
+
screenshot: "base64data",
|
|
1198
|
+
screenshotWidthPx: 835,
|
|
1199
|
+
screenshotHeightPx: 540,
|
|
1200
|
+
screenWidthPt: 1728,
|
|
1201
|
+
screenHeightPt: 1117,
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
const xScale = Number(result.content.match(/image_x \* ([\d.]+)/)?.[1]);
|
|
1205
|
+
const yScale = Number(result.content.match(/image_y \* ([\d.]+)/)?.[1]);
|
|
1206
|
+
expect(Math.round(675 * xScale)).toBe(1397);
|
|
1207
|
+
expect(Math.round(330 * yScale)).toBe(683);
|
|
1208
|
+
expect(result.content).toContain("screen points, not screenshot pixels");
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test.each([
|
|
1212
|
+
{ capture_window_id: 12 },
|
|
1213
|
+
{ captureWindowId: 12 },
|
|
1214
|
+
{ captureDisplayId: 2 },
|
|
1215
|
+
])(
|
|
1216
|
+
"does not scale a scoped screenshot by desktop dimensions: %j",
|
|
1217
|
+
(input) => {
|
|
1218
|
+
setup();
|
|
1219
|
+
|
|
1220
|
+
const result = proxy.formatObservation(
|
|
1221
|
+
{
|
|
1222
|
+
screenshot: "base64data",
|
|
1223
|
+
screenshotWidthPx: 800,
|
|
1224
|
+
screenshotHeightPx: 600,
|
|
1225
|
+
screenWidthPt: 1600,
|
|
1226
|
+
screenHeightPt: 1200,
|
|
1227
|
+
},
|
|
1228
|
+
undefined,
|
|
1229
|
+
false,
|
|
1230
|
+
{ toolName: "computer_use_observe", input },
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
expect(result.content).toContain("screenshot is capture-relative");
|
|
1234
|
+
expect(result.content).not.toContain("round(image_");
|
|
1235
|
+
},
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
test.each([undefined, 0, -1, Number.NaN, Number.POSITIVE_INFINITY])(
|
|
1239
|
+
"does not invent a mapping for an unavailable or invalid dimension: %s",
|
|
1240
|
+
(screenWidthPt) => {
|
|
1241
|
+
setup();
|
|
1242
|
+
|
|
1243
|
+
const result = proxy.formatObservation({
|
|
1244
|
+
screenshot: "base64data",
|
|
1245
|
+
screenshotWidthPx: 800,
|
|
1246
|
+
screenshotHeightPx: 600,
|
|
1247
|
+
screenWidthPt,
|
|
1248
|
+
screenHeightPt: 1200,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
expect(result.content).toContain("coordinate mapping is unavailable");
|
|
1252
|
+
expect(result.content).not.toContain("round(image_");
|
|
1253
|
+
},
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
test("omits coordinate guidance when no screenshot accompanies dimensions", () => {
|
|
1257
|
+
setup();
|
|
1258
|
+
|
|
1259
|
+
const result = proxy.formatObservation({
|
|
1260
|
+
axTree: "Button [1]",
|
|
1261
|
+
screenshotWidthPx: 800,
|
|
1262
|
+
screenshotHeightPx: 600,
|
|
1263
|
+
screenWidthPt: 1600,
|
|
1264
|
+
screenHeightPt: 1200,
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
expect(result.content).not.toContain("coordinate");
|
|
1189
1268
|
});
|
|
1190
1269
|
|
|
1191
1270
|
test("escapes </ax-tree> in AX tree content", () => {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
19
|
|
|
20
|
+
import sharp from "sharp";
|
|
21
|
+
|
|
20
22
|
// ── Module mocks ─────────────────────────────────────────────────────────────
|
|
21
23
|
// Must be registered before the host-cu-routes module is loaded.
|
|
22
24
|
|
|
@@ -487,7 +489,14 @@ describe("handleHostCuResult: request body validation", () => {
|
|
|
487
489
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
488
490
|
});
|
|
489
491
|
|
|
490
|
-
test("forwards
|
|
492
|
+
test("forwards observation metadata with dimensions from the screenshot bytes", async () => {
|
|
493
|
+
const screenshot = (
|
|
494
|
+
await sharp({
|
|
495
|
+
create: { width: 835, height: 540, channels: 3, background: "white" },
|
|
496
|
+
})
|
|
497
|
+
.jpeg()
|
|
498
|
+
.toBuffer()
|
|
499
|
+
).toString("base64");
|
|
491
500
|
const requestId = "req-cu-full-observation";
|
|
492
501
|
registerPending(requestId);
|
|
493
502
|
|
|
@@ -496,7 +505,7 @@ describe("handleHostCuResult: request body validation", () => {
|
|
|
496
505
|
requestId,
|
|
497
506
|
axTree: "Button [1]",
|
|
498
507
|
axDiff: "+Button [1]",
|
|
499
|
-
screenshot
|
|
508
|
+
screenshot,
|
|
500
509
|
screenshotWidthPx: 1600,
|
|
501
510
|
screenshotHeightPx: 1000,
|
|
502
511
|
screenWidthPt: 800,
|
|
@@ -512,9 +521,9 @@ describe("handleHostCuResult: request body validation", () => {
|
|
|
512
521
|
expect(cuResolveSpy[0].payload).toMatchObject({
|
|
513
522
|
axTree: "Button [1]",
|
|
514
523
|
axDiff: "+Button [1]",
|
|
515
|
-
screenshot
|
|
516
|
-
screenshotWidthPx:
|
|
517
|
-
screenshotHeightPx:
|
|
524
|
+
screenshot,
|
|
525
|
+
screenshotWidthPx: 835,
|
|
526
|
+
screenshotHeightPx: 540,
|
|
518
527
|
screenWidthPt: 800,
|
|
519
528
|
screenHeightPt: 500,
|
|
520
529
|
executionResult: "Clicked",
|
|
@@ -45,6 +45,8 @@ mock.module("../daemon/host-bash-proxy.js", () => ({
|
|
|
45
45
|
},
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
import { buildShellInvocation } from "@vellumai/environments/shell";
|
|
49
|
+
|
|
48
50
|
import { hostShellTool } from "../tools/host-terminal/host-shell.js";
|
|
49
51
|
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
50
52
|
|
|
@@ -139,7 +141,7 @@ describe("host_bash tool", () => {
|
|
|
139
141
|
// Verify spawn was called with 'bash' directly — not 'bwrap' or 'sandbox-exec'
|
|
140
142
|
expect(spawnCalls.length).toBe(1);
|
|
141
143
|
expect(spawnCalls[0].command).toBe("bash");
|
|
142
|
-
expect(spawnCalls[0].args).toEqual(
|
|
144
|
+
expect(spawnCalls[0].args).toEqual(buildShellInvocation("echo hello").args);
|
|
143
145
|
});
|
|
144
146
|
});
|
|
145
147
|
|
|
@@ -205,7 +207,9 @@ describe("host_bash — baseline: no sandbox isolation", () => {
|
|
|
205
207
|
expect(spawnCalls[0].command).toBe("bash");
|
|
206
208
|
expect(spawnCalls[0].args[0]).toBe("-c");
|
|
207
209
|
expect(spawnCalls[0].args[1]).toBe("--");
|
|
208
|
-
expect(spawnCalls[0].args[2]).toBe(
|
|
210
|
+
expect(spawnCalls[0].args[2]).toBe(
|
|
211
|
+
buildShellInvocation("ls -la /tmp").args[2],
|
|
212
|
+
);
|
|
209
213
|
});
|
|
210
214
|
|
|
211
215
|
test("host_bash always spawns plain bash without wrapCommand", async () => {
|