返回 DeepSeek-Reasonix
manual-session-smoke.mjs
根目录 / desktop / electron / scripts / manual-session-smoke.mjs
1 // Packaged application only: no service override or development handshake bypass.
2 import assert from "node:assert/strict";
3 import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import path from "node:path";
6 import { _electron as electron } from "playwright";
7 import { once } from "node:events";
8 import { execFileSync } from "node:child_process";
9
10 const executablePath = process.argv[2];
11 if (!executablePath) throw new Error("usage: node manual-session-smoke.mjs <packaged executable>");
12 const home = mkdtempSync(path.join(tmpdir(), "reasonix-manual-package-"));
13 const evidence = process.env.REASONIX_MANUAL_EVIDENCE || "/tmp/reasonix-manual-package-evidence";
14 mkdirSync(evidence, { recursive: true });
15 const env = { ...process.env, REASONIX_HOME: home, REASONIX_STATE_HOME: home, REASONIX_CACHE_HOME: path.join(home,"cache") };
16 delete env.REASONIX_DEV;
17 delete env.REASONIX_DESKTOP_SERVICE;
18 let shell;
19 const results = [];
20 async function launch() {
21 shell = await electron.launch({ executablePath, env, timeout: 60000 });
22 const page = await shell.firstWindow();
23 await page.waitForURL("reasonix://app/index.html", { timeout: 60000 });
24 await page.waitForFunction(() => !!window.reasonixDesktop, null, { timeout: 30000 });
25 await page.waitForFunction(() => !document.querySelector(".boot-shell"), null, { timeout: 60000 });
26 await page.locator(".app").waitFor({state:"visible",timeout:60000});
27 // An isolated first launch opens provider setup. Leave it through its normal
28 // Back control; no credentials or production configuration are installed.
29 await page.addLocatorHandler(page.locator(".management-screen__back:visible"),async button=>{await button.click();});
30 // The React splash is distinct from index.html's boot shell. Actionability
31 // proves that an actual user can reach the welcome surface underneath it.
32 await page.locator(".sidebar__quick-action:visible").first().click({trial:true,timeout:60000});
33 return page;
34 }
35 const rpc = (page, command, ...args) => page.evaluate(({command,args}) => window.reasonixDesktop.invoke(command,args),{command,args});
36 try {
37 let page = await launch();
38 results.push({version:await rpc(page,"Version"),platform:await rpc(page,"Platform")});
39 const operations = [];
40 for (let index=0;index<3;index++) operations.push(await rpc(page,"BeginManualSessionCreation",{operationId:`packaged-manual-${index}`,workspaceId:"",scope:"global"}));
41 for (const operation of operations) {
42 const deadline=Date.now()+60000;
43 let current=await rpc(page,"GetManualSessionCreation",operation.operationId);
44 while((current.phase==="reserved" || current.phase==="starting") && Date.now()<deadline) {
45 await new Promise(resolve=>setTimeout(resolve,100));
46 current=await rpc(page,"GetManualSessionCreation",operation.operationId);
47 }
48 assert.equal(current.phase,"ready",JSON.stringify(current));
49 assert.deepEqual((await rpc(page,"BeginManualSessionCreation",{operationId:operation.operationId,workspaceId:"",scope:"global"})).ref,operation.ref);
50 }
51 assert.equal(new Set(operations.map(op=>op.ref.sessionId)).size,3);
52 assert.deepEqual(await rpc(page,"ListSessionDraftSummaries"),[]);
53 const ref=operations[0].ref;
54 const state=await rpc(page,"GetSessionComposerState",ref);
55 const image="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQ0AAAAASUVORK5CYII=";
56 const attachment=await rpc(page,"SavePastedImageForComposerTarget",{kind:"session",session:ref},image);
57 const contentJson=JSON.stringify({text:"packaged unsent input 中文",attachments:[{path:attachment,displayName:"recovered.png"}],pastedBlocks:[],workspaceRefs:[],sessionRefs:[],selectedTextRefs:[],invocations:[],goalDraft:true});
58 await rpc(page,"SaveSessionComposerState",{ref,expectedRevision:state.revision,contentVersion:1,contentJson});
59 await page.screenshot({path:path.join(evidence,"created.png")});
60 await shell.close(); shell=undefined;
61 page=await launch();
62 assert.equal((await rpc(page,"GetSessionComposerState",ref)).contentJson,contentJson);
63 assert.match(await rpc(page,"AttachmentDataURLForComposerTarget",{kind:"session",session:ref},attachment),/^data:image\/png;base64,/);
64 const shellProcess=shell.process();
65 const services=execFileSync("ps",["-axo","pid=,ppid=,comm="],{encoding:"utf8"}).split("\n").map(line=>line.trim().split(/\s+/)).filter(parts=>Number(parts[1])===shellProcess.pid && parts.slice(2).join(" ").includes("/Resources/service/reasonix-desktop")).map(parts=>Number(parts[0]));
66 assert.equal(services.length,1,"identify this isolated application's service before the crash");
67 const exited=once(shellProcess,"exit");
68 shellProcess.kill("SIGKILL"); await exited; shell=undefined;
69 const alive=pid=>{try {process.kill(pid,0);return true;} catch {return false;}};
70 const deadline=Date.now()+30000;
71 while(services.some(alive) && Date.now()<deadline) await new Promise(resolve=>setTimeout(resolve,100));
72 assert.equal(services.some(alive),false,"orphan service must release its leases after shell crash");
73 page=await launch();
74 assert.equal((await rpc(page,"GetSessionComposerState",ref)).contentJson,contentJson,"crash recovers acknowledged input");
75 for (const op of operations) await rpc(page,"ArchiveSessionTarget",{ref:op.ref});
76 await shell.close(); shell=undefined;
77 page=await launch();
78 assert.equal((await rpc(page,"GetSessionComposerState",ref)).contentJson,contentJson);
79 assert.deepEqual(await rpc(page,"ListSessionDraftSummaries"),[]);
80 const snapshot=await rpc(page,"GetWorkspaceSnapshot");
81 const identities=snapshot.workspaces.flatMap(workspace=>workspace.sessionIds);
82 assert.deepEqual([...new Set(identities)].sort(),operations.map(op=>op.ref.sessionId).sort(),"restart must not create any replacement identity");
83 assert.equal(identities.filter(id=>!snapshot.archivedSessionIds.includes(id)).length,0);
84 assert.equal(await page.locator("textarea.composer__input:not([aria-hidden=true])").count(),0,"empty welcome must not expose an implicit input");
85 await page.locator(".session-loading-indicator").waitFor({state:"hidden",timeout:10000});
86 await page.screenshot({path:path.join(evidence,"archived-empty.png")});
87 await shell.close(); shell=undefined;
88 results.push({passed:["matched shell/service handshake","three independent sessions and retry identities","normal exit and SQLite reopen","acknowledged input survives shell crash","image reference and preview survive restart","input survives archive and restart","last archive creates no replacement"]});
89 writeFileSync(path.join(evidence,"results.json"),JSON.stringify({home,executablePath,results},null,2));
90 console.log(`PASS packaged creation, persistence, archive and restart: ${evidence}`);
91 } catch (error) {
92 if (shell) {
93 const page=await shell.firstWindow();
94 await page.screenshot({path:path.join(evidence,"failure.png")});
95 writeFileSync(path.join(evidence,"failure.txt"),await page.locator("body").innerText());
96 }
97 throw error;
98 } finally { if(shell) await shell.close(); }
99
99 lines Plain Text