@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* the conversation was newly created or reused.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
10
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import type { PairingOptions } from "../notifications/conversation-pairing.js";
|
|
11
13
|
|
|
12
14
|
// -- Mocks (must be declared before importing modules that depend on them) ----
|
|
13
15
|
|
|
@@ -18,12 +20,129 @@ mock.module("../util/logger.js", () => ({
|
|
|
18
20
|
}),
|
|
19
21
|
}));
|
|
20
22
|
|
|
23
|
+
// Mock destination-resolver for broadcaster tests
|
|
24
|
+
mock.module("../notifications/destination-resolver.js", () => ({
|
|
25
|
+
resolveDestinations: (channels: string[]) => {
|
|
26
|
+
const m = new Map();
|
|
27
|
+
for (const ch of channels) {
|
|
28
|
+
m.set(ch, { channel: ch, endpoint: `mock-${ch}` });
|
|
29
|
+
}
|
|
30
|
+
return m;
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// Mock deliveries-store to avoid DB access
|
|
35
|
+
mock.module("../notifications/deliveries-store.js", () => ({
|
|
36
|
+
createDelivery: () => {},
|
|
37
|
+
updateDeliveryStatus: () => {},
|
|
38
|
+
findDeliveryByDecisionAndChannel: () => undefined,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Configurable mock for conversation-pairing
|
|
42
|
+
let nextPairingResult:
|
|
43
|
+
| import("../notifications/conversation-pairing.js").PairingResult
|
|
44
|
+
| null = null;
|
|
45
|
+
let pairingCallCount = 0;
|
|
46
|
+
|
|
47
|
+
mock.module("../notifications/conversation-pairing.js", () => ({
|
|
48
|
+
pairDeliveryWithConversation: async (
|
|
49
|
+
_signal: unknown,
|
|
50
|
+
_channel: string,
|
|
51
|
+
_copy: unknown,
|
|
52
|
+
_options?: PairingOptions,
|
|
53
|
+
) => {
|
|
54
|
+
if (nextPairingResult) {
|
|
55
|
+
const result = nextPairingResult;
|
|
56
|
+
nextPairingResult = null;
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
const id = `mock-conv-${++pairingCallCount}`;
|
|
60
|
+
return {
|
|
61
|
+
conversationId: id,
|
|
62
|
+
messageId: `mock-msg-${pairingCallCount}`,
|
|
63
|
+
strategy: "start_new_conversation" as const,
|
|
64
|
+
createdNewConversation: true,
|
|
65
|
+
threadDecisionFallbackUsed: false,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
|
|
21
70
|
import type { ServerMessage } from "../daemon/ipc-contract.js";
|
|
22
71
|
import { VellumAdapter } from "../notifications/adapters/macos.js";
|
|
72
|
+
import { NotificationBroadcaster } from "../notifications/broadcaster.js";
|
|
73
|
+
import type { NotificationSignal } from "../notifications/signal.js";
|
|
74
|
+
import type {
|
|
75
|
+
ChannelAdapter,
|
|
76
|
+
ChannelDeliveryPayload,
|
|
77
|
+
ChannelDestination,
|
|
78
|
+
DeliveryResult,
|
|
79
|
+
NotificationChannel,
|
|
80
|
+
NotificationDecision,
|
|
81
|
+
} from "../notifications/types.js";
|
|
82
|
+
|
|
83
|
+
// -- Helpers -----------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function makeSignal(
|
|
86
|
+
overrides?: Partial<NotificationSignal>,
|
|
87
|
+
): NotificationSignal {
|
|
88
|
+
return {
|
|
89
|
+
signalId: "sig-deeplink-001",
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
sourceChannel: "scheduler",
|
|
92
|
+
sourceSessionId: "sess-001",
|
|
93
|
+
sourceEventName: "test.event",
|
|
94
|
+
contextPayload: {},
|
|
95
|
+
attentionHints: {
|
|
96
|
+
requiresAction: false,
|
|
97
|
+
urgency: "medium",
|
|
98
|
+
isAsyncBackground: true,
|
|
99
|
+
visibleInSourceNow: false,
|
|
100
|
+
},
|
|
101
|
+
...overrides,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function makeDecision(
|
|
106
|
+
overrides?: Partial<NotificationDecision>,
|
|
107
|
+
): NotificationDecision {
|
|
108
|
+
return {
|
|
109
|
+
shouldNotify: true,
|
|
110
|
+
selectedChannels: ["vellum"],
|
|
111
|
+
reasoningSummary: "Deep-link test decision",
|
|
112
|
+
renderedCopy: {
|
|
113
|
+
vellum: { title: "Test Alert", body: "Something happened" },
|
|
114
|
+
},
|
|
115
|
+
dedupeKey: "deeplink-test-001",
|
|
116
|
+
confidence: 0.9,
|
|
117
|
+
fallbackUsed: false,
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class MockAdapter implements ChannelAdapter {
|
|
123
|
+
readonly channel: NotificationChannel;
|
|
124
|
+
sent: ChannelDeliveryPayload[] = [];
|
|
125
|
+
|
|
126
|
+
constructor(channel: NotificationChannel) {
|
|
127
|
+
this.channel = channel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async send(
|
|
131
|
+
payload: ChannelDeliveryPayload,
|
|
132
|
+
_dest: ChannelDestination,
|
|
133
|
+
): Promise<DeliveryResult> {
|
|
134
|
+
this.sent.push(payload);
|
|
135
|
+
return { success: true };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
23
138
|
|
|
24
139
|
// -- Tests -------------------------------------------------------------------
|
|
25
140
|
|
|
26
141
|
describe("notification deep-link metadata", () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
nextPairingResult = null;
|
|
144
|
+
});
|
|
145
|
+
|
|
27
146
|
describe("VellumAdapter", () => {
|
|
28
147
|
test("broadcasts notification_intent with deepLinkMetadata from payload", async () => {
|
|
29
148
|
const messages: ServerMessage[] = [];
|
|
@@ -221,5 +340,250 @@ describe("notification deep-link metadata", () => {
|
|
|
221
340
|
const metadata = msg.deepLinkMetadata as Record<string, unknown>;
|
|
222
341
|
expect(metadata.conversationId).toBe("conv-reused-thread-042");
|
|
223
342
|
});
|
|
343
|
+
|
|
344
|
+
// ── Reused thread deep-link stability regressions ─────────────────
|
|
345
|
+
|
|
346
|
+
test("reused thread preserves the same conversationId across follow-up notifications", async () => {
|
|
347
|
+
const messages: ServerMessage[] = [];
|
|
348
|
+
const adapter = new VellumAdapter((msg) => messages.push(msg));
|
|
349
|
+
|
|
350
|
+
const stableConversationId = "conv-bound-telegram-dest-001";
|
|
351
|
+
|
|
352
|
+
// First notification to a bound destination
|
|
353
|
+
await adapter.send(
|
|
354
|
+
{
|
|
355
|
+
sourceEventName: "guardian.question",
|
|
356
|
+
copy: { title: "Question 1", body: "Allow file read?" },
|
|
357
|
+
deepLinkTarget: {
|
|
358
|
+
conversationId: stableConversationId,
|
|
359
|
+
messageId: "msg-seed-1",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{ channel: "vellum" },
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Follow-up notification reuses the same bound conversation
|
|
366
|
+
await adapter.send(
|
|
367
|
+
{
|
|
368
|
+
sourceEventName: "guardian.question",
|
|
369
|
+
copy: { title: "Question 2", body: "Allow network access?" },
|
|
370
|
+
deepLinkTarget: {
|
|
371
|
+
conversationId: stableConversationId,
|
|
372
|
+
messageId: "msg-seed-2",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
{ channel: "vellum" },
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(messages).toHaveLength(2);
|
|
379
|
+
|
|
380
|
+
const meta1 = (messages[0] as unknown as Record<string, unknown>)
|
|
381
|
+
.deepLinkMetadata as Record<string, unknown>;
|
|
382
|
+
const meta2 = (messages[1] as unknown as Record<string, unknown>)
|
|
383
|
+
.deepLinkMetadata as Record<string, unknown>;
|
|
384
|
+
|
|
385
|
+
// Both deep links point to the same conversation
|
|
386
|
+
expect(meta1.conversationId).toBe(stableConversationId);
|
|
387
|
+
expect(meta2.conversationId).toBe(stableConversationId);
|
|
388
|
+
|
|
389
|
+
// But each has a distinct messageId for scroll-to-message targeting
|
|
390
|
+
expect(meta1.messageId).toBe("msg-seed-1");
|
|
391
|
+
expect(meta2.messageId).toBe("msg-seed-2");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("reused thread deep-link messageId changes per delivery for scroll targeting", async () => {
|
|
395
|
+
const messages: ServerMessage[] = [];
|
|
396
|
+
const adapter = new VellumAdapter((msg) => messages.push(msg));
|
|
397
|
+
|
|
398
|
+
const conversationId = "conv-reused-scroll-test";
|
|
399
|
+
|
|
400
|
+
await adapter.send(
|
|
401
|
+
{
|
|
402
|
+
sourceEventName: "reminder.fired",
|
|
403
|
+
copy: { title: "Reminder", body: "First" },
|
|
404
|
+
deepLinkTarget: { conversationId, messageId: "msg-a" },
|
|
405
|
+
},
|
|
406
|
+
{ channel: "vellum" },
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await adapter.send(
|
|
410
|
+
{
|
|
411
|
+
sourceEventName: "reminder.fired",
|
|
412
|
+
copy: { title: "Reminder", body: "Second" },
|
|
413
|
+
deepLinkTarget: { conversationId, messageId: "msg-b" },
|
|
414
|
+
},
|
|
415
|
+
{ channel: "vellum" },
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const meta1 = (messages[0] as unknown as Record<string, unknown>)
|
|
419
|
+
.deepLinkMetadata as Record<string, unknown>;
|
|
420
|
+
const meta2 = (messages[1] as unknown as Record<string, unknown>)
|
|
421
|
+
.deepLinkMetadata as Record<string, unknown>;
|
|
422
|
+
|
|
423
|
+
// Same conversation but different message targets
|
|
424
|
+
expect(meta1.conversationId).toBe(conversationId);
|
|
425
|
+
expect(meta2.conversationId).toBe(conversationId);
|
|
426
|
+
expect(meta1.messageId).not.toBe(meta2.messageId);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("deep-link metadata is stable when conversation is reused via binding-key continuation", async () => {
|
|
430
|
+
const messages: ServerMessage[] = [];
|
|
431
|
+
const adapter = new VellumAdapter((msg) => messages.push(msg));
|
|
432
|
+
|
|
433
|
+
// Simulates the binding-key continuation path: multiple notifications
|
|
434
|
+
// to the same SMS destination reuse the same bound conversation, and
|
|
435
|
+
// the deep-link metadata should reflect the bound conversation ID
|
|
436
|
+
// rather than creating a new one each time.
|
|
437
|
+
const boundConvId = "conv-sms-bound-+15551234567";
|
|
438
|
+
|
|
439
|
+
for (const body of ["Alert 1", "Alert 2", "Alert 3"]) {
|
|
440
|
+
await adapter.send(
|
|
441
|
+
{
|
|
442
|
+
sourceEventName: "activity.complete",
|
|
443
|
+
copy: { title: "Activity", body },
|
|
444
|
+
deepLinkTarget: { conversationId: boundConvId },
|
|
445
|
+
},
|
|
446
|
+
{ channel: "vellum" },
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
expect(messages).toHaveLength(3);
|
|
451
|
+
|
|
452
|
+
// All three notifications deep-link to the same bound conversation
|
|
453
|
+
for (const msg of messages) {
|
|
454
|
+
const metadata = (msg as unknown as Record<string, unknown>)
|
|
455
|
+
.deepLinkMetadata as Record<string, unknown>;
|
|
456
|
+
expect(metadata.conversationId).toBe(boundConvId);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ── NotificationBroadcaster deep-link injection ──────────────────────
|
|
462
|
+
//
|
|
463
|
+
// These tests exercise the production code path where the broadcaster
|
|
464
|
+
// calls pairDeliveryWithConversation() and merges the pairing result's
|
|
465
|
+
// conversationId/messageId into deepLinkTarget before passing to the
|
|
466
|
+
// adapter. This catches regressions that the adapter-only tests above
|
|
467
|
+
// would miss (e.g. broadcaster stops merging pairing results).
|
|
468
|
+
|
|
469
|
+
describe("NotificationBroadcaster deep-link injection", () => {
|
|
470
|
+
test("broadcaster merges pairing conversationId into deepLinkTarget for vellum", async () => {
|
|
471
|
+
const vellumAdapter = new MockAdapter("vellum");
|
|
472
|
+
const broadcaster = new NotificationBroadcaster([vellumAdapter]);
|
|
473
|
+
|
|
474
|
+
nextPairingResult = {
|
|
475
|
+
conversationId: "conv-paired-abc",
|
|
476
|
+
messageId: "msg-paired-abc",
|
|
477
|
+
strategy: "start_new_conversation" as const,
|
|
478
|
+
createdNewConversation: true,
|
|
479
|
+
threadDecisionFallbackUsed: false,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const signal = makeSignal();
|
|
483
|
+
const decision = makeDecision();
|
|
484
|
+
|
|
485
|
+
await broadcaster.broadcastDecision(signal, decision);
|
|
486
|
+
|
|
487
|
+
expect(vellumAdapter.sent).toHaveLength(1);
|
|
488
|
+
const deepLink = vellumAdapter.sent[0].deepLinkTarget;
|
|
489
|
+
expect(deepLink).toBeDefined();
|
|
490
|
+
expect(deepLink!.conversationId).toBe("conv-paired-abc");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("broadcaster merges pairing messageId into deepLinkTarget for vellum", async () => {
|
|
494
|
+
const vellumAdapter = new MockAdapter("vellum");
|
|
495
|
+
const broadcaster = new NotificationBroadcaster([vellumAdapter]);
|
|
496
|
+
|
|
497
|
+
nextPairingResult = {
|
|
498
|
+
conversationId: "conv-paired-def",
|
|
499
|
+
messageId: "msg-paired-def",
|
|
500
|
+
strategy: "start_new_conversation" as const,
|
|
501
|
+
createdNewConversation: true,
|
|
502
|
+
threadDecisionFallbackUsed: false,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const signal = makeSignal();
|
|
506
|
+
const decision = makeDecision();
|
|
507
|
+
|
|
508
|
+
await broadcaster.broadcastDecision(signal, decision);
|
|
509
|
+
|
|
510
|
+
expect(vellumAdapter.sent).toHaveLength(1);
|
|
511
|
+
const deepLink = vellumAdapter.sent[0].deepLinkTarget;
|
|
512
|
+
expect(deepLink).toBeDefined();
|
|
513
|
+
expect(deepLink!.messageId).toBe("msg-paired-def");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("reused conversation deep-link points to the reused conversationId", async () => {
|
|
517
|
+
const vellumAdapter = new MockAdapter("vellum");
|
|
518
|
+
const broadcaster = new NotificationBroadcaster([vellumAdapter]);
|
|
519
|
+
|
|
520
|
+
nextPairingResult = {
|
|
521
|
+
conversationId: "conv-reused-xyz",
|
|
522
|
+
messageId: "msg-reused-xyz",
|
|
523
|
+
strategy: "start_new_conversation" as const,
|
|
524
|
+
createdNewConversation: false,
|
|
525
|
+
threadDecisionFallbackUsed: false,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const signal = makeSignal();
|
|
529
|
+
const decision = makeDecision({
|
|
530
|
+
threadActions: {
|
|
531
|
+
vellum: {
|
|
532
|
+
action: "reuse_existing",
|
|
533
|
+
conversationId: "conv-original-placeholder",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
await broadcaster.broadcastDecision(signal, decision);
|
|
539
|
+
|
|
540
|
+
expect(vellumAdapter.sent).toHaveLength(1);
|
|
541
|
+
const deepLink = vellumAdapter.sent[0].deepLinkTarget;
|
|
542
|
+
expect(deepLink).toBeDefined();
|
|
543
|
+
// The deep-link should use the pairing result, not the original placeholder
|
|
544
|
+
expect(deepLink!.conversationId).toBe("conv-reused-xyz");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("deep-link conversationId is stable across multiple deliveries to the same reused conversation", async () => {
|
|
548
|
+
const vellumAdapter = new MockAdapter("vellum");
|
|
549
|
+
const broadcaster = new NotificationBroadcaster([vellumAdapter]);
|
|
550
|
+
|
|
551
|
+
const stableConvId = "conv-stable-reuse-001";
|
|
552
|
+
|
|
553
|
+
// First delivery
|
|
554
|
+
nextPairingResult = {
|
|
555
|
+
conversationId: stableConvId,
|
|
556
|
+
messageId: "msg-delivery-1",
|
|
557
|
+
strategy: "start_new_conversation" as const,
|
|
558
|
+
createdNewConversation: false,
|
|
559
|
+
threadDecisionFallbackUsed: false,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
await broadcaster.broadcastDecision(makeSignal(), makeDecision());
|
|
563
|
+
|
|
564
|
+
// Second delivery — same conversation reused via binding-key
|
|
565
|
+
nextPairingResult = {
|
|
566
|
+
conversationId: stableConvId,
|
|
567
|
+
messageId: "msg-delivery-2",
|
|
568
|
+
strategy: "start_new_conversation" as const,
|
|
569
|
+
createdNewConversation: false,
|
|
570
|
+
threadDecisionFallbackUsed: false,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
await broadcaster.broadcastDecision(makeSignal(), makeDecision());
|
|
574
|
+
|
|
575
|
+
expect(vellumAdapter.sent).toHaveLength(2);
|
|
576
|
+
|
|
577
|
+
const deepLink1 = vellumAdapter.sent[0].deepLinkTarget;
|
|
578
|
+
const deepLink2 = vellumAdapter.sent[1].deepLinkTarget;
|
|
579
|
+
|
|
580
|
+
// Both deliveries point to the same stable conversation
|
|
581
|
+
expect(deepLink1!.conversationId).toBe(stableConvId);
|
|
582
|
+
expect(deepLink2!.conversationId).toBe(stableConvId);
|
|
583
|
+
|
|
584
|
+
// But each has a distinct messageId for scroll targeting
|
|
585
|
+
expect(deepLink1!.messageId).toBe("msg-delivery-1");
|
|
586
|
+
expect(deepLink2!.messageId).toBe("msg-delivery-2");
|
|
587
|
+
});
|
|
224
588
|
});
|
|
225
589
|
});
|
|
@@ -67,9 +67,9 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
67
67
|
deleteSecureKey: (account: string) => {
|
|
68
68
|
if (account in secureKeyStore) {
|
|
69
69
|
delete secureKeyStore[account];
|
|
70
|
-
return
|
|
70
|
+
return "deleted";
|
|
71
71
|
}
|
|
72
|
-
return
|
|
72
|
+
return "not-found";
|
|
73
73
|
},
|
|
74
74
|
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
75
75
|
getBackendType: () => "encrypted",
|
|
@@ -10,7 +10,10 @@ const TEST_DIR = join(
|
|
|
10
10
|
|
|
11
11
|
import { mock } from "bun:test";
|
|
12
12
|
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
+
const realPlatform = require("../util/platform.js");
|
|
13
15
|
mock.module("../util/platform.js", () => ({
|
|
16
|
+
...realPlatform,
|
|
14
17
|
getRootDir: () => TEST_DIR,
|
|
15
18
|
getDataDir: () => TEST_DIR,
|
|
16
19
|
getWorkspaceDir: () => TEST_DIR,
|
|
@@ -34,19 +37,29 @@ mock.module("../util/platform.js", () => ({
|
|
|
34
37
|
isWindows: () => process.platform === "win32",
|
|
35
38
|
getPlatformName: () => process.platform,
|
|
36
39
|
getClipboardCommand: () => null,
|
|
40
|
+
readSessionToken: () => null,
|
|
37
41
|
removeSocketFile: () => {},
|
|
38
42
|
migratePath: () => {},
|
|
39
43
|
migrateToWorkspaceLayout: () => {},
|
|
40
44
|
migrateToDataLayout: () => {},
|
|
45
|
+
readLockfile: () => null,
|
|
46
|
+
writeLockfile: () => {},
|
|
41
47
|
}));
|
|
42
48
|
|
|
49
|
+
const noopLogger = new Proxy({} as Record<string, unknown>, {
|
|
50
|
+
get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
54
|
+
const realLogger = require("../util/logger.js");
|
|
43
55
|
mock.module("../util/logger.js", () => ({
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}),
|
|
56
|
+
...realLogger,
|
|
57
|
+
getLogger: () => noopLogger,
|
|
58
|
+
getCliLogger: () => noopLogger,
|
|
48
59
|
isDebug: () => false,
|
|
49
60
|
truncateForLog: (v: string) => v,
|
|
61
|
+
initLogger: () => {},
|
|
62
|
+
pruneOldLogFiles: () => 0,
|
|
50
63
|
}));
|
|
51
64
|
|
|
52
65
|
const { buildStarterTaskPlaybookSection, buildSystemPrompt } =
|
|
@@ -24,7 +24,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
24
24
|
|
|
25
25
|
daemon: { standaloneRecording: true },
|
|
26
26
|
provider: "mock-provider",
|
|
27
|
-
permissions: { mode: "
|
|
27
|
+
permissions: { mode: "workspace" },
|
|
28
28
|
apiKeys: {},
|
|
29
29
|
sandbox: { enabled: false },
|
|
30
30
|
timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 },
|
|
@@ -21,7 +21,10 @@ const noopLogger = {
|
|
|
21
21
|
child: () => noopLogger,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
+
const realLogger = require("../util/logger.js");
|
|
24
26
|
mock.module("../util/logger.js", () => ({
|
|
27
|
+
...realLogger,
|
|
25
28
|
getLogger: () => noopLogger,
|
|
26
29
|
isDebug: () => false,
|
|
27
30
|
truncateForLog: (v: string) => v,
|
|
@@ -34,7 +37,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
34
37
|
daemon: { standaloneRecording: true },
|
|
35
38
|
provider: "mock-provider",
|
|
36
39
|
model: "mock-model",
|
|
37
|
-
permissions: { mode: "
|
|
40
|
+
permissions: { mode: "workspace" },
|
|
38
41
|
apiKeys: {},
|
|
39
42
|
sandbox: { enabled: false },
|
|
40
43
|
timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 },
|
|
@@ -342,8 +345,10 @@ mock.module("../providers/provider-send-message.js", () => ({
|
|
|
342
345
|
// ── Mock external conversation store ───────────────────────────────────────
|
|
343
346
|
|
|
344
347
|
mock.module("../memory/external-conversation-store.js", () => ({
|
|
348
|
+
getBindingByChannelChat: () => null,
|
|
345
349
|
getBindingsForConversations: () => new Map(),
|
|
346
350
|
upsertBinding: () => {},
|
|
351
|
+
upsertOutboundBinding: () => {},
|
|
347
352
|
}));
|
|
348
353
|
|
|
349
354
|
// ── Mock subagent manager ──────────────────────────────────────────────────
|
|
@@ -24,7 +24,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
24
24
|
|
|
25
25
|
daemon: { standaloneRecording: true },
|
|
26
26
|
provider: "mock-provider",
|
|
27
|
-
permissions: { mode: "
|
|
27
|
+
permissions: { mode: "workspace" },
|
|
28
28
|
apiKeys: {},
|
|
29
29
|
sandbox: { enabled: false },
|
|
30
30
|
timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 },
|
|
@@ -32,7 +32,10 @@ const testDir = mkdtempSync(join(tmpdir(), "relay-server-test-"));
|
|
|
32
32
|
|
|
33
33
|
// ── Platform + logger mocks (must come before any source imports) ────
|
|
34
34
|
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
const realPlatform = require("../util/platform.js");
|
|
35
37
|
mock.module("../util/platform.js", () => ({
|
|
38
|
+
...realPlatform,
|
|
36
39
|
getDataDir: () => testDir,
|
|
37
40
|
isMacOS: () => process.platform === "darwin",
|
|
38
41
|
isLinux: () => process.platform === "linux",
|
|
@@ -44,7 +47,10 @@ mock.module("../util/platform.js", () => ({
|
|
|
44
47
|
ensureDataDir: () => {},
|
|
45
48
|
}));
|
|
46
49
|
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
|
+
const realLogger = require("../util/logger.js");
|
|
47
52
|
mock.module("../util/logger.js", () => ({
|
|
53
|
+
...realLogger,
|
|
48
54
|
getLogger: () =>
|
|
49
55
|
new Proxy({} as Record<string, unknown>, {
|
|
50
56
|
get: () => () => {},
|
|
@@ -61,10 +67,12 @@ mock.module("../daemon/identity-helpers.js", () => ({
|
|
|
61
67
|
// ── User-reference mock (isolate from real USER.md) ──────────────────
|
|
62
68
|
|
|
63
69
|
let mockUserReference = "my human";
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
71
|
+
const realUserReference = require("../config/user-reference.js");
|
|
64
72
|
mock.module("../config/user-reference.js", () => ({
|
|
73
|
+
...realUserReference,
|
|
65
74
|
resolveUserReference: () => mockUserReference,
|
|
66
75
|
resolveUserPronouns: () => null,
|
|
67
|
-
DEFAULT_USER_REFERENCE: "my human",
|
|
68
76
|
resolveGuardianName: (guardianDisplayName?: string | null) => {
|
|
69
77
|
if (mockUserReference !== "my human") {
|
|
70
78
|
return mockUserReference;
|