vellum 0.2.12 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +171 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +402 -5
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +28 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +96 -8
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +50 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +26 -2
- package/src/config/schema.ts +178 -9
- package/src/config/types.ts +3 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/defaults.ts +11 -0
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/conversation-routes.ts +12 -5
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { sanitizeUrlForDisplay
|
|
2
|
+
import { sanitizeUrlForDisplay } from '../cli.js';
|
|
3
3
|
|
|
4
4
|
describe('sanitizeUrlForDisplay', () => {
|
|
5
5
|
test('removes userinfo from absolute URLs', () => {
|
|
@@ -24,58 +24,3 @@ describe('sanitizeUrlForDisplay', () => {
|
|
|
24
24
|
expect(sanitizeUrlForDisplay(rawValue)).toBe('not-a-url //[REDACTED]@example.com');
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
-
|
|
28
|
-
describe('formatPrincipalTag', () => {
|
|
29
|
-
test('returns empty string for core principals', () => {
|
|
30
|
-
expect(formatPrincipalTag({ principalKind: 'core' })).toBe('');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('returns empty string when principalKind is absent', () => {
|
|
34
|
-
expect(formatPrincipalTag({})).toBe('');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('formats skill with name, version, and target', () => {
|
|
38
|
-
const tag = formatPrincipalTag({
|
|
39
|
-
principalKind: 'skill',
|
|
40
|
-
principalId: 'weather-skill',
|
|
41
|
-
principalVersion: 'sha256:abcdef1234567890fedcba',
|
|
42
|
-
executionTarget: 'host',
|
|
43
|
-
});
|
|
44
|
-
expect(tag).toBe('[skill: weather-skill@abcdef12 \u2192 host]');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('formats skill without version hash', () => {
|
|
48
|
-
const tag = formatPrincipalTag({
|
|
49
|
-
principalKind: 'skill',
|
|
50
|
-
principalId: 'my-skill',
|
|
51
|
-
});
|
|
52
|
-
expect(tag).toBe('[skill: my-skill]');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('formats skill with sandbox target', () => {
|
|
56
|
-
const tag = formatPrincipalTag({
|
|
57
|
-
principalKind: 'skill',
|
|
58
|
-
principalId: 'deploy-skill',
|
|
59
|
-
principalVersion: 'sha256:1111222233334444',
|
|
60
|
-
executionTarget: 'sandbox',
|
|
61
|
-
});
|
|
62
|
-
expect(tag).toBe('[skill: deploy-skill@11112222 \u2192 sandbox]');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('falls back to principalKind when principalId is absent', () => {
|
|
66
|
-
const tag = formatPrincipalTag({
|
|
67
|
-
principalKind: 'skill',
|
|
68
|
-
principalVersion: 'sha256:aabbccdd',
|
|
69
|
-
});
|
|
70
|
-
expect(tag).toBe('[skill: skill@aabbccdd]');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('strips non-sha256 version prefix (e.g. v1:)', () => {
|
|
74
|
-
const tag = formatPrincipalTag({
|
|
75
|
-
principalKind: 'skill',
|
|
76
|
-
principalId: 'runtime-skill',
|
|
77
|
-
principalVersion: 'v1:abcdef1234567890',
|
|
78
|
-
});
|
|
79
|
-
expect(tag).toBe('[skill: runtime-skill@abcdef12]');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -55,6 +55,7 @@ import { _setBackend } from '../security/secure-keys.js';
|
|
|
55
55
|
import { loadConfig, invalidateConfigCache } from '../config/loader.js';
|
|
56
56
|
import { AssistantConfigSchema } from '../config/schema.js';
|
|
57
57
|
import { DEFAULT_CONFIG } from '../config/defaults.js';
|
|
58
|
+
import { buildElevenLabsVoiceSpec, resolveVoiceQualityProfile } from '../calls/voice-quality.js';
|
|
58
59
|
|
|
59
60
|
// ---------------------------------------------------------------------------
|
|
60
61
|
// Helpers
|
|
@@ -138,6 +139,8 @@ describe('AssistantConfigSchema', () => {
|
|
|
138
139
|
reaskCooldownTurns: 3,
|
|
139
140
|
resolverLlmTimeoutMs: 12000,
|
|
140
141
|
relevanceThreshold: 0.3,
|
|
142
|
+
askOnIrrelevantTurns: false,
|
|
143
|
+
conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
|
|
141
144
|
});
|
|
142
145
|
});
|
|
143
146
|
|
|
@@ -148,6 +151,27 @@ describe('AssistantConfigSchema', () => {
|
|
|
148
151
|
expect(result.success).toBe(false);
|
|
149
152
|
});
|
|
150
153
|
|
|
154
|
+
test('rejects invalid memory.conflicts.askOnIrrelevantTurns', () => {
|
|
155
|
+
const result = AssistantConfigSchema.safeParse({
|
|
156
|
+
memory: { conflicts: { askOnIrrelevantTurns: 123 } },
|
|
157
|
+
});
|
|
158
|
+
expect(result.success).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('rejects invalid memory.conflicts.conflictableKinds entry', () => {
|
|
162
|
+
const result = AssistantConfigSchema.safeParse({
|
|
163
|
+
memory: { conflicts: { conflictableKinds: ['invalid_kind'] } },
|
|
164
|
+
});
|
|
165
|
+
expect(result.success).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('rejects empty memory.conflicts.conflictableKinds', () => {
|
|
169
|
+
const result = AssistantConfigSchema.safeParse({
|
|
170
|
+
memory: { conflicts: { conflictableKinds: [] } },
|
|
171
|
+
});
|
|
172
|
+
expect(result.success).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
151
175
|
test('applies memory.profile defaults', () => {
|
|
152
176
|
const result = AssistantConfigSchema.parse({});
|
|
153
177
|
expect(result.memory.profile).toEqual({
|
|
@@ -473,9 +497,9 @@ describe('AssistantConfigSchema', () => {
|
|
|
473
497
|
expect(result.success).toBe(false);
|
|
474
498
|
});
|
|
475
499
|
|
|
476
|
-
test('defaults permissions.mode to
|
|
500
|
+
test('defaults permissions.mode to workspace', () => {
|
|
477
501
|
const result = AssistantConfigSchema.parse({});
|
|
478
|
-
expect(result.permissions).toEqual({ mode: '
|
|
502
|
+
expect(result.permissions).toEqual({ mode: 'workspace' });
|
|
479
503
|
});
|
|
480
504
|
|
|
481
505
|
test('accepts explicit permissions.mode strict', () => {
|
|
@@ -492,6 +516,13 @@ describe('AssistantConfigSchema', () => {
|
|
|
492
516
|
expect(result.permissions.mode).toBe('legacy');
|
|
493
517
|
});
|
|
494
518
|
|
|
519
|
+
test('accepts explicit permissions.mode workspace', () => {
|
|
520
|
+
const result = AssistantConfigSchema.parse({
|
|
521
|
+
permissions: { mode: 'workspace' },
|
|
522
|
+
});
|
|
523
|
+
expect(result.permissions.mode).toBe('workspace');
|
|
524
|
+
});
|
|
525
|
+
|
|
495
526
|
test('rejects invalid permissions.mode', () => {
|
|
496
527
|
const result = AssistantConfigSchema.safeParse({
|
|
497
528
|
permissions: { mode: 'permissive' },
|
|
@@ -655,6 +686,26 @@ describe('AssistantConfigSchema', () => {
|
|
|
655
686
|
safety: {
|
|
656
687
|
denyCategories: [],
|
|
657
688
|
},
|
|
689
|
+
voice: {
|
|
690
|
+
mode: 'twilio_standard',
|
|
691
|
+
language: 'en-US',
|
|
692
|
+
transcriptionProvider: 'Deepgram',
|
|
693
|
+
fallbackToStandardOnError: true,
|
|
694
|
+
elevenlabs: {
|
|
695
|
+
voiceId: '',
|
|
696
|
+
voiceModelId: '',
|
|
697
|
+
speed: 1.0,
|
|
698
|
+
stability: 0.5,
|
|
699
|
+
similarityBoost: 0.75,
|
|
700
|
+
useSpeakerBoost: true,
|
|
701
|
+
agentId: '',
|
|
702
|
+
apiBaseUrl: 'https://api.elevenlabs.io',
|
|
703
|
+
registerCallTimeoutMs: 5000,
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
callerIdentity: {
|
|
707
|
+
allowPerCallOverride: true,
|
|
708
|
+
},
|
|
658
709
|
});
|
|
659
710
|
});
|
|
660
711
|
|
|
@@ -745,6 +796,344 @@ describe('AssistantConfigSchema', () => {
|
|
|
745
796
|
});
|
|
746
797
|
expect(result.success).toBe(false);
|
|
747
798
|
});
|
|
799
|
+
|
|
800
|
+
// ── Calls voice config ──────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
test('config without calls.voice parses correctly and produces defaults', () => {
|
|
803
|
+
const result = AssistantConfigSchema.parse({});
|
|
804
|
+
expect(result.calls.voice.mode).toBe('twilio_standard');
|
|
805
|
+
expect(result.calls.voice.language).toBe('en-US');
|
|
806
|
+
expect(result.calls.voice.transcriptionProvider).toBe('Deepgram');
|
|
807
|
+
expect(result.calls.voice.fallbackToStandardOnError).toBe(true);
|
|
808
|
+
expect(result.calls.voice.elevenlabs.voiceId).toBe('');
|
|
809
|
+
expect(result.calls.voice.elevenlabs.voiceModelId).toBe('');
|
|
810
|
+
expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
|
|
811
|
+
expect(result.calls.voice.elevenlabs.stability).toBe(0.5);
|
|
812
|
+
expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
|
|
813
|
+
expect(result.calls.voice.elevenlabs.useSpeakerBoost).toBe(true);
|
|
814
|
+
expect(result.calls.voice.elevenlabs.agentId).toBe('');
|
|
815
|
+
expect(result.calls.voice.elevenlabs.apiBaseUrl).toBe('https://api.elevenlabs.io');
|
|
816
|
+
expect(result.calls.voice.elevenlabs.registerCallTimeoutMs).toBe(5000);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test('legacy style field is silently stripped by schema', () => {
|
|
820
|
+
const result = AssistantConfigSchema.parse({
|
|
821
|
+
calls: { voice: { elevenlabs: { style: 0.5 } } },
|
|
822
|
+
});
|
|
823
|
+
expect((result.calls.voice.elevenlabs as Record<string, unknown>).style).toBeUndefined();
|
|
824
|
+
expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test('rejects calls.voice.elevenlabs.speed below 0.7', () => {
|
|
828
|
+
const result = AssistantConfigSchema.safeParse({
|
|
829
|
+
calls: { voice: { elevenlabs: { speed: 0.5 } } },
|
|
830
|
+
});
|
|
831
|
+
expect(result.success).toBe(false);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('rejects calls.voice.elevenlabs.speed above 1.2', () => {
|
|
835
|
+
const result = AssistantConfigSchema.safeParse({
|
|
836
|
+
calls: { voice: { elevenlabs: { speed: 1.5 } } },
|
|
837
|
+
});
|
|
838
|
+
expect(result.success).toBe(false);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test('accepts valid calls.voice overrides', () => {
|
|
842
|
+
const result = AssistantConfigSchema.parse({
|
|
843
|
+
calls: {
|
|
844
|
+
voice: {
|
|
845
|
+
mode: 'twilio_elevenlabs_tts',
|
|
846
|
+
language: 'es-ES',
|
|
847
|
+
transcriptionProvider: 'Google',
|
|
848
|
+
fallbackToStandardOnError: false,
|
|
849
|
+
elevenlabs: {
|
|
850
|
+
voiceId: 'abc123',
|
|
851
|
+
stability: 0.8,
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
expect(result.calls.voice.mode).toBe('twilio_elevenlabs_tts');
|
|
857
|
+
expect(result.calls.voice.language).toBe('es-ES');
|
|
858
|
+
expect(result.calls.voice.transcriptionProvider).toBe('Google');
|
|
859
|
+
expect(result.calls.voice.fallbackToStandardOnError).toBe(false);
|
|
860
|
+
expect(result.calls.voice.elevenlabs.voiceId).toBe('abc123');
|
|
861
|
+
expect(result.calls.voice.elevenlabs.stability).toBe(0.8);
|
|
862
|
+
// Defaults preserved for unset fields
|
|
863
|
+
expect(result.calls.voice.elevenlabs.voiceModelId).toBe('');
|
|
864
|
+
expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test('rejects invalid calls.voice.mode', () => {
|
|
868
|
+
const result = AssistantConfigSchema.safeParse({
|
|
869
|
+
calls: { voice: { mode: 'invalid_mode' } },
|
|
870
|
+
});
|
|
871
|
+
expect(result.success).toBe(false);
|
|
872
|
+
if (!result.success) {
|
|
873
|
+
const msgs = result.error.issues.map(i => i.message);
|
|
874
|
+
expect(msgs.some(m => m.includes('calls.voice.mode'))).toBe(true);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test('rejects invalid calls.voice.transcriptionProvider', () => {
|
|
879
|
+
const result = AssistantConfigSchema.safeParse({
|
|
880
|
+
calls: { voice: { transcriptionProvider: 'AWS' } },
|
|
881
|
+
});
|
|
882
|
+
expect(result.success).toBe(false);
|
|
883
|
+
if (!result.success) {
|
|
884
|
+
const msgs = result.error.issues.map(i => i.message);
|
|
885
|
+
expect(msgs.some(m => m.includes('calls.voice.transcriptionProvider'))).toBe(true);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('rejects calls.voice.elevenlabs.stability out of range', () => {
|
|
890
|
+
const result = AssistantConfigSchema.safeParse({
|
|
891
|
+
calls: { voice: { elevenlabs: { stability: 1.5 } } },
|
|
892
|
+
});
|
|
893
|
+
expect(result.success).toBe(false);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test('rejects calls.voice.elevenlabs.registerCallTimeoutMs below 1000', () => {
|
|
897
|
+
const result = AssistantConfigSchema.safeParse({
|
|
898
|
+
calls: { voice: { elevenlabs: { registerCallTimeoutMs: 500 } } },
|
|
899
|
+
});
|
|
900
|
+
expect(result.success).toBe(false);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
test('rejects calls.voice.elevenlabs.registerCallTimeoutMs above 15000', () => {
|
|
904
|
+
const result = AssistantConfigSchema.safeParse({
|
|
905
|
+
calls: { voice: { elevenlabs: { registerCallTimeoutMs: 20000 } } },
|
|
906
|
+
});
|
|
907
|
+
expect(result.success).toBe(false);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test('accepts optional calls.model', () => {
|
|
911
|
+
const result = AssistantConfigSchema.parse({
|
|
912
|
+
calls: { model: 'claude-haiku-4-5-20251001' },
|
|
913
|
+
});
|
|
914
|
+
expect(result.calls.model).toBe('claude-haiku-4-5-20251001');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test('calls.model is undefined by default', () => {
|
|
918
|
+
const result = AssistantConfigSchema.parse({});
|
|
919
|
+
expect(result.calls.model).toBeUndefined();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ── Caller identity config ────────────────────────────────────────
|
|
923
|
+
|
|
924
|
+
test('applies calls.callerIdentity defaults', () => {
|
|
925
|
+
const result = AssistantConfigSchema.parse({});
|
|
926
|
+
expect(result.calls.callerIdentity).toEqual({
|
|
927
|
+
allowPerCallOverride: true,
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('accepts valid calls.callerIdentity overrides', () => {
|
|
932
|
+
const result = AssistantConfigSchema.parse({
|
|
933
|
+
calls: {
|
|
934
|
+
callerIdentity: {
|
|
935
|
+
allowPerCallOverride: false,
|
|
936
|
+
userNumber: '+14155559999',
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
expect(result.calls.callerIdentity.allowPerCallOverride).toBe(false);
|
|
941
|
+
expect(result.calls.callerIdentity.userNumber).toBe('+14155559999');
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test('legacy defaultMode field is silently stripped by schema', () => {
|
|
945
|
+
// Backward compatibility: existing configs with defaultMode should parse
|
|
946
|
+
// without error — Zod strips unrecognized keys by default.
|
|
947
|
+
const result = AssistantConfigSchema.parse({
|
|
948
|
+
calls: {
|
|
949
|
+
callerIdentity: { defaultMode: 'user_number', allowPerCallOverride: true },
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
expect((result.calls.callerIdentity as Record<string, unknown>).defaultMode).toBeUndefined();
|
|
953
|
+
expect(result.calls.callerIdentity.allowPerCallOverride).toBe(true);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test('rejects non-boolean calls.callerIdentity.allowPerCallOverride', () => {
|
|
957
|
+
const result = AssistantConfigSchema.safeParse({
|
|
958
|
+
calls: { callerIdentity: { allowPerCallOverride: 'yes' } },
|
|
959
|
+
});
|
|
960
|
+
expect(result.success).toBe(false);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test('default behavior unchanged when callerIdentity omitted', () => {
|
|
964
|
+
const result = AssistantConfigSchema.parse({
|
|
965
|
+
calls: { enabled: true },
|
|
966
|
+
});
|
|
967
|
+
expect(result.calls.callerIdentity.allowPerCallOverride).toBe(true);
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// ---------------------------------------------------------------------------
|
|
972
|
+
// Tests: Voice quality profile resolver
|
|
973
|
+
// ---------------------------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
describe('resolveVoiceQualityProfile', () => {
|
|
976
|
+
test('returns correct profile for twilio_standard', () => {
|
|
977
|
+
const config = AssistantConfigSchema.parse({});
|
|
978
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
979
|
+
expect(profile.mode).toBe('twilio_standard');
|
|
980
|
+
expect(profile.ttsProvider).toBe('Google');
|
|
981
|
+
expect(profile.voice).toBe('Google.en-US-Journey-O');
|
|
982
|
+
expect(profile.transcriptionProvider).toBe('Deepgram');
|
|
983
|
+
expect(profile.fallbackToStandardOnError).toBe(true);
|
|
984
|
+
expect(profile.validationErrors).toEqual([]);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
test('returns correct profile for twilio_elevenlabs_tts with valid voiceId', () => {
|
|
988
|
+
const config = AssistantConfigSchema.parse({
|
|
989
|
+
calls: {
|
|
990
|
+
voice: {
|
|
991
|
+
mode: 'twilio_elevenlabs_tts',
|
|
992
|
+
elevenlabs: { voiceId: 'test-voice-id' },
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
997
|
+
expect(profile.mode).toBe('twilio_elevenlabs_tts');
|
|
998
|
+
expect(profile.ttsProvider).toBe('ElevenLabs');
|
|
999
|
+
expect(profile.voice).toBe('test-voice-id');
|
|
1000
|
+
expect(profile.validationErrors).toEqual([]);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test('falls back for twilio_elevenlabs_tts with empty voiceId and fallback enabled', () => {
|
|
1004
|
+
const config = AssistantConfigSchema.parse({
|
|
1005
|
+
calls: {
|
|
1006
|
+
voice: {
|
|
1007
|
+
mode: 'twilio_elevenlabs_tts',
|
|
1008
|
+
fallbackToStandardOnError: true,
|
|
1009
|
+
elevenlabs: { voiceId: '' },
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
1014
|
+
expect(profile.mode).toBe('twilio_standard');
|
|
1015
|
+
expect(profile.ttsProvider).toBe('Google');
|
|
1016
|
+
expect(profile.voice).toBe('Google.en-US-Journey-O');
|
|
1017
|
+
expect(profile.validationErrors.length).toBe(1);
|
|
1018
|
+
expect(profile.validationErrors[0]).toContain('falling back');
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test('returns errors for twilio_elevenlabs_tts with empty voiceId and fallback disabled', () => {
|
|
1022
|
+
const config = AssistantConfigSchema.parse({
|
|
1023
|
+
calls: {
|
|
1024
|
+
voice: {
|
|
1025
|
+
mode: 'twilio_elevenlabs_tts',
|
|
1026
|
+
fallbackToStandardOnError: false,
|
|
1027
|
+
elevenlabs: { voiceId: '' },
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
1032
|
+
expect(profile.mode).toBe('twilio_elevenlabs_tts');
|
|
1033
|
+
expect(profile.validationErrors.length).toBe(1);
|
|
1034
|
+
expect(profile.validationErrors[0]).toContain('voiceId is required');
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test('returns correct profile for elevenlabs_agent with valid agentId', () => {
|
|
1038
|
+
const config = AssistantConfigSchema.parse({
|
|
1039
|
+
calls: {
|
|
1040
|
+
voice: {
|
|
1041
|
+
mode: 'elevenlabs_agent',
|
|
1042
|
+
elevenlabs: { agentId: 'agent-123', voiceId: 'v1' },
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
1047
|
+
expect(profile.mode).toBe('elevenlabs_agent');
|
|
1048
|
+
expect(profile.ttsProvider).toBe('ElevenLabs');
|
|
1049
|
+
expect(profile.voice).toBe('v1');
|
|
1050
|
+
expect(profile.agentId).toBe('agent-123');
|
|
1051
|
+
expect(profile.validationErrors).toEqual([]);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test('falls back for elevenlabs_agent with empty agentId and fallback enabled', () => {
|
|
1055
|
+
const config = AssistantConfigSchema.parse({
|
|
1056
|
+
calls: {
|
|
1057
|
+
voice: {
|
|
1058
|
+
mode: 'elevenlabs_agent',
|
|
1059
|
+
fallbackToStandardOnError: true,
|
|
1060
|
+
elevenlabs: { agentId: '' },
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
1065
|
+
expect(profile.mode).toBe('twilio_standard');
|
|
1066
|
+
expect(profile.validationErrors.length).toBe(1);
|
|
1067
|
+
expect(profile.validationErrors[0]).toContain('agentId is empty');
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test('returns errors for elevenlabs_agent with empty agentId and fallback disabled', () => {
|
|
1071
|
+
const config = AssistantConfigSchema.parse({
|
|
1072
|
+
calls: {
|
|
1073
|
+
voice: {
|
|
1074
|
+
mode: 'elevenlabs_agent',
|
|
1075
|
+
fallbackToStandardOnError: false,
|
|
1076
|
+
elevenlabs: { agentId: '' },
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
const profile = resolveVoiceQualityProfile(config);
|
|
1081
|
+
expect(profile.mode).toBe('elevenlabs_agent');
|
|
1082
|
+
expect(profile.validationErrors.length).toBe(1);
|
|
1083
|
+
expect(profile.validationErrors[0]).toContain('agentId is required');
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
// Tests: buildElevenLabsVoiceSpec
|
|
1089
|
+
// ---------------------------------------------------------------------------
|
|
1090
|
+
|
|
1091
|
+
describe('buildElevenLabsVoiceSpec', () => {
|
|
1092
|
+
test('produces Twilio-compliant voice string: voiceId-model-speed_stability_similarity', () => {
|
|
1093
|
+
const spec = buildElevenLabsVoiceSpec({
|
|
1094
|
+
voiceId: 'abc123',
|
|
1095
|
+
voiceModelId: 'turbo_v2_5',
|
|
1096
|
+
speed: 1.0,
|
|
1097
|
+
stability: 0.5,
|
|
1098
|
+
similarityBoost: 0.75,
|
|
1099
|
+
});
|
|
1100
|
+
expect(spec).toBe('abc123-turbo_v2_5-1_0.5_0.75');
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test('returns empty string when voiceId is empty', () => {
|
|
1104
|
+
const spec = buildElevenLabsVoiceSpec({
|
|
1105
|
+
voiceId: '',
|
|
1106
|
+
voiceModelId: 'turbo_v2_5',
|
|
1107
|
+
speed: 1.0,
|
|
1108
|
+
stability: 0.5,
|
|
1109
|
+
similarityBoost: 0.75,
|
|
1110
|
+
});
|
|
1111
|
+
expect(spec).toBe('');
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test('formats custom parameters correctly', () => {
|
|
1115
|
+
const spec = buildElevenLabsVoiceSpec({
|
|
1116
|
+
voiceId: 'myVoice',
|
|
1117
|
+
voiceModelId: 'eleven_multilingual_v2',
|
|
1118
|
+
speed: 0.9,
|
|
1119
|
+
stability: 0.8,
|
|
1120
|
+
similarityBoost: 0.9,
|
|
1121
|
+
});
|
|
1122
|
+
expect(spec).toBe('myVoice-eleven_multilingual_v2-0.9_0.8_0.9');
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
test('default config uses a bare voiceId when no model override is set', () => {
|
|
1126
|
+
const config = AssistantConfigSchema.parse({
|
|
1127
|
+
calls: {
|
|
1128
|
+
voice: {
|
|
1129
|
+
mode: 'twilio_elevenlabs_tts',
|
|
1130
|
+
elevenlabs: { voiceId: 'test' },
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
const spec = buildElevenLabsVoiceSpec(config.calls.voice.elevenlabs);
|
|
1135
|
+
expect(spec).toBe('test');
|
|
1136
|
+
});
|
|
748
1137
|
});
|
|
749
1138
|
|
|
750
1139
|
// ---------------------------------------------------------------------------
|
|
@@ -924,10 +1313,10 @@ describe('loadConfig with schema validation', () => {
|
|
|
924
1313
|
expect(config.auditLog.retentionDays).toBe(0);
|
|
925
1314
|
});
|
|
926
1315
|
|
|
927
|
-
test('defaults permissions.mode to
|
|
1316
|
+
test('defaults permissions.mode to workspace when not specified', () => {
|
|
928
1317
|
writeConfig({});
|
|
929
1318
|
const config = loadConfig();
|
|
930
|
-
expect(config.permissions).toEqual({ mode: '
|
|
1319
|
+
expect(config.permissions).toEqual({ mode: 'workspace' });
|
|
931
1320
|
});
|
|
932
1321
|
|
|
933
1322
|
test('loads explicit permissions.mode strict', () => {
|
|
@@ -939,7 +1328,7 @@ describe('loadConfig with schema validation', () => {
|
|
|
939
1328
|
test('falls back for invalid permissions.mode', () => {
|
|
940
1329
|
writeConfig({ permissions: { mode: 'yolo' } });
|
|
941
1330
|
const config = loadConfig();
|
|
942
|
-
expect(config.permissions.mode).toBe('
|
|
1331
|
+
expect(config.permissions.mode).toBe('workspace');
|
|
943
1332
|
});
|
|
944
1333
|
|
|
945
1334
|
test('does not mutate default apiKeys when fallback config is overridden by env keys', () => {
|
|
@@ -994,6 +1383,14 @@ describe('loadConfig with schema validation', () => {
|
|
|
994
1383
|
expect(config.calls.userConsultTimeoutSeconds).toBe(120);
|
|
995
1384
|
expect(config.calls.disclosure.enabled).toBe(true);
|
|
996
1385
|
expect(config.calls.safety.denyCategories).toEqual([]);
|
|
1386
|
+
expect(config.calls.voice.mode).toBe('twilio_standard');
|
|
1387
|
+
expect(config.calls.voice.language).toBe('en-US');
|
|
1388
|
+
expect(config.calls.voice.transcriptionProvider).toBe('Deepgram');
|
|
1389
|
+
expect(config.calls.voice.elevenlabs.voiceId).toBe('');
|
|
1390
|
+
expect(config.calls.model).toBeUndefined();
|
|
1391
|
+
expect(config.calls.callerIdentity).toEqual({
|
|
1392
|
+
allowPerCallOverride: true,
|
|
1393
|
+
});
|
|
997
1394
|
});
|
|
998
1395
|
});
|
|
999
1396
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { areStatementsCoherent, computeConflictRelevance, tokenizeForConflictRelevance as _tokenizeForConflictRelevance, overlapRatio as _overlapRatio } from '../memory/conflict-intent.js';
|
|
3
|
+
|
|
4
|
+
describe('tokenizeForConflictRelevance hardening', () => {
|
|
5
|
+
test('excludes numeric-only tokens from relevance', () => {
|
|
6
|
+
const relevance = computeConflictRelevance(
|
|
7
|
+
'Check PR 5526',
|
|
8
|
+
{ existingStatement: 'Track PR 5525 for review.', candidateStatement: 'Track PR 5526 for review.' },
|
|
9
|
+
);
|
|
10
|
+
// Numeric tokens "5526" and "5525" should be excluded, so overlap is minimal
|
|
11
|
+
expect(relevance).toBeLessThan(0.5);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('excludes URL boilerplate tokens from relevance', () => {
|
|
15
|
+
const relevance = computeConflictRelevance(
|
|
16
|
+
'Check https://github.com/org/repo/pull/123',
|
|
17
|
+
{
|
|
18
|
+
existingStatement: 'Review https://github.com/org/repo/pull/456',
|
|
19
|
+
candidateStatement: 'Review https://github.com/org/repo/pull/789',
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
// URL tokens like "https", "github", "pull" should be excluded;
|
|
23
|
+
// only real content tokens like "repo" remain, keeping relevance low
|
|
24
|
+
expect(relevance).toBeLessThanOrEqual(0.5);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('URL-embedded tracking tokens are stripped, standalone usage preserved', () => {
|
|
28
|
+
// URLs containing "issue", "pull", etc. are stripped entirely before tokenizing
|
|
29
|
+
const urlRelevance = computeConflictRelevance(
|
|
30
|
+
'Check https://github.com/org/repo/issues/42',
|
|
31
|
+
{
|
|
32
|
+
existingStatement: 'Review https://github.com/org/repo/issues/10',
|
|
33
|
+
candidateStatement: 'Review https://github.com/org/repo/issues/11',
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
expect(urlRelevance).toBeLessThanOrEqual(0.5);
|
|
37
|
+
|
|
38
|
+
// Standalone "issue" is preserved as a meaningful token
|
|
39
|
+
const standaloneRelevance = computeConflictRelevance(
|
|
40
|
+
'should I file an issue?',
|
|
41
|
+
{
|
|
42
|
+
existingStatement: 'File an issue when bugs are found.',
|
|
43
|
+
candidateStatement: 'Skip filing an issue for minor bugs.',
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
expect(standaloneRelevance).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('strips scheme-less bare domain URLs from relevance', () => {
|
|
50
|
+
const relevance = computeConflictRelevance(
|
|
51
|
+
'Check github.com/org/repo/pull/123',
|
|
52
|
+
{
|
|
53
|
+
existingStatement: 'Review gitlab.com/org/repo/issues/456',
|
|
54
|
+
candidateStatement: 'Review github.com/org/repo/pull/789',
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
// Bare URLs should be stripped entirely; tokens like "pull", "issues"
|
|
58
|
+
// embedded in paths must not contribute to overlap
|
|
59
|
+
expect(relevance).toBeLessThanOrEqual(0.5);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('preserves dotted identifiers that look like file paths', () => {
|
|
63
|
+
const relevance = computeConflictRelevance(
|
|
64
|
+
'Use index.ts/runtime parser',
|
|
65
|
+
{
|
|
66
|
+
existingStatement: 'Keep index.ts/runtime approach.',
|
|
67
|
+
candidateStatement: 'Switch to config.ts/runtime approach.',
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
// File-like identifiers should NOT be stripped as URLs
|
|
71
|
+
expect(relevance).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('still computes meaningful relevance for real content tokens', () => {
|
|
75
|
+
const relevance = computeConflictRelevance(
|
|
76
|
+
'Should I use React for frontend?',
|
|
77
|
+
{
|
|
78
|
+
existingStatement: 'Use React for frontend work.',
|
|
79
|
+
candidateStatement: 'Use Vue for frontend work.',
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
// Real content tokens like "react", "frontend" should still match
|
|
83
|
+
expect(relevance).toBeGreaterThan(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('statement coherence (areStatementsCoherent)', () => {
|
|
88
|
+
test('unrelated statements are incoherent', () => {
|
|
89
|
+
expect(areStatementsCoherent(
|
|
90
|
+
'The default model for the summarize CLI is google/gemini-3-flash-preview.',
|
|
91
|
+
"User's favorite color is blue.",
|
|
92
|
+
)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('related statements are coherent', () => {
|
|
96
|
+
expect(areStatementsCoherent(
|
|
97
|
+
"User's favorite color is blue.",
|
|
98
|
+
"User's favorite color is green.",
|
|
99
|
+
)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('topically similar preferences are coherent', () => {
|
|
103
|
+
expect(areStatementsCoherent(
|
|
104
|
+
'Use React for frontend work.',
|
|
105
|
+
'Use Vue for frontend work.',
|
|
106
|
+
)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('completely disjoint technical topics are incoherent', () => {
|
|
110
|
+
expect(areStatementsCoherent(
|
|
111
|
+
'Always use PostgreSQL for database storage.',
|
|
112
|
+
'The preferred terminal font is JetBrains Mono.',
|
|
113
|
+
)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('short technical terms (3 chars) are preserved for coherence', () => {
|
|
117
|
+
// "vim" and "css" are 3 chars — should not be filtered
|
|
118
|
+
expect(areStatementsCoherent(
|
|
119
|
+
'Use Vim for editing.',
|
|
120
|
+
'Use Emacs instead of Vim.',
|
|
121
|
+
)).toBe(true);
|
|
122
|
+
|
|
123
|
+
expect(areStatementsCoherent(
|
|
124
|
+
'Use CSS grid for layouts.',
|
|
125
|
+
'Use CSS flexbox for layouts.',
|
|
126
|
+
)).toBe(true);
|
|
127
|
+
|
|
128
|
+
expect(areStatementsCoherent(
|
|
129
|
+
'Use npm for installs.',
|
|
130
|
+
'Use npm with --legacy-peer-deps.',
|
|
131
|
+
)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('short terms with no shared context are still incoherent', () => {
|
|
135
|
+
// No shared tokens at all — completely different topics
|
|
136
|
+
expect(areStatementsCoherent(
|
|
137
|
+
'Vim is the preferred editor.',
|
|
138
|
+
'CSS grid handles page layouts.',
|
|
139
|
+
)).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|