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
|
@@ -8,6 +8,8 @@ let resolverCallCount = 0;
|
|
|
8
8
|
let markAskedCalls: string[] = [];
|
|
9
9
|
let conflictScopeCalls: string[] = [];
|
|
10
10
|
let memoryEnabled = true;
|
|
11
|
+
let askOnIrrelevantTurns = false;
|
|
12
|
+
let resolveConflictCalls: Array<{ id: string; input: { status: string; resolutionNote?: string | null } }> = [];
|
|
11
13
|
let pendingConflicts: Array<{
|
|
12
14
|
id: string;
|
|
13
15
|
scopeId: string;
|
|
@@ -23,6 +25,8 @@ let pendingConflicts: Array<{
|
|
|
23
25
|
updatedAt: number;
|
|
24
26
|
existingStatement: string;
|
|
25
27
|
candidateStatement: string;
|
|
28
|
+
existingKind: string;
|
|
29
|
+
candidateKind: string;
|
|
26
30
|
}> = [];
|
|
27
31
|
|
|
28
32
|
let resolverResult: {
|
|
@@ -96,6 +100,8 @@ mock.module('../config/loader.js', () => ({
|
|
|
96
100
|
reaskCooldownTurns: 3,
|
|
97
101
|
resolverLlmTimeoutMs: 250,
|
|
98
102
|
relevanceThreshold: 0.2,
|
|
103
|
+
askOnIrrelevantTurns,
|
|
104
|
+
conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
|
|
99
105
|
},
|
|
100
106
|
},
|
|
101
107
|
}),
|
|
@@ -212,6 +218,16 @@ mock.module('../memory/conflict-store.js', () => ({
|
|
|
212
218
|
return true;
|
|
213
219
|
},
|
|
214
220
|
applyConflictResolution: () => true,
|
|
221
|
+
resolveConflict: (id: string, input: { status: string; resolutionNote?: string | null }) => {
|
|
222
|
+
resolveConflictCalls.push({ id, input });
|
|
223
|
+
// Remove dismissed conflicts so the second listPendingConflictDetails call
|
|
224
|
+
// reflects the dismissal (mirrors real DB behavior).
|
|
225
|
+
if (input.status === 'dismissed') {
|
|
226
|
+
const idx = pendingConflicts.findIndex((c) => c.id === id);
|
|
227
|
+
if (idx !== -1) pendingConflicts.splice(idx, 1);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
},
|
|
215
231
|
}));
|
|
216
232
|
|
|
217
233
|
mock.module('../memory/clarification-resolver.js', () => ({
|
|
@@ -249,7 +265,7 @@ mock.module('../agent/loop.js', () => ({
|
|
|
249
265
|
}));
|
|
250
266
|
|
|
251
267
|
import { Session, type SessionMemoryPolicy } from '../daemon/session.js';
|
|
252
|
-
import { looksLikeClarificationReply } from '../daemon/session-conflict-gate.js';
|
|
268
|
+
import { ConflictGate, looksLikeClarificationReply } from '../daemon/session-conflict-gate.js';
|
|
253
269
|
|
|
254
270
|
function makeSession(memoryPolicy?: SessionMemoryPolicy): Session {
|
|
255
271
|
const provider = {
|
|
@@ -279,7 +295,9 @@ describe('Session conflict soft gate', () => {
|
|
|
279
295
|
resolverCallCount = 0;
|
|
280
296
|
markAskedCalls = [];
|
|
281
297
|
conflictScopeCalls = [];
|
|
298
|
+
resolveConflictCalls = [];
|
|
282
299
|
memoryEnabled = true;
|
|
300
|
+
askOnIrrelevantTurns = false;
|
|
283
301
|
pendingConflicts = [];
|
|
284
302
|
persistedMessages.length = 0;
|
|
285
303
|
resolverResult = {
|
|
@@ -306,6 +324,8 @@ describe('Session conflict soft gate', () => {
|
|
|
306
324
|
updatedAt: 1,
|
|
307
325
|
existingStatement: 'Use React for frontend work.',
|
|
308
326
|
candidateStatement: 'Use Vue for frontend work.',
|
|
327
|
+
existingKind: 'preference',
|
|
328
|
+
candidateKind: 'preference',
|
|
309
329
|
}];
|
|
310
330
|
|
|
311
331
|
const session = makeSession();
|
|
@@ -325,9 +345,9 @@ describe('Session conflict soft gate', () => {
|
|
|
325
345
|
expect(events.some((event) => event.type === 'message_complete')).toBe(true);
|
|
326
346
|
});
|
|
327
347
|
|
|
328
|
-
test('irrelevant unresolved conflict does not inject side-question
|
|
348
|
+
test('irrelevant unresolved conflict does not inject side-question when askOnIrrelevantTurns is false (default)', async () => {
|
|
329
349
|
pendingConflicts = [{
|
|
330
|
-
id: 'conflict-irrelevant',
|
|
350
|
+
id: 'conflict-irrelevant-silent',
|
|
331
351
|
scopeId: 'default',
|
|
332
352
|
existingItemId: 'existing-b',
|
|
333
353
|
candidateItemId: 'candidate-b',
|
|
@@ -341,6 +361,8 @@ describe('Session conflict soft gate', () => {
|
|
|
341
361
|
updatedAt: 1,
|
|
342
362
|
existingStatement: 'Use Postgres as the default database.',
|
|
343
363
|
candidateStatement: 'Use MySQL as the default database.',
|
|
364
|
+
existingKind: 'preference',
|
|
365
|
+
candidateKind: 'preference',
|
|
344
366
|
}];
|
|
345
367
|
const session = makeSession();
|
|
346
368
|
await session.loadFromDb();
|
|
@@ -348,17 +370,57 @@ describe('Session conflict soft gate', () => {
|
|
|
348
370
|
const events: ServerMessage[] = [];
|
|
349
371
|
await session.processMessage('How do I set up pre-commit hooks?', [], (event) => events.push(event));
|
|
350
372
|
|
|
373
|
+
// Agent loop runs without conflict side-question injection
|
|
351
374
|
expect(runCalls).toHaveLength(1);
|
|
352
375
|
const injectedUser = runCalls[0][runCalls[0].length - 1];
|
|
353
376
|
expect(injectedUser.role).toBe('user');
|
|
354
377
|
const injectedText = extractText(injectedUser);
|
|
355
378
|
expect(injectedText).not.toContain('Memory clarification request');
|
|
356
|
-
expect(injectedText).not.toContain('Should I assume Postgres or MySQL?');
|
|
357
379
|
expect(resolverCallCount).toBe(0);
|
|
358
380
|
expect(markAskedCalls).toEqual([]);
|
|
359
381
|
expect(events.some((event) => event.type === 'message_complete')).toBe(true);
|
|
360
382
|
});
|
|
361
383
|
|
|
384
|
+
test('irrelevant unresolved conflict injects soft clarification when askOnIrrelevantTurns is explicitly true', async () => {
|
|
385
|
+
askOnIrrelevantTurns = true;
|
|
386
|
+
pendingConflicts = [{
|
|
387
|
+
id: 'conflict-irrelevant',
|
|
388
|
+
scopeId: 'default',
|
|
389
|
+
existingItemId: 'existing-b',
|
|
390
|
+
candidateItemId: 'candidate-b',
|
|
391
|
+
relationship: 'ambiguous_contradiction',
|
|
392
|
+
status: 'pending_clarification',
|
|
393
|
+
clarificationQuestion: 'Should I assume Postgres or MySQL?',
|
|
394
|
+
resolutionNote: null,
|
|
395
|
+
lastAskedAt: null,
|
|
396
|
+
resolvedAt: null,
|
|
397
|
+
createdAt: 1,
|
|
398
|
+
updatedAt: 1,
|
|
399
|
+
existingStatement: 'Use Postgres as the default database.',
|
|
400
|
+
candidateStatement: 'Use MySQL as the default database.',
|
|
401
|
+
existingKind: 'preference',
|
|
402
|
+
candidateKind: 'preference',
|
|
403
|
+
}];
|
|
404
|
+
const session = makeSession();
|
|
405
|
+
await session.loadFromDb();
|
|
406
|
+
|
|
407
|
+
const events: ServerMessage[] = [];
|
|
408
|
+
await session.processMessage('How do I set up pre-commit hooks?', [], (event) => events.push(event));
|
|
409
|
+
|
|
410
|
+
// Agent loop still runs (soft ask, not a hard block)
|
|
411
|
+
expect(runCalls).toHaveLength(1);
|
|
412
|
+
const injectedUser = runCalls[0][runCalls[0].length - 1];
|
|
413
|
+
expect(injectedUser.role).toBe('user');
|
|
414
|
+
const injectedText = extractText(injectedUser);
|
|
415
|
+
// With askOnIrrelevantTurns=true, the irrelevant conflict is soft-injected
|
|
416
|
+
expect(injectedText).toContain('Memory clarification request');
|
|
417
|
+
expect(injectedText).toContain('Should I assume Postgres or MySQL?');
|
|
418
|
+
expect(resolverCallCount).toBe(0);
|
|
419
|
+
// Zero-relevance conflicts are surfaced but not tracked as asked
|
|
420
|
+
expect(markAskedCalls).toEqual([]);
|
|
421
|
+
expect(events.some((event) => event.type === 'message_complete')).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
362
424
|
test('recently asked conflicts still resolve directional clarification replies', async () => {
|
|
363
425
|
pendingConflicts = [{
|
|
364
426
|
id: 'conflict-followup',
|
|
@@ -375,6 +437,8 @@ describe('Session conflict soft gate', () => {
|
|
|
375
437
|
updatedAt: 1,
|
|
376
438
|
existingStatement: 'Use Postgres as the default database.',
|
|
377
439
|
candidateStatement: 'Use MySQL as the default database.',
|
|
440
|
+
existingKind: 'preference',
|
|
441
|
+
candidateKind: 'preference',
|
|
378
442
|
}];
|
|
379
443
|
|
|
380
444
|
const session = makeSession();
|
|
@@ -416,6 +480,8 @@ describe('Session conflict soft gate', () => {
|
|
|
416
480
|
updatedAt: 1,
|
|
417
481
|
existingStatement: 'Use Postgres as the default database.',
|
|
418
482
|
candidateStatement: 'Use MySQL as the default database.',
|
|
483
|
+
existingKind: 'preference',
|
|
484
|
+
candidateKind: 'preference',
|
|
419
485
|
}];
|
|
420
486
|
|
|
421
487
|
const session = makeSession();
|
|
@@ -456,6 +522,8 @@ describe('Session conflict soft gate', () => {
|
|
|
456
522
|
updatedAt: 1,
|
|
457
523
|
existingStatement: 'Use Postgres as the default database.',
|
|
458
524
|
candidateStatement: 'Use MySQL as the default database.',
|
|
525
|
+
existingKind: 'preference',
|
|
526
|
+
candidateKind: 'preference',
|
|
459
527
|
}];
|
|
460
528
|
|
|
461
529
|
const session = makeSession();
|
|
@@ -498,6 +566,8 @@ describe('Session conflict soft gate', () => {
|
|
|
498
566
|
updatedAt: 1,
|
|
499
567
|
existingStatement: 'Use Postgres as the default database.',
|
|
500
568
|
candidateStatement: 'Use MySQL as the default database.',
|
|
569
|
+
existingKind: 'preference',
|
|
570
|
+
candidateKind: 'preference',
|
|
501
571
|
}];
|
|
502
572
|
|
|
503
573
|
const session = makeSession();
|
|
@@ -523,9 +593,9 @@ describe('Session conflict soft gate', () => {
|
|
|
523
593
|
expect(runCalls).toHaveLength(1);
|
|
524
594
|
});
|
|
525
595
|
|
|
526
|
-
test('irrelevant conflicts remain silent across subsequent turns', async () => {
|
|
596
|
+
test('irrelevant conflicts remain silent across subsequent turns when askOnIrrelevantTurns is false (default)', async () => {
|
|
527
597
|
pendingConflicts = [{
|
|
528
|
-
id: 'conflict-
|
|
598
|
+
id: 'conflict-silent-multi',
|
|
529
599
|
scopeId: 'default',
|
|
530
600
|
existingItemId: 'existing-c',
|
|
531
601
|
candidateItemId: 'candidate-c',
|
|
@@ -539,6 +609,8 @@ describe('Session conflict soft gate', () => {
|
|
|
539
609
|
updatedAt: 1,
|
|
540
610
|
existingStatement: 'Use pnpm for workspace installs.',
|
|
541
611
|
candidateStatement: 'Use npm for workspace installs.',
|
|
612
|
+
existingKind: 'preference',
|
|
613
|
+
candidateKind: 'preference',
|
|
542
614
|
}];
|
|
543
615
|
|
|
544
616
|
const session = makeSession();
|
|
@@ -550,11 +622,52 @@ describe('Session conflict soft gate', () => {
|
|
|
550
622
|
expect(runCalls).toHaveLength(2);
|
|
551
623
|
const firstUserText = extractText(runCalls[0][runCalls[0].length - 1]);
|
|
552
624
|
const secondUserText = extractText(runCalls[1][runCalls[1].length - 1]);
|
|
625
|
+
// Both turns: no soft injection because askOnIrrelevantTurns=false
|
|
553
626
|
expect(firstUserText).not.toContain('Memory clarification request');
|
|
554
627
|
expect(secondUserText).not.toContain('Memory clarification request');
|
|
555
628
|
expect(markAskedCalls).toEqual([]);
|
|
556
629
|
});
|
|
557
630
|
|
|
631
|
+
test('zero-relevance conflict is soft-asked on every turn (not tracked) when askOnIrrelevantTurns is explicitly true', async () => {
|
|
632
|
+
askOnIrrelevantTurns = true;
|
|
633
|
+
pendingConflicts = [{
|
|
634
|
+
id: 'conflict-cooldown',
|
|
635
|
+
scopeId: 'default',
|
|
636
|
+
existingItemId: 'existing-c',
|
|
637
|
+
candidateItemId: 'candidate-c',
|
|
638
|
+
relationship: 'ambiguous_contradiction',
|
|
639
|
+
status: 'pending_clarification',
|
|
640
|
+
clarificationQuestion: 'Should I use pnpm or npm?',
|
|
641
|
+
resolutionNote: null,
|
|
642
|
+
lastAskedAt: null,
|
|
643
|
+
resolvedAt: null,
|
|
644
|
+
createdAt: 1,
|
|
645
|
+
updatedAt: 1,
|
|
646
|
+
existingStatement: 'Use pnpm for workspace installs.',
|
|
647
|
+
candidateStatement: 'Use npm for workspace installs.',
|
|
648
|
+
existingKind: 'preference',
|
|
649
|
+
candidateKind: 'preference',
|
|
650
|
+
}];
|
|
651
|
+
|
|
652
|
+
const session = makeSession();
|
|
653
|
+
await session.loadFromDb();
|
|
654
|
+
|
|
655
|
+
await session.processMessage('How should I structure my repo?', [], () => {});
|
|
656
|
+
await session.processMessage('What branch naming should I use?', [], () => {});
|
|
657
|
+
|
|
658
|
+
expect(runCalls).toHaveLength(2);
|
|
659
|
+
const firstUserText = extractText(runCalls[0][runCalls[0].length - 1]);
|
|
660
|
+
const secondUserText = extractText(runCalls[1][runCalls[1].length - 1]);
|
|
661
|
+
// First turn: askOnIrrelevantTurns=true causes soft injection
|
|
662
|
+
expect(firstUserText).toContain('Memory clarification request');
|
|
663
|
+
// Second turn: cooldown prevents re-asking (but since relevance is 0,
|
|
664
|
+
// the first ask was not tracked, so cooldown doesn't apply — the conflict
|
|
665
|
+
// is surfaced again on the second turn too)
|
|
666
|
+
expect(secondUserText).toContain('Memory clarification request');
|
|
667
|
+
// Zero-relevance conflicts are never tracked as asked
|
|
668
|
+
expect(markAskedCalls).toEqual([]);
|
|
669
|
+
});
|
|
670
|
+
|
|
558
671
|
test('passes session scopeId through to conflict store queries', async () => {
|
|
559
672
|
pendingConflicts = [{
|
|
560
673
|
id: 'conflict-scoped',
|
|
@@ -571,6 +684,8 @@ describe('Session conflict soft gate', () => {
|
|
|
571
684
|
updatedAt: 1,
|
|
572
685
|
existingStatement: 'Use tabs for indentation.',
|
|
573
686
|
candidateStatement: 'Use spaces for indentation.',
|
|
687
|
+
existingKind: 'preference',
|
|
688
|
+
candidateKind: 'preference',
|
|
574
689
|
}];
|
|
575
690
|
|
|
576
691
|
const session = makeSession({
|
|
@@ -618,6 +733,8 @@ describe('Session conflict soft gate', () => {
|
|
|
618
733
|
updatedAt: 1,
|
|
619
734
|
existingStatement: 'Use React for frontend work.',
|
|
620
735
|
candidateStatement: 'Use Vue for frontend work.',
|
|
736
|
+
existingKind: 'preference',
|
|
737
|
+
candidateKind: 'preference',
|
|
621
738
|
}];
|
|
622
739
|
|
|
623
740
|
const session = makeSession();
|
|
@@ -631,6 +748,117 @@ describe('Session conflict soft gate', () => {
|
|
|
631
748
|
expect(resolverCallCount).toBe(0);
|
|
632
749
|
expect(markAskedCalls).toEqual([]);
|
|
633
750
|
});
|
|
751
|
+
|
|
752
|
+
test('pending transient conflict is dismissed and not asked', async () => {
|
|
753
|
+
pendingConflicts = [{
|
|
754
|
+
id: 'conflict-transient',
|
|
755
|
+
scopeId: 'default',
|
|
756
|
+
existingItemId: 'existing-transient',
|
|
757
|
+
candidateItemId: 'candidate-transient',
|
|
758
|
+
relationship: 'ambiguous_contradiction',
|
|
759
|
+
status: 'pending_clarification',
|
|
760
|
+
clarificationQuestion: 'Which PR should we track?',
|
|
761
|
+
resolutionNote: null,
|
|
762
|
+
lastAskedAt: null,
|
|
763
|
+
resolvedAt: null,
|
|
764
|
+
createdAt: 1,
|
|
765
|
+
updatedAt: 1,
|
|
766
|
+
existingStatement: 'Track PR #5526 for review.',
|
|
767
|
+
candidateStatement: 'Track PR #5525 for review.',
|
|
768
|
+
existingKind: 'instruction',
|
|
769
|
+
candidateKind: 'instruction',
|
|
770
|
+
}];
|
|
771
|
+
|
|
772
|
+
const session = makeSession();
|
|
773
|
+
await session.loadFromDb();
|
|
774
|
+
|
|
775
|
+
const events: ServerMessage[] = [];
|
|
776
|
+
await session.processMessage('Check latest PRs', [], (event) => events.push(event));
|
|
777
|
+
|
|
778
|
+
// Should run normal agent loop, no clarification asked
|
|
779
|
+
expect(runCalls).toHaveLength(1);
|
|
780
|
+
expect(markAskedCalls).toEqual([]);
|
|
781
|
+
// The conflict should have been dismissed
|
|
782
|
+
expect(resolveConflictCalls).toEqual([{
|
|
783
|
+
id: 'conflict-transient',
|
|
784
|
+
input: {
|
|
785
|
+
status: 'dismissed',
|
|
786
|
+
resolutionNote: 'Dismissed by conflict policy (transient/non-durable).',
|
|
787
|
+
},
|
|
788
|
+
}]);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test('incoherent conflict (zero statement overlap) is dismissed', async () => {
|
|
792
|
+
pendingConflicts = [{
|
|
793
|
+
id: 'conflict-incoherent',
|
|
794
|
+
scopeId: 'default',
|
|
795
|
+
existingItemId: 'existing-incoherent',
|
|
796
|
+
candidateItemId: 'candidate-incoherent',
|
|
797
|
+
relationship: 'ambiguous_contradiction',
|
|
798
|
+
status: 'pending_clarification',
|
|
799
|
+
clarificationQuestion: 'I have conflicting notes: "The default model for the summarize CLI is google/gemini-3-flash-preview" vs "User\'s favorite color is blue." Which one is correct?',
|
|
800
|
+
resolutionNote: null,
|
|
801
|
+
lastAskedAt: null,
|
|
802
|
+
resolvedAt: null,
|
|
803
|
+
createdAt: 1,
|
|
804
|
+
updatedAt: 1,
|
|
805
|
+
existingStatement: 'The default model for the summarize CLI is google/gemini-3-flash-preview.',
|
|
806
|
+
candidateStatement: "User's favorite color is blue.",
|
|
807
|
+
existingKind: 'preference',
|
|
808
|
+
candidateKind: 'preference',
|
|
809
|
+
}];
|
|
810
|
+
|
|
811
|
+
const session = makeSession();
|
|
812
|
+
await session.loadFromDb();
|
|
813
|
+
|
|
814
|
+
const events: ServerMessage[] = [];
|
|
815
|
+
await session.processMessage('my favorite color is white', [], (event) => events.push(event));
|
|
816
|
+
|
|
817
|
+
// Should run normal agent loop, no clarification asked
|
|
818
|
+
expect(runCalls).toHaveLength(1);
|
|
819
|
+
expect(markAskedCalls).toEqual([]);
|
|
820
|
+
// The conflict should have been dismissed as incoherent
|
|
821
|
+
expect(resolveConflictCalls).toEqual([{
|
|
822
|
+
id: 'conflict-incoherent',
|
|
823
|
+
input: {
|
|
824
|
+
status: 'dismissed',
|
|
825
|
+
resolutionNote: 'Dismissed by conflict policy (incoherent — zero statement overlap).',
|
|
826
|
+
},
|
|
827
|
+
}]);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test('pending durable preference conflict still follows normal flow', async () => {
|
|
831
|
+
pendingConflicts = [{
|
|
832
|
+
id: 'conflict-durable',
|
|
833
|
+
scopeId: 'default',
|
|
834
|
+
existingItemId: 'existing-durable',
|
|
835
|
+
candidateItemId: 'candidate-durable',
|
|
836
|
+
relationship: 'ambiguous_contradiction',
|
|
837
|
+
status: 'pending_clarification',
|
|
838
|
+
clarificationQuestion: 'Do you want React or Vue?',
|
|
839
|
+
resolutionNote: null,
|
|
840
|
+
lastAskedAt: null,
|
|
841
|
+
resolvedAt: null,
|
|
842
|
+
createdAt: 1,
|
|
843
|
+
updatedAt: 1,
|
|
844
|
+
existingStatement: 'Use React for frontend work.',
|
|
845
|
+
candidateStatement: 'Use Vue for frontend work.',
|
|
846
|
+
existingKind: 'preference',
|
|
847
|
+
candidateKind: 'preference',
|
|
848
|
+
}];
|
|
849
|
+
|
|
850
|
+
const session = makeSession();
|
|
851
|
+
await session.loadFromDb();
|
|
852
|
+
|
|
853
|
+
const events: ServerMessage[] = [];
|
|
854
|
+
await session.processMessage('Should I use React or Vue?', [], (event) => events.push(event));
|
|
855
|
+
|
|
856
|
+
// Should ask clarification for relevant durable conflict
|
|
857
|
+
expect(runCalls).toHaveLength(0);
|
|
858
|
+
expect(markAskedCalls).toEqual(['conflict-durable']);
|
|
859
|
+
// No dismissal should have happened
|
|
860
|
+
expect(resolveConflictCalls).toEqual([]);
|
|
861
|
+
});
|
|
634
862
|
});
|
|
635
863
|
|
|
636
864
|
describe('looksLikeClarificationReply', () => {
|
|
@@ -698,3 +926,211 @@ describe('looksLikeClarificationReply', () => {
|
|
|
698
926
|
expect(looksLikeClarificationReply('sounds good')).toBe(false);
|
|
699
927
|
});
|
|
700
928
|
});
|
|
929
|
+
|
|
930
|
+
describe('ConflictGate askOnIrrelevantTurns knob', () => {
|
|
931
|
+
|
|
932
|
+
const baseConfig = {
|
|
933
|
+
enabled: true,
|
|
934
|
+
gateMode: 'soft' as const,
|
|
935
|
+
relevanceThreshold: 0.2,
|
|
936
|
+
reaskCooldownTurns: 3,
|
|
937
|
+
resolverLlmTimeoutMs: 250,
|
|
938
|
+
conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'] as readonly string[],
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
beforeEach(() => {
|
|
942
|
+
markAskedCalls = [];
|
|
943
|
+
pendingConflicts = [];
|
|
944
|
+
resolveConflictCalls = [];
|
|
945
|
+
resolverCallCount = 0;
|
|
946
|
+
resolverResult = {
|
|
947
|
+
resolution: 'still_unclear',
|
|
948
|
+
strategy: 'heuristic',
|
|
949
|
+
resolvedStatement: null,
|
|
950
|
+
explanation: 'Need user clarification.',
|
|
951
|
+
};
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test('with askOnIrrelevantTurns=false, irrelevant conflict is not asked', async () => {
|
|
955
|
+
pendingConflicts = [{
|
|
956
|
+
id: 'conflict-irrel-false',
|
|
957
|
+
scopeId: 'default',
|
|
958
|
+
existingItemId: 'existing-irrel',
|
|
959
|
+
candidateItemId: 'candidate-irrel',
|
|
960
|
+
relationship: 'ambiguous_contradiction',
|
|
961
|
+
status: 'pending_clarification',
|
|
962
|
+
clarificationQuestion: 'Should I assume Postgres or MySQL?',
|
|
963
|
+
resolutionNote: null,
|
|
964
|
+
lastAskedAt: null,
|
|
965
|
+
resolvedAt: null,
|
|
966
|
+
createdAt: 1,
|
|
967
|
+
updatedAt: 1,
|
|
968
|
+
existingStatement: 'Use Postgres as the default database.',
|
|
969
|
+
candidateStatement: 'Use MySQL as the default database.',
|
|
970
|
+
existingKind: 'preference',
|
|
971
|
+
candidateKind: 'preference',
|
|
972
|
+
}];
|
|
973
|
+
|
|
974
|
+
const gate = new ConflictGate();
|
|
975
|
+
const result = await gate.evaluate(
|
|
976
|
+
'How do I set up pre-commit hooks?',
|
|
977
|
+
{ ...baseConfig, askOnIrrelevantTurns: false },
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
expect(result).toBeNull();
|
|
981
|
+
expect(markAskedCalls).toEqual([]);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test('with askOnIrrelevantTurns=true, irrelevant conflict is asked as non-relevant', async () => {
|
|
985
|
+
pendingConflicts = [{
|
|
986
|
+
id: 'conflict-irrel-true',
|
|
987
|
+
scopeId: 'default',
|
|
988
|
+
existingItemId: 'existing-irrel2',
|
|
989
|
+
candidateItemId: 'candidate-irrel2',
|
|
990
|
+
relationship: 'ambiguous_contradiction',
|
|
991
|
+
status: 'pending_clarification',
|
|
992
|
+
clarificationQuestion: 'Should I assume Postgres or MySQL?',
|
|
993
|
+
resolutionNote: null,
|
|
994
|
+
lastAskedAt: null,
|
|
995
|
+
resolvedAt: null,
|
|
996
|
+
createdAt: 1,
|
|
997
|
+
updatedAt: 1,
|
|
998
|
+
existingStatement: 'Use Postgres as the default database.',
|
|
999
|
+
candidateStatement: 'Use MySQL as the default database.',
|
|
1000
|
+
existingKind: 'preference',
|
|
1001
|
+
candidateKind: 'preference',
|
|
1002
|
+
}];
|
|
1003
|
+
|
|
1004
|
+
const gate = new ConflictGate();
|
|
1005
|
+
const result = await gate.evaluate(
|
|
1006
|
+
'How do I set up pre-commit hooks?',
|
|
1007
|
+
{ ...baseConfig, askOnIrrelevantTurns: true },
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
expect(result).not.toBeNull();
|
|
1011
|
+
expect(result!.relevant).toBe(false);
|
|
1012
|
+
expect(result!.question).toContain('Postgres or MySQL');
|
|
1013
|
+
// Zero-relevance conflicts are surfaced but not tracked as asked
|
|
1014
|
+
expect(markAskedCalls).toEqual([]);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
test('zero-relevance conflict asked via askOnIrrelevantTurns does not cause wasRecentlyAsked on next turn', async () => {
|
|
1018
|
+
pendingConflicts = [{
|
|
1019
|
+
id: 'conflict-zero-rel',
|
|
1020
|
+
scopeId: 'default',
|
|
1021
|
+
existingItemId: 'existing-zero',
|
|
1022
|
+
candidateItemId: 'candidate-zero',
|
|
1023
|
+
relationship: 'ambiguous_contradiction',
|
|
1024
|
+
status: 'pending_clarification',
|
|
1025
|
+
clarificationQuestion: 'Should I assume Postgres or MySQL?',
|
|
1026
|
+
resolutionNote: null,
|
|
1027
|
+
lastAskedAt: null,
|
|
1028
|
+
resolvedAt: null,
|
|
1029
|
+
createdAt: 1,
|
|
1030
|
+
updatedAt: 1,
|
|
1031
|
+
existingStatement: 'Use Postgres as the default database.',
|
|
1032
|
+
candidateStatement: 'Use MySQL as the default database.',
|
|
1033
|
+
existingKind: 'preference',
|
|
1034
|
+
candidateKind: 'preference',
|
|
1035
|
+
}];
|
|
1036
|
+
|
|
1037
|
+
const gate = new ConflictGate();
|
|
1038
|
+
|
|
1039
|
+
// First turn: zero-relevance conflict is surfaced via askOnIrrelevantTurns
|
|
1040
|
+
const result1 = await gate.evaluate(
|
|
1041
|
+
'How do I set up pre-commit hooks?',
|
|
1042
|
+
{ ...baseConfig, askOnIrrelevantTurns: true },
|
|
1043
|
+
);
|
|
1044
|
+
expect(result1).not.toBeNull();
|
|
1045
|
+
expect(result1!.relevant).toBe(false);
|
|
1046
|
+
// Not tracked as asked because relevance is 0
|
|
1047
|
+
expect(markAskedCalls).toEqual([]);
|
|
1048
|
+
|
|
1049
|
+
// Second turn: an unrelated short imperative that looks like a clarification reply.
|
|
1050
|
+
// If the zero-relevance conflict had been tracked, wasRecentlyAsked would return
|
|
1051
|
+
// true and shouldAttemptConflictResolution would try to resolve it — which is wrong.
|
|
1052
|
+
// Since we don't track zero-relevance asks, the resolver should NOT be called.
|
|
1053
|
+
const result2 = await gate.evaluate(
|
|
1054
|
+
'keep it',
|
|
1055
|
+
{ ...baseConfig, askOnIrrelevantTurns: false },
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
// The conflict should not have been resolved by the resolver
|
|
1059
|
+
expect(resolverCallCount).toBe(0);
|
|
1060
|
+
// With askOnIrrelevantTurns=false and the conflict being irrelevant, result is null
|
|
1061
|
+
expect(result2).toBeNull();
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
test('zero-relevance conflict on primary askable path (relevanceThreshold=0) is tracked as asked', async () => {
|
|
1065
|
+
pendingConflicts = [{
|
|
1066
|
+
id: 'conflict-zero-threshold',
|
|
1067
|
+
scopeId: 'default',
|
|
1068
|
+
existingItemId: 'existing-zt',
|
|
1069
|
+
candidateItemId: 'candidate-zt',
|
|
1070
|
+
relationship: 'ambiguous_contradiction',
|
|
1071
|
+
status: 'pending_clarification',
|
|
1072
|
+
clarificationQuestion: 'Should I assume Postgres or MySQL?',
|
|
1073
|
+
resolutionNote: null,
|
|
1074
|
+
lastAskedAt: null,
|
|
1075
|
+
resolvedAt: null,
|
|
1076
|
+
createdAt: 1,
|
|
1077
|
+
updatedAt: 1,
|
|
1078
|
+
existingStatement: 'Use Postgres as the default database.',
|
|
1079
|
+
candidateStatement: 'Use MySQL as the default database.',
|
|
1080
|
+
existingKind: 'preference',
|
|
1081
|
+
candidateKind: 'preference',
|
|
1082
|
+
}];
|
|
1083
|
+
|
|
1084
|
+
const gate = new ConflictGate();
|
|
1085
|
+
// relevanceThreshold=0 means zero-relevance conflicts pass the primary askable filter
|
|
1086
|
+
const result1 = await gate.evaluate(
|
|
1087
|
+
'How do I set up pre-commit hooks?',
|
|
1088
|
+
{ ...baseConfig, relevanceThreshold: 0, askOnIrrelevantTurns: false },
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
expect(result1).not.toBeNull();
|
|
1092
|
+
expect(result1!.relevant).toBe(true);
|
|
1093
|
+
// Should be tracked as asked since it came through the primary askable path
|
|
1094
|
+
expect(markAskedCalls).toEqual(['conflict-zero-threshold']);
|
|
1095
|
+
|
|
1096
|
+
// Second turn within cooldown: the conflict should NOT be re-asked
|
|
1097
|
+
const result2 = await gate.evaluate(
|
|
1098
|
+
'Another unrelated question',
|
|
1099
|
+
{ ...baseConfig, relevanceThreshold: 0, askOnIrrelevantTurns: false },
|
|
1100
|
+
);
|
|
1101
|
+
expect(result2).toBeNull();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test('relevant conflict is asked regardless of askOnIrrelevantTurns value', async () => {
|
|
1105
|
+
pendingConflicts = [{
|
|
1106
|
+
id: 'conflict-rel-knob',
|
|
1107
|
+
scopeId: 'default',
|
|
1108
|
+
existingItemId: 'existing-rel',
|
|
1109
|
+
candidateItemId: 'candidate-rel',
|
|
1110
|
+
relationship: 'ambiguous_contradiction',
|
|
1111
|
+
status: 'pending_clarification',
|
|
1112
|
+
clarificationQuestion: 'Do you want React or Vue for frontend work?',
|
|
1113
|
+
resolutionNote: null,
|
|
1114
|
+
lastAskedAt: null,
|
|
1115
|
+
resolvedAt: null,
|
|
1116
|
+
createdAt: 1,
|
|
1117
|
+
updatedAt: 1,
|
|
1118
|
+
existingStatement: 'Use React for frontend work.',
|
|
1119
|
+
candidateStatement: 'Use Vue for frontend work.',
|
|
1120
|
+
existingKind: 'preference',
|
|
1121
|
+
candidateKind: 'preference',
|
|
1122
|
+
}];
|
|
1123
|
+
|
|
1124
|
+
// Test with askOnIrrelevantTurns=false — relevant conflicts should still be asked
|
|
1125
|
+
const gate = new ConflictGate();
|
|
1126
|
+
const result = await gate.evaluate(
|
|
1127
|
+
'Should I use React or Vue here?',
|
|
1128
|
+
{ ...baseConfig, askOnIrrelevantTurns: false },
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
expect(result).not.toBeNull();
|
|
1132
|
+
expect(result!.relevant).toBe(true);
|
|
1133
|
+
expect(result!.question).toContain('React or Vue');
|
|
1134
|
+
expect(markAskedCalls).toEqual(['conflict-rel-knob']);
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
@@ -188,6 +188,9 @@ mock.module('../calls/call-state.js', () => ({
|
|
|
188
188
|
registerCallQuestionNotifier: () => {},
|
|
189
189
|
unregisterCallQuestionNotifier: () => {},
|
|
190
190
|
fireCallQuestionNotifier: () => {},
|
|
191
|
+
registerCallTranscriptNotifier: () => {},
|
|
192
|
+
unregisterCallTranscriptNotifier: () => {},
|
|
193
|
+
fireCallTranscriptNotifier: () => {},
|
|
191
194
|
registerCallCompletionNotifier: () => {},
|
|
192
195
|
unregisterCallCompletionNotifier: () => {},
|
|
193
196
|
fireCallCompletionNotifier: () => {},
|