|
1 | 1 | // Event routing: channel selection, sender filtering (self/control/allowlist), |
2 | 2 | // reactions, deduped delivery-failure captures, sender agent identities, |
3 | 3 | // external events, and media. |
| 4 | +import * as fs from "node:fs"; |
| 5 | +import * as os from "node:os"; |
| 6 | +import * as path from "node:path"; |
4 | 7 | import { beforeEach, describe, expect, it, vi } from "vitest"; |
5 | 8 | import type { ResolvedConfig } from "../../src/config.js"; |
6 | 9 | import { defaultGatewayConfig } from "../../src/config.js"; |
7 | 10 | import { createNotifyOnce } from "../../src/gateway/dedup.js"; |
8 | 11 | import type { DispatchDeps } from "../../src/gateway/dispatch.js"; |
9 | 12 | import { dispatchEvent } from "../../src/gateway/dispatch.js"; |
| 13 | +import { |
| 14 | + activateHostedSmsCapture, |
| 15 | + beginHostedSmsAttempt, |
| 16 | + saveHostedCall, |
| 17 | + settleHostedSmsAttempt, |
| 18 | +} from "../../src/gateway/hosted-call-registry.js"; |
10 | 19 | import { downloadMedia, mediaDir } from "../../src/gateway/media.js"; |
11 | 20 | import { frameInbound } from "../../src/gateway/prompts.js"; |
12 | 21 | import type { VerifiedEvent } from "../../src/gateway/types.js"; |
@@ -270,6 +279,73 @@ describe("dispatchEvent reactions", () => { |
270 | 279 | }); |
271 | 280 |
|
272 | 281 | describe("dispatchEvent delivery failures", () => { |
| 282 | + it("does not start generic recovery for a settled hosted SMS", async () => { |
| 283 | + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-hosted-delivery-")); |
| 284 | + process.env.INKBOX_OPENCODE_HOME = dir; |
| 285 | + try { |
| 286 | + saveHostedCall({ |
| 287 | + identityId: "ident-1", |
| 288 | + callId: "call-1", |
| 289 | + eventId: "event-1", |
| 290 | + state: "running", |
| 291 | + event: { |
| 292 | + id: "event-1", |
| 293 | + event_type: "call.ended", |
| 294 | + timestamp: "2026-08-01T00:00:00Z", |
| 295 | + data: { call: { id: "call-1", mode: "hosted_agent" } }, |
| 296 | + } as never, |
| 297 | + }); |
| 298 | + activateHostedSmsCapture({ |
| 299 | + identityId: "ident-1", |
| 300 | + callId: "call-1", |
| 301 | + sessionID: "session-1", |
| 302 | + phase: "initial", |
| 303 | + expectedTarget: "+15551112222", |
| 304 | + }); |
| 305 | + const guard = beginHostedSmsAttempt({ |
| 306 | + sessionID: "session-1", |
| 307 | + target: "+15551112222", |
| 308 | + hasConversationId: false, |
| 309 | + }); |
| 310 | + if (!guard) throw new Error("expected hosted SMS guard"); |
| 311 | + settleHostedSmsAttempt(guard, "success", undefined, "hosted-message-1"); |
| 312 | + saveHostedCall({ |
| 313 | + identityId: "ident-1", |
| 314 | + callId: "call-1", |
| 315 | + eventId: "event-1", |
| 316 | + state: "completed", |
| 317 | + outcome: "success", |
| 318 | + retryable: false, |
| 319 | + event: { |
| 320 | + id: "event-1", |
| 321 | + event_type: "call.ended", |
| 322 | + timestamp: "2026-08-01T00:00:00Z", |
| 323 | + data: { call: { id: "call-1", mode: "hosted_agent" } }, |
| 324 | + } as never, |
| 325 | + }); |
| 326 | + |
| 327 | + const deps = makeDeps(); |
| 328 | + const ok = await dispatchEvent( |
| 329 | + deps, |
| 330 | + event("text.delivery_failed", { |
| 331 | + text_message: { |
| 332 | + id: "hosted-message-1", |
| 333 | + remote_phone_number: "+15551112222", |
| 334 | + error_detail: "handset unreachable", |
| 335 | + error_code: "undelivered", |
| 336 | + }, |
| 337 | + }), |
| 338 | + ); |
| 339 | + |
| 340 | + expect(ok).toBe(true); |
| 341 | + expect(deps.sessions.runCapture).not.toHaveBeenCalled(); |
| 342 | + expect(deps.contacts.resolve).not.toHaveBeenCalled(); |
| 343 | + } finally { |
| 344 | + delete process.env.INKBOX_OPENCODE_HOME; |
| 345 | + fs.rmSync(dir, { recursive: true, force: true }); |
| 346 | + } |
| 347 | + }); |
| 348 | + |
273 | 349 | it("runs a capture on the first failure and dedupes a repeat with the same id", async () => { |
274 | 350 | const deps = makeDeps(); |
275 | 351 | const failure = event("text.delivery_failed", { |
|
0 commit comments