@vellumai/assistant 0.5.7 → 0.5.9
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/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/eslint.config.mjs +0 -31
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-agent-loop.ts +6 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-runtime-assembly.ts +61 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/inline-command-expansions.ts +7 -7
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/sensitive-output-placeholders.ts +2 -2
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { ContactWithChannels } from "../contacts/types.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mock state for resolveCallHints tests
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
let mockAssistantName: string | null = "Nova";
|
|
10
|
+
let mockGuardianName: string = "Alex";
|
|
11
|
+
let mockTargetContact: ContactWithChannels | null = null;
|
|
12
|
+
let mockRecentContacts: ContactWithChannels[] = [];
|
|
13
|
+
let mockFindContactByAddressThrows = false;
|
|
14
|
+
let mockListContactsThrows = false;
|
|
15
|
+
|
|
16
|
+
const logWarnFn = mock(() => {});
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Mock modules — must be before importing module under test
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
mock.module("../daemon/identity-helpers.js", () => ({
|
|
23
|
+
getAssistantName: () => mockAssistantName,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module("../prompts/user-reference.js", () => ({
|
|
27
|
+
DEFAULT_USER_REFERENCE: "my human",
|
|
28
|
+
resolveGuardianName: () => mockGuardianName,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
mock.module("../contacts/contact-store.js", () => ({
|
|
32
|
+
findContactByAddress: (_type: string, _address: string) => {
|
|
33
|
+
if (mockFindContactByAddressThrows) {
|
|
34
|
+
throw new Error("DB error: findContactByAddress");
|
|
35
|
+
}
|
|
36
|
+
return mockTargetContact;
|
|
37
|
+
},
|
|
38
|
+
findGuardianForChannel: () => null,
|
|
39
|
+
listGuardianChannels: () => null,
|
|
40
|
+
listContacts: (_limit?: number) => {
|
|
41
|
+
if (mockListContactsThrows) {
|
|
42
|
+
throw new Error("DB error: listContacts");
|
|
43
|
+
}
|
|
44
|
+
return mockRecentContacts;
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Bun's mock.module for "../util/logger.js" doesn't intercept the transitive
|
|
49
|
+
// import in stt-hints.ts due to a Bun limitation. Mocking pino at the package
|
|
50
|
+
// level works because getLogger uses a Proxy that lazily creates a pino child
|
|
51
|
+
// logger — intercepting pino itself captures all log calls.
|
|
52
|
+
const mockChildLogger = {
|
|
53
|
+
debug: () => {},
|
|
54
|
+
info: () => {},
|
|
55
|
+
warn: logWarnFn,
|
|
56
|
+
error: () => {},
|
|
57
|
+
child: () => mockChildLogger,
|
|
58
|
+
};
|
|
59
|
+
const mockPinoLogger = Object.assign(() => mockChildLogger, {
|
|
60
|
+
destination: () => ({}),
|
|
61
|
+
multistream: () => ({}),
|
|
62
|
+
});
|
|
63
|
+
mock.module("pino", () => ({ default: mockPinoLogger }));
|
|
64
|
+
mock.module("pino-pretty", () => ({ default: () => ({}) }));
|
|
65
|
+
|
|
66
|
+
// Import after mocking
|
|
67
|
+
import { buildSttHints, resolveCallHints, type SttHintsInput } from "../calls/stt-hints.js";
|
|
68
|
+
|
|
69
|
+
function emptyInput(): SttHintsInput {
|
|
70
|
+
return {
|
|
71
|
+
staticHints: [],
|
|
72
|
+
assistantName: null,
|
|
73
|
+
guardianName: null,
|
|
74
|
+
taskDescription: null,
|
|
75
|
+
targetContactName: null,
|
|
76
|
+
callerContactName: null,
|
|
77
|
+
inviteFriendName: null,
|
|
78
|
+
inviteGuardianName: null,
|
|
79
|
+
recentContactNames: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("buildSttHints", () => {
|
|
84
|
+
test("empty inputs produce empty string", () => {
|
|
85
|
+
expect(buildSttHints(emptyInput())).toBe("");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("static hints included verbatim", () => {
|
|
89
|
+
const input = emptyInput();
|
|
90
|
+
input.staticHints = ["Vellum", "Acme"];
|
|
91
|
+
expect(buildSttHints(input)).toBe("Vellum,Acme");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("assistant name included", () => {
|
|
95
|
+
const input = emptyInput();
|
|
96
|
+
input.assistantName = "Nova";
|
|
97
|
+
expect(buildSttHints(input)).toBe("Nova");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("guardian name included", () => {
|
|
101
|
+
const input = emptyInput();
|
|
102
|
+
input.guardianName = "Alex";
|
|
103
|
+
expect(buildSttHints(input)).toBe("Alex");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('default guardian name "my human" excluded', () => {
|
|
107
|
+
const input = emptyInput();
|
|
108
|
+
input.guardianName = "my human";
|
|
109
|
+
expect(buildSttHints(input)).toBe("");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("guardian name with whitespace around default sentinel excluded", () => {
|
|
113
|
+
const input = emptyInput();
|
|
114
|
+
input.guardianName = " my human ";
|
|
115
|
+
expect(buildSttHints(input)).toBe("");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("invite friend name included", () => {
|
|
119
|
+
const input = emptyInput();
|
|
120
|
+
input.inviteFriendName = "Alice";
|
|
121
|
+
expect(buildSttHints(input)).toBe("Alice");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("invite guardian name included", () => {
|
|
125
|
+
const input = emptyInput();
|
|
126
|
+
input.inviteGuardianName = "Bob";
|
|
127
|
+
expect(buildSttHints(input)).toBe("Bob");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("target contact name included", () => {
|
|
131
|
+
const input = emptyInput();
|
|
132
|
+
input.targetContactName = "Charlie";
|
|
133
|
+
expect(buildSttHints(input)).toBe("Charlie");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("caller contact name included", () => {
|
|
137
|
+
const input = emptyInput();
|
|
138
|
+
input.callerContactName = "Diana";
|
|
139
|
+
expect(buildSttHints(input)).toBe("Diana");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("recent contact names included", () => {
|
|
143
|
+
const input = emptyInput();
|
|
144
|
+
input.recentContactNames = ["Dave", "Eve"];
|
|
145
|
+
expect(buildSttHints(input)).toBe("Dave,Eve");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("proper nouns extracted from task description", () => {
|
|
149
|
+
const input = emptyInput();
|
|
150
|
+
input.taskDescription = "Call John Smith at Acme Corp";
|
|
151
|
+
const result = buildSttHints(input);
|
|
152
|
+
expect(result).toContain("John");
|
|
153
|
+
expect(result).toContain("Smith");
|
|
154
|
+
expect(result).toContain("Acme");
|
|
155
|
+
expect(result).toContain("Corp");
|
|
156
|
+
// "Call" is the first word of the sentence — should not be extracted
|
|
157
|
+
expect(result).not.toContain("Call");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("proper nouns extracted across sentence boundaries", () => {
|
|
161
|
+
const input = emptyInput();
|
|
162
|
+
input.taskDescription = "Meet with Alice. Then call Bob! Ask Charlie? Done.";
|
|
163
|
+
const result = buildSttHints(input);
|
|
164
|
+
expect(result).toContain("Alice");
|
|
165
|
+
expect(result).toContain("Bob");
|
|
166
|
+
expect(result).toContain("Charlie");
|
|
167
|
+
// First words of sentences should be excluded
|
|
168
|
+
expect(result).not.toContain("Meet");
|
|
169
|
+
expect(result).not.toContain("Then");
|
|
170
|
+
expect(result).not.toContain("Ask");
|
|
171
|
+
expect(result).not.toContain("Done");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("duplicates removed (case-insensitive)", () => {
|
|
175
|
+
const input = emptyInput();
|
|
176
|
+
input.staticHints = ["Vellum", "vellum", "VELLUM"];
|
|
177
|
+
input.recentContactNames = ["Vellum"];
|
|
178
|
+
const result = buildSttHints(input);
|
|
179
|
+
// Should appear only once — the first occurrence is kept
|
|
180
|
+
expect(result).toBe("Vellum");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("empty and whitespace-only entries filtered", () => {
|
|
184
|
+
const input = emptyInput();
|
|
185
|
+
input.staticHints = ["", " ", "Valid", " ", "Also Valid"];
|
|
186
|
+
expect(buildSttHints(input)).toBe("Valid,Also Valid");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("entries are trimmed", () => {
|
|
190
|
+
const input = emptyInput();
|
|
191
|
+
input.staticHints = [" Padded ", " Spaces "];
|
|
192
|
+
expect(buildSttHints(input)).toBe("Padded,Spaces");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("output truncated at MAX_HINTS_LENGTH without partial words", () => {
|
|
196
|
+
const input = emptyInput();
|
|
197
|
+
// Create hints that will exceed 500 chars when joined
|
|
198
|
+
const longHints: string[] = [];
|
|
199
|
+
for (let i = 0; i < 100; i++) {
|
|
200
|
+
longHints.push(`Hint${i}LongWord`);
|
|
201
|
+
}
|
|
202
|
+
input.staticHints = longHints;
|
|
203
|
+
const result = buildSttHints(input);
|
|
204
|
+
expect(result.length).toBeLessThanOrEqual(500);
|
|
205
|
+
// Should not end with a comma (that would indicate a truncation right after a separator)
|
|
206
|
+
expect(result).not.toMatch(/,$/);
|
|
207
|
+
// Every comma-separated part should be a complete hint from our input
|
|
208
|
+
const parts = result.split(",");
|
|
209
|
+
for (const part of parts) {
|
|
210
|
+
expect(input.staticHints).toContain(part);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("all sources combined in correct order", () => {
|
|
215
|
+
const input: SttHintsInput = {
|
|
216
|
+
staticHints: ["StaticOne"],
|
|
217
|
+
assistantName: "Nova",
|
|
218
|
+
guardianName: "Alex",
|
|
219
|
+
taskDescription: "Call John at Acme",
|
|
220
|
+
targetContactName: "Target",
|
|
221
|
+
callerContactName: "Caller",
|
|
222
|
+
inviteFriendName: "Friend",
|
|
223
|
+
inviteGuardianName: "Guardian",
|
|
224
|
+
recentContactNames: ["Recent"],
|
|
225
|
+
};
|
|
226
|
+
const result = buildSttHints(input);
|
|
227
|
+
const parts = result.split(",");
|
|
228
|
+
// Verify all expected hints are present
|
|
229
|
+
expect(parts).toContain("StaticOne");
|
|
230
|
+
expect(parts).toContain("Nova");
|
|
231
|
+
expect(parts).toContain("Alex");
|
|
232
|
+
expect(parts).toContain("John");
|
|
233
|
+
expect(parts).toContain("Acme");
|
|
234
|
+
expect(parts).toContain("Target");
|
|
235
|
+
expect(parts).toContain("Caller");
|
|
236
|
+
expect(parts).toContain("Friend");
|
|
237
|
+
expect(parts).toContain("Guardian");
|
|
238
|
+
expect(parts).toContain("Recent");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("surnames after abbreviation periods are preserved", () => {
|
|
242
|
+
const input = emptyInput();
|
|
243
|
+
input.taskDescription = "Call Dr. Smith at Acme";
|
|
244
|
+
const result = buildSttHints(input);
|
|
245
|
+
expect(result).toContain("Smith");
|
|
246
|
+
expect(result).toContain("Acme");
|
|
247
|
+
// "Dr" should also appear as a capitalized word
|
|
248
|
+
expect(result).toContain("Dr");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("multiple abbreviation titles preserve following names", () => {
|
|
252
|
+
const input = emptyInput();
|
|
253
|
+
input.taskDescription = "Meet Mr. Johnson and Mrs. Williams at the office";
|
|
254
|
+
const result = buildSttHints(input);
|
|
255
|
+
expect(result).toContain("Johnson");
|
|
256
|
+
expect(result).toContain("Williams");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("non-ASCII letters preserved in names", () => {
|
|
260
|
+
const input = emptyInput();
|
|
261
|
+
input.taskDescription = "Call José García and Łukasz Nowak";
|
|
262
|
+
const result = buildSttHints(input);
|
|
263
|
+
expect(result).toContain("José");
|
|
264
|
+
expect(result).toContain("García");
|
|
265
|
+
expect(result).toContain("Łukasz");
|
|
266
|
+
expect(result).toContain("Nowak");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("accented uppercase letters detected as proper nouns", () => {
|
|
270
|
+
const input = emptyInput();
|
|
271
|
+
input.taskDescription = "Talk to Zoë about the project";
|
|
272
|
+
const result = buildSttHints(input);
|
|
273
|
+
expect(result).toContain("Zoë");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("null and empty string names are excluded", () => {
|
|
277
|
+
const input = emptyInput();
|
|
278
|
+
input.assistantName = "";
|
|
279
|
+
input.guardianName = "";
|
|
280
|
+
input.targetContactName = null;
|
|
281
|
+
input.inviteFriendName = null;
|
|
282
|
+
input.inviteGuardianName = null;
|
|
283
|
+
expect(buildSttHints(input)).toBe("");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// resolveCallHints — wiring and error resilience
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
function makeContact(displayName: string): ContactWithChannels {
|
|
292
|
+
const now = Date.now();
|
|
293
|
+
return {
|
|
294
|
+
id: `contact-${displayName.toLowerCase()}`,
|
|
295
|
+
displayName,
|
|
296
|
+
notes: null,
|
|
297
|
+
lastInteraction: null,
|
|
298
|
+
interactionCount: 0,
|
|
299
|
+
createdAt: now,
|
|
300
|
+
updatedAt: now,
|
|
301
|
+
role: "contact",
|
|
302
|
+
contactType: "human",
|
|
303
|
+
principalId: null,
|
|
304
|
+
userFile: null,
|
|
305
|
+
channels: [],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
describe("resolveCallHints", () => {
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
mockAssistantName = "Nova";
|
|
312
|
+
mockGuardianName = "Alex";
|
|
313
|
+
mockTargetContact = null;
|
|
314
|
+
mockRecentContacts = [];
|
|
315
|
+
mockFindContactByAddressThrows = false;
|
|
316
|
+
mockListContactsThrows = false;
|
|
317
|
+
logWarnFn.mockClear();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("happy path wires all sources correctly", () => {
|
|
321
|
+
mockTargetContact = makeContact("Alice");
|
|
322
|
+
mockRecentContacts = [makeContact("Bob"), makeContact("Charlie")];
|
|
323
|
+
|
|
324
|
+
const session = {
|
|
325
|
+
task: "Call Alice at Acme",
|
|
326
|
+
toNumber: "+15551234567",
|
|
327
|
+
fromNumber: "+15559876543",
|
|
328
|
+
direction: "outbound" as const,
|
|
329
|
+
inviteFriendName: "Dave",
|
|
330
|
+
inviteGuardianName: "Eve",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const result = resolveCallHints(session, ["StaticHint"]);
|
|
334
|
+
const parts = result.split(",");
|
|
335
|
+
|
|
336
|
+
expect(parts).toContain("StaticHint");
|
|
337
|
+
expect(parts).toContain("Nova");
|
|
338
|
+
expect(parts).toContain("Alex");
|
|
339
|
+
expect(parts).toContain("Alice");
|
|
340
|
+
expect(parts).toContain("Dave");
|
|
341
|
+
expect(parts).toContain("Eve");
|
|
342
|
+
expect(parts).toContain("Bob");
|
|
343
|
+
expect(parts).toContain("Charlie");
|
|
344
|
+
// Proper nouns from task description
|
|
345
|
+
expect(parts).toContain("Acme");
|
|
346
|
+
expect(logWarnFn).not.toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("findContactByAddress failure is caught and logged without throwing", () => {
|
|
350
|
+
mockFindContactByAddressThrows = true;
|
|
351
|
+
mockRecentContacts = [makeContact("Bob")];
|
|
352
|
+
|
|
353
|
+
const session = {
|
|
354
|
+
task: null,
|
|
355
|
+
toNumber: "+15551234567",
|
|
356
|
+
fromNumber: "+15559876543",
|
|
357
|
+
direction: "outbound" as const,
|
|
358
|
+
inviteFriendName: null,
|
|
359
|
+
inviteGuardianName: null,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Should not throw
|
|
363
|
+
const result = resolveCallHints(session, []);
|
|
364
|
+
const parts = result.split(",");
|
|
365
|
+
|
|
366
|
+
// Target contact should be absent (lookup failed)
|
|
367
|
+
// But other sources should still work
|
|
368
|
+
expect(parts).toContain("Nova");
|
|
369
|
+
expect(parts).toContain("Alex");
|
|
370
|
+
expect(parts).toContain("Bob");
|
|
371
|
+
expect(logWarnFn).toHaveBeenCalled();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("listContacts failure is caught and logged without throwing", () => {
|
|
375
|
+
mockListContactsThrows = true;
|
|
376
|
+
mockTargetContact = makeContact("Alice");
|
|
377
|
+
|
|
378
|
+
const session = {
|
|
379
|
+
task: null,
|
|
380
|
+
toNumber: "+15551234567",
|
|
381
|
+
fromNumber: "+15559876543",
|
|
382
|
+
direction: "outbound" as const,
|
|
383
|
+
inviteFriendName: null,
|
|
384
|
+
inviteGuardianName: null,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Should not throw
|
|
388
|
+
const result = resolveCallHints(session, []);
|
|
389
|
+
const parts = result.split(",");
|
|
390
|
+
|
|
391
|
+
// Recent contacts should be absent (listing failed)
|
|
392
|
+
// But other sources should still work
|
|
393
|
+
expect(parts).toContain("Nova");
|
|
394
|
+
expect(parts).toContain("Alex");
|
|
395
|
+
expect(parts).toContain("Alice");
|
|
396
|
+
expect(logWarnFn).toHaveBeenCalled();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("inbound call resolves caller contact from fromNumber", () => {
|
|
400
|
+
mockTargetContact = makeContact("Alice");
|
|
401
|
+
mockRecentContacts = [makeContact("Bob")];
|
|
402
|
+
|
|
403
|
+
const session = {
|
|
404
|
+
task: null,
|
|
405
|
+
toNumber: "+15559876543",
|
|
406
|
+
fromNumber: "+15551234567",
|
|
407
|
+
direction: "inbound" as const,
|
|
408
|
+
inviteFriendName: null,
|
|
409
|
+
inviteGuardianName: null,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const result = resolveCallHints(session, []);
|
|
413
|
+
const parts = result.split(",");
|
|
414
|
+
|
|
415
|
+
// For inbound, the contact found via fromNumber should appear as caller, not target
|
|
416
|
+
expect(parts).toContain("Alice");
|
|
417
|
+
expect(parts).toContain("Nova");
|
|
418
|
+
expect(parts).toContain("Alex");
|
|
419
|
+
expect(parts).toContain("Bob");
|
|
420
|
+
expect(logWarnFn).not.toHaveBeenCalled();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("null session produces hints from assistant name, guardian name, and recent contacts", () => {
|
|
424
|
+
mockRecentContacts = [makeContact("RecentOne"), makeContact("RecentTwo")];
|
|
425
|
+
|
|
426
|
+
const result = resolveCallHints(null, ["Static"]);
|
|
427
|
+
const parts = result.split(",");
|
|
428
|
+
|
|
429
|
+
expect(parts).toContain("Static");
|
|
430
|
+
expect(parts).toContain("Nova");
|
|
431
|
+
expect(parts).toContain("Alex");
|
|
432
|
+
expect(parts).toContain("RecentOne");
|
|
433
|
+
expect(parts).toContain("RecentTwo");
|
|
434
|
+
// No target contact lookup should have been attempted (no session)
|
|
435
|
+
expect(logWarnFn).not.toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -175,6 +175,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
175
175
|
importance: 0.7,
|
|
176
176
|
fingerprint: "fp-assistant-inferred",
|
|
177
177
|
verificationState: "assistant_inferred",
|
|
178
|
+
sourceType: "extraction",
|
|
179
|
+
sourceMessageRole: "assistant",
|
|
178
180
|
scopeId: "default",
|
|
179
181
|
firstSeenAt: now + 10,
|
|
180
182
|
lastSeenAt: now + 10,
|
|
@@ -189,6 +191,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
189
191
|
importance: 0.8,
|
|
190
192
|
fingerprint: "fp-user-reported",
|
|
191
193
|
verificationState: "user_reported",
|
|
194
|
+
sourceType: "extraction",
|
|
195
|
+
sourceMessageRole: "user",
|
|
192
196
|
scopeId: "default",
|
|
193
197
|
firstSeenAt: now + 20,
|
|
194
198
|
lastSeenAt: now + 20,
|
|
@@ -203,6 +207,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
203
207
|
importance: 0.5,
|
|
204
208
|
fingerprint: "fp-other-conv",
|
|
205
209
|
verificationState: "assistant_inferred",
|
|
210
|
+
sourceType: "extraction",
|
|
211
|
+
sourceMessageRole: "assistant",
|
|
206
212
|
scopeId: "default",
|
|
207
213
|
firstSeenAt: now + 30,
|
|
208
214
|
lastSeenAt: now + 30,
|
|
@@ -217,6 +223,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
217
223
|
importance: 0.4,
|
|
218
224
|
fingerprint: "fp-already-superseded",
|
|
219
225
|
verificationState: "assistant_inferred",
|
|
226
|
+
sourceType: "extraction",
|
|
227
|
+
sourceMessageRole: "assistant",
|
|
220
228
|
scopeId: "default",
|
|
221
229
|
firstSeenAt: now + 5,
|
|
222
230
|
lastSeenAt: now + 5,
|
|
@@ -449,6 +457,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
449
457
|
importance: 0.7,
|
|
450
458
|
fingerprint: "fp-cross-sourced",
|
|
451
459
|
verificationState: "assistant_inferred",
|
|
460
|
+
sourceType: "extraction",
|
|
461
|
+
sourceMessageRole: "assistant",
|
|
452
462
|
scopeId: "default",
|
|
453
463
|
firstSeenAt: now + 10,
|
|
454
464
|
lastSeenAt: now + 20,
|
|
@@ -572,6 +582,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
572
582
|
importance: 0.7,
|
|
573
583
|
fingerprint: "fp-cross-sched",
|
|
574
584
|
verificationState: "assistant_inferred",
|
|
585
|
+
sourceType: "extraction",
|
|
586
|
+
sourceMessageRole: "assistant",
|
|
575
587
|
scopeId: "default",
|
|
576
588
|
firstSeenAt: now + 10,
|
|
577
589
|
lastSeenAt: now + 20,
|
|
@@ -688,6 +700,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
688
700
|
importance: 0.7,
|
|
689
701
|
fingerprint: "fp-good-corroboration",
|
|
690
702
|
verificationState: "assistant_inferred",
|
|
703
|
+
sourceType: "extraction",
|
|
704
|
+
sourceMessageRole: "assistant",
|
|
691
705
|
scopeId: "default",
|
|
692
706
|
firstSeenAt: now + 10,
|
|
693
707
|
lastSeenAt: now + 20,
|