Files
PaperClipAI/server/src/__tests__/voice-session-store.integration.test.ts
T
Dotta ef4695b77f Merge branch 'codex/sleep-24-voice-inbound-retention' into codex/sleep-25-voice-call-reports
* codex/sleep-24-voice-inbound-retention:
  docs: record retention acceptance after denial repair
  fix(server): return inbound denial before provider cleanup
2026-10-25 05:41:41 -05:00

2029 lines
163 KiB
TypeScript

import { createChatReconciliationCoordinator } from "../app.js";
import { subscribeDatabaseWork } from "@paperclipai/db";
import { DELIVERY_QUEUES } from "../services/delivery-work-notifications.js";
import * as voiceReplyNotifications from "../services/voice/voice-reply-notifications.js";
import { voiceReplySchedule } from "../services/voice/voice-reply-schedule.js";
import { registerVoiceReplyWork } from "../services/voice/voice-reply-work.js";
import { pushVoiceReplies } from "../services/voice/voice-reply-push.js";
import { notifyVoiceReplies } from "../services/voice/voice-reply-notifications.js";
import { toolAccessService } from "../services/tool-access.js";
import { createDeliveryWorkCoordinator } from "../services/delivery-work-coordinator.js";
import { idleWorkSnapshot } from "../services/task-admission.js";
import { resolveExternalTestDatabaseUrl } from "./helpers/external-test-database.js";
import express from "express";
import request from "supertest";
import { errorHandler } from "../middleware/error-handler.js";
import { voiceSessionRoutes } from "../routes/voice-sessions.js";
import { issueThreadInteractionService } from "../services/issue-thread-interactions.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { documentService } from "../services/documents.js";
import { applyConnectorSkills, prepareConnectorSkillDelivery, resolveConnectorAssignments } from "../services/connector-runtime.js";
import { VOICE_RESULT_NOTIFICATION } from "@paperclipai/shared";
import { createHash, createHmac, randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { chatActions, instanceUserRoles, principalPermissionGrants, environments, chatVoiceInboundCalls, chatVoicePhoneLines, chatVoiceReports, heartbeatRuns, toolProfileBindings, chatVoiceCallbacks, agentWakeupRequests, issueThreadInteractions, issueQuestionResponseDeliveries, issueComments, agents, chatConversations, chatDeliveries, chatEndpoints, chatPublications, chatVoiceReplies, chatVoiceSessions, chatVoiceToolCalls, companies, companyMemberships, companySecrets, connectionGrants, connectionGrantMembers, createDb, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, issues, toolApplications, toolConnections, toolConnectionInstalls } from "@paperclipai/db";
import { configureSpekoSessionTools } from "../services/voice/speko-tool-setup.js";
import { instanceSettingsService } from "../services/instance-settings.js";
import { chatChannelService } from "../services/chat-channels.js";
import { voiceInboundService } from "../services/voice/voice-inbound-service.js";
import { voiceSessionService } from "../services/voice/voice-session-service.js";
import { captureVoiceDeliveryDiagnostics } from "../services/voice/voice-delivery-diagnostics.js";
import { parseSpekoDeliveryDiagnostics } from "../services/voice/speko-delivery-diagnostics.js";
import { SpekoProviderError } from "../services/voice/speko-provider.js";
import { voiceSessionStore, serializeVoiceSession, currentVoiceCredentialFingerprint } from "../services/voice/voice-session-store.js";
import type { SpekoToolEnvelope } from "../services/voice/speko-protocol.js";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { liveVoiceExecutionGuidance } from "../services/voice/voice-execution-guidance.js";
import { buildPaperclipRuntimeMcpServers } from "../services/heartbeat.js";
import { createToolGatewayService } from "../services/tool-gateway.js";
import { initializeRunIdentity } from "../services/run-identity.js";
import { syncSpekoVoiceTools, spekoToolsForSession, executeSpekoVoiceTool } from "../services/voice/speko-agent-tools.js";
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_VOICE_TEST_DATABASE_URL");
const support = external ? { supported: true } : await getEmbeddedPostgresTestSupport();
(support.supported ? describe : describe.skip)("durable Speko voice sessions", () => {
let db: ReturnType<typeof createDb>;
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
beforeAll(async () => {
if (!external) database = await startEmbeddedPostgresTestDatabase("paperclip-speko-store-");
db = createDb(external ?? database!.connectionString, { idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 });
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterAll(async () => { await db?.$client.end({ timeout: 1 }); await database?.cleanup(); });
async function fixture() {
const companyId = randomUUID(), endpointId = randomUUID(), agentId = randomUUID(), applicationId = randomUUID(), connectionId = randomUUID(), issueId = randomUUID(), conversationId = randomUUID(), callerId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Voice test", issuePrefix: `V${companyId.slice(0, 7).toUpperCase()}` });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: callerId, status: "active", membershipRole: "member" });
await db.insert(agents).values({ id: agentId, companyId, name: "Existing agent", role: "operator", status: "idle", adapterType: "paperclip_runner" });
await db.insert(issues).values({ id: issueId, companyId, title: "Existing task", assigneeAgentId: agentId, status: "in_progress" });
await db.insert(toolApplications).values({ id: applicationId, companyId, applicationKey: `chat:speko:${endpointId}`, name: "Speko", type: "chat", status: "active" });
await db.insert(toolConnections).values({ id: connectionId, companyId, applicationId, name: "Speko", uid: endpointId, connectionPurpose: "channel", transport: "voice", config: { provider: "speko" }, status: "active", enabled: true });
await db.insert(chatEndpoints).values({ id: endpointId, companyId, connectionId, provider: "speko", publicId: randomUUID(), assignedAgentId: agentId, status: "active", setup: { step: "complete", runtimeGeneration: 1 } });
await db.insert(chatConversations).values({ id: conversationId, companyId, endpointId, issueId, externalConversationId: `speko:${conversationId}`, externalThreadId: `speko:${conversationId}`, externalLabel: "Voice", isDirectMessage: true });
const store = voiceSessionStore(db, { allowLocalBoard: false });
const prepareBinding = vi.fn(async () => ({ issueId, conversationId }));
const request = { companyId, endpointId, caller: { id: callerId, authority: "member" as const }, idempotencyKey: randomUUID(), requestFingerprint: "request", maxDurationSeconds: 600, prepareBinding };
const reserved = await store.reserve(request);
if (!reserved.created) throw new Error("Expected reservation");
const sessionId = reserved.session.id, providerSessionId = randomUUID();
await db.update(chatVoiceSessions).set({ state: "active", providerSessionId }).where(eq(chatVoiceSessions.id, sessionId));
const accept = vi.fn(store.stageDelivery);
const toolInput = (tool: SpekoToolEnvelope["tool"], args: SpekoToolEnvelope["args"], toolId = randomUUID()) => {
const envelope = { session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool, args } as SpekoToolEnvelope;
return { companyId, endpointId, sessionId, token: reserved.token, envelope, webhookId: `msg_${toolId}`, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex"), accept };
};
async function publication(state: "streaming" | "published" = "published") {
const [row] = await db.insert(chatPublications).values({ companyId, endpointId, conversationId, issueId, idempotencyKey: randomUUID(), state, payload: { text: "Approved result" } }).returning();
return row;
}
return { companyId, endpointId, agentId, connectionId, issueId, conversationId, callerId, sessionId, store, request, reserved, accept, toolInput, publication };
}
it.each(["hangup", "signed callback", "reply ended"])("wakes report refresh from real %s and advances it on a later callback", async trigger => {
const f = await inboundFixture();
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.id, f.sessionId));
await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller,
{ approve: true, approvalCode: f.call.approvalCode, issueId: f.issueId });
const [bound] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = () => vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); }, { timeout: 5000 });
const quiet = async () => {
const reads = vi.spyOn(db, "select"), transactions = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(10_000); expect(reads).not.toHaveBeenCalled(); expect(transactions).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); transactions.mockRestore(); }
};
try {
await f.service.registerReportWork(coordinator, () => true).ready;
await settle(); expect(coordinator.nextWakeAt()).toBeNull(); await quiet();
if (trigger === "hangup") await f.service.end(f.companyId, bound.sessionId!, f.input.caller);
else if (trigger === "signed callback") {
const ended = f.sign({ type: "call.status", call_id: f.call.providerSessionId, status: "ended" });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
} else {
const publication = await f.publication(); await f.store.enqueuePublication(f.companyId, publication.id);
f.transport.sendCallMessage.mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", false, 409));
await f.service.pushReplies(25, f.companyId, f.endpointId);
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
}
await settle(); expect(f.transport.callReport).toHaveBeenCalledOnce();
const [saved] = await db.select().from(chatVoiceReports).where(eq(chatVoiceReports.sessionId, bound.sessionId!));
expect(saved.status).toBe("available"); expect(coordinator.nextWakeAt()).toBe(saved.nextCheckAt.getTime());
await quiet(); expect(f.transport.callReport).toHaveBeenCalledOnce();
const report = f.sign({ type: "call.report", session_id: f.call.providerSessionId, organization_id: f.event.organization_id });
await f.service.inbound.lifecycle(f.endpoint.publicId, report.body, report.headers);
await settle(); expect(f.transport.callReport).toHaveBeenCalledTimes(2); expect(onError).not.toHaveBeenCalled();
} finally {
await coordinator.stop(); vi.useRealTimers();
const ended = f.sign({ type: "call.status", call_id: f.call.providerSessionId, status: "ended" });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.companyId, f.companyId));
}
});
it("retains a saved report and retries guest transcript projection after a document lock", async () => {
const f = await inboundFixture(true);
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.id, f.sessionId));
await f.tool("submit_request", { text: "Draft a public inquiry" });
const docs = documentService(db), key = `voice-transcript-${f.call.sessionId}`;
await docs.upsertIssueDocument({ issueId: f.call.intakeIssueId!, key, format: "markdown", body: "Older transcript" });
await docs.lockIssueDocument({ issueId: f.call.intakeIssueId!, key, lockedByUserId: f.callerId });
const ended = f.sign({ type: "call.report", session_id: f.call.providerSessionId, organization_id: f.event.organization_id });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = () => vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); }, { timeout: 5000 });
try {
await f.service.registerReportWork(coordinator, () => true).ready; await settle();
const [saved] = await db.select().from(chatVoiceReports).where(eq(chatVoiceReports.sessionId, f.call.sessionId!));
expect(saved.status).toBe("available"); expect(saved.transcript).toHaveLength(1);
expect((await docs.getIssueDocumentByKey(f.call.intakeIssueId!, key))!.body).toBe("Older transcript");
await docs.unlockIssueDocument(f.call.intakeIssueId!, key);
await vi.advanceTimersByTimeAsync(saved.nextCheckAt.getTime() - Date.now() + 1000); await settle();
const projected = await docs.getIssueDocumentByKey(f.call.intakeIssueId!, key);
expect(projected!.body).toContain("A test request");
expect(projected!.sourceTrust).toMatchObject({ preset: "low_trust_review", disposition: "quarantined" });
expect(f.transport.callReport).toHaveBeenCalledTimes(2); expect(onError).not.toHaveBeenCalled();
} finally {
await coordinator.stop(); vi.useRealTimers();
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.companyId, f.companyId));
}
});
it("limits a new pre-call wake to its admission instead of scanning endpoint history", async () => {
const f = await inboundFixture(); const hints: string[] = [];
const unsubscribe = subscribeDatabaseWork(db, DELIVERY_QUEUES.voiceInboundRetention,
(event, scopes) => { if (event === "settled") hints.push(...scopes ?? ["*"]); });
try {
const id = randomUUID(), incoming = f.sign({ ...f.event, session_id: id, call_id: id });
await f.service.inbound.lifecycle(f.endpoint.publicId, incoming.body, incoming.headers);
const [saved] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.providerSessionId, id));
expect(hints).toEqual([`admission:${saved.id}`]);
} finally {
unsubscribe(); await db.delete(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.companyId, f.companyId));
}
});
it("leaves due history intact during chat maintenance and deletes it through retention", async () => {
const f = await inboundFixture();
const ended = f.sign({ type: "call.status", call_id: f.call.providerSessionId, status: "ended" });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
await db.update(chatVoiceInboundCalls).set({ expiresAt: new Date(0), updatedAt: new Date(0) }).where(eq(chatVoiceInboundCalls.id, f.call.id));
const onError = vi.fn();
const maintenance = createChatReconciliationCoordinator({
processPendingDeliveries: async () => undefined, projectRunMilestones: async () => 0,
flushPublications: async () => undefined, onError,
});
try {
maintenance.reconcile(); await maintenance.drain();
expect(await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id))).toHaveLength(1);
expect(onError).not.toHaveBeenCalled();
} finally { maintenance.stop(); await maintenance.drain(); }
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
try {
await f.service.inbound.registerRetentionWork(coordinator, () => true).ready;
expect(await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id))).toHaveLength(0);
expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); }
});
it.each(["signed callback", "cleanup receipt"])("schedules retained history from a real %s", async confirmation => {
const f = await inboundFixture();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = () => vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 5000 });
const quiet = async (ms = 120_000) => {
const reads = vi.spyOn(db, "select"), transactions = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(ms); expect(reads).not.toHaveBeenCalled(); expect(transactions).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); transactions.mockRestore(); }
};
try {
await f.service.inbound.registerRetentionWork(coordinator, () => true).ready;
await settle(); expect(coordinator.nextWakeAt()).toBeNull(); await quiet(10_000);
if (confirmation === "signed callback") {
const ended = f.sign({ type: "call.status", call_id: f.call.providerSessionId, status: "ended" });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
} else {
await f.service.inbound.registerCleanupWork(coordinator, () => true).ready;
await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller,
{ approve: false, approvalCode: f.call.approvalCode });
await settle();
expect(f.transport.endSession).toHaveBeenCalledWith(f.call.providerSessionId);
}
await settle();
const [saved] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(saved.cleanupConfirmedAt).not.toBeNull();
const dueAt = Math.max(saved.updatedAt.getTime(), saved.expiresAt.getTime()) + 7 * 86_400_000;
expect(coordinator.nextWakeAt()).toBe(dueAt); await quiet();
await vi.advanceTimersByTimeAsync(dueAt - Date.now() + 1000); await settle();
expect(await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id))).toHaveLength(0);
expect(coordinator.nextWakeAt()).toBeNull(); await quiet(); expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); vi.useRealTimers(); }
});
it("returns a committed denial while provider cleanup is still waiting", async () => {
const f = await inboundFixture();
let release!: () => void;
const pendingProvider = new Promise<void>(resolve => { release = resolve; });
f.transport.verifyAgent.mockImplementation(async () => {
await pendingProvider;
return { id: "agent_test", organizationId: `org_${f.companyId}`, name: "Test voice" };
});
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
let denial: ReturnType<typeof f.service.inbound.decide> | undefined;
try {
await f.service.inbound.registerCleanupWork(coordinator, () => true).ready;
let returned = false;
denial = f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller,
{ approve: false, approvalCode: f.call.approvalCode }).then(result => { returned = true; return result; });
await vi.waitFor(() => expect(returned).toBe(true), { timeout: 2000 });
expect(await denial).toMatchObject({ state: "denied" });
await vi.waitFor(() => expect(f.transport.verifyAgent).toHaveBeenCalled(), { timeout: 5000 });
expect(f.transport.endSession).not.toHaveBeenCalled();
const [pending] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(pending.cleanupConfirmedAt).toBeNull();
expect(pending.cleanupNextAttemptAt).not.toBeNull();
release();
await vi.waitFor(async () => {
const [settled] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(settled.cleanupConfirmedAt).not.toBeNull();
}, { timeout: 5000 });
expect(f.transport.endSession).toHaveBeenCalledOnce(); expect(onError).not.toHaveBeenCalled();
} finally {
release(); await denial; await coordinator.stop();
}
});
it("wakes inbound cleanup from a real signed pre-call and retains an unconfirmed end", async () => {
const f = await inboundFixture();
const ended = f.sign({ type: "call.status", call_id: f.call.providerSessionId, status: "ended" });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = () => vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 5000 });
const quiet = async (ms: number) => {
const reads = vi.spyOn(db, "select"), transactions = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(ms); expect(reads).not.toHaveBeenCalled(); expect(transactions).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); transactions.mockRestore(); }
};
try {
await f.service.inbound.registerCleanupWork(coordinator, () => true).ready;
await settle(); expect(coordinator.nextWakeAt()).toBeNull(); await quiet(10_000);
const providerSessionId = randomUUID(), incoming = f.sign({ ...f.event, call_id: providerSessionId, session_id: providerSessionId });
await f.service.inbound.lifecycle(f.endpoint.publicId, incoming.body, incoming.headers);
await settle();
const [call] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.providerSessionId, providerSessionId));
expect(coordinator.nextWakeAt()).toBe(call.expiresAt.getTime());
await quiet(60_000); expect(f.transport.inspectSession).not.toHaveBeenCalled();
// Disabling new voice work must not erase an existing physical cleanup.
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: false });
await settle();
f.credentials.mockResolvedValue({ apiKey: "rotated-key-in-original-account", agentId: "agent_test" });
f.transport.endSession.mockResolvedValueOnce({ confirmed: false });
await vi.advanceTimersByTimeAsync(call.expiresAt.getTime() - Date.now()); await settle();
expect(f.transport.endSession).toHaveBeenCalledWith(providerSessionId);
const [pending] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, call.id));
expect(pending.cleanupConfirmedAt).toBeNull();
expect(coordinator.nextWakeAt()).toBe(pending.cleanupNextAttemptAt!.getTime());
await quiet(10_000); expect(f.transport.endSession).toHaveBeenCalledOnce();
await vi.advanceTimersByTimeAsync(pending.cleanupNextAttemptAt!.getTime() - Date.now()); await settle();
await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(coordinator.nextWakeAt()).toBeNull(); });
expect(f.transport.endSession).toHaveBeenCalledTimes(2); await quiet(120_000);
expect(onError).not.toHaveBeenCalled();
expect(f.transport.createPhoneSession).not.toHaveBeenCalled();
} finally { await coordinator.stop(); vi.useRealTimers(); await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true }); }
});
it("retains rejected inbound cleanup across provider failure and history retention", async () => {
const f = await inboundFixture(true);
f.transport.endSession.mockRejectedValueOnce(new Error("provider unavailable"));
const result = await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller,
{ approve: false, approvalCode: f.call.approvalCode });
expect(result.state).toBe("ended");
await f.service.inbound.reconcile(1000);
const [pending] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(pending.cleanupConfirmedAt).toBeNull(); expect(pending.cleanupNextAttemptAt).not.toBeNull();
await db.update(chatVoiceInboundCalls).set({ expiresAt: new Date(0), updatedAt: new Date(0) }).where(eq(chatVoiceInboundCalls.id, f.call.id));
await f.service.inbound.pruneHistory();
expect(await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id))).toHaveLength(1);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
try {
await f.service.inbound.registerCleanupWork(coordinator, () => true).ready;
expect(coordinator.nextWakeAt()).toBe(pending.cleanupNextAttemptAt!.getTime());
await vi.advanceTimersByTimeAsync(pending.cleanupNextAttemptAt!.getTime() - Date.now());
await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); expect(coordinator.nextWakeAt()).toBeNull(); });
expect(f.transport.endSession).toHaveBeenCalledTimes(2);
expect((await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id)))[0].cleanupConfirmedAt).not.toBeNull();
expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); vi.useRealTimers(); }
});
it("does not clean up an inbound call through a different provider account", async () => {
const f = await inboundFixture();
f.credentials.mockResolvedValue({ apiKey: "key-for-another-account", agentId: "other-agent" });
f.transport.verifyAgent.mockResolvedValue({ id: "other-agent", organizationId: "another-account", name: "Other voice" });
await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller,
{ approve: false, approvalCode: f.call.approvalCode });
await f.service.inbound.reconcile(1000);
expect(f.transport.inspectSession).not.toHaveBeenCalled(); expect(f.transport.endSession).not.toHaveBeenCalled();
const [pending] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(pending.cleanupConfirmedAt).toBeNull(); expect(pending.cleanupNextAttemptAt).not.toBeNull();
// A usable key for the saved account restores cleanup without redialing.
f.credentials.mockResolvedValue({ apiKey: "restored-account-key", agentId: "agent_test" });
f.transport.verifyAgent.mockResolvedValue({ id: "agent_test", organizationId: `org_${f.companyId}`, name: "Test voice" });
await db.update(chatVoiceInboundCalls).set({ cleanupNextAttemptAt: new Date(0) }).where(eq(chatVoiceInboundCalls.id, f.call.id));
await f.service.inbound.reconcile(1000);
expect(f.transport.endSession).toHaveBeenCalledWith(f.call.providerSessionId);
expect((await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id)))[0].cleanupConfirmedAt).not.toBeNull();
expect(f.transport.createPhoneSession).not.toHaveBeenCalled();
});
it("restores blocked replies through the production live-call recovery wiring", async () => {
const f = await pushFixture();
await db.update(companyMemberships).set({ status: "suspended" }).where(eq(companyMemberships.principalId, f.callerId));
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn();
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = () => vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 5000 });
try {
// Reply denial is remembered by the service's own schedule. No test calls
// authorityAvailable: only production recovery may reactivate this reply.
await f.service.registerReplyWork(coordinator, () => true).ready;
await settle();
expect(coordinator.nextWakeAt()).toBeNull();
expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
await db.update(companyMemberships).set({ status: "active" }).where(eq(companyMemberships.principalId, f.callerId));
await f.service.registerRecoveryWork(coordinator, () => true).ready;
await vi.advanceTimersByTimeAsync(31_000);
await vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 5000 });
expect(f.transport.inspectSession).toHaveBeenCalled();
expect(f.transport.endSession).not.toHaveBeenCalled();
// Repeated healthy inspections do not republish the delivered answer.
await vi.advanceTimersByTimeAsync(31_000); await settle();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
f.transport.inspectSession.mockResolvedValue({ status: "ended", endedAt: new Date().toISOString() });
await vi.advanceTimersByTimeAsync(31_000); await settle();
await vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(coordinator.nextWakeAt()).toBeNull();
expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 5000 });
const reads = vi.spyOn(db, "select"), transactions = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(120_000); expect(reads).not.toHaveBeenCalled(); expect(transactions).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); transactions.mockRestore(); }
expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); vi.useRealTimers(); }
});
it.each(["hangup", "revocation", "feature disable", "endpoint pause", "connection disable", "task reassignment"] as const)("wakes live recovery from a real start and settles after %s", async reason => {
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
const f = await nativeServiceFixture();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = async (terminal = false) => vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(idleWorkSnapshot().active).toBe(0);
if (terminal) expect(coordinator.nextWakeAt()).toBeNull();
else expect(coordinator.nextWakeAt()).toBeGreaterThan(Date.now());
});
const quiet = async () => {
const reads = vi.spyOn(db, "select"), writes = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(120_000); expect(reads).not.toHaveBeenCalled(); expect(writes).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); writes.mockRestore(); }
};
try {
await f.service.registerRecoveryWork(coordinator, () => true).ready;
await settle(true); await quiet();
const started = await f.service.start(f.input); await settle();
expect(f.transport.createBrowserSession).toHaveBeenCalledOnce();
expect(f.transport.inspectSession).not.toHaveBeenCalled();
if (reason === "hangup") f.transport.inspectSession.mockResolvedValue({ status: "ended", endedAt: new Date().toISOString() });
if (reason === "revocation") await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.callerId));
if (reason === "feature disable") await instanceSettingsService(db).updateExperimental({ enableChatConnectors: false });
if (reason === "endpoint pause") {
const chat = chatChannelService(db, { heartbeat: { wakeup: vi.fn() } });
try { await chat.configure(f.endpointId, { action: "pause" }, f.callerId); }
finally { await chat.shutdown(); }
}
if (reason === "connection disable") await toolAccessService(db).updateConnection(f.connectionId, { enabled: false });
if (reason === "task reassignment") await db.update(issues).set({ assigneeAgentId: null }).where(eq(issues.id, f.issueId));
if (["hangup", "revocation", "task reassignment"].includes(reason)) await vi.advanceTimersByTimeAsync(31_000);
await settle(true);
const [row] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, started.session.id));
expect(row.state).toBe("ended");
if (reason !== "hangup") expect(f.transport.endSession).toHaveBeenCalledOnce();
await quiet(); expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); f.service.unregisterAgentTools(); vi.useRealTimers(); }
});
it("retires uncertain creation at its saved grace deadline without creating another provider call", async () => {
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
const f = await nativeServiceFixture();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
try {
await f.service.registerRecoveryWork(coordinator, () => true).ready;
f.transport.createBrowserSession.mockRejectedValue(new SpekoProviderError("provider_unavailable", true));
await expect(f.service.start({ ...f.input, maxDurationSeconds: 30 })).rejects.toMatchObject({ status: 409 });
const row = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.idempotencyKey, f.input.idempotencyKey)).then(rows => rows[0]);
const deadline = row.expiresAt.getTime() + 120_001;
await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); expect(coordinator.nextWakeAt()).toBe(deadline); });
const reads = vi.spyOn(db, "select");
try { await vi.advanceTimersByTimeAsync(100_000); expect(reads).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); }
await vi.advanceTimersByTimeAsync(deadline - Date.now());
await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); expect(coordinator.nextWakeAt()).toBeNull(); });
expect((await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, row.id)))[0].state).toBe("expired");
expect(f.transport.createBrowserSession).toHaveBeenCalledOnce(); expect(f.transport.endSession).not.toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); f.service.unregisterAgentTools(); vi.useRealTimers(); }
});
it("recovers a real guest session and settles from its signed lifecycle event", async () => {
const f = await inboundFixture(true);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn(), coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
const settle = async () => vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); });
try {
await f.service.registerRecoveryWork(coordinator, () => true).ready;
expect(coordinator.nextWakeAt()).toBeNull();
await f.tool("submit_request", { text: "Help draft a public question" });
await settle(); expect(coordinator.nextWakeAt()).toBeGreaterThan(Date.now());
await vi.advanceTimersByTimeAsync(31_000); await settle();
expect(f.transport.inspectSession).toHaveBeenCalledWith(f.event.session_id);
expect(f.transport.endSession).not.toHaveBeenCalled();
const ended = f.sign({ type: "call.status", call_id: f.event.call_id, status: "ended" });
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
await settle(); expect(coordinator.nextWakeAt()).toBeNull();
const reads = vi.spyOn(db, "select"), writes = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(120_000); expect(reads).not.toHaveBeenCalled(); expect(writes).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); writes.mockRestore(); }
expect(onError).not.toHaveBeenCalled();
} finally { await coordinator.stop(); f.service.unregisterAgentTools(); vi.useRealTimers(); }
});
it("reports signing-secret presence for interrupted setup without returning credential refs", async () => {
const f = await fixture();
const chat = chatChannelService(db, {heartbeat: {wakeup: vi.fn()}});
expect((await chat.get(f.endpointId)).setup.webhookSecretConfigured).toBe(false);
await db.update(toolConnections).set({credentialSecretRefs: [{configPath: "credentials.signingSecret", secretId: randomUUID()}]}).where(eq(toolConnections.id, f.connectionId));
const endpoint = await chat.get(f.endpointId);
expect(endpoint.setup.webhookSecretConfigured).toBe(true);
expect(endpoint).not.toHaveProperty("credentialSecretRefs");
});
it("deduplicates concurrent session requests before creating a second task", async () => {
const f = await fixture();
const results = await Promise.all([f.store.reserve(f.request), f.store.reserve(f.request)]);
expect(results.every((r) => !r.created && r.session.id === f.sessionId)).toBe(true);
expect(f.request.prepareBinding).toHaveBeenCalledTimes(1);
await expect(f.store.reserve({ ...f.request, requestFingerprint: "different" })).rejects.toMatchObject({ status: 409 });
expect(JSON.stringify(serializeVoiceSession(f.reserved.session))).not.toContain(f.reserved.token);
expect(serializeVoiceSession(f.reserved.session)).not.toHaveProperty("toolTokenHash");
});
it("accepts one durable delivery across concurrent tool retries and survives store restart", async () => {
const f = await fixture(), request = f.toolInput("submit_request", { text: "Do the work" });
const [a, b] = await Promise.all([f.store.runTool(request), f.store.runTool(request)]);
expect(a).toEqual(b);
expect(f.accept).toHaveBeenCalledTimes(1);
const restarted = voiceSessionStore(db, { allowLocalBoard: false });
expect(await restarted.runTool(request)).toEqual(a);
expect(await db.select().from(chatVoiceToolCalls).where(eq(chatVoiceToolCalls.sessionId, f.sessionId))).toHaveLength(1);
await expect(restarted.runTool({ ...request, fingerprint: "changed" })).rejects.toMatchObject({ status: 409 });
});
it("rejects Backlog voice work before claiming that it was accepted", async () => {
const f = await fixture();
await db.update(issues).set({status: "backlog"}).where(eq(issues.id, f.issueId));
await expect(f.store.runTool(f.toolInput("submit_request", {text: "Inspect disk space"}))).rejects.toMatchObject({status: 409, message: "Move this task to Todo in Paperclip before requesting voice work."});
expect(await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpointId))).toHaveLength(0);
expect(await db.select().from(chatVoiceToolCalls).where(eq(chatVoiceToolCalls.sessionId, f.sessionId))).toHaveLength(0);
});
it("delivers urgent live-call guidance only after accepted work and retires it after hangup or revocation", async () => {
const f = await fixture(), scope = { companyId: f.companyId, issueId: f.issueId, agentId: f.agentId };
expect(await liveVoiceExecutionGuidance(db, scope)).toBeNull();
await f.store.runTool(f.toolInput("submit_request", { text: "Inspect disk space" }));
expect(await liveVoiceExecutionGuidance(db, scope)).toContain("Get a useful spoken response to them as quickly as possible");
await db.update(chatEndpoints).set({status: "verifying"}).where(eq(chatEndpoints.id, f.endpointId));
expect(await liveVoiceExecutionGuidance(db, scope)).toContain("Live voice request");
await db.update(chatEndpoints).set({status: "active"}).where(eq(chatEndpoints.id, f.endpointId));
expect(await liveVoiceExecutionGuidance(db, { ...scope, companyId: randomUUID() })).toBeNull();
expect(await liveVoiceExecutionGuidance(db, { ...scope, agentId: randomUUID() })).toBeNull();
expect(await liveVoiceExecutionGuidance(db, scope, new Date(Date.now() + 900_000))).toBeNull();
await db.update(chatEndpoints).set({setup: {step: "complete", runtimeGeneration: 2}}).where(eq(chatEndpoints.id, f.endpointId));
expect(await liveVoiceExecutionGuidance(db, scope)).toBeNull();
await db.update(chatEndpoints).set({setup: {step: "complete", runtimeGeneration: 1}, status: "paused"}).where(eq(chatEndpoints.id, f.endpointId));
expect(await liveVoiceExecutionGuidance(db, scope)).toBeNull();
await db.update(chatEndpoints).set({status: "active"}).where(eq(chatEndpoints.id, f.endpointId));
await db.update(chatVoiceSessions).set({state: "ended", endedAt: new Date()}).where(eq(chatVoiceSessions.id, f.sessionId));
expect(await liveVoiceExecutionGuidance(db, scope)).toBeNull();
});
it("reports queueing, running follow-ups, and finished execution without inventing an answer or exposing logs", async () => {
const f = await fixture();
const read = () => f.store.runTool(f.toolInput("get_updates", {cursor: 0}));
expect(await read()).toMatchObject({work: {state: "idle"}, updates: []});
const accepted = await f.store.runTool(f.toolInput("submit_request", {text: "Check disk space"}));
expect(await read()).toMatchObject({work: {state: "queued"}, updates: []});
// A different task's running agent must not become this call's status.
const [otherTask] = await db.insert(issues).values({companyId: f.companyId, title: "Private other task", assigneeAgentId: f.agentId}).returning();
await db.insert(heartbeatRuns).values({companyId: f.companyId, agentId: f.agentId, issueId: otherTask.id, status: "running", error: "private log"});
expect(await read()).toMatchObject({work: {state: "queued"}});
await db.update(chatDeliveries).set({state: "processed"}).where(eq(chatDeliveries.id, accepted.requestId as string));
const [run] = await db.insert(heartbeatRuns).values({companyId: f.companyId, agentId: f.agentId, issueId: f.issueId, status: "running", contextSnapshot: {issueId: f.issueId}, error: "private log"}).returning();
await db.update(issues).set({executionRunId: run.id}).where(eq(issues.id, f.issueId));
expect(await read()).toMatchObject({work: {state: "running", followUpQueued: false}});
const followUp = await f.store.runTool(f.toolInput("submit_request", {text: "Please also report free space"}));
expect(await read()).toMatchObject({work: {state: "running", followUpQueued: true}});
await db.update(chatDeliveries).set({state: "processed"}).where(eq(chatDeliveries.id, followUp.requestId as string));
await db.update(heartbeatRuns).set({status: "succeeded"}).where(eq(heartbeatRuns.id, run.id));
const waiting = await read();
expect(waiting).toMatchObject({status: "pending", work: {state: "awaiting_reply"}, updates: []});
expect(JSON.stringify(waiting)).not.toContain("private log");
expect(JSON.stringify(waiting)).not.toContain("Execution finished");
const publication = await f.publication(); await f.store.enqueuePublication(f.companyId, publication.id);
expect(await read()).toMatchObject({status: "updates", updates: [{text: "Approved result"}]});
await db.update(heartbeatRuns).set({status: "failed", error: "secret provider credentials"}).where(eq(heartbeatRuns.id, run.id));
const failed = await read(); expect(failed).toMatchObject({work: {state: "failed"}});
expect(JSON.stringify(failed)).not.toContain("secret provider credentials");
});
it("advertises server push without restarting work or changing browser and terminal behavior", async () => {
const f = await fixture();
const read = () => f.store.runTool(f.toolInput("get_updates", {cursor: 0}));
await db.update(chatVoiceSessions).set({mode: "inbound_phone"}).where(eq(chatVoiceSessions.id, f.sessionId));
expect(await read()).not.toHaveProperty("nextAction");
const accepted = await f.store.runTool(f.toolInput("submit_request", {text: "Check disk space"}));
const request = f.toolInput("get_updates", {cursor: 0});
const pending = await f.store.runTool(request);
expect(pending).toMatchObject({status: "pending", updates: [], delivery: expect.stringContaining("pushed into this live call")});
expect(await f.store.runTool(request)).toEqual(pending);
expect(await f.store.runTool(f.toolInput("get_updates", {cursor: 0, repeat: true}))).not.toHaveProperty("nextAction");
await db.update(chatVoiceSessions).set({mode: "browser"}).where(eq(chatVoiceSessions.id, f.sessionId));
expect(await read()).not.toHaveProperty("nextAction");
await db.insert(chatVoiceCallbacks).values({companyId: f.companyId, endpointId: f.endpointId, userId: f.callerId, phoneNumber: "+12015551234", enabled: true});
await db.update(chatVoiceSessions).set({mode: "outbound_phone"}).where(eq(chatVoiceSessions.id, f.sessionId));
expect(await read()).not.toHaveProperty("nextAction");
await db.update(chatDeliveries).set({state: "failed"}).where(eq(chatDeliveries.id, accepted.requestId as string));
expect(await read()).not.toHaveProperty("nextAction");
const publication = await f.publication(); await f.store.enqueuePublication(f.companyId, publication.id);
const delivered = await read();
expect(delivered).toMatchObject({status: "updates", updates: [{text: "Approved result"}]});
expect(delivered).not.toHaveProperty("nextAction");
expect(await db.select().from(chatDeliveries).where(eq(chatDeliveries.id, accepted.requestId as string))).toHaveLength(1);
});
it("rejects a replayed webhook identity before accepting different work", async () => {
const f = await fixture(), original = f.toolInput("submit_request", { text: "First request" });
await f.store.runTool(original);
await expect(f.store.runTool({ ...f.toolInput("submit_request", { text: "Different request" }), webhookId: original.webhookId })).rejects.toMatchObject({ status: 409 });
expect(f.accept).toHaveBeenCalledTimes(1);
expect(await db.select().from(chatVoiceToolCalls).where(eq(chatVoiceToolCalls.sessionId, f.sessionId))).toHaveLength(1);
});
it("repeats only an already delivered answer and retains truthful playback evidence", async () => {
const f = await fixture(); const first = await f.publication();
await f.store.enqueuePublication(f.companyId, first.id);
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 0, repeat: true }))).toMatchObject({ status: "pending", updates: [] });
const delivered = await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
const next = await f.publication(); await f.store.enqueuePublication(f.companyId, next.id);
const repeated = await f.store.runTool(f.toolInput("get_updates", { cursor: 1, repeat: true }));
expect(repeated).toMatchObject({ repeated: true, playback: "unknown", updates: delivered.updates });
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 1 }))).toMatchObject({ updates: [{ publicationId: next.id }] });
const rows = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.sessionId));
expect(rows).toHaveLength(2); expect(rows.every(row => row.deliveredAt && row.spokenAt === null)).toBe(true);
expect(f.accept).not.toHaveBeenCalled();
await db.update(companyMemberships).set({ status: "suspended" }).where(eq(companyMemberships.companyId, f.companyId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 2, repeat: true }))).rejects.toMatchObject({ status: 403 });
});
it("does not restore legacy access when a managed connection loses its final install", async () => {
const f = await fixture();
await db.insert(connectionGrants).values({ companyId: f.companyId, connectionId: f.connectionId, kind: "organization", isDefault: true });
const [install] = await db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: f.connectionId, targetType: "agent", targetId: f.agentId }).returning();
await expect(f.store.notification(f.companyId, f.sessionId, f.callerId)).resolves.toBeNull();
await db.delete(toolConnectionInstalls).where(eq(toolConnectionInstalls.id, install.id));
await expect(f.store.notification(f.companyId, f.sessionId, f.callerId)).rejects.toMatchObject({ status: 403 });
await expect(f.store.runTool(f.toolInput("submit_request", { text: "Unauthorized after uninstall" }))).rejects.toMatchObject({ status: 403 });
expect(f.accept).not.toHaveBeenCalled();
});
it("does not replay an old tool receipt after its approved publication changes", async () => {
const f = await fixture(); const publication = await f.publication();
await f.store.enqueuePublication(f.companyId, publication.id);
const request = f.toolInput("get_updates", { cursor: 0 });
await f.store.runTool(request);
await db.update(chatPublications).set({ payload: { text: "Corrected approved response" } }).where(eq(chatPublications.id, publication.id));
await expect(f.store.runTool(request)).rejects.toMatchObject({ status: 409 });
});
it("repeats a missed audible answer after rejoining, without replaying it automatically or crossing callers", async () => {
const f = await fixture(); const publication = await f.publication();
await f.store.enqueuePublication(f.companyId, publication.id);
await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
await db.update(chatVoiceSessions).set({ state: "ended", endedAt: new Date() }).where(eq(chatVoiceSessions.id, f.sessionId));
async function rejoin(callerId: string) {
const reserved = await f.store.reserve({ ...f.request, caller: { id: callerId, authority: "member" }, idempotencyKey: randomUUID() });
if (!reserved.created) throw new Error("Expected a new media call");
const providerSessionId = randomUUID(), toolId = randomUUID();
await db.update(chatVoiceSessions).set({ state: "active", providerSessionId }).where(eq(chatVoiceSessions.id, reserved.session.id));
expect(await f.store.notification(f.companyId, reserved.session.id, callerId)).toBeNull();
const envelope = { session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool: "get_updates" as const, args: { cursor: 0, repeat: true } };
return f.store.runTool({ ...f.toolInput("get_updates", { cursor: 0 }), sessionId: reserved.session.id, token: reserved.token, envelope, webhookId: toolId, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex") });
}
expect(await rejoin(f.callerId)).toMatchObject({ updates: [{ publicationId: publication.id }], repeated: true, playback: "unknown" });
const otherCaller = randomUUID();
await db.insert(companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: otherCaller, status: "active", membershipRole: "member" });
expect(await rejoin(otherCaller)).toMatchObject({ status: "pending", updates: [] });
});
it.each(["dispatching", "unknown", "completed"])("does not recollect a prior call's %s delivery or accepted prefix on rejoin", async status => {
const f = await fixture(), publication = await f.publication();
await f.store.enqueuePublication(f.companyId, publication.id);
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.sessionId));
await db.insert(chatActions).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId,
kind: "speko_voice_reply_push", providerActionId: `voice_reply_push:${reply.id}`, status,
payload: {sessionId: f.sessionId, replyId: reply.id, publicationId: publication.id, partIndex: 0, partCount: 2, questionNotification: false}});
await db.update(chatVoiceSessions).set({state: "ended", endedAt: new Date()}).where(eq(chatVoiceSessions.id, f.sessionId));
const reserved = await f.store.reserve({...f.request, idempotencyKey: randomUUID()});
await db.update(chatVoiceSessions).set({state: "active", providerSessionId: randomUUID()}).where(eq(chatVoiceSessions.id, reserved.session.id));
expect(await f.store.notification(f.companyId, reserved.session.id, f.callerId)).toBeNull();
await f.store.enqueuePublication(f.companyId, publication.id);
expect(await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, reserved.session.id))).toHaveLength(0);
});
it("marks media active only after a valid session-scoped provider request", async () => {
const f = await fixture();
await db.update(chatVoiceSessions).set({ state: "connecting" }).where(eq(chatVoiceSessions.id, f.sessionId));
const request = f.toolInput("get_updates", { cursor: 0 });
await expect(f.store.runTool({ ...request, token: "invalid" })).rejects.toMatchObject({ status: 403 });
expect((await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.sessionId)))[0].state).toBe("connecting");
await f.store.runTool(request);
expect((await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.sessionId)))[0].state).toBe("active");
});
it("rolls back both acceptance and receipt on failure", async () => {
const f = await fixture();
const request = f.toolInput("submit_request", { text: "Do the work" });
await expect(f.store.runTool({ ...request, accept: async (...args) => { await f.accept(...args); throw new Error("crash"); } })).rejects.toThrow("crash");
expect(await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpointId))).toHaveLength(0);
expect(await db.select().from(chatVoiceToolCalls).where(eq(chatVoiceToolCalls.sessionId, f.sessionId))).toHaveLength(0);
expect(await f.store.runTool(request)).toMatchObject({ status: "accepted" });
});
it("never substitutes company, session, or token authority", async () => {
const a = await fixture(), b = await fixture(), request = a.toolInput("submit_request", { text: "Private work" });
await expect(a.store.runTool({ ...request, companyId: b.companyId })).rejects.toMatchObject({ status: 404 });
await expect(a.store.runTool({ ...request, endpointId: b.endpointId })).rejects.toMatchObject({ status: 403 });
await expect(a.store.runTool({ ...request, token: b.reserved.token })).rejects.toMatchObject({ status: 403 });
await expect(a.store.runTool({ ...request, envelope: { ...request.envelope, session_id: b.reserved.session.id } })).rejects.toMatchObject({ status: 403 });
expect(a.accept).not.toHaveBeenCalled();
});
it.each(["paused", "revoked", "archived"] as const)("denies new tools after endpoint becomes %s", async (status) => {
const f = await fixture();
await db.update(chatEndpoints).set({ status }).where(eq(chatEndpoints.id, f.endpointId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
});
it("fences old generations and credential rotations", async () => {
const f = await fixture();
await db.update(chatEndpoints).set({ setup: { step: "complete", runtimeGeneration: 2 } }).where(eq(chatEndpoints.id, f.endpointId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
await db.update(chatEndpoints).set({ setup: { step: "complete", runtimeGeneration: 1 } }).where(eq(chatEndpoints.id, f.endpointId));
await db.update(toolConnections).set({ credentialSecretRefs: [{ configPath: "apiKey", secretId: randomUUID(), versionSelector: "latest" }] }).where(eq(toolConnections.id, f.connectionId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
});
it("retires active calls when a latest credential rotates without changing its reference", async () => {
const f = await fixture();
const [secret] = await db.insert(companySecrets).values({ companyId: f.companyId, key: "SPEKO_TEST", name: "Speko fixture" }).returning();
const refs = [{ configPath: "apiKey", secretId: secret.id, versionSelector: "latest" as const }];
await db.update(toolConnections).set({ credentialSecretRefs: refs }).where(eq(toolConnections.id, f.connectionId));
await db.update(chatVoiceSessions).set({ credentialFingerprint: await currentVoiceCredentialFingerprint(db, f.companyId, refs) }).where(eq(chatVoiceSessions.id, f.sessionId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).resolves.toMatchObject({ status: "pending" });
await db.update(companySecrets).set({ latestVersion: 2 }).where(eq(companySecrets.id, secret.id));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
await expect(f.store.notification(f.companyId, f.sessionId, f.callerId)).rejects.toMatchObject({ status: 403 });
});
it("honors the credential's current human audience and revoked grants", async () => {
const f = await fixture();
const [grant] = await db.insert(connectionGrants).values({ companyId: f.companyId, connectionId: f.connectionId, kind: "organization", isDefault: true }).returning();
await db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: f.connectionId, targetType: "agent", targetId: f.agentId });
const [member] = await db.insert(connectionGrantMembers).values({ companyId: f.companyId, grantId: grant.id, subjectType: "user", subjectId: "another-person" }).returning();
await expect(f.store.runTool(f.toolInput("submit_request", { text: "Denied" }))).rejects.toMatchObject({ status: 403 });
expect(f.accept).not.toHaveBeenCalled();
await db.update(connectionGrantMembers).set({ subjectId: f.callerId }).where(eq(connectionGrantMembers.id, member.id));
await expect(f.store.runTool(f.toolInput("submit_request", { text: "Allowed" }))).resolves.toMatchObject({ status: "accepted" });
await db.update(connectionGrants).set({ status: "revoked" }).where(eq(connectionGrants.id, grant.id));
await expect(f.store.reserve(f.request)).rejects.toMatchObject({ status: 403 });
await expect(f.store.notification(f.companyId, f.sessionId, f.callerId)).rejects.toMatchObject({ status: 403 });
});
it("rechecks membership and rejects read-only users and unapproved callers", async () => {
const f = await fixture();
await db.update(companyMemberships).set({ membershipRole: "viewer" }).where(eq(companyMemberships.principalId, f.callerId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
await db.update(companyMemberships).set({ membershipRole: "member" }).where(eq(companyMemberships.principalId, f.callerId));
await db.update(chatVoiceSessions).set({ callerAuthority: "pending_approval" }).where(eq(chatVoiceSessions.id, f.sessionId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
await expect(f.store.reserve({ ...f.request, caller: { id: "local-board", authority: "local_board" } })).rejects.toMatchObject({ status: 403 });
});
it("preserves accepted work after hangup but still enforces revoked access", async () => {
const f = await fixture();
await f.store.runTool(f.toolInput("submit_request", { text: "Finish this after I hang up" }));
await db.update(chatVoiceSessions).set({ state: "ended", endedAt: new Date() }).where(eq(chatVoiceSessions.id, f.sessionId));
expect(await db.transaction((tx) => f.store.authorizePrincipal(tx, f.companyId, f.endpointId, `voice:${f.sessionId}`))).toMatchObject({ allowed: true, userId: f.callerId });
await expect(f.store.runTool(f.toolInput("submit_request", { text: "Late replay" }))).rejects.toMatchObject({ status: 403 });
await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.callerId));
await expect(db.transaction((tx) => f.store.authorizePrincipal(tx, f.companyId, f.endpointId, `voice:${f.sessionId}`))).rejects.toMatchObject({ status: 403 });
});
it("publishes each approved answer once, preserves retry receipts, and never assumes speech", async () => {
const f = await fixture(), publication = await f.publication("streaming");
await Promise.all([f.store.enqueuePublication(f.companyId, publication.id), f.store.enqueuePublication(f.companyId, publication.id)]);
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toBeNull();
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).toMatchObject({ status: "pending", updates: [] });
await db.update(chatPublications).set({ state: "published" }).where(eq(chatPublications.id, publication.id));
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toEqual({ sessionId: f.sessionId, generation: 1, publicationId: publication.id, attempt: 0 });
const request = f.toolInput("get_updates", { cursor: 0 }), reply = await f.store.runTool(request);
expect(reply).toMatchObject({ cursor: 1, updates: [{ text: "Approved result", publicationId: publication.id }] });
expect(await f.store.runTool(request)).toEqual(reply);
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).toMatchObject({ status: "pending", updates: [] });
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toBeNull();
const records = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.sessionId));
expect(records).toHaveLength(1);
expect(records[0].deliveredAt).toBeInstanceOf(Date);
expect(records[0].spokenAt).toBeNull();
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 999 }))).rejects.toMatchObject({ status: 409 });
});
it("skips routine progress and retries the oldest unclaimed answer without duplicating delivery", async () => {
const f = await fixture(), progress = await f.publication();
await db.update(chatPublications).set({ payload: { text: "Working", progressState: "working" } }).where(eq(chatPublications.id, progress.id));
await f.store.enqueuePublication(f.companyId, progress.id);
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toBeNull();
const first = await f.publication(), second = await f.publication();
await f.store.enqueuePublication(f.companyId, first.id);
await f.store.enqueuePublication(f.companyId, second.id);
await db.update(chatVoiceReplies).set({ createdAt: new Date(Date.now() - 21_000) }).where(eq(chatVoiceReplies.publicationId, first.id));
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toMatchObject({ publicationId: first.id, attempt: 2 });
const reply = await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
expect(reply).toMatchObject({ cursor: 2, updates: [{ publicationId: first.id }, { publicationId: second.id }] });
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 2 }))).toMatchObject({ status: "pending", updates: [] });
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toBeNull();
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).toMatchObject({ status: "pending", updates: [] });
});
it("bounds one retrieval to eight replies and stops at a clarification", async () => {
const f = await fixture();
const publications = [];
for (let index = 0; index < 10; index++) {
const publication = await f.publication(); publications.push(publication);
await f.store.enqueuePublication(f.companyId, publication.id);
}
await db.update(chatPublications).set({ payload: { text: "Please clarify", interactionId: randomUUID() } }).where(eq(chatPublications.id, publications[2]!.id));
const first = await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
expect(first).toMatchObject({ cursor: 3, updates: publications.slice(0, 3).map(p => ({ publicationId: p.id })) });
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toMatchObject({ publicationId: publications[3]!.id });
const next = await f.store.runTool(f.toolInput("get_updates", { cursor: 3 }));
expect(next).toMatchObject({ cursor: 10, updates: publications.slice(3).map(p => ({ publicationId: p.id })) });
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toBeNull();
});
it("leaves excess replies pending after a bounded batch", async () => {
const f = await fixture();
for (let index = 0; index < 9; index++) { const p = await f.publication(); await f.store.enqueuePublication(f.companyId, p.id); }
const first = await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
expect(first.cursor).toBe(8); expect(first.updates).toHaveLength(8);
const next = await f.store.runTool(f.toolInput("get_updates", { cursor: 8 }));
expect(next.cursor).toBe(9); expect(next.updates).toHaveLength(1);
});
it("delivers an earlier stream that completes after a newer answer without moving the cursor backwards", async () => {
const f = await fixture(), slow = await f.publication("streaming"), ready = await f.publication();
await f.store.enqueuePublication(f.companyId, slow.id);
await f.store.enqueuePublication(f.companyId, ready.id);
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).toMatchObject({ cursor: 2, updates: [{ publicationId: ready.id }] });
await db.update(chatPublications).set({ state: "published" }).where(eq(chatPublications.id, slow.id));
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 2 }))).toMatchObject({ cursor: 2, updates: [{ cursor: 1, publicationId: slow.id }] });
expect(await f.store.notification(f.companyId, f.sessionId, f.callerId)).toBeNull();
});
it("denies changed task assignment, expired calls, and another caller's notification feed", async () => {
const f = await fixture();
await expect(f.store.notification(f.companyId, f.sessionId, randomUUID())).rejects.toMatchObject({ status: 403 });
await db.update(issues).set({ assigneeAgentId: null }).where(eq(issues.id, f.issueId));
await expect(f.store.runTool(f.toolInput("submit_request", { text: "Wrong agent" }))).rejects.toMatchObject({ status: 403 });
await db.update(issues).set({ assigneeAgentId: f.agentId }).where(eq(issues.id, f.issueId));
await db.update(chatVoiceSessions).set({ createdAt: new Date(Date.now() - 120_000), expiresAt: new Date(Date.now() - 1) }).where(eq(chatVoiceSessions.id, f.sessionId));
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
});
it("rechecks same-company private task access on tools, notifications, retries and queued work", async () => {
const f = await fixture();
await f.store.runTool(f.toolInput("submit_request", { text: "Accepted before privacy changes" }));
await db.update(issues).set({ visibility: "private" }).where(eq(issues.id, f.issueId));
await expect(f.store.reserve(f.request)).rejects.toMatchObject({ status: 403 });
await expect(f.store.notification(f.companyId, f.sessionId, f.callerId)).rejects.toMatchObject({ status: 403 });
await expect(f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).rejects.toMatchObject({ status: 403 });
await expect(db.transaction(tx => f.store.authorizePrincipal(tx, f.companyId, f.endpointId, `voice:${f.sessionId}`))).rejects.toMatchObject({ status: 403 });
});
it("recovers results completed after hangup without replaying already delivered results", async () => {
const f = await fixture(), delivered = await f.publication();
await f.store.enqueuePublication(f.companyId, delivered.id);
await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
await db.update(chatVoiceSessions).set({ state: "ended", endedAt: new Date() }).where(eq(chatVoiceSessions.id, f.sessionId));
const missed = await f.publication();
await f.store.enqueuePublication(f.companyId, missed.id);
const resumed = await f.store.reserve({ ...f.request, idempotencyKey: randomUUID() });
await db.update(chatVoiceSessions).set({ state: "active", providerSessionId: randomUUID() }).where(eq(chatVoiceSessions.id, resumed.session.id));
expect(await f.store.notification(f.companyId, resumed.session.id, f.callerId)).toMatchObject({ publicationId: missed.id });
const replies = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, resumed.session.id));
expect(replies.map(r => r.publicationId)).toEqual([missed.id]);
});
async function questionFixture() {
const f = await fixture(), id = randomUUID();
await db.insert(issueThreadInteractions).values({ id, companyId: f.companyId, issueId: f.issueId, kind: "ask_user_questions",
createdByAgentId: f.agentId, addresseeUserId: f.callerId, effectiveResolverPolicy: "addressee_only",
payload: { version: 1, questions: [{ id: "color", prompt: "Which color?", selectionMode: "single", required: true,
allowOther: false, options: [{ id: "cobalt", label: "Cobalt" }] }] } });
const publication = await f.publication();
await db.update(chatPublications).set({ payload: { text: "Which color?", interactionId: id, progressState: "waiting_for_input" } }).where(eq(chatPublications.id, publication.id));
await f.store.enqueuePublication(f.companyId, publication.id);
const answer = f.toolInput("answer_question", { interactionId: id, answers: [{ questionId: "color", optionIds: ["cobalt"] }] });
return { ...f, id, answer };
}
it("answers only a question presented to this session and writes one canonical continuation receipt", async () => {
const f = await questionFixture();
await expect(f.store.runTool(f.answer)).rejects.toMatchObject({ status: 403 });
expect(await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }))).toMatchObject({ updates: [{ question: { interactionId: f.id, questions: [{ id: "color" }] } }] });
const answer = await f.store.runTool(f.answer);
expect(answer).toMatchObject({ status: "answered", interactionId: f.id });
expect(await f.store.runTool(f.answer)).toEqual(answer);
expect(await db.select().from(issueQuestionResponseDeliveries).where(eq(issueQuestionResponseDeliveries.interactionId, f.id))).toHaveLength(1);
expect((await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, f.id)))[0]).toMatchObject({ status: "answered", resolvedByUserId: f.callerId });
expect(await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpointId))).toHaveLength(0);
});
it("presents an unanswered question again after reconnect so the caller can answer it", async () => {
const f = await questionFixture();
await f.store.runTool(f.toolInput("get_updates", {cursor: 0}));
await db.update(chatVoiceSessions).set({state: "ended", endedAt: new Date()}).where(eq(chatVoiceSessions.id, f.sessionId));
const resumed = await f.store.reserve({...f.request, idempotencyKey: randomUUID()});
if (!resumed.created) throw new Error("Expected resumed call");
const providerSessionId = randomUUID();
await db.update(chatVoiceSessions).set({state: "active", providerSessionId}).where(eq(chatVoiceSessions.id, resumed.session.id));
const tool = (name: SpekoToolEnvelope["tool"], args: SpekoToolEnvelope["args"]) => {
const toolId = randomUUID();
const envelope = {session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool: name, args} as SpekoToolEnvelope;
return f.store.runTool({...f.toolInput(name, args), sessionId: resumed.session.id, token: resumed.token,
envelope, webhookId: toolId, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex")});
};
expect(await f.store.notification(f.companyId, resumed.session.id, f.callerId)).not.toBeNull();
expect(await tool("get_updates", {cursor: 0})).toMatchObject({updates: [{question: {interactionId: f.id}}]});
expect(await tool("answer_question", {interactionId: f.id, answers: [{questionId: "color", optionIds: ["cobalt"]}]})).toMatchObject({status: "answered"});
expect(await db.select().from(issueQuestionResponseDeliveries).where(eq(issueQuestionResponseDeliveries.interactionId, f.id))).toHaveLength(1);
});
it("refuses voice answers when the exact question recipient changes", async () => {
const f = await questionFixture();
await f.store.runTool(f.toolInput("get_updates", { cursor: 0 }));
await db.update(issueThreadInteractions).set({ addresseeUserId: randomUUID() }).where(eq(issueThreadInteractions.id, f.id));
await expect(f.store.runTool(f.answer)).rejects.toMatchObject({ status: 403 });
expect(await db.select().from(issueQuestionResponseDeliveries).where(eq(issueQuestionResponseDeliveries.interactionId, f.id))).toHaveLength(0);
});
async function nativeServiceFixture() {
const f = await fixture();
await db.update(chatVoiceSessions).set({ state: "ended", endedAt: new Date() }).where(eq(chatVoiceSessions.id, f.sessionId));
const hooks: {id: string; url: string; events: string[]; allAgents: boolean; agentIds: string[]; filterTags: Record<string,string>}[] = [];
const transport = {
listPhoneNumbers: vi.fn(async () => []), assignPhoneNumber: vi.fn(async () => {}),
listWebhooks: vi.fn(async () => [...hooks]), configureWebhook: vi.fn(async (agentId: string, url: string, _secret: string, id?: string) => { const hook = {id: id ?? "hook_test", url, events: ["call.pre_call", "call.status", "call.report"], allAgents: false, agentIds: [agentId], filterTags: {}}; hooks.splice(0, hooks.length, hook); return {id: hook.id}; }),
listTools: vi.fn(async () => []),
configureTool: vi.fn(async () => ({ id: "tool_test" })),
configureVoiceDefaults: vi.fn(async () => {}),
verifyAgent: vi.fn(async () => ({ id: "agent_test", organizationId: "org_test", name: "Test voice" })),
createPhoneSession: vi.fn(async (_input: { to: string; bindingId: string }) => ({ sessionId: randomUUID(), status: "dialing" as const })),
createBrowserSession: vi.fn(async (_input: {toolToken: string}) => ({ sessionId: randomUUID(), transportToken: "short-lived-media-token", transportUrl: "wss://media.example.test" })),
inspectSession: vi.fn(async () => ({ status: "active", endedAt: null as string | null })),
callReport: vi.fn(async () => ({ complete: true, transcript: [{ id: "turn1", index: 0, speaker: "caller" as const, text: "A test request", startedAt: new Date().toISOString(), endedAt: null, interrupted: false }], costMicroUsd: "120000", durationSeconds: 60, providerUpdatedAt: new Date() })),
callDeliveryDiagnostics: vi.fn(async (id: string, deliveries: readonly {messageId: string; acceptedAt: string}[]) => parseSpekoDeliveryDiagnostics(
{id, report: {session_id: id, updated_at: new Date().toISOString(), transcript: {entries: []}}}, {events: []}, id, deliveries)),
sendCallMessage: vi.fn(async (_id: string, _text: string, _mode?: "respond" | "context") => ({messageId: "message_test"})),
endSession: vi.fn(async () => ({ confirmed: true })),
};
const credentials = vi.fn(async () => ({ apiKey: "server-only-key", agentId: "agent_test" }));
const service = voiceSessionService(db, { allowLocalBoard: false, credentials, provider: () => transport, onQuestionAnswered: vi.fn() });
const input = { companyId: f.companyId, endpointId: f.endpointId, issueId: f.issueId, caller: f.request.caller, idempotencyKey: randomUUID(), maxDurationSeconds: 600 };
return { ...f, service, transport, credentials, input };
}
it("loads history and starts calls for Cloud owners without local admin roles, retaining membership revocation", async () => {
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
const f = await nativeServiceFixture();
await db.update(companyMemberships).set({ membershipRole: "owner" }).where(eq(companyMemberships.principalId, f.callerId));
expect(await db.select().from(instanceUserRoles).where(eq(instanceUserRoles.userId, f.callerId))).toEqual([]);
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.actor = { type: "board", source: "cloud_tenant", userId: f.callerId, isInstanceAdmin: true, companyIds: [f.companyId] };
next();
});
app.use("/api", voiceSessionRoutes(db, f.service));
app.use(errorHandler);
const historyPath = `/api/companies/${f.companyId}/voice-history/${f.endpointId}`;
const history = await request(app).get(historyPath).expect(200);
expect(history.body).toHaveLength(1);
const started = await request(app).post(`/api/companies/${f.companyId}/voice-sessions`)
.send({ endpointId: f.endpointId, issueId: f.issueId, idempotencyKey: randomUUID() }).expect(201);
const [persisted] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, started.body.session.id));
expect(persisted).toMatchObject({ callerId: f.callerId, callerAuthority: "member", companyId: f.companyId });
expect(f.transport.createBrowserSession).toHaveBeenCalledTimes(1);
await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.callerId));
await request(app).get(historyPath).expect(403);
await request(app).post(`/api/companies/${f.companyId}/voice-sessions`)
.send({ endpointId: f.endpointId, issueId: f.issueId, idempotencyKey: randomUUID() }).expect(403);
expect(f.transport.createBrowserSession).toHaveBeenCalledTimes(1);
});
async function pushFixture() {
await instanceSettingsService(db).updateExperimental({enableChatConnectors: true});
const f = await nativeServiceFixture();
const {session} = await f.service.start(f.input);
await db.update(chatVoiceSessions).set({mode: "inbound_phone", state: "active"}).where(eq(chatVoiceSessions.id, session.id));
const publication = await f.publication();
await f.store.enqueuePublication(f.companyId, publication.id);
const push = () => f.service.pushReplies(25, f.companyId, f.endpointId);
return {...f, session, publication, push};
}
async function replyWorker(f: Awaited<ReturnType<typeof pushFixture>>, canRun = () => true) {
const service = voiceReplySchedule(db, { store: f.store, push: sessionId => pushVoiceReplies(db, {
store: f.store, credentials: f.credentials, provider: () => f.transport, limit: 1, sessionId, recoverDispatches: false,
}) });
const onError = vi.fn();
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun, canReconcile: () => true, onError });
// This shared integration database retains unrelated fixtures. Scope startup
// to this company's real records so they cannot dispatch another test's work.
const load = service.load;
service.load = (scope, signal) => load(scope ?? { kind: "company", id: f.companyId }, signal);
await registerVoiceReplyWork(coordinator, service, canRun).ready;
const settled = () => vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(idleWorkSnapshot().active).toBe(0);
expect(coordinator.nextWakeAt()).toBeNull();
}, { timeout: 5000 });
const quiet = async (ms = 120_000) => {
const reads = vi.spyOn(db, "select"), transactions = vi.spyOn(db, "transaction");
try { await vi.advanceTimersByTimeAsync(ms); expect(reads).not.toHaveBeenCalled(); expect(transactions).not.toHaveBeenCalled(); }
finally { reads.mockRestore(); transactions.mockRestore(); }
};
return { coordinator, onError, settled, quiet, service };
}
it("schedules phone replies only after publication and becomes quiet after acceptance", async () => {
const f = await pushFixture();
await db.update(chatPublications).set({ state: "streaming" }).where(eq(chatPublications.id, f.publication.id));
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const worker = await replyWorker(f);
try {
await worker.settled(); await worker.quiet(); expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
await db.transaction(async tx => {
await notifyVoiceReplies(tx, { kind: "endpoint", id: f.endpointId });
await tx.update(chatPublications).set({ state: "published" }).where(eq(chatPublications.id, f.publication.id));
});
await worker.settled();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
expect((await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.session.id)))[0].deliveredAt).not.toBeNull();
await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("schedules phone reply retries at the saved rate-limit deadline without early database scans", async () => {
const f = await pushFixture();
f.transport.sendCallMessage.mockRejectedValueOnce(new SpekoProviderError("rate_limited", false, 429));
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
let worker = await replyWorker(f);
try {
await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); expect(worker.coordinator.nextWakeAt()).toBeGreaterThan(Date.now() + 5000); });
const [action] = await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId));
expect(action.status).toBe("retry"); expect(worker.coordinator.nextWakeAt()).toBe(action.result!.retryAt);
await worker.coordinator.stop(); worker = await replyWorker(f);
expect(worker.coordinator.nextWakeAt()).toBe(action.result!.retryAt);
await worker.quiet(2000); expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
await vi.advanceTimersByTimeAsync(Number(action.result!.retryAt) - Date.now()); await worker.settled();
expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2); await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("schedules abandoned phone dispatch recovery even after its session has ended", async () => {
const f = await pushFixture(); await f.push();
const [action] = await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId));
await db.update(chatActions).set({ status: "dispatching", result: null, updatedAt: new Date(Date.now() - 60_000) }).where(eq(chatActions.id, action.id));
await db.update(chatVoiceSessions).set({ state: "ended", endedAt: new Date() }).where(eq(chatVoiceSessions.id, f.session.id));
f.transport.sendCallMessage.mockClear();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const worker = await replyWorker(f);
try {
await worker.settled();
expect((await db.select().from(chatActions).where(eq(chatActions.id, action.id)))[0]).toMatchObject({ status: "unknown", result: { errorCode: "message_delivery_unknown" } });
expect(f.transport.sendCallMessage).not.toHaveBeenCalled(); await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("schedules the first new phone reply from the real enqueue producer after nested commit", async () => {
const f = await pushFixture(); await f.push();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const worker = await replyWorker(f);
try {
await worker.settled(); await worker.quiet();
const before = f.transport.sendCallMessage.mock.calls.length;
await expect(db.transaction(async tx => {
const [publication] = await tx.insert(chatPublications).values({ companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: { text: "Rolled back" } }).returning();
await voiceSessionStore(tx as unknown as typeof db, { allowLocalBoard: false }).enqueuePublication(f.companyId, publication.id);
await vi.advanceTimersByTimeAsync(2000); expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(before);
throw new Error("rollback publication");
})).rejects.toThrow("rollback publication");
await worker.settled(); await worker.quiet(2000); expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(before);
await db.transaction(async tx => {
const [publication] = await tx.insert(chatPublications).values({ companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: { text: "Committed answer" } }).returning();
await voiceSessionStore(tx as unknown as typeof db, { allowLocalBoard: false }).enqueuePublication(f.companyId, publication.id);
await vi.advanceTimersByTimeAsync(2000); expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(before);
});
await worker.settled(); expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(before + 1);
expect(f.transport.sendCallMessage.mock.calls.at(-1)?.[1]).toBe("Committed answer");
await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("schedules the next phone answer when real get_updates consumes its question barrier", async () => {
const f = await pushFixture(), interactionId = randomUUID();
await db.insert(issueThreadInteractions).values({ id: interactionId, companyId: f.companyId, issueId: f.issueId,
kind: "ask_user_questions", status: "pending", addresseeUserId: f.callerId, effectiveResolverPolicy: "addressee_only",
payload: { version: 1, questions: [{ id: "color", prompt: "Which color?", selectionMode: "single", required: true, options: [{ id: "cobalt", label: "Cobalt" }] }] } });
await db.update(chatPublications).set({ payload: { text: "Which color?", interactionId } }).where(eq(chatPublications.id, f.publication.id));
const [later] = await db.insert(chatPublications).values({ companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: { text: "Later answer" } }).returning();
await f.store.enqueuePublication(f.companyId, later.id);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const worker = await replyWorker(f);
try {
await worker.settled(); expect(f.transport.sendCallMessage).toHaveBeenCalledOnce(); await worker.quiet();
const token = f.transport.createBrowserSession.mock.calls[0]![0].toolToken;
const toolId = randomUUID(), { sessionId: providerSessionId } = await f.transport.createBrowserSession.mock.results[0]!.value;
const envelope = { session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool: "get_updates" as const, args: { cursor: 0 } };
expect(await f.store.runTool({ ...f.toolInput("get_updates", { cursor: 0 }), sessionId: f.session.id, token,
envelope, webhookId: toolId, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex") })).toMatchObject({ updates: [{ question: { interactionId } }] });
await worker.settled(); expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2);
expect(f.transport.sendCallMessage.mock.calls.at(-1)?.[1]).toBe("Later answer");
await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("preserves a newly committed phone reply through drain without passive SQL", async () => {
const f = await pushFixture(); await f.push();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
let enabled = true;
const worker = await replyWorker(f, () => enabled);
try {
await worker.settled(); enabled = false;
const [publication] = await db.insert(chatPublications).values({ companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: { text: "After resume" } }).returning();
await f.store.enqueuePublication(f.companyId, publication.id);
await worker.quiet(60_000); expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
enabled = true; await worker.settled();
expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2); expect(f.transport.sendCallMessage.mock.calls.at(-1)?.[1]).toBe("After resume");
await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it.each(["normal publication", "operator resolution"] as const)("wakes phone delivery from actual %s after its early enqueue has gone idle", async path => {
const f = await pushFixture();
await db.update(chatPublications).set({ state: path === "normal publication" ? "pending" : "delivery_unknown" })
.where(eq(chatPublications.id, f.publication.id));
const chat = chatChannelService(db, { heartbeat: { wakeup: vi.fn() } });
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const worker = await replyWorker(f);
let release!: () => void, reached!: () => void;
const barrier = new Promise<void>(resolve => { release = resolve; });
const entered = new Promise<void>(resolve => { reached = resolve; });
const notify = voiceReplyNotifications.notifyVoicePublication;
const notification = vi.spyOn(voiceReplyNotifications, "notifyVoicePublication").mockImplementationOnce(async (...args) => {
reached(); await barrier; return notify(...args);
});
let publishing: Promise<unknown> | undefined;
try {
await worker.settled(); await worker.quiet(2000);
publishing = path === "normal publication"
? chat.processPendingPublications(25, { maintenance: false })
: chat.resolvePublication(f.endpointId, f.publication.id, "mark_delivered", f.callerId);
await Promise.race([entered, publishing.then(() => { throw new Error("Publication ended before its final commit boundary"); })]);
// Hold the real final publication transaction before its voice intent.
// An earlier enqueue wake can fully drain while the row is not published.
await vi.advanceTimersByTimeAsync(2000);
expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
expect(worker.coordinator.nextWakeAt()).toBeNull();
release(); await publishing; await worker.settled();
expect((await db.select().from(chatPublications).where(eq(chatPublications.id, f.publication.id)))[0].state).toBe("published");
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce(); await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally {
release(); await publishing; notification.mockRestore(); await worker.coordinator.stop(); await chat.shutdown(); vi.useRealTimers();
}
});
it("reconstructs and quarantines every abandoned phone dispatch across inventory and work batches", async () => {
const f = await pushFixture();
await db.update(chatVoiceSessions).set({ state: "ended", endedAt: new Date() }).where(eq(chatVoiceSessions.id, f.session.id));
await db.insert(chatActions).values(Array.from({ length: 105 }, () => ({ companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, kind: "speko_voice_reply_push", providerActionId: randomUUID(), status: "dispatching",
payload: { sessionId: f.session.id, replyId: randomUUID() }, updatedAt: new Date(Date.now() - 60_000) })));
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const worker = await replyWorker(f);
try {
await worker.settled();
const actions = await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId));
expect(actions).toHaveLength(105); expect(actions.every(action => action.status === "unknown")).toBe(true);
expect(f.transport.sendCallMessage).not.toHaveBeenCalled(); await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled();
} finally { await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("retains phone reply work after a failed state read and retries without intervening scans", async () => {
const f = await pushFixture();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const transaction = vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("reply database unavailable"));
const worker = await replyWorker(f);
transaction.mockRestore();
try {
expect(worker.onError).toHaveBeenCalledOnce(); expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
const retryAt = worker.coordinator.nextWakeAt()!;
expect(retryAt).toBeGreaterThan(Date.now() + 20_000); await worker.quiet(10_000);
await vi.advanceTimersByTimeAsync(retryAt - Date.now()); await worker.settled();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce(); expect(worker.onError).toHaveBeenCalledOnce(); await worker.quiet();
} finally { transaction.mockRestore(); await worker.coordinator.stop(); vi.useRealTimers(); }
});
it("notifies phone questions once and waits for authorized retrieval before later answers", async () => {
const f = await pushFixture(), interactionId = randomUUID();
await db.insert(issueThreadInteractions).values({id: interactionId, companyId: f.companyId, issueId: f.issueId, kind: "ask_user_questions", status: "pending", addresseeUserId: f.callerId, effectiveResolverPolicy: "addressee_only", payload: {version: 1, questions: [{id: "color", prompt: "Which color?", selectionMode: "single", required: true, options: [{id: "cobalt", label: "Cobalt"}]}]}});
await db.update(chatPublications).set({payload: {text: "Which color?", interactionId}}).where(eq(chatPublications.id, f.publication.id));
const [later] = await db.insert(chatPublications).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: {text: "Approved later answer"}}).returning();
await f.store.enqueuePublication(f.companyId, later.id);
await f.push(); await f.push();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
expect(f.transport.sendCallMessage.mock.calls[0]?.[1]).toContain("Call get_updates");
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.publicationId, f.publication.id));
expect(reply.deliveredAt).toBeNull();
// The service starts a new call; the first fixture call's token is retired.
const token = f.transport.createBrowserSession.mock.calls[0]![0].toolToken;
const toolId = randomUUID();
const {sessionId: providerSessionId} = await f.transport.createBrowserSession.mock.results[0]!.value;
const envelope = {session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool: "get_updates" as const, args: {cursor: 0}};
expect(await f.store.runTool({...f.toolInput("get_updates", {cursor: 0}), sessionId: f.session.id, token, envelope, webhookId: toolId, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex")})).toMatchObject({updates: [{question: {interactionId}}]});
await f.push();
expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2);
});
it("sends long approved phone answers as durable ordered parts and resumes after a rate limit", async () => {
const f = await pushFixture(), text = "😀 answer ".repeat(4000) + "FINAL";
await db.update(chatPublications).set({payload: {text}}).where(eq(chatPublications.id, f.publication.id));
const [later] = await db.insert(chatPublications).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: {text: "Later answer B"}}).returning();
await f.store.enqueuePublication(f.companyId, later.id);
f.transport.sendCallMessage.mockResolvedValueOnce({messageId: "part-0"}).mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", false, 429));
await f.push();
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.publicationId, f.publication.id));
expect(reply.deliveredAt).toBeNull();
const actions = await db.select().from(chatActions).where(eq(chatActions.kind, "speko_voice_reply_push"));
const retry = actions.find(action => action.payload.replyId === reply.id && action.status === "retry")!;
expect(retry.payload).toMatchObject({partIndex: 1, partCount: 3});
// Polling must not replay an already accepted prefix as a complete answer.
const token = f.transport.createBrowserSession.mock.calls[0]![0].toolToken;
const toolId = randomUUID(), {sessionId: providerSessionId} = await f.transport.createBrowserSession.mock.results[0]!.value;
const envelope = {session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool: "get_updates" as const, args: {cursor: 0}};
expect(await f.store.runTool({...f.toolInput("get_updates", {cursor: 0}), sessionId: f.session.id, token, envelope, webhookId: toolId, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex")})).toMatchObject({updates: []});
await db.update(chatActions).set({result: {retryAt: 0}}).where(eq(chatActions.id, retry.id));
// Reconstruct the service to prove continuation comes from durable receipts.
await voiceSessionService(db, {allowLocalBoard: false, credentials: f.credentials, provider: () => f.transport, onQuestionAnswered: vi.fn()}).pushReplies(25, f.companyId, f.endpointId);
await f.push();
const calls = f.transport.sendCallMessage.mock.calls;
expect(calls).toHaveLength(5); expect(calls[4]?.[1]).toBe("Later answer B"); expect(calls[1]?.[1]).toBe(calls[2]?.[1]);
expect([calls[0]![1], calls[2]![1], calls[3]![1]].join("")).toBe(text);
for (const call of calls) {expect(call[1].length).toBeLessThanOrEqual(16_000); expect(/^[\uDC00-\uDFFF]|[\uD800-\uDBFF]$/.test(call[1])).toBe(false);}
const [delivered] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.id, reply.id));
expect(delivered.deliveredAt).toBeInstanceOf(Date); expect(delivered.spokenAt).toBeNull();
});
it("does not retry an uncertain long-answer part or send its suffix", async () => {
const f = await pushFixture();
await db.update(chatPublications).set({payload: {text: "answer ".repeat(5000)}}).where(eq(chatPublications.id, f.publication.id));
const [later] = await db.insert(chatPublications).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: {text: "Later answer B"}}).returning();
await f.store.enqueuePublication(f.companyId, later.id);
f.transport.sendCallMessage.mockResolvedValueOnce({messageId: "part-0"}).mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", true));
await f.push(); await f.push();
expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2);
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.publicationId, f.publication.id));
expect(reply.deliveredAt).toBeNull();
});
it("pushes a published phone answer exactly once across concurrent dispatch and restart", async () => {
const f = await pushFixture();
await Promise.all([f.push(), f.push()]); await f.push();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
expect(f.transport.sendCallMessage).toHaveBeenCalledWith(expect.any(String), "Approved result", "respond");
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.session.id));
expect(reply?.deliveredAt).toBeInstanceOf(Date); expect(reply?.spokenAt).toBeNull();
const [action] = await db.select().from(chatActions).where(eq(chatActions.providerActionId, `voice_reply_push:${reply!.id}`));
expect(action).toMatchObject({status: "completed", result: {messageId: "message_test", playback: "unknown"}});
});
it("retains correlated post-call diagnostics once and preserves acceptance/playback semantics", async () => {
const f = await pushFixture(); await f.push();
await db.update(chatVoiceSessions).set({state: "ended", endedAt: new Date()}).where(eq(chatVoiceSessions.id, f.session.id));
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.session.id));
await Promise.all([captureVoiceDeliveryDiagnostics(db, session!, f.transport, true), captureVoiceDeliveryDiagnostics(db, session!, f.transport, true)]);
await captureVoiceDeliveryDiagnostics(db, session!, f.transport, true);
const [action] = await db.select().from(chatActions).where(eq(chatActions.providerActionId, `voice_reply_push:${(await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, session!.id)))[0]!.id}`));
expect(f.transport.callDeliveryDiagnostics).toHaveBeenCalledOnce();
expect(action).toMatchObject({status: "completed", result: {messageId: "message_test", playback: "unknown",
deliveryDiagnostics: {state: "collected", attempts: 1, final: true, message: {messageId: "message_test", playback: "unknown"}}}});
expect(action!.result!.deliveryDiagnostics).not.toHaveProperty("transcript");
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, session!.id));
expect(reply!.deliveredAt).not.toBeNull(); expect(reply!.spokenAt).toBeNull();
});
it("bounds failed diagnostic reads across restarts without replaying accepted speech", async () => {
const f = await pushFixture(); await f.push();
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.session.id));
f.transport.callDeliveryDiagnostics.mockRejectedValue(new SpekoProviderError("provider_unavailable", false, 503));
for (let i = 0; i < 5; i++) await captureVoiceDeliveryDiagnostics(db, session!, f.transport, true);
expect(f.transport.callDeliveryDiagnostics).toHaveBeenCalledTimes(3);
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
const [action] = await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId));
expect(action).toMatchObject({status: "completed", result: {playback: "unknown", deliveryDiagnostics: {attempts: 3, state: "failed", errorCode: "provider_unavailable", httpStatus: 503}}});
});
it("never blindly repeats a push after an ambiguous provider outcome", async () => {
const f = await pushFixture();
f.transport.sendCallMessage.mockRejectedValue(new SpekoProviderError("provider_unavailable", true));
await f.push(); await f.push();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.session.id));
expect(reply?.deliveredAt).toBeNull();
const [action] = await db.select().from(chatActions).where(eq(chatActions.providerActionId, `voice_reply_push:${reply!.id}`));
expect(action?.status).toBe("unknown");
});
it("handles an ended call without claiming delivery or creating another call", async () => {
const f = await pushFixture();
f.transport.sendCallMessage.mockRejectedValue(new SpekoProviderError("provider_unavailable", false, 409));
await f.push(); await f.push();
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.session.id));
expect(session?.state).toBe("ended"); expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
expect(f.transport.createPhoneSession).not.toHaveBeenCalled();
});
it("pushes only published replies under current connection and caller authority", async () => {
const f = await pushFixture();
await db.update(chatPublications).set({state: "streaming"}).where(eq(chatPublications.id, f.publication.id));
await f.push(); expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
await db.update(chatPublications).set({state: "published"}).where(eq(chatPublications.id, f.publication.id));
await db.delete(companyMemberships).where(eq(companyMemberships.principalId, f.callerId));
await f.push(); expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
});
it("backs off a definitive rate limit and recovers a crash before publication enqueue", async () => {
const f = await pushFixture();
await db.delete(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.session.id));
f.transport.sendCallMessage.mockRejectedValueOnce(new SpekoProviderError("rate_limited", false, 429));
await f.push(); await f.push(); expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.session.id));
await db.update(chatActions).set({result: {retryAt: 0}}).where(eq(chatActions.providerActionId, `voice_reply_push:${reply!.id}`));
await f.push(); expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2);
});
it("does not push when experimental connectors are disabled", async () => {
const f = await pushFixture();
await instanceSettingsService(db).updateExperimental({enableChatConnectors: false});
await f.push();
expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
await instanceSettingsService(db).updateExperimental({enableChatConnectors: true});
});
it("keeps a reply out of pull delivery during push and after an uncertain outcome", async () => {
const f = await pushFixture();
await db.update(chatVoiceSessions).set({state: "ended"}).where(eq(chatVoiceSessions.id, f.session.id));
await db.update(chatVoiceSessions).set({state: "active", mode: "inbound_phone"}).where(eq(chatVoiceSessions.id, f.sessionId));
await f.store.enqueuePublication(f.companyId, f.publication.id);
let reject!: (error: Error) => void, started!: () => void;
const sending = new Promise<void>(resolve => { started = resolve; });
f.transport.sendCallMessage.mockImplementation(() => {
started();
return new Promise((_resolve, rejectPromise) => { reject = rejectPromise; });
});
const push = f.push();
await sending;
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.sessionId));
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.sessionId));
// The persisted dispatch intent is visible while the network call is pending.
const [action] = await db.select().from(chatActions).where(eq(chatActions.providerActionId, `voice_reply_push:${reply!.id}`));
expect(action?.status).toBe("dispatching");
expect(await f.store.runTool(f.toolInput("get_updates", {cursor: 0}))).toMatchObject({updates: []});
expect(reply?.deliveredAt).toBeNull();
reject(new SpekoProviderError("provider_unavailable", true));
await push;
expect(await f.store.runTool(f.toolInput("get_updates", {cursor: 0}))).toMatchObject({updates: []});
await f.push();
expect(f.transport.sendCallMessage).toHaveBeenCalledOnce();
expect(session?.state).toBe("active");
});
it("preserves an ambiguous dispatch after restart instead of speaking it twice", async () => {
const f = await pushFixture();
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.sessionId, f.session.id));
await db.insert(chatActions).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId, kind: "speko_voice_reply_push", providerActionId: `voice_reply_push:${reply!.id}`, status: "dispatching", payload: {sessionId: f.session.id, replyId: reply!.id}, updatedAt: new Date(Date.now() - 60_000)});
await f.push();
expect(f.transport.sendCallMessage).not.toHaveBeenCalled();
const [action] = await db.select().from(chatActions).where(eq(chatActions.providerActionId, `voice_reply_push:${reply!.id}`));
expect(action?.status).toBe("unknown");
});
async function inboundFixture(guestIntake = false, sandboxId?: string) {
const f = await nativeServiceFixture();
await instanceSettingsService(db).updateExperimental({enableChatConnectors: true, enableIsolatedWorkspaces: guestIntake});
const signingSecret = `whsec_${Buffer.alloc(32, 7).toString("base64")}`;
f.credentials.mockResolvedValue({apiKey: "server-only-key", agentId: "agent_test", signingSecret} as never);
f.transport.verifyAgent.mockResolvedValue({ id: "agent_test", organizationId: `org_${f.companyId}`, name: "Test voice" });
await db.update(chatEndpoints).set({providerAccountId: `org_${f.companyId}`, botExternalId: "agent_test"}).where(eq(chatEndpoints.id, f.endpointId));
await db.insert(chatVoicePhoneLines).values({companyId: f.companyId, endpointId: f.endpointId, providerNumberId: randomUUID(), phoneNumber: "+12015550123", enabled: true, guestIntake, lowTrustEnvironmentId: sandboxId ?? null});
const [line] = await db.select().from(chatVoicePhoneLines).where(eq(chatVoicePhoneLines.endpointId, f.endpointId));
const [endpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpointId));
const providerSessionId = randomUUID();
const sign = (value: unknown, webhookId = `msg_${randomUUID()}`) => {
const body = Buffer.from(JSON.stringify(value)), timestamp = String(Math.floor(Date.now()/1000));
const signature = createHmac("sha256", Buffer.from(signingSecret.slice(6), "base64")).update(`${webhookId}.${timestamp}.`).update(body).digest("base64");
return {body, headers: {"webhook-id": webhookId, "webhook-timestamp": timestamp, "webhook-signature": `v1,${signature}`}};
};
const event = {type: "call.pre_call", session_id: providerSessionId, call_id: providerSessionId, organization_id: `org_${f.companyId}`, direction: "inbound", phone_number_id: line.providerNumberId, dialed_number: line.phoneNumber, from: "+12015551234"};
const signed = sign(event);
const response = await f.service.inbound.lifecycle(endpoint.publicId, signed.body, signed.headers);
const capability = (response as {toolSecrets: {paperclip_session_token: string}}).toolSecrets.paperclip_session_token;
const [call] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.providerSessionId, providerSessionId));
const tool = async (name: "submit_request" | "get_updates", args: unknown, toolId = randomUUID()) => {
const signed = sign({session_id: providerSessionId, tool_call_id: toolId, idempotency_key: `${providerSessionId}:${toolId}`, tool: name, args});
const result = await f.service.tool(endpoint.publicId, signed.body, {...signed.headers, authorization: `Bearer ${capability}`});
Object.assign(call, (await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, call.id)))[0]);
return result;
};
return {...f, call, event, signed, sign, response, endpoint, capability, tool};
}
it("bounds admission and retains no task for abandoned public rings", async () => {
const f = await inboundFixture(true);
expect(f.call.intakeIssueId).toBeNull(); expect(f.call.sessionId).toBeNull();
expect(await f.tool("get_updates", {cursor: 0})).toMatchObject({status: "ready_for_intake"});
for (let i = 1; i < 10; i++) {
const sessionId = randomUUID();
const signed = f.sign({...f.event, call_id: sessionId, session_id: sessionId});
await f.service.inbound.lifecycle(f.endpoint.publicId, signed.body, signed.headers);
}
const blockedId = randomUUID(), blocked = f.sign({...f.event, call_id: blockedId, session_id: blockedId});
await expect(f.service.inbound.lifecycle(f.endpoint.publicId, blocked.body, blocked.headers)).rejects.toMatchObject({status: 409});
expect(await f.service.inbound.lifecycle(f.endpoint.publicId, f.signed.body, f.signed.headers)).toEqual(f.response);
expect(await db.select().from(issues).where(eq(issues.originId, f.endpointId))).toHaveLength(0);
expect((await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.endpointId, f.endpointId))).map(session => session.id)).toEqual([f.sessionId]);
const ended = f.sign({type: "call.status", call_id: f.event.call_id, status: "ended"});
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
await db.update(chatVoiceInboundCalls).set({expiresAt: new Date(Date.now() - 8 * 86_400_000), updatedAt: new Date(Date.now() - 8 * 86_400_000)}).where(eq(chatVoiceInboundCalls.id, f.call.id));
await f.service.inbound.reconcile(100);
expect(await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id))).toHaveLength(0);
});
it("pins a connection-selected sandbox only to its new public task", async () => {
const sandboxId = randomUUID();
await db.insert(environments).values({id: sandboxId, name: `Phone sandbox ${sandboxId}`, driver: "sandbox", config: {provider: "daytona"}});
const f = await inboundFixture(true, sandboxId);
expect(f.call.intakeIssueId).toBeNull();
expect((await f.tool("submit_request", {text: "Hello"})).status).toBe("accepted");
const [task] = await db.select().from(issues).where(eq(issues.id, f.call.intakeIssueId!));
const [agent] = await db.select().from(agents).where(eq(agents.id, f.agentId));
expect(task.executionWorkspaceSettings).toMatchObject({mode: "isolated_workspace", environmentId: sandboxId, workspaceStrategy: {type: "cloud_sandbox"}});
expect(agent.defaultEnvironmentId).not.toBe(sandboxId);
await db.update(issues).set({executionWorkspaceSettings: {mode: "isolated_workspace", environmentId: randomUUID()}}).where(eq(issues.id, task.id));
await expect(f.tool("get_updates", {cursor: 0})).rejects.toMatchObject({status: 403});
});
it("keeps accepted guest work in its saved sandbox when line settings change, but rejects an archived sandbox", async () => {
const first = randomUUID(), next = randomUUID();
await db.insert(environments).values([{id: first, name: first, driver: "sandbox", config: {provider: "daytona"}},
{id: next, name: next, driver: "sandbox", config: {provider: "daytona"}}]);
const f = await inboundFixture(true, first);
await f.tool("submit_request", {text: "Inspect disk space"});
await db.update(chatVoicePhoneLines).set({lowTrustEnvironmentId: next}).where(eq(chatVoicePhoneLines.endpointId, f.endpointId));
expect(await f.tool("submit_request", {text: "Also check free space"})).toMatchObject({status: "accepted"});
const [task] = await db.select().from(issues).where(eq(issues.id, f.call.intakeIssueId!));
expect(task.executionWorkspaceSettings?.environmentId).toBe(first);
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.call.sessionId!));
await db.update(chatVoiceSessions).set({state: "ended", endedAt: new Date()}).where(eq(chatVoiceSessions.id, session.id));
await db.update(chatVoiceInboundCalls).set({state: "ended"}).where(eq(chatVoiceInboundCalls.id, f.call.id));
const principal = `voice:${session.id}`;
expect(await db.transaction(tx => voiceSessionStore(db, {allowLocalBoard: false}).authorizePrincipal(tx, f.companyId, f.endpointId, principal))).toMatchObject({allowed: true});
await db.update(environments).set({status: "archived"}).where(eq(environments.id, first));
await expect(db.transaction(tx => voiceSessionStore(db, {allowLocalBoard: false}).authorizePrincipal(tx, f.companyId, f.endpointId, principal))).rejects.toMatchObject({status: 403});
});
it("confines explicitly enabled public calls to one new low-trust conversation task", async () => {
const f = await inboundFixture(true), toolId = randomUUID();
expect(f.response).not.toHaveProperty("approvalCode");
expect(f.response.firstMessage).toBe("Thanks for calling. What would you like to work on?");
expect(f.response.systemPrompt).not.toContain(f.call.approvalCode);
// The provider must not inherit browser notification-only waiting or a
// protected question tool that this public session cannot use.
expect(f.response.systemPrompt).toContain("Paperclip pushes approved task answers");
expect(f.response.systemPrompt).toContain("Do not repeatedly poll while pending.");
expect(f.response.systemPrompt).toContain("Do not call answer_question");
expect(f.response.systemPrompt).toContain("Do not make the caller wait for a Paperclip turn");
expect(f.response.systemPrompt).toContain("Submit follow-ups even while earlier work is running");
expect(f.response.systemPrompt).toContain("inspect its actual environment");
expect(f.response.systemPrompt).toContain("How much disk space do you have on your computer?");
expect(f.response.systemPrompt).toContain("What is the capital of France?");
expect(f.response.idleRePrompts).toMatchObject({enabled: true, delayMs: 5000, maxPrompts: 10, messages: ["I'm still working on it."]});
expect(f.call.intakeIssueId).toBeNull();
const first = await f.tool("submit_request", {text: "Help me draft a public inquiry"}, toolId);
expect(first).toMatchObject({status: "accepted", authorization: "guest_intake"});
const [task] = await db.select().from(issues).where(eq(issues.id, f.call.intakeIssueId!));
expect(task.description).toContain("You are on a live phone call");
expect(task.description).toContain("Immediately post a brief, caller-safe task comment");
expect(task).toMatchObject({status: "todo", assigneeAgentId: f.agentId, responsibleUserId: null, sourceTrust: {disposition: "quarantined", preset: "low_trust_review"}, executionPolicy: {authorizationPolicy: {trustBoundary: {issueIds: [task.id], allowedAgentIds: [f.agentId], allowedToolClasses: [], allowedSecretBindingIds: []}}}});
const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.issueId, task.id));
expect(conversation.communicationGuidance).toContain("Keep your existing identity, runtime, tools");
expect(conversation.communicationGuidance).toContain("own low-trust task");
expect(await f.tool("submit_request", {text: "Help me draft a public inquiry"}, toolId)).toEqual(first);
await expect(f.tool("submit_request", {text: "Changed replay"}, toolId)).rejects.toMatchObject({status: 409});
expect(await f.service.inbound.lifecycle(f.endpoint.publicId, f.signed.body, f.signed.headers)).toEqual(f.response);
expect(await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.providerSessionId, f.event.session_id))).toHaveLength(1);
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, f.call.sessionId!));
const authority = await db.transaction(tx => f.store.authorizePrincipal(tx, f.companyId, f.endpointId, `voice:${session.id}`));
expect(authority).toMatchObject({allowed: true, userId: null});
const [publication] = await db.insert(chatPublications).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: session.conversationId, issueId: task.id, idempotencyKey: randomUUID(), state: "published", payload: {text: "Here is a draft for your public inquiry."}}).returning();
await f.store.enqueuePublication(f.companyId, publication.id);
expect(await f.tool("get_updates", {cursor: 0})).toMatchObject({updates: [{text: "Here is a draft for your public inquiry."}]});
await db.update(chatVoiceSessions).set({issueId: f.issueId}).where(eq(chatVoiceSessions.id, session.id));
await expect(f.tool("get_updates", {cursor: 0})).rejects.toMatchObject({status: 403});
await db.update(chatVoiceSessions).set({issueId: task.id}).where(eq(chatVoiceSessions.id, session.id));
await db.update(issues).set({executionPolicy: {mode: "normal", commentRequired: true, stages: []}}).where(eq(issues.id, task.id));
await expect(f.tool("submit_request", {text: "Must never run after losing containment"})).rejects.toMatchObject({status: 403});
});
it("routes opted-in public speech and follow-ups through the real chat queue with low-trust wakeups", async () => {
const f = await inboundFixture(true);
const wakeup = vi.fn(async (agentId, opts) => {
const request = opts.durableChatRequest;
await db.transaction(async tx => {
await request.authorize(tx);
await tx.insert(agentWakeupRequests).values({id: request.id, companyId: request.companyId, agentId, source: opts.source ?? "assignment", triggerDetail: opts.triggerDetail, reason: opts.reason, payload: opts.payload, requestedByActorType: opts.requestedByActorType, requestedByActorId: opts.requestedByActorId, idempotencyKey: request.idempotencyKey, requestedAt: request.requestedAt, status: "queued"}).onConflictDoNothing();
});
return {accepted: true};
});
const chat = chatChannelService(db, {heartbeat: {wakeup}, deferWebhookProcessing: true});
try {
for (const text of ["Help draft a public inquiry", "Keep it short"] ) {
const accepted = await f.tool("submit_request", {text});
await expect.poll(async () => {
await chat.processPendingDeliveries(25, accepted.requestId as string);
const [delivery] = await db.select().from(chatDeliveries).where(eq(chatDeliveries.id, accepted.requestId as string));
return delivery?.state;
}, {timeout: 10_000}).toBe("processed");
}
expect(wakeup).toHaveBeenCalledTimes(2);
const ended = await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller, {approve: false, approvalCode: f.call.approvalCode});
expect(ended.state).toBe("ended");
await expect(f.tool("submit_request", {text: "Cannot submit after the call ends"})).rejects.toMatchObject({status: 403});
expect(await db.transaction(tx => f.store.authorizePrincipal(tx, f.companyId, f.endpointId, `voice:${f.call.sessionId}`))).toMatchObject({allowed: true, userId: null});
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, f.call.intakeIssueId!));
expect(comments.map(c => c.body)).toEqual(["Help draft a public inquiry", "Keep it short"]);
expect(comments.every(c => !c.authorUserId && c.sourceTrust?.preset === "low_trust_review")).toBe(true);
const [task] = await db.select().from(issues).where(eq(issues.id, f.call.intakeIssueId!));
expect(task.executionWorkspaceSettings).toMatchObject({mode: "isolated_workspace", workspaceStrategy: {type: "cloud_sandbox"}});
expect(task.executionPolicy?.authorizationPolicy).toMatchObject({trustBoundary: {issueIds: [task.id], allowedToolClasses: [], allowedSecretBindingIds: []}});
await db.update(chatVoicePhoneLines).set({guestIntake: false}).where(eq(chatVoicePhoneLines.endpointId, f.endpointId));
await expect(db.transaction(tx => f.store.authorizePrincipal(tx, f.companyId, f.endpointId, `voice:${f.call.sessionId}`))).rejects.toMatchObject({status: 403});
} finally { await chat.shutdown(); }
});
it("retains both spoken sides once on the public call task and preserves quarantine", async () => {
const f = await inboundFixture(true), now = new Date().toISOString();
await f.tool("submit_request", {text: "Help draft an inquiry"});
f.transport.callReport.mockResolvedValue({complete: true, transcript: [
{id: "caller-turn", index: 0, speaker: "caller", text: "Help draft an inquiry", startedAt: now, endedAt: null, interrupted: false},
{id: "agent-turn", index: 1, speaker: "agent", text: "Here is your draft", startedAt: now, endedAt: null, interrupted: true},
], costMicroUsd: "120000", durationSeconds: 60, providerUpdatedAt: new Date()} as never);
const ended = f.sign({type: "call.report", session_id: f.event.session_id, organization_id: f.event.organization_id});
await f.service.inbound.lifecycle(f.endpoint.publicId, ended.body, ended.headers);
await f.service.reconcile(1000);
const docs = documentService(db), key = `voice-transcript-${f.call.sessionId}`;
const first = await docs.getIssueDocumentByKey(f.call.intakeIssueId!, key);
expect(first).toMatchObject({sourceTrust: {preset: "low_trust_review", disposition: "quarantined"}});
expect(first?.body).toContain("**Caller**"); expect(first?.body).toContain("**Agent**");
expect(first?.body).toContain("Here is your draft"); expect(first?.body).toContain("interrupted");
await db.update(chatVoiceReports).set({nextCheckAt: new Date(0)}).where(eq(chatVoiceReports.sessionId, f.call.sessionId!));
await f.service.reconcile(1000);
const repeated = await docs.getIssueDocumentByKey(f.call.intakeIssueId!, key);
expect(repeated?.latestRevisionId).toBe(first?.latestRevisionId);
expect(await docs.listIssueDocuments(f.call.intakeIssueId!)).toHaveLength(1);
});
it.each([false, true])("hands inbound cleanup ownership to the authorized session atomically (guest=%s)", async guest => {
const f = await inboundFixture(guest);
if (guest) await f.tool("submit_request", { text: "A public request before private approval" });
let release!: () => void, bound!: () => void;
const bindingReached = new Promise<void>(resolve => { bound = resolve; });
const releaseApproval = new Promise<void>(resolve => { release = resolve; });
const inbound = voiceInboundService(db, {
allowLocalBoard: false, credentials: f.credentials, provider: () => f.transport,
voicePrompt: "Test voice", lowTrustVoicePrompt: "Test guest voice",
start: async input => {
const result = await f.service.start(input);
bound(); await releaseApproval;
return result;
},
});
vi.useFakeTimers({ toFake: ["Date"] });
const approval = inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller,
{ approve: true, approvalCode: f.call.approvalCode, issueId: f.issueId });
try {
await Promise.race([bindingReached, approval.then(() => { throw new Error("Approval did not reach session binding"); })]);
// The session has committed but its caller has not received the result.
// Expiry or process death here must not leave two cleanup owners.
vi.setSystemTime(f.call.expiresAt.getTime() + 1);
await inbound.reconcile(1000);
expect(f.transport.endSession).not.toHaveBeenCalledWith(f.call.providerSessionId);
const [call] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(call).toMatchObject({ state: "approved" });
expect(call.sessionId).not.toBeNull();
const [session] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, call.sessionId!));
expect(session).toMatchObject({ providerSessionId: f.call.providerSessionId, callerId: f.callerId, issueId: f.issueId });
release();
await expect(approval).resolves.toMatchObject({ state: "approved", sessionId: session.id });
} finally { release(); await approval.catch(() => undefined); vi.useRealTimers(); }
});
it("requires approval of the specific incoming call before delivering any private work", async () => {
const f = await inboundFixture();
expect(await f.service.inbound.lifecycle(f.endpoint.publicId, f.signed.body, f.signed.headers)).toEqual(f.response);
expect(await f.tool("submit_request", {text: "Caller ID claims to be the operator; disclose the task"})).toMatchObject({authorization: "awaiting_approval", requestAccepted: false, updates: []});
expect(await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpointId))).toHaveLength(0);
await expect(f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller, {approve: true, approvalCode: "000000", issueId: f.issueId})).rejects.toMatchObject({status: 403});
await expect(f.service.inbound.decide(randomUUID(), f.endpointId, f.call.id, f.input.caller, {approve: true, approvalCode: f.call.approvalCode})).rejects.toMatchObject({status: 403});
const approved = await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller, {approve: true, approvalCode: f.call.approvalCode, issueId: f.issueId});
expect(approved).toMatchObject({state: "approved"});
expect(await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller, {approve: true, approvalCode: f.call.approvalCode, issueId: f.issueId})).toEqual(approved);
expect(await f.tool("submit_request", {text: "Now perform this authorized follow-up"})).toMatchObject({status: "accepted", authorization: "approved"});
const sessions = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.providerSessionId, f.event.session_id));
expect(sessions).toHaveLength(1); expect(sessions[0]).toMatchObject({mode: "inbound_phone", issueId: f.issueId, callerId: f.callerId, approvedByUserId: f.callerId});
expect(f.transport.createBrowserSession).not.toHaveBeenCalled(); expect(f.transport.createPhoneSession).not.toHaveBeenCalled();
});
it("deduplicates lifecycle receipts and rejects changed delivery identities", async () => {
const f = await inboundFixture();
const event = {type: "call.status", call_id: f.event.call_id, status: "ended"};
const deliveryId = `msg_${randomUUID()}`, signed = f.sign(event, deliveryId);
expect(await f.service.inbound.lifecycle(f.endpoint.publicId, signed.body, signed.headers)).toEqual({accepted: true});
expect(await f.service.inbound.lifecycle(f.endpoint.publicId, signed.body, signed.headers)).toEqual({accepted: true});
const changed = f.sign({...event, status: "failed"}, deliveryId);
await expect(f.service.inbound.lifecycle(f.endpoint.publicId, changed.body, changed.headers)).rejects.toMatchObject({status: 409});
const [call] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(call.state).toBe("ended");
const history = await f.service.inbound.history(f.companyId, f.endpointId, f.input.caller);
expect(history).toHaveLength(1); expect(history[0]).toMatchObject({id: call.id, state: "ended"});
expect(JSON.stringify(history)).not.toContain("approvalCode"); expect(JSON.stringify(history)).not.toContain("toolToken");
});
it("uses task-comment follow-ups for guest clarifications and rejects protected question waits", async () => {
const f = await inboundFixture(true);
await f.tool("submit_request", {text: "Draft a public inquiry"});
const taskId = f.call.intakeIssueId!;
expect(await liveVoiceExecutionGuidance(db, {companyId: f.companyId, issueId: taskId, agentId: f.agentId})).toContain("Do not use ask_user_questions");
for (const actor of [{agentId: f.agentId}, {systemId: "native-status-committer"}]) {
await expect(issueThreadInteractionService(db).create({companyId: f.companyId, id: taskId}, {
kind: "ask_user_questions", continuationPolicy: "wake_assignee", payload: {version: 1,
questions: [{id: "topic", prompt: "Which topic?", selectionMode: "single", required: true, options: [{id: "public", label: "Public inquiry"}]}]},
}, actor)).rejects.toMatchObject({status: 422, details: {code: "voice_guest_use_task_comment"}});
}
expect(await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, taskId))).toHaveLength(0);
expect(await f.tool("submit_request", {text: "Make it about shipping"})).toMatchObject({status: "accepted", authorization: "guest_intake"});
expect(await db.select().from(chatVoiceToolCalls).where(eq(chatVoiceToolCalls.sessionId, f.call.sessionId!))).toHaveLength(2);
});
it("retires guest authority after an assignment or line permission change", async () => {
const f = await inboundFixture(true);
await f.tool("submit_request", {text: "Please record a public inquiry"});
const [call] = await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.id, f.call.id));
expect(await f.service.inbound.authorizeGuest(call.sessionId!)).toBe(true);
await db.update(chatVoicePhoneLines).set({guestIntake: false}).where(eq(chatVoicePhoneLines.endpointId, f.endpointId));
expect(await f.service.inbound.authorizeGuest(call.sessionId!)).toBe(false);
await expect(f.tool("submit_request", {text: "Disabled guests cannot continue"})).rejects.toMatchObject({status: 403});
await db.update(chatVoicePhoneLines).set({guestIntake: true}).where(eq(chatVoicePhoneLines.endpointId, f.endpointId));
await db.update(issues).set({assigneeAgentId: null}).where(eq(issues.id, call.intakeIssueId!));
expect(await f.service.inbound.authorizeGuest(call.sessionId!)).toBe(false);
});
it("denies expired and rejected calls and closes the existing provider leg without creating work", async () => {
const f = await inboundFixture();
expect(await f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller, {approve: false, approvalCode: f.call.approvalCode})).toMatchObject({state: "denied"});
await f.service.inbound.reconcile(1000);
expect(f.transport.endSession).toHaveBeenCalledWith(f.event.session_id);
expect(await f.tool("submit_request", {text: "Try again"})).toMatchObject({authorization: "denied", requestAccepted: false});
const expired = await inboundFixture();
await db.update(chatVoiceInboundCalls).set({expiresAt: new Date(0)}).where(eq(chatVoiceInboundCalls.id, expired.call.id));
expect(await expired.tool("get_updates", {cursor: 0})).toMatchObject({authorization: "expired", updates: []});
await expect(expired.service.inbound.decide(expired.companyId, expired.endpointId, expired.call.id, expired.input.caller, {approve: true, approvalCode: expired.call.approvalCode})).rejects.toMatchObject({status: 409});
await expired.service.inbound.reconcile(1000);
expect(expired.transport.endSession).toHaveBeenCalledWith(expired.event.session_id);
expect(await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, expired.endpointId))).toHaveLength(0);
});
it("rejects unsigned, cross-number, cross-workspace and stale incoming admissions", async () => {
const f = await inboundFixture();
await expect(f.service.inbound.lifecycle(f.endpoint.publicId, f.signed.body, {...f.signed.headers, "webhook-signature": "v1,invalid"})).rejects.toMatchObject({code: "invalid_signature"});
for (const event of [{...f.event, phone_number_id: "other"}, {...f.event, organization_id: "other"}, {...f.event, call_id: "other"}]) {
const signed = f.sign(event); await expect(f.service.inbound.lifecycle(f.endpoint.publicId, signed.body, signed.headers)).rejects.toMatchObject({status: 403});
}
await db.update(chatEndpoints).set({setup: {step: "complete", runtimeGeneration: 2}}).where(eq(chatEndpoints.id, f.endpointId));
await expect(f.tool("get_updates", {cursor: 0})).rejects.toMatchObject({status: 403});
await expect(f.service.inbound.decide(f.companyId, f.endpointId, f.call.id, f.input.caller, {approve: true, approvalCode: f.call.approvalCode})).rejects.toMatchObject({status: 409});
});
it("retains one safe report per call and rejects history after access revocation", async () => {
const f = await nativeServiceFixture(), first = await f.service.start(f.input);
await f.service.end(f.companyId, first.session.id, f.input.caller);
await f.service.reconcile(1000);
const detail = await f.service.callDetail(f.companyId, first.session.id, f.input.caller);
expect(detail.report).toMatchObject({ status: "available", costMicroUsd: "120000", transcript: [{ id: "turn1", text: "A test request" }] });
expect(await f.service.history(f.companyId, f.endpointId, f.input.caller)).toEqual(expect.arrayContaining([detail]));
await db.update(chatVoiceReports).set({transcript: [...detail.report.transcript, {...detail.report.transcript[0]!, id: "legacy-control", index: 1, text: VOICE_RESULT_NOTIFICATION}]}).where(eq(chatVoiceReports.sessionId, first.session.id));
expect((await f.service.callDetail(f.companyId, first.session.id, f.input.caller)).report.transcript).toEqual(detail.report.transcript);
await db.update(chatVoiceReports).set({ nextCheckAt: new Date(0) }).where(eq(chatVoiceReports.sessionId, first.session.id));
const latest = new Date(Date.now() + 10000);
f.transport.callReport.mockResolvedValueOnce({ ...await f.transport.callReport(), costMicroUsd: "240000", providerUpdatedAt: latest });
await f.service.reconcile(1000);
await db.update(chatVoiceReports).set({ nextCheckAt: new Date(0) }).where(eq(chatVoiceReports.sessionId, first.session.id));
f.transport.callReport.mockResolvedValueOnce({ ...await f.transport.callReport(), costMicroUsd: "1", providerUpdatedAt: new Date(0) });
await f.service.reconcile(1000);
expect((await f.service.callDetail(f.companyId, first.session.id, f.input.caller)).report.costMicroUsd).toBe("240000");
expect(await db.select().from(chatVoiceReports).where(eq(chatVoiceReports.sessionId, first.session.id))).toHaveLength(1);
await expect(f.service.callDetail(f.companyId, first.session.id, { id: randomUUID(), authority: "member" })).rejects.toMatchObject({ status: 403 });
await expect(f.service.callDetail(randomUUID(), first.session.id, f.input.caller)).rejects.toMatchObject({ status: 404 });
await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.callerId));
await expect(f.service.history(f.companyId, f.endpointId, f.input.caller)).rejects.toMatchObject({ status: 403 });
await expect(f.service.callDetail(f.companyId, first.session.id, f.input.caller)).rejects.toMatchObject({ status: 403 });
expect(f.transport.createPhoneSession).not.toHaveBeenCalled();
});
it("does not create provider media when membership is revoked during credential resolution", async () => {
const f = await nativeServiceFixture();
f.credentials.mockImplementationOnce(async () => {
await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.callerId));
return { apiKey: "server-only-key", agentId: "agent_test" };
});
await expect(f.service.start(f.input)).rejects.toMatchObject({ status: 409, details: { code: "voice_creation_failed" } });
expect(f.transport.createBrowserSession).not.toHaveBeenCalled();
});
it("returns the existing call for concurrent starts with different request keys", async () => {
const f = await nativeServiceFixture();
const results = await Promise.allSettled([f.service.start(f.input), f.service.start({ ...f.input, idempotencyKey: randomUUID() })]);
const accepted = results.find(result => result.status === "fulfilled");
const rejected = results.find(result => result.status === "rejected");
expect(accepted?.status).toBe("fulfilled");
expect(rejected?.status).toBe("rejected");
if (accepted?.status !== "fulfilled" || rejected?.status !== "rejected") throw new Error("Expected one active call");
expect(rejected.reason).toMatchObject({ status: 409, details: { code: "voice_call_already_active", sessionId: accepted.value.session.id } });
expect(f.transport.createBrowserSession).toHaveBeenCalledTimes(1);
});
it("continues one caller's conversation across five calls, with explicit new conversations", async () => {
const f = await nativeServiceFixture();
const input = { ...f.input, issueId: undefined };
const first = await f.service.start(input);
await f.service.end(f.companyId, first.session.id, f.request.caller);
for (let turn = 0; turn < 4; turn++) {
const next = await f.service.start({ ...input, idempotencyKey: randomUUID() });
expect(next.session.issueId).toBe(first.session.issueId);
await f.service.end(f.companyId, next.session.id, f.request.caller);
}
const fresh = await f.service.start({ ...input, newConversation: true, idempotencyKey: randomUUID() });
expect(fresh.session.issueId).not.toBe(first.session.issueId);
const other = randomUUID();
await db.insert(companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: other, status: "active", membershipRole: "member" });
const independent = await f.service.start({ ...input, caller: { id: other, authority: "member" }, idempotencyKey: randomUUID() });
expect(independent.session.issueId).not.toBe(fresh.session.issueId);
expect(independent.session.issueId).not.toBe(first.session.issueId);
});
it("allows hangup but denies private session inspection after membership revocation", async () => {
const f = await nativeServiceFixture(), result = await f.service.start(f.input);
await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.callerId));
await expect(f.service.inspect(f.companyId, result.session.id, f.request.caller)).rejects.toMatchObject({ status: 403 });
expect(await f.service.end(f.companyId, result.session.id, f.request.caller)).toMatchObject({ state: "ended" });
expect(f.transport.endSession).toHaveBeenCalledOnce();
});
it("creates one hosted session, returns only short-lived media, and resumes the existing task", async () => {
const f = await nativeServiceFixture();
const first = await f.service.start(f.input), retry = await f.service.start(f.input);
expect(first.session.issueId).toBe(f.issueId);
expect(first.media?.transportToken).toBe("short-lived-media-token");
expect(JSON.stringify(first)).not.toContain("server-only-key");
expect(retry.session.id).toBe(first.session.id);
expect(retry.media).toBeUndefined();
expect(f.transport.createBrowserSession).toHaveBeenCalledTimes(1);
const row = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, first.session.id));
expect(JSON.stringify(row)).not.toContain("short-lived-media-token");
expect((await f.service.end(f.companyId, first.session.id, f.input.caller)).state).toBe("ended");
expect((await db.select().from(issues).where(eq(issues.id, f.issueId)))[0].status).toBe("in_progress");
});
it("starts from an agent by creating one task through the ordinary issue service", async () => {
const f = await nativeServiceFixture();
const input = { ...f.input, issueId: undefined };
const first = await f.service.start(input), retry = await f.service.start(input);
expect(first.session.issueId).not.toBe(f.issueId);
expect(retry.session.issueId).toBe(first.session.issueId);
expect((await db.select().from(issues).where(eq(issues.id, first.session.issueId)))[0]).toMatchObject({ assigneeAgentId: f.agentId, originKind: "chat_channel" });
expect(f.transport.createBrowserSession).toHaveBeenCalledTimes(1);
});
it("quarantines an uncertain provider creation without redialing on retry", async () => {
const f = await nativeServiceFixture();
f.transport.createBrowserSession.mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", true));
await expect(f.service.start(f.input)).rejects.toMatchObject({ status: 409, details: { code: "voice_creation_unknown" } });
expect((await f.service.start(f.input)).session.state).toBe("creation_unknown");
expect(f.transport.createBrowserSession).toHaveBeenCalledTimes(1);
});
it("does not claim ended until the provider confirms teardown", async () => {
const f = await nativeServiceFixture(), started = await f.service.start(f.input);
f.transport.endSession.mockResolvedValue({ confirmed: false });
expect((await f.service.end(f.companyId, started.session.id, f.input.caller)).state).toBe("ending");
f.transport.inspectSession.mockResolvedValue({ status: "ended", endedAt: new Date().toISOString() });
expect((await f.service.inspect(f.companyId, started.session.id, f.input.caller)).state).toBe("ended");
});
it("joins simultaneous hangups and confirms a room already closed by the browser", async () => {
const f = await nativeServiceFixture(), started = await f.service.start(f.input);
f.transport.endSession.mockRejectedValue(new SpekoProviderError("provider_unavailable", true));
f.transport.inspectSession.mockResolvedValue({ status: "ended", endedAt: new Date().toISOString() });
const replies = await Promise.all([f.service.end(f.companyId, started.session.id, f.input.caller), f.service.end(f.companyId, started.session.id, f.input.caller)]);
expect(replies.every((reply) => reply.state === "ended")).toBe(true);
expect(f.transport.endSession).toHaveBeenCalledTimes(1);
});
it("reconciles a provider hangup even without a browser end request", async () => {
const f = await nativeServiceFixture(), started = await f.service.start(f.input);
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
await db.update(chatVoiceSessions).set({ updatedAt: new Date(Date.now() - 60_000) }).where(eq(chatVoiceSessions.id, started.session.id));
f.transport.inspectSession.mockResolvedValue({ status: "ended", endedAt: new Date().toISOString() });
await f.service.reconcile(1000);
const [row] = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.id, started.session.id));
expect(row.state).toBe("ended");
expect(row.endedAt).toBeInstanceOf(Date);
expect(f.transport.endSession).not.toHaveBeenCalledWith((await f.transport.createBrowserSession.mock.results[0]!.value).sessionId);
});
it("retries definitively rejected tool setup and repairs completed tools on reconnect", async () => {
const f = await nativeServiceFixture();
const tools: { id: string; name: string; source: { kind: string; url: string } }[] = [];
f.transport.listTools.mockImplementation(async () => structuredClone(tools) as never[]);
f.transport.configureTool.mockImplementation(async (_agentId: string, definition: any, _secret: string, id?: string) => {
const tool = { id: id ?? randomUUID(), name: definition.name, source: definition.source };
if (!id) tools.push(tool);
return { id: tool.id };
});
f.transport.configureTool.mockRejectedValueOnce(new SpekoProviderError("rate_limited", false, 429));
const input = { companyId: f.companyId, endpointId: f.endpointId, agentId: "agent_test", callbackUrl: "https://voice.example.test/tools", signingSecret: "whsec_fixture", client: f.transport, assertOwned: async () => {} };
await expect(configureSpekoSessionTools(db, input)).rejects.toMatchObject({ outcomeUnknown: false });
await configureSpekoSessionTools(db, input);
expect(tools).toHaveLength(3);
await configureSpekoSessionTools(db, input);
expect(tools).toHaveLength(3);
expect(f.transport.configureTool).toHaveBeenCalledTimes(7);
expect(f.transport.configureTool.mock.calls.slice(-3).every((call) => Boolean(call[3]))).toBe(true);
});
it.each(["current", "legacy"])("moves only receipt-owned %s tools and hooks when callback origin and signing secret change", async proofVersion => {
const f = await nativeServiceFixture();
const tools: {id: string; name: string; source: {kind: string; url: string}}[] = [];
f.transport.listTools.mockImplementation(async () => structuredClone(tools) as never[]);
f.transport.configureTool.mockImplementation(async (_agent: string, definition: any, _secret: string, id?: string) => {
const tool = {id: id ?? randomUUID(), name: definition.name, source: definition.source};
const index = tools.findIndex(t => t.id === tool.id);
if (index === -1) tools.push(tool); else tools[index] = tool;
return {id: tool.id};
});
const input = {companyId: f.companyId, endpointId: f.endpointId, agentId: "agent_test", callbackUrl: "https://old-tunnel.example.test/tools", signingSecret: "whsec_fixture", client: f.transport, assertOwned: async () => {}};
await configureSpekoSessionTools(db, input);
const oldIds = tools.map(t => t.id);
if (proofVersion === "legacy") {
const actions = await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId));
for (const action of actions) await db.update(chatActions).set({payload: {name: action.payload.name, fingerprint: action.payload.fingerprint}}).where(eq(chatActions.id, action.id));
}
const rotated = {...input, callbackUrl: "https://new-tunnel.example.test/tools", signingSecret: "whsec_rotated",
...(proofVersion === "legacy" ? {previousSigningSecret: input.signingSecret} : {})};
await configureSpekoSessionTools(db, rotated);
expect(f.transport.configureTool.mock.calls.slice(-3).every(call => call[2] === "whsec_rotated")).toBe(true);
expect(tools.map(t => t.id)).toEqual(oldIds);
expect(tools.every(t => t.source.url === "https://new-tunnel.example.test/tools")).toBe(true);
expect(f.transport.configureWebhook.mock.calls.at(-1)?.[3]).toBe("hook_test");
expect(f.transport.configureWebhook.mock.calls.at(-1)?.[2]).toBe("whsec_rotated");
// Even the same provider ID is no longer ours if its URL was changed outside
// Paperclip. A historical ID alone cannot authorize overwriting it.
tools[0]!.source.url = "https://another-app.example.test/tools";
f.transport.configureTool.mockClear();
await expect(configureSpekoSessionTools(db, {...input, callbackUrl: "https://third-tunnel.example.test/tools"})).rejects.toMatchObject({status: 409});
expect(f.transport.configureTool).not.toHaveBeenCalled();
});
it("quarantines uncertain tool creation until the matching tool can be recovered", async () => {
const f = await nativeServiceFixture();
const tools: { id: string; name: string; source: { kind: string; url: string } }[] = [];
f.transport.listTools.mockImplementation(async () => structuredClone(tools) as never[]);
f.transport.configureTool.mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", true));
const input = { companyId: f.companyId, endpointId: f.endpointId, agentId: "agent_test", callbackUrl: "https://voice.example.test/tools", signingSecret: "whsec_fixture", client: f.transport, assertOwned: async () => {} };
await expect(configureSpekoSessionTools(db, input)).rejects.toMatchObject({ outcomeUnknown: true });
await expect(configureSpekoSessionTools(db, input)).rejects.toMatchObject({ status: 409 });
expect(f.transport.configureTool).toHaveBeenCalledTimes(1);
tools.push({ id: "recovered_tool", name: "submit_request", source: { kind: "webhook", url: input.callbackUrl } });
f.transport.configureTool.mockImplementation(async (_agentId: string, definition: any, _secret: string, id?: string) => {
const tool = { id: id ?? randomUUID(), name: definition.name, source: definition.source };
if (!id) tools.push(tool);
return { id: tool.id };
});
await configureSpekoSessionTools(db, input);
expect(f.transport.configureTool.mock.calls[1]?.[3]).toBe("recovered_tool");
expect(tools).toHaveLength(3);
});
it("drains real chat deliveries into one task and durable agent wakeups", async () => {
const f = await fixture();
const wakeup = vi.fn(async (agentId, opts) => {
const request = opts.durableChatRequest;
if (!request) throw new Error("Voice requires a durable wakeup");
await db.transaction(async (tx) => {
await request.authorize(tx);
await tx.insert(agentWakeupRequests).values({ id: request.id, companyId: request.companyId, agentId, source: opts.source ?? "assignment", triggerDetail: opts.triggerDetail, reason: opts.reason, payload: opts.payload, requestedByActorType: opts.requestedByActorType, requestedByActorId: opts.requestedByActorId, idempotencyKey: request.idempotencyKey, requestedAt: request.requestedAt, status: "queued" }).onConflictDoNothing();
});
return { accepted: true };
});
const chat = chatChannelService(db, { heartbeat: { wakeup }, deferWebhookProcessing: true });
try {
const accepted = await f.store.runTool(f.toolInput("submit_request", { text: "Research the shipping options" }));
await chat.processPendingDeliveries(25, accepted.requestId as string);
const delivery = await db.select().from(chatDeliveries).where(eq(chatDeliveries.id, accepted.requestId as string));
expect(delivery[0], JSON.stringify(delivery[0])).toMatchObject({ state: "processed", conversationId: f.conversationId });
const followup = await f.store.runTool(f.toolInput("submit_request", { text: "Also compare the delivery dates" }));
await chat.processPendingDeliveries(25, followup.requestId as string);
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, f.issueId));
expect(comments.map((comment) => comment.body)).toEqual(expect.arrayContaining(["Research the shipping options", "Also compare the delivery dates"]));
expect(wakeup).toHaveBeenCalledTimes(2);
expect(await db.select().from(issues).where(eq(issues.companyId, f.companyId))).toHaveLength(1);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, f.companyId))).toHaveLength(2);
} finally { await chat.shutdown(); }
});
async function phoneToolFixture() {
const f = await nativeServiceFixture();
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
const [endpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpointId));
await db.insert(principalPermissionGrants).values({companyId: f.companyId, principalType: "user", principalId: f.callerId, permissionKey: "agents:configure"});
await db.transaction(tx => syncSpekoVoiceTools(tx, endpoint, f.callerId));
await f.service.saveCallbackPreference(f.companyId, f.endpointId, f.request.caller, { phoneNumber: "+12015551234", enabled: true });
const runId = randomUUID();
await db.insert(heartbeatRuns).values({ id: runId, companyId: f.companyId, agentId: f.agentId, status: "running", contextSnapshot: { issueId: f.issueId } });
await initializeRunIdentity(db, { companyId: f.companyId, agentId: f.agentId, runId, issueId: f.issueId, responsibleUserId: f.callerId, cause: "queued" });
const gateway = createToolGatewayService(db, { toolActionSigningSecret: "speko-outbound-fixture-only" });
const gatewaySession = await gateway.createSession({ companyId: f.companyId, agentId: f.agentId, runId, issueId: f.issueId });
return { ...f, runId, gateway, gatewaySession };
}
it("keeps personal callback preferences separate from authorized agent tool installation", async () => {
const f = await nativeServiceFixture();
const [endpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpointId));
for (const enabled of [false, true]) await f.service.saveCallbackPreference(f.companyId, f.endpointId, f.request.caller, {phoneNumber: "+12015551234", enabled});
expect(await db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, f.connectionId))).toHaveLength(0);
expect(await db.select().from(toolProfileBindings).where(eq(toolProfileBindings.targetId, f.agentId))).toHaveLength(0);
await expect(db.transaction(tx => syncSpekoVoiceTools(tx, endpoint, f.callerId))).rejects.toMatchObject({status: 403});
expect(await db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, f.connectionId))).toHaveLength(0);
await db.insert(principalPermissionGrants).values({companyId: f.companyId, principalType: "user", principalId: f.callerId, permissionKey: "agents:configure"});
await db.transaction(tx => syncSpekoVoiceTools(tx, endpoint, f.callerId));
expect(await db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, f.connectionId))).toHaveLength(1);
});
it("delivers Speko guidance to existing agents only while the connection remains assigned and enabled", async () => {
const f = await phoneToolFixture();
const binding = { companyId: f.companyId, agentId: f.agentId };
const assigned = async () => (await resolveConnectorAssignments(db, binding)).filter(item => item.key === "speko");
const assignments = await assigned();
expect(assignments).toMatchObject([{ skillKey: "paperclipai/paperclip/speko", resources: [{ id: f.endpointId, connectionId: f.connectionId }], tools: [] }]);
const config = await applyConnectorSkills({}, [], assignments);
const skill = config.paperclipRuntimeSkills[0];
const markdown = await readFile(join(skill.source, "SKILL.md"), "utf8");
expect(markdown).toContain("one focused clarification");
expect(markdown).toContain("durable Paperclip continuation");
expect(markdown).toContain("never blindly redial");
expect(markdown).not.toContain("+12015551234");
for (const adapter of ["paperclip_runner", "claude_local", "codex_local"]) {
expect((await prepareConnectorSkillDelivery({ ...config, engine: "cli" }, adapter)).config.paperclipRuntimeSkills).toEqual([skill]);
}
expect((await prepareConnectorSkillDelivery(config, "cursor_local")).instructions).toContain("name: speko");
expect((await prepareConnectorSkillDelivery(config, "paperclip_runner")).instructions).toContain("name: speko");
expect((await resolveConnectorAssignments(db, { ...binding, agentId: randomUUID() })).filter(item => item.key === "speko")).toEqual([]);
expect((await resolveConnectorAssignments(db, { ...binding, companyId: randomUUID() })).filter(item => item.key === "speko")).toEqual([]);
// Browser voice guidance remains available when phone callbacks are disabled.
await db.update(chatVoiceCallbacks).set({ enabled: false }).where(eq(chatVoiceCallbacks.endpointId, f.endpointId));
expect(await assigned()).toHaveLength(1);
for (const status of ["paused", "revoked", "archived"] as const) {
await db.update(chatEndpoints).set({ status }).where(eq(chatEndpoints.id, f.endpointId));
expect(await assigned()).toEqual([]);
}
await db.update(chatEndpoints).set({ status: "active" }).where(eq(chatEndpoints.id, f.endpointId));
await db.update(toolConnections).set({ enabled: false }).where(eq(toolConnections.id, f.connectionId));
expect(await assigned()).toEqual([]);
await db.update(toolConnections).set({ enabled: true }).where(eq(toolConnections.id, f.connectionId));
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: false });
expect(await assigned()).toEqual([]);
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
expect(await assigned()).toHaveLength(1);
await db.delete(toolProfileBindings).where(eq(toolProfileBindings.targetId, f.agentId));
expect(await assigned()).toEqual([]);
const removed = await applyConnectorSkills(config, config.paperclipRuntimeSkills, []);
expect(removed.paperclipRuntimeSkills).toEqual([]);
});
it.each([false, true])("makes call_my_phone discoverable through the governed gateway and reuses a durable attempt (continued: %s)", async continued => {
const f = await phoneToolFixture();
if (continued) {
await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
const [run] = await db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: f.agentId, status: "running", nativeIssueId: f.issueId, contextSnapshot: {} }).returning();
await initializeRunIdentity(db, { companyId: f.companyId, agentId: f.agentId, runId: run.id, issueId: f.issueId, responsibleUserId: f.callerId, parentRunId: f.runId, cause: "monitor" });
f.gatewaySession = await f.gateway.createSession({ companyId: f.companyId, agentId: f.agentId, runId: run.id });
}
const tools = await f.gateway.listToolsForSession(f.gatewaySession.token);
const tool = tools.find(tool => tool.providerType === "paperclip_speko_voice");
expect(tool).toMatchObject({ upstreamToolName: "call_my_phone", connectionId: f.connectionId });
const call = { sessionToken: f.gatewaySession.token, tool: tool!.name, parameters: {}, idempotencyKey: randomUUID() };
const result = await f.gateway.executeTool(call);
expect(result).toBeDefined();
expect(f.transport.createPhoneSession).toHaveBeenCalledOnce();
expect(f.transport.createPhoneSession.mock.calls[0]![0]).toMatchObject({ to: "+12015551234", bindingId: expect.any(String) });
await f.gateway.executeTool(call);
expect(f.transport.createPhoneSession).toHaveBeenCalledOnce();
const sessions = await db.select().from(chatVoiceSessions).where(eq(chatVoiceSessions.endpointId, f.endpointId));
expect(sessions.filter(s => s.mode === "outbound_phone")).toHaveLength(1);
expect(JSON.stringify(result)).not.toContain("+12015551234");
expect(JSON.stringify(result)).not.toContain("short-lived-media-token");
});
it("does not restore an agent's removed tool binding when callback settings are saved again", async () => {
const f = await phoneToolFixture();
await db.delete(toolProfileBindings).where(eq(toolProfileBindings.targetId, f.agentId));
await f.service.saveCallbackPreference(f.companyId, f.endpointId, f.request.caller, { phoneNumber: "+12015551234", enabled: true });
expect((await f.gateway.listToolsForSession(f.gatewaySession.token)).some(t => t.providerType === "paperclip_speko_voice")).toBe(false);
});
it("requires the current task requester, company, active endpoint, opt-in and agent assignment", async () => {
const f = await phoneToolFixture();
expect(await spekoToolsForSession(db, f.gatewaySession)).toHaveLength(1);
await expect(executeSpekoVoiceTool(db, f.gatewaySession, f.endpointId, { to: "+12015559999" }, randomUUID())).rejects.toThrow();
expect(await spekoToolsForSession(db, { ...f.gatewaySession, companyId: randomUUID() })).toHaveLength(0);
expect(await spekoToolsForSession(db, { ...f.gatewaySession, issueId: randomUUID() })).toHaveLength(0);
expect(await spekoToolsForSession(db, { ...f.gatewaySession, identityContextId: randomUUID() })).toHaveLength(0);
await f.service.saveCallbackPreference(f.companyId, f.endpointId, f.request.caller, { phoneNumber: "+12015551234", enabled: false });
expect(await spekoToolsForSession(db, f.gatewaySession)).toHaveLength(0);
await expect(executeSpekoVoiceTool(db, f.gatewaySession, f.endpointId, {}, randomUUID())).rejects.toMatchObject({ status: 403 });
expect(f.transport.createPhoneSession).not.toHaveBeenCalled();
});
it("keeps callback numbers personal and refuses callers from another company", async () => {
const f = await phoneToolFixture();
const secondUser = randomUUID();
await db.insert(companyMemberships).values({ companyId: f.companyId, principalId: secondUser, principalType: "user", status: "active", membershipRole: "member" });
expect(await f.service.callbackPreference(f.companyId, f.endpointId, { id: secondUser, authority: "member" })).toBeNull();
await expect(f.service.callbackPreference(f.companyId, f.endpointId, { id: randomUUID(), authority: "member" })).rejects.toMatchObject({ status: 403 });
await expect(f.service.callbackPreference(randomUUID(), f.endpointId, f.request.caller)).rejects.toMatchObject({ status: 404 });
});
it("never redials a phone call whose creation response was lost", async () => {
const f = await phoneToolFixture();
f.transport.createPhoneSession.mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", true));
const invocationId = randomUUID();
await expect(executeSpekoVoiceTool(db, f.gatewaySession, f.endpointId, {}, invocationId)).rejects.toMatchObject({ status: 409 });
const retry = await executeSpekoVoiceTool(db, f.gatewaySession, f.endpointId, {}, invocationId);
expect(retry).toMatchObject({ session: { state: "creation_unknown", mode: "outbound_phone" } });
expect(f.transport.createPhoneSession).toHaveBeenCalledOnce();
});
it("isolates signed outbound polling from browser hints without creating an admission", async () => {
const f = await inboundFixture();
const event = {type: "call.pre_call", session_id: randomUUID(), organization_id: `org_${f.companyId}`, direction: "outbound"};
const signed = f.sign(event);
const response = await f.service.inbound.lifecycle(f.endpoint.publicId, signed.body, signed.headers);
expect(response).toMatchObject({idleRePrompts: {enabled: true}});
expect(response).not.toHaveProperty("toolSecrets");
const web = f.sign({...event, direction: "web"});
expect(await f.service.inbound.lifecycle(f.endpoint.publicId, web.body, web.headers)).toEqual({idleRePrompts: {enabled: false}});
expect(await db.select().from(chatVoiceInboundCalls).where(eq(chatVoiceInboundCalls.companyId, f.companyId))).toHaveLength(1);
});
it("delivers the phone tool to the assigned agent's actual runtime MCP server", async () => {
const f = await phoneToolFixture();
const previousUrl = process.env.PAPERCLIP_API_URL;
process.env.PAPERCLIP_API_URL = "http://127.0.0.1:3100";
try {
const servers = await buildPaperclipRuntimeMcpServers({ db, agent: { id: f.agentId, companyId: f.companyId, name: "Phone tool agent" }, runId: f.runId });
expect(servers).toHaveLength(1);
expect(servers[0]).toMatchObject({ url: expect.stringContaining("/mcp/gateways/") });
} finally { if (previousUrl === undefined) delete process.env.PAPERCLIP_API_URL; else process.env.PAPERCLIP_API_URL = previousUrl; }
});
});