@vellumai/assistant 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +6 -0
package/openapi.yaml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
openapi: 3.0.0
|
|
4
4
|
info:
|
|
5
5
|
title: Vellum Assistant API
|
|
6
|
-
version: 0.
|
|
6
|
+
version: 0.8.0
|
|
7
7
|
description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
|
|
8
8
|
servers:
|
|
9
9
|
- url: http://127.0.0.1:7821
|
|
@@ -7635,6 +7635,26 @@ paths:
|
|
|
7635
7635
|
- k
|
|
7636
7636
|
- sample
|
|
7637
7637
|
additionalProperties: false
|
|
7638
|
+
/v1/memory/v2/list-concept-pages:
|
|
7639
|
+
post:
|
|
7640
|
+
operationId: memory_v2_listconceptpages_post
|
|
7641
|
+
summary: List all memory v2 concept pages with metadata
|
|
7642
|
+
description:
|
|
7643
|
+
Returns slugs, body sizes, edge counts, and last-modified timestamps for every concept page on disk.
|
|
7644
|
+
Read-only; used by the desktop About → Memories surface to render a browse-able list.
|
|
7645
|
+
tags:
|
|
7646
|
+
- memory
|
|
7647
|
+
responses:
|
|
7648
|
+
"200":
|
|
7649
|
+
description: Successful response
|
|
7650
|
+
requestBody:
|
|
7651
|
+
required: true
|
|
7652
|
+
content:
|
|
7653
|
+
application/json:
|
|
7654
|
+
schema:
|
|
7655
|
+
type: object
|
|
7656
|
+
properties: {}
|
|
7657
|
+
additionalProperties: false
|
|
7638
7658
|
/v1/memory/v2/rebuild-corpus-stats:
|
|
7639
7659
|
post:
|
|
7640
7660
|
operationId: memory_v2_rebuildcorpusstats_post
|
|
@@ -7661,9 +7681,7 @@ paths:
|
|
|
7661
7681
|
post:
|
|
7662
7682
|
operationId: memory_v2_reembedskills_post
|
|
7663
7683
|
summary: Re-seed v2 skill entries from the current skill catalog
|
|
7664
|
-
description:
|
|
7665
|
-
Synchronously re-runs seedV2SkillEntries against the current skill catalog. Gated on memory-v2-enabled flag
|
|
7666
|
-
and config.memory.v2.enabled.
|
|
7684
|
+
description: Synchronously re-runs seedV2SkillEntries against the current skill catalog. Gated on config.memory.v2.enabled.
|
|
7667
7685
|
tags:
|
|
7668
7686
|
- memory
|
|
7669
7687
|
responses:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@vellumai/credential-storage": "file:../packages/credential-storage",
|
|
45
45
|
"@vellumai/egress-proxy": "file:../packages/egress-proxy",
|
|
46
46
|
"@vellumai/gateway-client": "file:../packages/gateway-client",
|
|
47
|
+
"@vellumai/ipc-server-utils": "file:../packages/ipc-server-utils",
|
|
47
48
|
"@vellumai/service-contracts": "file:../packages/service-contracts",
|
|
48
49
|
"@vellumai/skill-host-contracts": "file:../packages/skill-host-contracts",
|
|
49
50
|
"@vellumai/slack-text": "file:../packages/slack-text",
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
"@vellumai/service-contracts",
|
|
79
80
|
"@vellumai/egress-proxy",
|
|
80
81
|
"@vellumai/gateway-client",
|
|
82
|
+
"@vellumai/ipc-server-utils",
|
|
81
83
|
"@vellumai/skill-host-contracts",
|
|
82
84
|
"@vellumai/slack-text",
|
|
83
85
|
"@vellumai/twilio-client"
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `annotatePersistedAssistantMessage` persisting the 3 risk-option
|
|
3
|
+
* arrays alongside the existing `_risk*` scalars.
|
|
4
|
+
*
|
|
5
|
+
* Phase B of the conflation track. Without these annotations, the Rule Editor
|
|
6
|
+
* Modal's chip ladder loses its scope/allowlist/directory options on chat-
|
|
7
|
+
* history reload and falls back to the synthesized `*` allowlist.
|
|
8
|
+
*
|
|
9
|
+
* The test exercises the full populate → annotate → persist round-trip:
|
|
10
|
+
* handleToolResult(event with 3 arrays)
|
|
11
|
+
* → state.toolRiskOutcomes captures them
|
|
12
|
+
* → annotatePersistedAssistantMessage writes _risk*Options onto the row
|
|
13
|
+
* → updateMessageContent receives the JSON-serialized output
|
|
14
|
+
*
|
|
15
|
+
* Read-side coverage (renderHistoryContent in handlers/shared.ts) lives in
|
|
16
|
+
* server-history-render.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
// ── Mock platform (must precede imports that read it) ─────────────────────────
|
|
21
|
+
mock.module("../util/logger.js", () => ({
|
|
22
|
+
getLogger: () =>
|
|
23
|
+
new Proxy({} as Record<string, unknown>, {
|
|
24
|
+
get: () => () => {},
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module("../config/loader.js", () => ({
|
|
29
|
+
getConfig: () => ({
|
|
30
|
+
skills: {
|
|
31
|
+
entries: {},
|
|
32
|
+
load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
|
|
33
|
+
install: { nodeManager: "npm" },
|
|
34
|
+
allowBundled: null,
|
|
35
|
+
remoteProviders: {
|
|
36
|
+
skillssh: { enabled: true },
|
|
37
|
+
clawhub: { enabled: true },
|
|
38
|
+
},
|
|
39
|
+
remotePolicy: {
|
|
40
|
+
blockSuspicious: true,
|
|
41
|
+
blockMalware: true,
|
|
42
|
+
maxSkillsShRisk: "medium",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
loadConfig: () => ({}),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
let mockedRowContent = "";
|
|
50
|
+
const updates: Array<{ id: string; content: string }> = [];
|
|
51
|
+
|
|
52
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
53
|
+
addMessage: () => ({ id: "mock-msg-id" }),
|
|
54
|
+
getMessageById: (id: string) =>
|
|
55
|
+
mockedRowContent ? { id, content: mockedRowContent } : null,
|
|
56
|
+
updateMessageContent: (id: string, content: string) => {
|
|
57
|
+
updates.push({ id, content });
|
|
58
|
+
},
|
|
59
|
+
provenanceFromTrustContext: () => ({}),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
mock.module("../memory/llm-request-log-store.js", () => ({
|
|
63
|
+
recordRequestLog: () => {},
|
|
64
|
+
backfillMessageIdOnLogs: () => {},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
|
68
|
+
import type {
|
|
69
|
+
EventHandlerDeps,
|
|
70
|
+
EventHandlerState,
|
|
71
|
+
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
72
|
+
import {
|
|
73
|
+
createEventHandlerState,
|
|
74
|
+
handleToolResult,
|
|
75
|
+
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
76
|
+
|
|
77
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function makeDeps(): EventHandlerDeps {
|
|
80
|
+
return {
|
|
81
|
+
ctx: {
|
|
82
|
+
conversationId: "test-conv",
|
|
83
|
+
provider: { name: "anthropic" },
|
|
84
|
+
traceEmitter: { emit: () => {} },
|
|
85
|
+
streamThinking: false,
|
|
86
|
+
emitActivityState: () => {},
|
|
87
|
+
markWorkspaceTopLevelDirty: () => {},
|
|
88
|
+
currentTurnSurfaces: [],
|
|
89
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
90
|
+
onEvent: () => {},
|
|
91
|
+
reqId: "test-req",
|
|
92
|
+
isFirstMessage: false,
|
|
93
|
+
shouldGenerateTitle: false,
|
|
94
|
+
rlog: new Proxy({} as Record<string, unknown>, {
|
|
95
|
+
get: () => () => {},
|
|
96
|
+
}) as unknown as EventHandlerDeps["rlog"],
|
|
97
|
+
turnChannelContext: {
|
|
98
|
+
userMessageChannel: "vellum",
|
|
99
|
+
assistantMessageChannel: "vellum",
|
|
100
|
+
} as unknown as EventHandlerDeps["turnChannelContext"],
|
|
101
|
+
turnInterfaceContext: {
|
|
102
|
+
userMessageInterface: "web",
|
|
103
|
+
assistantMessageInterface: "web",
|
|
104
|
+
} as unknown as EventHandlerDeps["turnInterfaceContext"],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setupState(toolUseId: string): EventHandlerState {
|
|
109
|
+
const state = createEventHandlerState();
|
|
110
|
+
state.lastAssistantMessageId = "msg-1";
|
|
111
|
+
state.toolUseIdToName.set(toolUseId, "bash");
|
|
112
|
+
state.toolCallTimestamps.set(toolUseId, { startedAt: Date.now() });
|
|
113
|
+
state.currentTurnToolUseIds.push(toolUseId);
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function findPersistedToolUse(
|
|
118
|
+
rawContent: string,
|
|
119
|
+
toolUseId: string,
|
|
120
|
+
): Record<string, unknown> {
|
|
121
|
+
const parsed = JSON.parse(rawContent) as Array<Record<string, unknown>>;
|
|
122
|
+
const block = parsed.find(
|
|
123
|
+
(b) => b.type === "tool_use" && b.id === toolUseId,
|
|
124
|
+
);
|
|
125
|
+
if (!block) throw new Error(`tool_use block ${toolUseId} not found`);
|
|
126
|
+
return block;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
describe("annotatePersistedAssistantMessage — risk-option arrays (Phase B)", () => {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
updates.length = 0;
|
|
134
|
+
mockedRowContent = "";
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("persists all 3 risk-option arrays from the live tool_result event", () => {
|
|
138
|
+
const toolUseId = "tu_persist_full";
|
|
139
|
+
const state = setupState(toolUseId);
|
|
140
|
+
|
|
141
|
+
mockedRowContent = JSON.stringify([
|
|
142
|
+
{
|
|
143
|
+
type: "tool_use",
|
|
144
|
+
id: toolUseId,
|
|
145
|
+
name: "bash",
|
|
146
|
+
input: { command: "rm -rf /tmp" },
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const scopeOptions = [
|
|
151
|
+
{ pattern: "exact", label: "exact: rm -rf /tmp" },
|
|
152
|
+
{ pattern: "by-program", label: "All rm" },
|
|
153
|
+
];
|
|
154
|
+
const allowlistOptions = [
|
|
155
|
+
{ label: "exact", description: "exact match", pattern: "rm -rf /tmp" },
|
|
156
|
+
{ label: "All rm", description: "All rm commands", pattern: "rm *" },
|
|
157
|
+
];
|
|
158
|
+
const directoryScopeOptions = [
|
|
159
|
+
{ scope: "/Users/me/code", label: "in code/" },
|
|
160
|
+
{ scope: "everywhere", label: "Everywhere" },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
handleToolResult(state, makeDeps(), {
|
|
164
|
+
type: "tool_result",
|
|
165
|
+
toolUseId,
|
|
166
|
+
content: "ok",
|
|
167
|
+
isError: false,
|
|
168
|
+
riskLevel: "high",
|
|
169
|
+
riskReason: "Modifies state",
|
|
170
|
+
matchedTrustRuleId: "rule_42",
|
|
171
|
+
riskScopeOptions: scopeOptions,
|
|
172
|
+
riskAllowlistOptions: allowlistOptions,
|
|
173
|
+
riskDirectoryScopeOptions: directoryScopeOptions,
|
|
174
|
+
approvalMode: "prompted",
|
|
175
|
+
approvalReason: "user_approved",
|
|
176
|
+
riskThreshold: "relaxed",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(updates).toHaveLength(1);
|
|
180
|
+
const block = findPersistedToolUse(updates[0].content, toolUseId);
|
|
181
|
+
// Existing scalars still flow through.
|
|
182
|
+
expect(block._riskLevel).toBe("high");
|
|
183
|
+
expect(block._riskReason).toBe("Modifies state");
|
|
184
|
+
expect(block._matchedTrustRuleId).toBe("rule_42");
|
|
185
|
+
expect(block._approvalMode).toBe("prompted");
|
|
186
|
+
expect(block._approvalReason).toBe("user_approved");
|
|
187
|
+
expect(block._riskThreshold).toBe("relaxed");
|
|
188
|
+
// New: 3 risk-option arrays persisted verbatim.
|
|
189
|
+
expect(block._riskScopeOptions).toEqual(scopeOptions);
|
|
190
|
+
expect(block._riskAllowlistOptions).toEqual(allowlistOptions);
|
|
191
|
+
expect(block._riskDirectoryScopeOptions).toEqual(directoryScopeOptions);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("omits empty arrays from the persisted block (saves DB space)", () => {
|
|
195
|
+
const toolUseId = "tu_persist_empty";
|
|
196
|
+
const state = setupState(toolUseId);
|
|
197
|
+
|
|
198
|
+
mockedRowContent = JSON.stringify([
|
|
199
|
+
{
|
|
200
|
+
type: "tool_use",
|
|
201
|
+
id: toolUseId,
|
|
202
|
+
name: "bash",
|
|
203
|
+
input: { command: "ls" },
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
handleToolResult(state, makeDeps(), {
|
|
208
|
+
type: "tool_result",
|
|
209
|
+
toolUseId,
|
|
210
|
+
content: "ok",
|
|
211
|
+
isError: false,
|
|
212
|
+
riskLevel: "low",
|
|
213
|
+
riskScopeOptions: [],
|
|
214
|
+
riskAllowlistOptions: [],
|
|
215
|
+
riskDirectoryScopeOptions: [],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(updates).toHaveLength(1);
|
|
219
|
+
const block = findPersistedToolUse(updates[0].content, toolUseId);
|
|
220
|
+
expect(block._riskLevel).toBe("low");
|
|
221
|
+
expect(block._riskScopeOptions).toBeUndefined();
|
|
222
|
+
expect(block._riskAllowlistOptions).toBeUndefined();
|
|
223
|
+
expect(block._riskDirectoryScopeOptions).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("omits absent (undefined) arrays from the persisted block", () => {
|
|
227
|
+
// Mirrors classic bash/file tools that don't always emit all 3 arrays —
|
|
228
|
+
// e.g. recall, file_read with riskLevel=low and no allowlist coverage.
|
|
229
|
+
const toolUseId = "tu_persist_absent";
|
|
230
|
+
const state = setupState(toolUseId);
|
|
231
|
+
|
|
232
|
+
mockedRowContent = JSON.stringify([
|
|
233
|
+
{
|
|
234
|
+
type: "tool_use",
|
|
235
|
+
id: toolUseId,
|
|
236
|
+
name: "recall",
|
|
237
|
+
input: { query: "anything" },
|
|
238
|
+
},
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
handleToolResult(state, makeDeps(), {
|
|
242
|
+
type: "tool_result",
|
|
243
|
+
toolUseId,
|
|
244
|
+
content: "ok",
|
|
245
|
+
isError: false,
|
|
246
|
+
riskLevel: "low",
|
|
247
|
+
// No risk-option arrays passed at all.
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(updates).toHaveLength(1);
|
|
251
|
+
const block = findPersistedToolUse(updates[0].content, toolUseId);
|
|
252
|
+
expect(block._riskLevel).toBe("low");
|
|
253
|
+
expect(block._riskScopeOptions).toBeUndefined();
|
|
254
|
+
expect(block._riskAllowlistOptions).toBeUndefined();
|
|
255
|
+
expect(block._riskDirectoryScopeOptions).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("partial coverage — only allowlist options present (e.g. tools with classifier but no scope ladder)", () => {
|
|
259
|
+
const toolUseId = "tu_partial";
|
|
260
|
+
const state = setupState(toolUseId);
|
|
261
|
+
|
|
262
|
+
mockedRowContent = JSON.stringify([
|
|
263
|
+
{
|
|
264
|
+
type: "tool_use",
|
|
265
|
+
id: toolUseId,
|
|
266
|
+
name: "file_write",
|
|
267
|
+
input: { path: "/tmp/foo.txt" },
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const allowlistOptions = [
|
|
272
|
+
{ label: "exact", description: "exact match", pattern: "/tmp/foo.txt" },
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
handleToolResult(state, makeDeps(), {
|
|
276
|
+
type: "tool_result",
|
|
277
|
+
toolUseId,
|
|
278
|
+
content: "ok",
|
|
279
|
+
isError: false,
|
|
280
|
+
riskLevel: "medium",
|
|
281
|
+
riskAllowlistOptions: allowlistOptions,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(updates).toHaveLength(1);
|
|
285
|
+
const block = findPersistedToolUse(updates[0].content, toolUseId);
|
|
286
|
+
expect(block._riskLevel).toBe("medium");
|
|
287
|
+
expect(block._riskAllowlistOptions).toEqual(allowlistOptions);
|
|
288
|
+
expect(block._riskScopeOptions).toBeUndefined();
|
|
289
|
+
expect(block._riskDirectoryScopeOptions).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -291,21 +291,13 @@ function seedPendingConfirmation(
|
|
|
291
291
|
conversation: Conversation,
|
|
292
292
|
requestId: string,
|
|
293
293
|
): void {
|
|
294
|
+
// Access private ownedIds so denyAllPending/dispose can find this request.
|
|
295
|
+
// promptResolve/promptReject callbacks are stored in pendingInteractions via
|
|
296
|
+
// registerPendingInteraction, which is called separately in each test.
|
|
294
297
|
const prompter = conversation["prompter"] as unknown as {
|
|
295
|
-
|
|
296
|
-
string,
|
|
297
|
-
{
|
|
298
|
-
resolve: (...args: unknown[]) => void;
|
|
299
|
-
reject: (...args: unknown[]) => void;
|
|
300
|
-
timer: ReturnType<typeof setTimeout>;
|
|
301
|
-
}
|
|
302
|
-
>;
|
|
298
|
+
ownedIds: Set<string>;
|
|
303
299
|
};
|
|
304
|
-
prompter.
|
|
305
|
-
resolve: () => {},
|
|
306
|
-
reject: () => {},
|
|
307
|
-
timer: setTimeout(() => {}, 60_000),
|
|
308
|
-
});
|
|
300
|
+
prompter.ownedIds.add(requestId);
|
|
309
301
|
}
|
|
310
302
|
|
|
311
303
|
/**
|
|
@@ -439,12 +431,12 @@ describe("approval cascading", () => {
|
|
|
439
431
|
makeConfirmationDetails(["bash:echo stale"]),
|
|
440
432
|
);
|
|
441
433
|
|
|
442
|
-
// Remove req-stale from the prompter's
|
|
434
|
+
// Remove req-stale from the prompter's ownedIds (simulating it was
|
|
443
435
|
// already resolved by another path before cascade reaches it)
|
|
444
436
|
const prompter = conversationObj["prompter"] as unknown as {
|
|
445
|
-
|
|
437
|
+
ownedIds: Set<string>;
|
|
446
438
|
};
|
|
447
|
-
prompter.
|
|
439
|
+
prompter.ownedIds.delete("req-stale");
|
|
448
440
|
|
|
449
441
|
// This should not throw — cascade should skip req-stale gracefully
|
|
450
442
|
expect(() => {
|
|
@@ -192,6 +192,8 @@ function makeIdleSession(opts?: {
|
|
|
192
192
|
processing = false;
|
|
193
193
|
},
|
|
194
194
|
handleConfirmationResponse: (requestId: string, decision: string) => {
|
|
195
|
+
// Simulate PermissionPrompter.resolveConfirmation(): prompter owns deregistration.
|
|
196
|
+
pendingInteractions.resolve(requestId);
|
|
195
197
|
opts?.onConfirmation?.(requestId, decision);
|
|
196
198
|
},
|
|
197
199
|
handleSecretResponse: (
|
|
@@ -199,6 +201,8 @@ function makeIdleSession(opts?: {
|
|
|
199
201
|
value?: string,
|
|
200
202
|
delivery?: string,
|
|
201
203
|
) => {
|
|
204
|
+
// Simulate SecretPrompter.resolveSecret(): prompter owns deregistration.
|
|
205
|
+
pendingInteractions.resolve(requestId);
|
|
202
206
|
opts?.onSecret?.(requestId, value, delivery);
|
|
203
207
|
},
|
|
204
208
|
} as unknown as Conversation;
|
|
@@ -285,6 +289,8 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
285
289
|
await new Promise<void>(() => {});
|
|
286
290
|
},
|
|
287
291
|
handleConfirmationResponse: (requestId: string, decision: string) => {
|
|
292
|
+
// Simulate PermissionPrompter.resolveConfirmation(): prompter owns deregistration.
|
|
293
|
+
pendingInteractions.resolve(requestId);
|
|
288
294
|
opts?.onConfirmation?.(requestId, decision);
|
|
289
295
|
},
|
|
290
296
|
handleSecretResponse: () => {},
|
|
@@ -389,18 +389,19 @@ describe("auto-analysis batch trigger uses analysis.batchSize cadence", () => {
|
|
|
389
389
|
const originalExtractionBatch = TEST_CONFIG.memory.extraction.batchSize;
|
|
390
390
|
const originalAnalysisBatch = TEST_CONFIG.analysis.batchSize;
|
|
391
391
|
|
|
392
|
+
const originalV2Enabled = TEST_CONFIG.memory.v2.enabled;
|
|
393
|
+
|
|
392
394
|
beforeEach(() => {
|
|
393
|
-
|
|
395
|
+
_setOverridesForTesting({ "auto-analyze": true });
|
|
396
|
+
// memory.v2.enabled gates v1 graph_extract enqueue; force off so
|
|
394
397
|
// these cadence tests can observe the v1 path.
|
|
395
|
-
|
|
396
|
-
"auto-analyze": true,
|
|
397
|
-
"memory-v2-enabled": false,
|
|
398
|
-
});
|
|
398
|
+
TEST_CONFIG.memory.v2.enabled = false;
|
|
399
399
|
TEST_CONFIG.memory.extraction.batchSize = 2;
|
|
400
400
|
TEST_CONFIG.analysis.batchSize = 5;
|
|
401
401
|
});
|
|
402
402
|
|
|
403
403
|
afterEach(() => {
|
|
404
|
+
TEST_CONFIG.memory.v2.enabled = originalV2Enabled;
|
|
404
405
|
TEST_CONFIG.memory.extraction.batchSize = originalExtractionBatch;
|
|
405
406
|
TEST_CONFIG.analysis.batchSize = originalAnalysisBatch;
|
|
406
407
|
});
|
|
@@ -544,10 +545,10 @@ describe("auto-analysis batch trigger uses analysis.batchSize cadence", () => {
|
|
|
544
545
|
});
|
|
545
546
|
|
|
546
547
|
// ─────────────────────────────────────────────────────────────────
|
|
547
|
-
// Indexer v1/v2 mutual exclusion: when memory
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
548
|
+
// Indexer v1/v2 mutual exclusion: when memory.v2.enabled is on, the
|
|
549
|
+
// v1 graph_extract enqueue is suppressed (v2 reads from buffer.md,
|
|
550
|
+
// so v1 graph data is unread). When v2 is disabled, v1 graph_extract
|
|
551
|
+
// fires.
|
|
551
552
|
// ─────────────────────────────────────────────────────────────────
|
|
552
553
|
|
|
553
554
|
describe("indexer v1/v2 mutual exclusion for graph_extract", () => {
|
|
@@ -564,8 +565,7 @@ describe("indexer v1/v2 mutual exclusion for graph_extract", () => {
|
|
|
564
565
|
TEST_CONFIG.memory.v2.enabled = originalV2Enabled;
|
|
565
566
|
});
|
|
566
567
|
|
|
567
|
-
test("v2 active (
|
|
568
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
568
|
+
test("v2 active (config on) → graph_extract not enqueued", async () => {
|
|
569
569
|
TEST_CONFIG.memory.v2.enabled = true;
|
|
570
570
|
|
|
571
571
|
const source = createConversation("v2-active");
|
|
@@ -574,20 +574,7 @@ describe("indexer v1/v2 mutual exclusion for graph_extract", () => {
|
|
|
574
574
|
expect(countJobsOfType("graph_extract", source.id)).toBe(0);
|
|
575
575
|
});
|
|
576
576
|
|
|
577
|
-
test("
|
|
578
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
579
|
-
TEST_CONFIG.memory.v2.enabled = true;
|
|
580
|
-
|
|
581
|
-
const source = createConversation("v2-flag-off");
|
|
582
|
-
await indexMessages(source.id, 2);
|
|
583
|
-
|
|
584
|
-
expect(countJobsOfType("graph_extract", source.id)).toBeGreaterThanOrEqual(
|
|
585
|
-
1,
|
|
586
|
-
);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test("config gate off (flag on) → graph_extract enqueued", async () => {
|
|
590
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
577
|
+
test("config gate off → graph_extract enqueued", async () => {
|
|
591
578
|
TEST_CONFIG.memory.v2.enabled = false;
|
|
592
579
|
|
|
593
580
|
const source = createConversation("v2-config-off");
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getEndCallListenWindowMs,
|
|
5
|
+
isDeniedNumber,
|
|
6
|
+
} from "../calls/call-constants.js";
|
|
4
7
|
|
|
5
8
|
describe("isDeniedNumber", () => {
|
|
6
9
|
// Numbers that MUST be blocked
|
|
@@ -39,3 +42,9 @@ describe("isDeniedNumber", () => {
|
|
|
39
42
|
});
|
|
40
43
|
}
|
|
41
44
|
});
|
|
45
|
+
|
|
46
|
+
describe("getEndCallListenWindowMs", () => {
|
|
47
|
+
test("leaves a brief response window before task-complete hangup", () => {
|
|
48
|
+
expect(getEndCallListenWindowMs()).toBe(15_000);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -105,11 +105,13 @@ mock.module("../security/credential-key.js", () => ({
|
|
|
105
105
|
|
|
106
106
|
let mockConsultationTimeoutMs = 90_000;
|
|
107
107
|
let mockSilenceTimeoutMs = 30_000;
|
|
108
|
+
let mockEndCallListenWindowMs = 0;
|
|
108
109
|
|
|
109
110
|
mock.module("../calls/call-constants.js", () => ({
|
|
110
111
|
getMaxCallDurationMs: () => 12 * 60 * 1000,
|
|
111
112
|
getUserConsultationTimeoutMs: () => mockConsultationTimeoutMs,
|
|
112
113
|
getSilenceTimeoutMs: () => mockSilenceTimeoutMs,
|
|
114
|
+
getEndCallListenWindowMs: () => mockEndCallListenWindowMs,
|
|
113
115
|
}));
|
|
114
116
|
|
|
115
117
|
// ── Voice session bridge mock ────────────────────────────────────────
|
|
@@ -467,6 +469,7 @@ describe("call-controller", () => {
|
|
|
467
469
|
// Reset consultation timeout to the default (long) value
|
|
468
470
|
mockConsultationTimeoutMs = 90_000;
|
|
469
471
|
mockSilenceTimeoutMs = 30_000;
|
|
472
|
+
mockEndCallListenWindowMs = 0;
|
|
470
473
|
// Reset TTS config to defaults so per-test mutations don't leak.
|
|
471
474
|
const cfg = loadConfig();
|
|
472
475
|
cfg.services.tts.provider = "elevenlabs";
|
|
@@ -755,6 +758,130 @@ describe("call-controller", () => {
|
|
|
755
758
|
controller.destroy();
|
|
756
759
|
});
|
|
757
760
|
|
|
761
|
+
test("END_CALL waits through the listen window before completing", async () => {
|
|
762
|
+
mockEndCallListenWindowMs = 25;
|
|
763
|
+
mockStartVoiceTurn.mockImplementation(
|
|
764
|
+
createMockVoiceTurn(["Thank you for calling, goodbye! ", "[END_CALL]"]),
|
|
765
|
+
);
|
|
766
|
+
const { session, relay, controller } = setupController();
|
|
767
|
+
|
|
768
|
+
await controller.handleCallerUtterance("That is all, thanks");
|
|
769
|
+
|
|
770
|
+
expect(relay.endCalled).toBe(false);
|
|
771
|
+
expect(getCallSession(session.id)!.status).toBe("in_progress");
|
|
772
|
+
|
|
773
|
+
await new Promise((r) => setTimeout(r, 35));
|
|
774
|
+
|
|
775
|
+
expect(relay.endCalled).toBe(true);
|
|
776
|
+
const updatedSession = getCallSession(session.id);
|
|
777
|
+
expect(updatedSession!.status).toBe("completed");
|
|
778
|
+
expect(updatedSession!.endedAt).not.toBeNull();
|
|
779
|
+
|
|
780
|
+
controller.destroy();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("delayed END_CALL completion skips side effects when session is already terminal", async () => {
|
|
784
|
+
mockEndCallListenWindowMs = 25;
|
|
785
|
+
mockStartVoiceTurn.mockImplementation(
|
|
786
|
+
createMockVoiceTurn(["Thank you for calling, goodbye! ", "[END_CALL]"]),
|
|
787
|
+
);
|
|
788
|
+
const { session, relay, controller } = setupController();
|
|
789
|
+
|
|
790
|
+
await controller.handleCallerUtterance("That is all, thanks");
|
|
791
|
+
|
|
792
|
+
const externalEndedAt = Date.now();
|
|
793
|
+
updateCallSession(session.id, {
|
|
794
|
+
status: "completed",
|
|
795
|
+
endedAt: externalEndedAt,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
await new Promise((r) => setTimeout(r, 35));
|
|
799
|
+
|
|
800
|
+
expect(relay.endCalled).toBe(false);
|
|
801
|
+
const updatedSession = getCallSession(session.id);
|
|
802
|
+
expect(updatedSession!.status).toBe("completed");
|
|
803
|
+
expect(updatedSession!.endedAt).toBe(externalEndedAt);
|
|
804
|
+
|
|
805
|
+
controller.destroy();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("callee speech during END_CALL listen window cancels pending completion", async () => {
|
|
809
|
+
mockEndCallListenWindowMs = 30;
|
|
810
|
+
const turnContents: string[] = [];
|
|
811
|
+
mockStartVoiceTurn.mockImplementation(
|
|
812
|
+
async (opts: {
|
|
813
|
+
content: string;
|
|
814
|
+
onTextDelta: (t: string) => void;
|
|
815
|
+
onComplete: () => void;
|
|
816
|
+
}) => {
|
|
817
|
+
turnContents.push(opts.content);
|
|
818
|
+
if (turnContents.length === 1) {
|
|
819
|
+
opts.onTextDelta("Goodbye! [END_CALL]");
|
|
820
|
+
} else {
|
|
821
|
+
opts.onTextDelta("Of course. I'm still here.");
|
|
822
|
+
}
|
|
823
|
+
opts.onComplete();
|
|
824
|
+
return { turnId: `run-${turnContents.length}`, abort: () => {} };
|
|
825
|
+
},
|
|
826
|
+
);
|
|
827
|
+
const { session, relay, controller } = setupController();
|
|
828
|
+
|
|
829
|
+
await controller.handleCallerUtterance("That is all, thanks");
|
|
830
|
+
expect(relay.endCalled).toBe(false);
|
|
831
|
+
|
|
832
|
+
await controller.handleCallerUtterance("Wait, one more thing");
|
|
833
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
834
|
+
|
|
835
|
+
expect(relay.endCalled).toBe(false);
|
|
836
|
+
expect(getCallSession(session.id)!.status).toBe("in_progress");
|
|
837
|
+
expect(turnContents).toContain("Wait, one more thing");
|
|
838
|
+
const allText = relay.sentTokens.map((t) => t.token).join("");
|
|
839
|
+
expect(allText).toContain("I'm still here.");
|
|
840
|
+
|
|
841
|
+
controller.destroy();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("END_CALL listen window restores in_progress after clearing pending guardian input", async () => {
|
|
845
|
+
mockEndCallListenWindowMs = 30;
|
|
846
|
+
const turnContents: string[] = [];
|
|
847
|
+
mockStartVoiceTurn.mockImplementation(
|
|
848
|
+
async (opts: {
|
|
849
|
+
content: string;
|
|
850
|
+
onTextDelta: (t: string) => void;
|
|
851
|
+
onComplete: () => void;
|
|
852
|
+
}) => {
|
|
853
|
+
turnContents.push(opts.content);
|
|
854
|
+
if (turnContents.length === 1) {
|
|
855
|
+
opts.onTextDelta("Let me check. [ASK_GUARDIAN: Is this okay?]");
|
|
856
|
+
} else if (turnContents.length === 2) {
|
|
857
|
+
opts.onTextDelta("Never mind, goodbye. [END_CALL]");
|
|
858
|
+
} else {
|
|
859
|
+
opts.onTextDelta("I'm still here.");
|
|
860
|
+
}
|
|
861
|
+
opts.onComplete();
|
|
862
|
+
return { turnId: `run-${turnContents.length}`, abort: () => {} };
|
|
863
|
+
},
|
|
864
|
+
);
|
|
865
|
+
const { session, relay, controller } = setupController();
|
|
866
|
+
|
|
867
|
+
await controller.handleCallerUtterance("Can you ask?");
|
|
868
|
+
expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
|
|
869
|
+
expect(getCallSession(session.id)!.status).toBe("waiting_on_user");
|
|
870
|
+
|
|
871
|
+
await controller.handleCallerUtterance("Actually never mind");
|
|
872
|
+
expect(controller.getPendingConsultationQuestionId()).toBeNull();
|
|
873
|
+
expect(getCallSession(session.id)!.status).toBe("in_progress");
|
|
874
|
+
expect(relay.endCalled).toBe(false);
|
|
875
|
+
|
|
876
|
+
await controller.handleCallerUtterance("Wait, one more thing");
|
|
877
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
878
|
+
|
|
879
|
+
expect(relay.endCalled).toBe(false);
|
|
880
|
+
expect(getCallSession(session.id)!.status).toBe("in_progress");
|
|
881
|
+
|
|
882
|
+
controller.destroy();
|
|
883
|
+
});
|
|
884
|
+
|
|
758
885
|
// ── handleUserAnswer ──────────────────────────────────────────────
|
|
759
886
|
|
|
760
887
|
test("handleUserAnswer: returns true immediately and fires LLM asynchronously", async () => {
|