| 1 | // A busy-window submit (the desktop thinks idle, the serve is still finishing) |
| 2 | // is answered with a durable queue receipt. The composer must show the queue |
| 3 | // entry immediately and treat the message as delivered-to-queue, never as a |
| 4 | // failure — local busy sends behave the same way. |
| 5 | import assert from "node:assert/strict"; |
| 6 | import { act } from "react"; |
| 7 | import { installDom, installBridgeApp, renderComposer } from "./composerInboxHarness"; |
| 8 | |
| 9 | const flush = () => new Promise<void>(resolve => setTimeout(resolve, 0)); |
| 10 | |
| 11 | async function paste(text: string) { |
| 12 | await act(async () => { |
| 13 | const event = new window.Event("paste", { bubbles: true, cancelable: true }); |
| 14 | Object.defineProperty(event, "clipboardData", { value: { files: [], items: [], types: ["text/plain"], getData: () => text } }); |
| 15 | document.querySelector("textarea.composer__input:not([aria-hidden=true])")!.dispatchEvent(event); |
| 16 | await flush(); |
| 17 | }); |
| 18 | } |
| 19 | async function send() { |
| 20 | await act(async () => { (document.querySelector(".composer__btn--send") as HTMLButtonElement).click(); await flush(); }); |
| 21 | } |
| 22 | const emptySnapshot = { revision: 1, paused: false, recovered: false, items: [], itemsCount: 0, bytes: 0, maxItems: 64, maxBytes: 1024 }; |
| 23 | const queuedSnapshot = { |
| 24 | revision: 2, paused: false, recovered: false, itemsCount: 1, bytes: 12, maxItems: 64, maxBytes: 1024, |
| 25 | items: [{ id: "queued-1", intent: "followup", state: "queued", preview: "queued while busy" }], |
| 26 | }; |
| 27 | const queuedError = () => Object.assign(new Error("reasonix_error:queued_followup"), { |
| 28 | code: "business", |
| 29 | data: { method: "SubmitRemoteTabWithSubmission", queuedFollowup: { itemId: "queued-1", disposition: "queued_followup", position: 0, paused: false } }, |
| 30 | }); |
| 31 | |
| 32 | installDom(); |
| 33 | installBridgeApp({ |
| 34 | InboxQueueForTarget: async () => ({ outcome: "applied", snapshot: emptySnapshot }), |
| 35 | InboxSnapshot: async () => queuedSnapshot, |
| 36 | }); |
| 37 | await renderComposer({ |
| 38 | running: false, |
| 39 | inboxSessionPath: "session-a", |
| 40 | onSend: async () => { throw queuedError(); }, |
| 41 | }); |
| 42 | await paste("queued while busy"); |
| 43 | await send(); |
| 44 | |
| 45 | // The queue strip reconciles through the snapshot refresh; let it settle. |
| 46 | await act(async () => { await flush(); await flush(); await new Promise(r => setTimeout(r, 60)); await flush(); }); |
| 47 | const body = document.body.textContent ?? ""; |
| 48 | assert.ok(body.includes("queued while busy"), "the queued message is visible in the pending queue"); |
| 49 | assert.equal(body.includes("Send failed"), false, "a queued submit is not reported as a failed send"); |
| 50 | assert.equal(body.includes("Refresh the session"), false, "a queued submit never asks the user to refresh"); |
| 51 | assert.equal((document.querySelector("textarea.composer__input:not([aria-hidden=true])") as HTMLTextAreaElement).value, "", |
| 52 | "the composer clears once the message is queued"); |
| 53 | |
| 54 | console.log("composer queued follow-up outcome: busy submits surface as a visible queue entry"); |
| 55 | process.exit(0); |
| 56 |