mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
Merge inbound denial recovery into voice retention
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
addbbfe35b
4 files changed
+73
-3
No files matched your search
@@ -138,8 +138,10 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
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();
|
||||
@@ -153,6 +155,39 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
} 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" });
|
||||
@@ -201,6 +236,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
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));
|
||||
@@ -225,6 +261,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
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();
|
||||
@@ -1511,6 +1548,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
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();
|
||||
|
||||
@@ -249,9 +249,9 @@ export function voiceInboundService(db: Db, options: {
|
||||
});
|
||||
if (admission.state === "approved") return projection(admission);
|
||||
if (!input.approve) {
|
||||
// Bound calls have already been handed to live-session recovery. Unbound
|
||||
// rejection uses the same durable claim as scheduled cleanup.
|
||||
await cleanup.check(admission.id).catch(error => logger.warn({ err: error, callId: admission.id }, "Incoming call termination remains pending"));
|
||||
// The committed admission signal owns cleanup. Return the saved denial
|
||||
// without waiting for provider account verification or physical hangup.
|
||||
// Bound calls have already been handed to live-session recovery.
|
||||
return projection(admission);
|
||||
}
|
||||
const row = await endpoint(companyId, endpointId), credentials = await options.credentials(row, admission.credentialFingerprint);
|
||||
|
||||
Reference in new issue
Block a user