vellum 0.2.13 → 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 +113 -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 +137 -18
- 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 +62 -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 +27 -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 +4 -4
- 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 +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- 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 +142 -34
- 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 +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -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/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/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
|
-
});
|
|
@@ -139,6 +139,8 @@ describe('AssistantConfigSchema', () => {
|
|
|
139
139
|
reaskCooldownTurns: 3,
|
|
140
140
|
resolverLlmTimeoutMs: 12000,
|
|
141
141
|
relevanceThreshold: 0.3,
|
|
142
|
+
askOnIrrelevantTurns: false,
|
|
143
|
+
conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
|
|
142
144
|
});
|
|
143
145
|
});
|
|
144
146
|
|
|
@@ -149,6 +151,27 @@ describe('AssistantConfigSchema', () => {
|
|
|
149
151
|
expect(result.success).toBe(false);
|
|
150
152
|
});
|
|
151
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
|
+
|
|
152
175
|
test('applies memory.profile defaults', () => {
|
|
153
176
|
const result = AssistantConfigSchema.parse({});
|
|
154
177
|
expect(result.memory.profile).toEqual({
|
|
@@ -474,9 +497,9 @@ describe('AssistantConfigSchema', () => {
|
|
|
474
497
|
expect(result.success).toBe(false);
|
|
475
498
|
});
|
|
476
499
|
|
|
477
|
-
test('defaults permissions.mode to
|
|
500
|
+
test('defaults permissions.mode to workspace', () => {
|
|
478
501
|
const result = AssistantConfigSchema.parse({});
|
|
479
|
-
expect(result.permissions).toEqual({ mode: '
|
|
502
|
+
expect(result.permissions).toEqual({ mode: 'workspace' });
|
|
480
503
|
});
|
|
481
504
|
|
|
482
505
|
test('accepts explicit permissions.mode strict', () => {
|
|
@@ -493,6 +516,13 @@ describe('AssistantConfigSchema', () => {
|
|
|
493
516
|
expect(result.permissions.mode).toBe('legacy');
|
|
494
517
|
});
|
|
495
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
|
+
|
|
496
526
|
test('rejects invalid permissions.mode', () => {
|
|
497
527
|
const result = AssistantConfigSchema.safeParse({
|
|
498
528
|
permissions: { mode: 'permissive' },
|
|
@@ -663,16 +693,19 @@ describe('AssistantConfigSchema', () => {
|
|
|
663
693
|
fallbackToStandardOnError: true,
|
|
664
694
|
elevenlabs: {
|
|
665
695
|
voiceId: '',
|
|
666
|
-
voiceModelId: '
|
|
696
|
+
voiceModelId: '',
|
|
697
|
+
speed: 1.0,
|
|
667
698
|
stability: 0.5,
|
|
668
699
|
similarityBoost: 0.75,
|
|
669
|
-
style: 0.0,
|
|
670
700
|
useSpeakerBoost: true,
|
|
671
701
|
agentId: '',
|
|
672
702
|
apiBaseUrl: 'https://api.elevenlabs.io',
|
|
673
703
|
registerCallTimeoutMs: 5000,
|
|
674
704
|
},
|
|
675
705
|
},
|
|
706
|
+
callerIdentity: {
|
|
707
|
+
allowPerCallOverride: true,
|
|
708
|
+
},
|
|
676
709
|
});
|
|
677
710
|
});
|
|
678
711
|
|
|
@@ -773,16 +806,38 @@ describe('AssistantConfigSchema', () => {
|
|
|
773
806
|
expect(result.calls.voice.transcriptionProvider).toBe('Deepgram');
|
|
774
807
|
expect(result.calls.voice.fallbackToStandardOnError).toBe(true);
|
|
775
808
|
expect(result.calls.voice.elevenlabs.voiceId).toBe('');
|
|
776
|
-
expect(result.calls.voice.elevenlabs.voiceModelId).toBe('
|
|
809
|
+
expect(result.calls.voice.elevenlabs.voiceModelId).toBe('');
|
|
810
|
+
expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
|
|
777
811
|
expect(result.calls.voice.elevenlabs.stability).toBe(0.5);
|
|
778
812
|
expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
|
|
779
|
-
expect(result.calls.voice.elevenlabs.style).toBe(0.0);
|
|
780
813
|
expect(result.calls.voice.elevenlabs.useSpeakerBoost).toBe(true);
|
|
781
814
|
expect(result.calls.voice.elevenlabs.agentId).toBe('');
|
|
782
815
|
expect(result.calls.voice.elevenlabs.apiBaseUrl).toBe('https://api.elevenlabs.io');
|
|
783
816
|
expect(result.calls.voice.elevenlabs.registerCallTimeoutMs).toBe(5000);
|
|
784
817
|
});
|
|
785
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
|
+
|
|
786
841
|
test('accepts valid calls.voice overrides', () => {
|
|
787
842
|
const result = AssistantConfigSchema.parse({
|
|
788
843
|
calls: {
|
|
@@ -805,7 +860,7 @@ describe('AssistantConfigSchema', () => {
|
|
|
805
860
|
expect(result.calls.voice.elevenlabs.voiceId).toBe('abc123');
|
|
806
861
|
expect(result.calls.voice.elevenlabs.stability).toBe(0.8);
|
|
807
862
|
// Defaults preserved for unset fields
|
|
808
|
-
expect(result.calls.voice.elevenlabs.voiceModelId).toBe('
|
|
863
|
+
expect(result.calls.voice.elevenlabs.voiceModelId).toBe('');
|
|
809
864
|
expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
|
|
810
865
|
});
|
|
811
866
|
|
|
@@ -863,6 +918,54 @@ describe('AssistantConfigSchema', () => {
|
|
|
863
918
|
const result = AssistantConfigSchema.parse({});
|
|
864
919
|
expect(result.calls.model).toBeUndefined();
|
|
865
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
|
+
});
|
|
866
969
|
});
|
|
867
970
|
|
|
868
971
|
// ---------------------------------------------------------------------------
|
|
@@ -893,7 +996,7 @@ describe('resolveVoiceQualityProfile', () => {
|
|
|
893
996
|
const profile = resolveVoiceQualityProfile(config);
|
|
894
997
|
expect(profile.mode).toBe('twilio_elevenlabs_tts');
|
|
895
998
|
expect(profile.ttsProvider).toBe('ElevenLabs');
|
|
896
|
-
expect(profile.voice).toBe('test-voice-id
|
|
999
|
+
expect(profile.voice).toBe('test-voice-id');
|
|
897
1000
|
expect(profile.validationErrors).toEqual([]);
|
|
898
1001
|
});
|
|
899
1002
|
|
|
@@ -943,7 +1046,7 @@ describe('resolveVoiceQualityProfile', () => {
|
|
|
943
1046
|
const profile = resolveVoiceQualityProfile(config);
|
|
944
1047
|
expect(profile.mode).toBe('elevenlabs_agent');
|
|
945
1048
|
expect(profile.ttsProvider).toBe('ElevenLabs');
|
|
946
|
-
expect(profile.voice).toBe('v1
|
|
1049
|
+
expect(profile.voice).toBe('v1');
|
|
947
1050
|
expect(profile.agentId).toBe('agent-123');
|
|
948
1051
|
expect(profile.validationErrors).toEqual([]);
|
|
949
1052
|
});
|
|
@@ -986,24 +1089,24 @@ describe('resolveVoiceQualityProfile', () => {
|
|
|
986
1089
|
// ---------------------------------------------------------------------------
|
|
987
1090
|
|
|
988
1091
|
describe('buildElevenLabsVoiceSpec', () => {
|
|
989
|
-
test('produces
|
|
1092
|
+
test('produces Twilio-compliant voice string: voiceId-model-speed_stability_similarity', () => {
|
|
990
1093
|
const spec = buildElevenLabsVoiceSpec({
|
|
991
1094
|
voiceId: 'abc123',
|
|
992
1095
|
voiceModelId: 'turbo_v2_5',
|
|
1096
|
+
speed: 1.0,
|
|
993
1097
|
stability: 0.5,
|
|
994
1098
|
similarityBoost: 0.75,
|
|
995
|
-
style: 0.0,
|
|
996
1099
|
});
|
|
997
|
-
expect(spec).toBe('abc123-turbo_v2_5-
|
|
1100
|
+
expect(spec).toBe('abc123-turbo_v2_5-1_0.5_0.75');
|
|
998
1101
|
});
|
|
999
1102
|
|
|
1000
1103
|
test('returns empty string when voiceId is empty', () => {
|
|
1001
1104
|
const spec = buildElevenLabsVoiceSpec({
|
|
1002
1105
|
voiceId: '',
|
|
1003
1106
|
voiceModelId: 'turbo_v2_5',
|
|
1107
|
+
speed: 1.0,
|
|
1004
1108
|
stability: 0.5,
|
|
1005
1109
|
similarityBoost: 0.75,
|
|
1006
|
-
style: 0.0,
|
|
1007
1110
|
});
|
|
1008
1111
|
expect(spec).toBe('');
|
|
1009
1112
|
});
|
|
@@ -1012,11 +1115,24 @@ describe('buildElevenLabsVoiceSpec', () => {
|
|
|
1012
1115
|
const spec = buildElevenLabsVoiceSpec({
|
|
1013
1116
|
voiceId: 'myVoice',
|
|
1014
1117
|
voiceModelId: 'eleven_multilingual_v2',
|
|
1118
|
+
speed: 0.9,
|
|
1015
1119
|
stability: 0.8,
|
|
1016
1120
|
similarityBoost: 0.9,
|
|
1017
|
-
style: 0.3,
|
|
1018
1121
|
});
|
|
1019
|
-
expect(spec).toBe('myVoice-eleven_multilingual_v2-0.8_0.
|
|
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');
|
|
1020
1136
|
});
|
|
1021
1137
|
});
|
|
1022
1138
|
|
|
@@ -1197,10 +1313,10 @@ describe('loadConfig with schema validation', () => {
|
|
|
1197
1313
|
expect(config.auditLog.retentionDays).toBe(0);
|
|
1198
1314
|
});
|
|
1199
1315
|
|
|
1200
|
-
test('defaults permissions.mode to
|
|
1316
|
+
test('defaults permissions.mode to workspace when not specified', () => {
|
|
1201
1317
|
writeConfig({});
|
|
1202
1318
|
const config = loadConfig();
|
|
1203
|
-
expect(config.permissions).toEqual({ mode: '
|
|
1319
|
+
expect(config.permissions).toEqual({ mode: 'workspace' });
|
|
1204
1320
|
});
|
|
1205
1321
|
|
|
1206
1322
|
test('loads explicit permissions.mode strict', () => {
|
|
@@ -1212,7 +1328,7 @@ describe('loadConfig with schema validation', () => {
|
|
|
1212
1328
|
test('falls back for invalid permissions.mode', () => {
|
|
1213
1329
|
writeConfig({ permissions: { mode: 'yolo' } });
|
|
1214
1330
|
const config = loadConfig();
|
|
1215
|
-
expect(config.permissions.mode).toBe('
|
|
1331
|
+
expect(config.permissions.mode).toBe('workspace');
|
|
1216
1332
|
});
|
|
1217
1333
|
|
|
1218
1334
|
test('does not mutate default apiKeys when fallback config is overridden by env keys', () => {
|
|
@@ -1272,6 +1388,9 @@ describe('loadConfig with schema validation', () => {
|
|
|
1272
1388
|
expect(config.calls.voice.transcriptionProvider).toBe('Deepgram');
|
|
1273
1389
|
expect(config.calls.voice.elevenlabs.voiceId).toBe('');
|
|
1274
1390
|
expect(config.calls.model).toBeUndefined();
|
|
1391
|
+
expect(config.calls.callerIdentity).toEqual({
|
|
1392
|
+
allowPerCallOverride: true,
|
|
1393
|
+
});
|
|
1275
1394
|
});
|
|
1276
1395
|
});
|
|
1277
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
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
isConflictKindEligible,
|
|
4
|
+
isConflictKindPairEligible,
|
|
5
|
+
isTransientTrackingStatement,
|
|
6
|
+
isDurableInstructionStatement,
|
|
7
|
+
isStatementConflictEligible,
|
|
8
|
+
} from '../memory/conflict-policy.js';
|
|
9
|
+
|
|
10
|
+
describe('conflict-policy', () => {
|
|
11
|
+
const config = { conflictableKinds: ['preference', 'profile', 'constraint'] };
|
|
12
|
+
|
|
13
|
+
describe('isConflictKindEligible', () => {
|
|
14
|
+
test('returns true for eligible kind', () => {
|
|
15
|
+
expect(isConflictKindEligible('preference', config)).toBe(true);
|
|
16
|
+
expect(isConflictKindEligible('profile', config)).toBe(true);
|
|
17
|
+
expect(isConflictKindEligible('constraint', config)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns false for ineligible kind', () => {
|
|
21
|
+
expect(isConflictKindEligible('project', config)).toBe(false);
|
|
22
|
+
expect(isConflictKindEligible('todo', config)).toBe(false);
|
|
23
|
+
expect(isConflictKindEligible('fact', config)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('isConflictKindPairEligible', () => {
|
|
28
|
+
test('returns true when both kinds are eligible', () => {
|
|
29
|
+
expect(isConflictKindPairEligible('preference', 'profile', config)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('returns false when existing kind is ineligible', () => {
|
|
33
|
+
expect(isConflictKindPairEligible('project', 'preference', config)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('returns false when candidate kind is ineligible', () => {
|
|
37
|
+
expect(isConflictKindPairEligible('preference', 'todo', config)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('returns false when both kinds are ineligible', () => {
|
|
41
|
+
expect(isConflictKindPairEligible('project', 'todo', config)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('isTransientTrackingStatement', () => {
|
|
46
|
+
test('detects PR URLs', () => {
|
|
47
|
+
expect(isTransientTrackingStatement('Track https://github.com/org/repo/pull/5526')).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('detects issue/ticket references', () => {
|
|
51
|
+
expect(isTransientTrackingStatement('Track PR #5526 and #5525')).toBe(true);
|
|
52
|
+
expect(isTransientTrackingStatement('See issue #42 for details')).toBe(true);
|
|
53
|
+
expect(isTransientTrackingStatement('Filed ticket 1234')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('detects tracking language', () => {
|
|
57
|
+
expect(isTransientTrackingStatement('While we wait for CI to pass')).toBe(true);
|
|
58
|
+
expect(isTransientTrackingStatement('This PR needs review')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('does not flag generic time words as transient', () => {
|
|
62
|
+
expect(isTransientTrackingStatement('The deadline is today')).toBe(false);
|
|
63
|
+
expect(isTransientTrackingStatement('I need this right now')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('does not flag durable statements', () => {
|
|
67
|
+
expect(isTransientTrackingStatement('Always answer with concise bullet points')).toBe(false);
|
|
68
|
+
expect(isTransientTrackingStatement('User prefers dark mode')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('does not false-positive on non-PR URLs', () => {
|
|
72
|
+
expect(isTransientTrackingStatement('Visit https://example.com for docs')).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('isDurableInstructionStatement', () => {
|
|
77
|
+
test('detects durable instruction cues', () => {
|
|
78
|
+
expect(isDurableInstructionStatement('Always answer with concise bullet points')).toBe(true);
|
|
79
|
+
expect(isDurableInstructionStatement('Never use semicolons in JavaScript')).toBe(true);
|
|
80
|
+
expect(isDurableInstructionStatement('Use concise format for status updates')).toBe(true);
|
|
81
|
+
expect(isDurableInstructionStatement('The default database is Postgres')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('rejects statements without durable cues', () => {
|
|
85
|
+
expect(isDurableInstructionStatement('Check the build output')).toBe(false);
|
|
86
|
+
expect(isDurableInstructionStatement('Run the migration script')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('isStatementConflictEligible', () => {
|
|
91
|
+
test('rejects transient statements for any kind', () => {
|
|
92
|
+
expect(isStatementConflictEligible('preference', 'Track PR #5526')).toBe(false);
|
|
93
|
+
expect(isStatementConflictEligible('instruction', 'This PR needs review')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('accepts durable instruction statements', () => {
|
|
97
|
+
expect(isStatementConflictEligible('instruction', 'Always use TypeScript strict mode')).toBe(true);
|
|
98
|
+
expect(isStatementConflictEligible('style', 'Default to concise format')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('rejects non-durable instruction statements', () => {
|
|
102
|
+
expect(isStatementConflictEligible('instruction', 'Run the build first')).toBe(false);
|
|
103
|
+
expect(isStatementConflictEligible('style', 'Check the output')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('accepts non-transient statements for non-instruction kinds', () => {
|
|
107
|
+
expect(isStatementConflictEligible('preference', 'User prefers dark mode')).toBe(true);
|
|
108
|
+
expect(isStatementConflictEligible('fact', 'User works at Acme Corp')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('rejects kinds not in conflictableKinds when config is provided', () => {
|
|
112
|
+
const policyConfig = { conflictableKinds: ['preference', 'profile'] };
|
|
113
|
+
expect(isStatementConflictEligible('fact', 'User works at Acme Corp', policyConfig)).toBe(false);
|
|
114
|
+
expect(isStatementConflictEligible('preference', 'User prefers dark mode', policyConfig)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('skips kind check when config is omitted', () => {
|
|
118
|
+
expect(isStatementConflictEligible('fact', 'User works at Acme Corp')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -233,6 +233,8 @@ describe('conflict-store', () => {
|
|
|
233
233
|
expect(details).toHaveLength(1);
|
|
234
234
|
expect(details[0].existingStatement).toBe('Existing statement details');
|
|
235
235
|
expect(details[0].candidateStatement).toBe('Candidate statement details');
|
|
236
|
+
expect(details[0].existingKind).toBe('fact');
|
|
237
|
+
expect(details[0].candidateKind).toBe('fact');
|
|
236
238
|
});
|
|
237
239
|
|
|
238
240
|
test('applyConflictResolution keeps candidate and resolves conflict row', () => {
|
|
@@ -32,9 +32,9 @@ mock.module('../config/loader.js', () => ({
|
|
|
32
32
|
import type { Database } from 'bun:sqlite';
|
|
33
33
|
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
34
34
|
import type { ToolContext } from '../tools/types.js';
|
|
35
|
-
import { executeContactUpsert } from '../
|
|
36
|
-
import { executeContactSearch } from '../
|
|
37
|
-
import { executeContactMerge } from '../
|
|
35
|
+
import { executeContactUpsert } from '../config/bundled-skills/contacts/tools/contact-upsert.js';
|
|
36
|
+
import { executeContactSearch } from '../config/bundled-skills/contacts/tools/contact-search.js';
|
|
37
|
+
import { executeContactMerge } from '../config/bundled-skills/contacts/tools/contact-merge.js';
|
|
38
38
|
|
|
39
39
|
initializeDb();
|
|
40
40
|
|