@vellumai/assistant 0.3.19 → 0.3.21
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/ARCHITECTURE.md +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +10 -2
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), 'invite-redemption-service-test-'));
|
|
8
|
+
|
|
9
|
+
mock.module('../util/platform.js', () => ({
|
|
10
|
+
getDataDir: () => testDir,
|
|
11
|
+
isMacOS: () => process.platform === 'darwin',
|
|
12
|
+
isLinux: () => process.platform === 'linux',
|
|
13
|
+
isWindows: () => process.platform === 'win32',
|
|
14
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
15
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
16
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
17
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
18
|
+
ensureDataDir: () => {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
mock.module('../util/logger.js', () => ({
|
|
22
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
23
|
+
get: () => () => {},
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { getSqlite, initializeDb, resetDb } from '../memory/db.js';
|
|
28
|
+
import { createInvite, revokeInvite as revokeStoreFn } from '../memory/ingress-invite-store.js';
|
|
29
|
+
import { upsertMember } from '../memory/ingress-member-store.js';
|
|
30
|
+
import { type InviteRedemptionOutcome,redeemInvite } from '../runtime/invite-redemption-service.js';
|
|
31
|
+
|
|
32
|
+
initializeDb();
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
resetDb();
|
|
36
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function resetTables() {
|
|
40
|
+
getSqlite().run('DELETE FROM assistant_ingress_members');
|
|
41
|
+
getSqlite().run('DELETE FROM assistant_ingress_invites');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('invite-redemption-service', () => {
|
|
45
|
+
beforeEach(resetTables);
|
|
46
|
+
|
|
47
|
+
test('redeems a valid invite and returns typed outcome', () => {
|
|
48
|
+
const { rawToken, invite } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
|
|
49
|
+
|
|
50
|
+
const outcome = redeemInvite({
|
|
51
|
+
rawToken,
|
|
52
|
+
sourceChannel: 'telegram',
|
|
53
|
+
externalUserId: 'user-1',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(outcome.ok).toBe(true);
|
|
57
|
+
expect(outcome).toEqual({
|
|
58
|
+
ok: true,
|
|
59
|
+
type: 'redeemed',
|
|
60
|
+
memberId: expect.any(String),
|
|
61
|
+
inviteId: invite.id,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('returns invalid_token for a bogus token', () => {
|
|
66
|
+
const outcome = redeemInvite({
|
|
67
|
+
rawToken: 'totally-bogus-token',
|
|
68
|
+
sourceChannel: 'telegram',
|
|
69
|
+
externalUserId: 'user-1',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(outcome).toEqual({ ok: false, reason: 'invalid_token' });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('returns expired for an expired invite', () => {
|
|
76
|
+
// Create an invite that expired 1 ms ago
|
|
77
|
+
const { rawToken } = createInvite({
|
|
78
|
+
sourceChannel: 'telegram',
|
|
79
|
+
maxUses: 1,
|
|
80
|
+
expiresInMs: -1,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const outcome = redeemInvite({
|
|
84
|
+
rawToken,
|
|
85
|
+
sourceChannel: 'telegram',
|
|
86
|
+
externalUserId: 'user-1',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(outcome).toEqual({ ok: false, reason: 'expired' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('returns revoked for a revoked invite', () => {
|
|
93
|
+
const { rawToken, invite } = createInvite({
|
|
94
|
+
sourceChannel: 'telegram',
|
|
95
|
+
maxUses: 1,
|
|
96
|
+
});
|
|
97
|
+
revokeStoreFn(invite.id);
|
|
98
|
+
|
|
99
|
+
const outcome = redeemInvite({
|
|
100
|
+
rawToken,
|
|
101
|
+
sourceChannel: 'telegram',
|
|
102
|
+
externalUserId: 'user-1',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(outcome).toEqual({ ok: false, reason: 'revoked' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('returns max_uses_reached when invite is fully consumed', () => {
|
|
109
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
|
|
110
|
+
|
|
111
|
+
// First redemption should succeed
|
|
112
|
+
const first = redeemInvite({
|
|
113
|
+
rawToken,
|
|
114
|
+
sourceChannel: 'telegram',
|
|
115
|
+
externalUserId: 'user-1',
|
|
116
|
+
});
|
|
117
|
+
expect(first.ok).toBe(true);
|
|
118
|
+
|
|
119
|
+
// Second attempt should fail — the invite is now fully redeemed
|
|
120
|
+
const second = redeemInvite({
|
|
121
|
+
rawToken,
|
|
122
|
+
sourceChannel: 'telegram',
|
|
123
|
+
externalUserId: 'user-2',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(second).toEqual({ ok: false, reason: 'max_uses_reached' });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('returns channel_mismatch when redeeming on wrong channel', () => {
|
|
130
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
|
|
131
|
+
|
|
132
|
+
const outcome = redeemInvite({
|
|
133
|
+
rawToken,
|
|
134
|
+
sourceChannel: 'sms',
|
|
135
|
+
externalUserId: 'user-1',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(outcome).toEqual({ ok: false, reason: 'channel_mismatch' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('returns missing_identity when no externalUserId or externalChatId', () => {
|
|
142
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
|
|
143
|
+
|
|
144
|
+
const outcome = redeemInvite({
|
|
145
|
+
rawToken,
|
|
146
|
+
sourceChannel: 'telegram',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(outcome).toEqual({ ok: false, reason: 'missing_identity' });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('returns already_member when user is already an active member', () => {
|
|
153
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
|
|
154
|
+
|
|
155
|
+
// Pre-create an active member
|
|
156
|
+
upsertMember({
|
|
157
|
+
sourceChannel: 'telegram',
|
|
158
|
+
externalUserId: 'existing-user',
|
|
159
|
+
status: 'active',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const outcome = redeemInvite({
|
|
163
|
+
rawToken,
|
|
164
|
+
sourceChannel: 'telegram',
|
|
165
|
+
externalUserId: 'existing-user',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(outcome.ok).toBe(true);
|
|
169
|
+
expect((outcome as Extract<InviteRedemptionOutcome, { type: 'already_member' }>).type).toBe('already_member');
|
|
170
|
+
expect((outcome as Extract<InviteRedemptionOutcome, { type: 'already_member' }>).memberId).toEqual(expect.any(String));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('returns invalid_token for a blocked member to avoid leaking membership status', () => {
|
|
174
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
|
|
175
|
+
|
|
176
|
+
// Pre-create a blocked member — simulates a guardian-initiated block
|
|
177
|
+
upsertMember({
|
|
178
|
+
sourceChannel: 'telegram',
|
|
179
|
+
externalUserId: 'blocked-user',
|
|
180
|
+
status: 'blocked',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const outcome = redeemInvite({
|
|
184
|
+
rawToken,
|
|
185
|
+
sourceChannel: 'telegram',
|
|
186
|
+
externalUserId: 'blocked-user',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(outcome).toEqual({ ok: false, reason: 'invalid_token' });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('does not return already_member for a revoked member', () => {
|
|
193
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
|
|
194
|
+
|
|
195
|
+
// Pre-create a revoked member
|
|
196
|
+
const member = upsertMember({
|
|
197
|
+
sourceChannel: 'telegram',
|
|
198
|
+
externalUserId: 'revoked-user',
|
|
199
|
+
status: 'revoked',
|
|
200
|
+
});
|
|
201
|
+
expect(member.status).toBe('revoked');
|
|
202
|
+
|
|
203
|
+
const outcome = redeemInvite({
|
|
204
|
+
rawToken,
|
|
205
|
+
sourceChannel: 'telegram',
|
|
206
|
+
externalUserId: 'revoked-user',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Should redeem, not return already_member
|
|
210
|
+
expect(outcome.ok).toBe(true);
|
|
211
|
+
expect((outcome as Extract<InviteRedemptionOutcome, { type: 'redeemed' }>).type).toBe('redeemed');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('raw token is not present in the outcome object', () => {
|
|
215
|
+
const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 1 });
|
|
216
|
+
|
|
217
|
+
const outcome = redeemInvite({
|
|
218
|
+
rawToken,
|
|
219
|
+
sourceChannel: 'telegram',
|
|
220
|
+
externalUserId: 'user-1',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Verify the raw token does not appear anywhere in the serialized outcome
|
|
224
|
+
const serialized = JSON.stringify(outcome);
|
|
225
|
+
expect(serialized).not.toContain(rawToken);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('channel enforcement blocks cross-channel redemption (voice invite via slack)', () => {
|
|
229
|
+
const { rawToken } = createInvite({ sourceChannel: 'voice', maxUses: 1 });
|
|
230
|
+
|
|
231
|
+
const outcome = redeemInvite({
|
|
232
|
+
rawToken,
|
|
233
|
+
sourceChannel: 'slack',
|
|
234
|
+
externalUserId: 'user-1',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(outcome).toEqual({ ok: false, reason: 'channel_mismatch' });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('returns invalid_token for an active member with a bogus token (no membership probing)', () => {
|
|
241
|
+
// Pre-create an active member
|
|
242
|
+
upsertMember({
|
|
243
|
+
sourceChannel: 'telegram',
|
|
244
|
+
externalUserId: 'probed-user',
|
|
245
|
+
status: 'active',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Attempt to redeem with a bogus token — must NOT leak membership status
|
|
249
|
+
const outcome = redeemInvite({
|
|
250
|
+
rawToken: 'completely-bogus-token',
|
|
251
|
+
sourceChannel: 'telegram',
|
|
252
|
+
externalUserId: 'probed-user',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(outcome).toEqual({ ok: false, reason: 'invalid_token' });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('returns expired for an active member with an expired invite token', () => {
|
|
259
|
+
// Create an expired invite
|
|
260
|
+
const { rawToken } = createInvite({
|
|
261
|
+
sourceChannel: 'telegram',
|
|
262
|
+
maxUses: 5,
|
|
263
|
+
expiresInMs: -1,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Pre-create an active member
|
|
267
|
+
upsertMember({
|
|
268
|
+
sourceChannel: 'telegram',
|
|
269
|
+
externalUserId: 'expired-token-user',
|
|
270
|
+
status: 'active',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Expired token must return expired, not already_member
|
|
274
|
+
const outcome = redeemInvite({
|
|
275
|
+
rawToken,
|
|
276
|
+
sourceChannel: 'telegram',
|
|
277
|
+
externalUserId: 'expired-token-user',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(outcome).toEqual({ ok: false, reason: 'expired' });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('returns channel_mismatch for an active member with a valid token for a different channel', () => {
|
|
284
|
+
// Create an invite for SMS
|
|
285
|
+
const { rawToken } = createInvite({
|
|
286
|
+
sourceChannel: 'sms',
|
|
287
|
+
maxUses: 5,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Pre-create an active member on telegram
|
|
291
|
+
upsertMember({
|
|
292
|
+
sourceChannel: 'telegram',
|
|
293
|
+
externalUserId: 'cross-channel-user',
|
|
294
|
+
status: 'active',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Valid token for wrong channel must return channel_mismatch, not already_member
|
|
298
|
+
const outcome = redeemInvite({
|
|
299
|
+
rawToken,
|
|
300
|
+
sourceChannel: 'telegram',
|
|
301
|
+
externalUserId: 'cross-channel-user',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(outcome).toEqual({ ok: false, reason: 'channel_mismatch' });
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -731,6 +731,27 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
731
731
|
type: 'voice_config_update',
|
|
732
732
|
activationKey: 'fn',
|
|
733
733
|
},
|
|
734
|
+
generate_avatar: {
|
|
735
|
+
type: 'generate_avatar',
|
|
736
|
+
description: 'a friendly purple cat with green eyes wearing a tiny hat',
|
|
737
|
+
},
|
|
738
|
+
guardian_actions_pending_request: {
|
|
739
|
+
type: 'guardian_actions_pending_request',
|
|
740
|
+
conversationId: 'conv-guardian-001',
|
|
741
|
+
},
|
|
742
|
+
guardian_action_decision: {
|
|
743
|
+
type: 'guardian_action_decision',
|
|
744
|
+
requestId: 'req-guardian-001',
|
|
745
|
+
action: 'approve_once',
|
|
746
|
+
conversationId: 'conv-guardian-001',
|
|
747
|
+
},
|
|
748
|
+
reorder_threads: {
|
|
749
|
+
type: 'reorder_threads',
|
|
750
|
+
updates: [
|
|
751
|
+
{ sessionId: 'sess-001', displayOrder: 0, isPinned: false },
|
|
752
|
+
{ sessionId: 'sess-002', displayOrder: 1, isPinned: true },
|
|
753
|
+
],
|
|
754
|
+
},
|
|
734
755
|
};
|
|
735
756
|
|
|
736
757
|
// ---------------------------------------------------------------------------
|
|
@@ -2020,6 +2041,42 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
2020
2041
|
emoji: '',
|
|
2021
2042
|
home: '',
|
|
2022
2043
|
},
|
|
2044
|
+
avatar_updated: {
|
|
2045
|
+
type: 'avatar_updated',
|
|
2046
|
+
avatarPath: '/Users/test/.vellum/workspace/data/avatar/custom-avatar.png',
|
|
2047
|
+
},
|
|
2048
|
+
generate_avatar_response: {
|
|
2049
|
+
type: 'generate_avatar_response',
|
|
2050
|
+
success: true,
|
|
2051
|
+
error: undefined,
|
|
2052
|
+
},
|
|
2053
|
+
guardian_actions_pending_response: {
|
|
2054
|
+
type: 'guardian_actions_pending_response',
|
|
2055
|
+
conversationId: 'conv-guardian-001',
|
|
2056
|
+
prompts: [
|
|
2057
|
+
{
|
|
2058
|
+
requestId: 'req-guardian-001',
|
|
2059
|
+
requestCode: 'REQ-GU',
|
|
2060
|
+
state: 'pending',
|
|
2061
|
+
questionText: 'Approve tool: bash',
|
|
2062
|
+
toolName: 'bash',
|
|
2063
|
+
actions: [
|
|
2064
|
+
{ action: 'approve_once', label: 'Approve once' },
|
|
2065
|
+
{ action: 'reject', label: 'Reject' },
|
|
2066
|
+
],
|
|
2067
|
+
expiresAt: 1700100000000,
|
|
2068
|
+
conversationId: 'conv-guardian-001',
|
|
2069
|
+
callSessionId: null,
|
|
2070
|
+
},
|
|
2071
|
+
],
|
|
2072
|
+
},
|
|
2073
|
+
guardian_action_decision_response: {
|
|
2074
|
+
type: 'guardian_action_decision_response',
|
|
2075
|
+
applied: true,
|
|
2076
|
+
reason: undefined,
|
|
2077
|
+
requestId: 'req-guardian-001',
|
|
2078
|
+
userText: undefined,
|
|
2079
|
+
},
|
|
2023
2080
|
};
|
|
2024
2081
|
|
|
2025
2082
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for notification decision fallback copy.
|
|
3
|
+
*
|
|
4
|
+
* Ensures fallback decisions still produce human-friendly copy when the
|
|
5
|
+
* decision-model call is unavailable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
mock.module('../channels/config.js', () => ({
|
|
11
|
+
getDeliverableChannels: () => ['vellum', 'telegram', 'sms'],
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module('../config/loader.js', () => ({
|
|
15
|
+
getConfig: () => ({
|
|
16
|
+
notifications: {
|
|
17
|
+
decisionModelIntent: 'latency-optimized',
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module('../notifications/decisions-store.js', () => ({
|
|
23
|
+
createDecision: () => {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module('../notifications/preference-summary.js', () => ({
|
|
27
|
+
getPreferenceSummary: () => undefined,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mock.module('../notifications/thread-candidates.js', () => ({
|
|
31
|
+
buildThreadCandidates: () => undefined,
|
|
32
|
+
serializeCandidatesForPrompt: () => undefined,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module('../providers/provider-send-message.js', () => ({
|
|
36
|
+
getConfiguredProvider: () => null,
|
|
37
|
+
createTimeout: () => ({
|
|
38
|
+
signal: new AbortController().signal,
|
|
39
|
+
cleanup: () => {},
|
|
40
|
+
}),
|
|
41
|
+
extractToolUse: () => null,
|
|
42
|
+
userMessage: (text: string) => ({ role: 'user', content: text }),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module('../util/logger.js', () => ({
|
|
46
|
+
getLogger: () =>
|
|
47
|
+
new Proxy({} as Record<string, unknown>, {
|
|
48
|
+
get: () => () => {},
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
import { evaluateSignal } from '../notifications/decision-engine.js';
|
|
53
|
+
import type { NotificationSignal } from '../notifications/signal.js';
|
|
54
|
+
import type { NotificationChannel } from '../notifications/types.js';
|
|
55
|
+
|
|
56
|
+
function makeSignal(overrides?: Partial<NotificationSignal>): NotificationSignal {
|
|
57
|
+
return {
|
|
58
|
+
signalId: 'sig-fallback-guardian-1',
|
|
59
|
+
assistantId: 'self',
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
sourceChannel: 'voice',
|
|
62
|
+
sourceSessionId: 'call-session-1',
|
|
63
|
+
sourceEventName: 'guardian.question',
|
|
64
|
+
contextPayload: {
|
|
65
|
+
questionText: 'What is the gate code?',
|
|
66
|
+
},
|
|
67
|
+
attentionHints: {
|
|
68
|
+
requiresAction: true,
|
|
69
|
+
urgency: 'high',
|
|
70
|
+
isAsyncBackground: false,
|
|
71
|
+
visibleInSourceNow: false,
|
|
72
|
+
},
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('notification decision fallback copy', () => {
|
|
78
|
+
test('uses human-friendly template copy for guardian.question', async () => {
|
|
79
|
+
const signal = makeSignal();
|
|
80
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
81
|
+
|
|
82
|
+
expect(decision.fallbackUsed).toBe(true);
|
|
83
|
+
expect(decision.renderedCopy.vellum?.title).toBe('Guardian Question');
|
|
84
|
+
expect(decision.renderedCopy.vellum?.body).toBe('What is the gate code?');
|
|
85
|
+
expect(decision.renderedCopy.vellum?.title).not.toBe('guardian.question');
|
|
86
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('Action required: guardian.question');
|
|
87
|
+
});
|
|
88
|
+
});
|