| 1 | import assert from "node:assert/strict"; |
| 2 | import React, { act } from "react"; |
| 3 | import { createRoot } from "react-dom/client"; |
| 4 | import { JSDOM } from "jsdom"; |
| 5 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 6 | import { makeSessionUIMock } from "../lib/sessionUIMock"; |
| 7 | import { flushAllSessionComposers, resumeSessionComposerEditing, sendPersistedComposer, useSessionComposerPersistence } from "../lib/sessionComposerPersistence"; |
| 8 | |
| 9 | const dom = new JSDOM("<div id='root'></div>",{url:"http://localhost"}); |
| 10 | Object.assign(globalThis,{window:dom.window,document:dom.window.document,IS_REACT_ACT_ENVIRONMENT:true}); |
| 11 | const backend=makeSessionUIMock(async()=>{}); |
| 12 | const previews:string[]=[]; |
| 13 | installDesktopHostStub({...backend,AttachmentDataURLForComposerTarget:async (_target:unknown,path:string)=>{previews.push(path);return "data:image/png;base64,aW1hZ2U=";}}); |
| 14 | let editor!:ReturnType<typeof useSessionComposerPersistence>; |
| 15 | function Probe({id}:{id:string}) {editor=useSessionComposerPersistence({hostId:"local",sessionId:id},id);return null;} |
| 16 | const root=createRoot(document.getElementById("root")!); |
| 17 | async function paint(id:string){await act(async()=>{root.render(React.createElement(Probe,{id}));await new Promise(resolve=>setTimeout(resolve,0));});} |
| 18 | async function flush(){await act(async()=>{await flushAllSessionComposers();resumeSessionComposerEditing();});} |
| 19 | try { |
| 20 | await paint("wiring"); |
| 21 | const owner=editor.target!; |
| 22 | await act(async()=>owner.onPatch!(owner.draftId,owner.generation,{text:"retained",attachments:[{path:"draft:temporary",draftId:"temporary",recoveryPath:".reasonix/attachments/kept.png",displayName:"kept.png",previewUrl:"blob:ephemeral"}]})); |
| 23 | await flush(); |
| 24 | const saved=JSON.parse((await backend.GetSessionComposerState({hostId:"local",sessionId:"wiring"})).contentJson); |
| 25 | assert.equal(saved.attachments[0].path,".reasonix/attachments/kept.png"); |
| 26 | assert.equal(saved.attachments[0].draftId,undefined); |
| 27 | assert.equal(saved.attachments[0].previewUrl,undefined); |
| 28 | await backend.SaveSessionComposerState({ref:{hostId:"local",sessionId:"wiring-restored"},expectedRevision:"0",contentJson:JSON.stringify(saved),contentVersion:1}); |
| 29 | await paint("wiring-restored"); |
| 30 | assert.equal(editor.target!.initial.attachments[0].previewUrl,"data:image/png;base64,aW1hZ2U="); |
| 31 | assert.ok(previews.includes(".reasonix/attachments/kept.png")); |
| 32 | await paint("wiring"); |
| 33 | |
| 34 | for (const code of ["inbox_capacity_items","inbox_capacity_bytes","inbox_item_too_large","channel_read_only","workspace_starting","workspace_start_failed","image_attachment_unreadable"]) { |
| 35 | await act(async()=>{await assert.rejects(sendPersistedComposer("wiring","retained","retained",code,async()=>{throw Error(`reasonix_error:${code}`);}));}); |
| 36 | assert.equal(editor.blocked,false,`${code} is a definite rejection, not an unknown execution`); |
| 37 | assert.equal(editor.target!.initial.text,"retained"); |
| 38 | } |
| 39 | await act(async()=>{await assert.rejects(sendPersistedComposer("wiring","retained","retained","lost-followup",async()=>{throw Error("lost RPC");},undefined,undefined,"guidance"));}); |
| 40 | assert.equal(editor.blocked,true); |
| 41 | await act(async()=>editor.settleSubmission("lost-followup")); |
| 42 | assert.equal(editor.blocked,false,"confirmed inbox receipt must settle the saved input as well"); |
| 43 | assert.equal(editor.target!.initial.text,""); |
| 44 | const nextOwner=editor.target!; |
| 45 | await act(async()=>nextOwner.onPatch!(nextOwner.draftId,nextOwner.generation,{text:"new input after recovery"})); |
| 46 | await act(async()=>editor.settleSubmission("lost-followup")); |
| 47 | assert.equal(editor.target!.initial.text,"new input after recovery","a repeated follow-up check cannot clear newer unsaved input"); |
| 48 | await flush(); |
| 49 | console.log("PASS persistent composer wiring: durable image source, explicit rejection and follow-up receipt settlement"); |
| 50 | } finally {await act(async()=>root.unmount());resumeSessionComposerEditing();dom.window.close();} |
| 51 |