返回 DeepSeek-Reasonix
session-composer-persistence.test.tsx
根目录 / desktop / frontend / src / __tests__ / session-composer-persistence.test.tsx
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 import type { SessionRef } from "../lib/sessionRef";
9
10 const dom = new JSDOM("<div id='root'></div>", {url:"http://localhost"});
11 Object.assign(globalThis,{window:dom.window,document:dom.window.document,IS_REACT_ACT_ENVIRONMENT:true});
12 const backend = makeSessionUIMock(async()=>{});
13 installDesktopHostStub(backend);
14 let editor!: ReturnType<typeof useSessionComposerPersistence>;
15 const a:SessionRef={hostId:"local",sessionId:"persist-a"}, b:SessionRef={hostId:"local",sessionId:"persist-b"};
16 function Probe({refValue}:{refValue:SessionRef}) { editor=useSessionComposerPersistence(refValue,refValue.sessionId); return null; }
17 const root=createRoot(document.getElementById("root")!);
18 async function paint(ref:SessionRef) { await act(async()=>{root.render(<Probe refValue={ref}/>); await new Promise(resolve=>setTimeout(resolve,0));}); }
19 function patch(text:string) { const target=editor.target!; target.onPatch!(target.draftId,target.generation,{text}); }
20 async function persist() { await act(async()=>{await flushAllSessionComposers();resumeSessionComposerEditing();}); }
21 try {
22 await paint(a);
23 await act(async()=>patch("source A"));
24 const source=editor.target!;
25 await paint(b);
26 await act(async()=>patch("source B"));
27 await act(async()=>source.onPatch!(source.draftId,source.generation,{text:"late A"}));
28 await persist();
29 assert.equal(JSON.parse((await backend.GetSessionComposerState(a)).contentJson).text,"late A");
30 assert.equal(JSON.parse((await backend.GetSessionComposerState(b)).contentJson).text,"source B");
31
32 // Exit waits for source-bound attachment work and persists its late result.
33 let resolveTask!:()=>void;
34 const attachment=new Promise<void>(resolve=>{resolveTask=resolve;});
35 const target=editor.target!;
36 const work=attachment.then(()=>target.onPatch!(target.draftId,target.generation,{text:"attachment completed"}));
37 target.trackTask!(target.draftId,target.generation,work);
38 let exited=false;
39 const exit=act(async()=>{await flushAllSessionComposers();exited=true;});
40 assert.equal(exited,false); resolveTask(); await exit;
41 assert.equal(JSON.parse((await backend.GetSessionComposerState(b)).contentJson).text,"attachment completed");
42 await act(async()=>resumeSessionComposerEditing());
43
44 // A second writer must not replace what this window is editing.
45 const prior=await backend.GetSessionComposerState(b);
46 await backend.SaveSessionComposerState({ref:b,expectedRevision:prior.revision,contentJson:'{"text":"other window"}',contentVersion:1});
47 await act(async()=>patch("local window"));
48 await persist();
49 assert.equal(editor.blocked,false);
50 assert.equal(JSON.parse((await backend.GetSessionComposerState(b)).contentJson).text,"local window");
51
52 // A lost response preserves input and must never call the send callback again.
53 const rejected=Object.assign(new Error("settings pending"),{data:{submissionOutcome:"not_accepted"}});
54 await act(async()=>{await assert.rejects(sendPersistedComposer(b.sessionId,"local window","local window","configuration-rejected",async()=>{throw rejected;}));});
55 assert.equal(editor.blocked,false,"definite rejection releases submission occupancy");
56 assert.equal(JSON.parse((await backend.GetSessionComposerState(b)).contentJson).text,"local window","configuration rejection preserves input");
57 let sends=0;
58 const unknown=Object.assign(new Error("submission not accepted\ntransport acknowledgement lost"),{data:{submissionOutcome:"unknown"}});
59 await act(async()=>{await assert.rejects(sendPersistedComposer(b.sessionId,"local window","local window","submission-lost",async()=>{sends++;throw unknown;}));});
60 await act(async()=>editor.retry());
61 assert.equal(sends,1); assert.equal(editor.blocked,true);
62 assert.equal(JSON.parse((await backend.GetSessionComposerState(b)).contentJson).text,"local window");
63 await act(async()=>{await assert.rejects(sendPersistedComposer(b.sessionId,"local window","local window","different-id",async()=>{sends++;}));});
64 assert.equal(sends,1);
65 await act(async()=>root.unmount());
66 console.log("session input: source isolation, exit barrier, CAS and unknown submission passed");
67 } finally { resumeSessionComposerEditing();dom.window.close(); }
68
68 lines Plain Text