返回 DeepSeek-Reasonix
draft-smoke.mjs
根目录 / desktop / electron / scripts / draft-smoke.mjs
1 // Real Electron + Go service, disposable storage, no provider invocation.
2 import assert from 'node:assert/strict';
3 import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
4 import { tmpdir } from 'node:os';
5 import { resolve, join } from 'node:path';
6 import { fileURLToPath } from 'node:url';
7 import { _electron } from 'playwright';
8
9 const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
10 const home = mkdtempSync(join(tmpdir(), 'reasonix-draft-native-'));
11 const workspace = join(home, 'workspace');
12 mkdirSync(workspace);
13 const report = { home, checks: [] };
14 const check = name => { report.checks.push(name); console.log(`PASS ${name}`); };
15 const launch = () => _electron.launch({ args: [root], env: { ...process.env,
16 REASONIX_HOME: home, REASONIX_STATE_HOME: home, REASONIX_CACHE_HOME: join(home,'cache'),
17 REASONIX_DEV: '1', REASONIX_DESKTOP_SERVICE: resolve(root,'../build/bin/reasonix-desktop-service') }, timeout:60000 });
18 async function ready(app) {
19 const page = await app.firstWindow();
20 await page.waitForFunction(() => Boolean(window.reasonixDesktop) && !document.querySelector('.boot-shell'), null, { timeout:60000 });
21 await page.waitForFunction(async () => { try { await window.reasonixDesktop.invoke('ListTabs', []); return true; } catch { return false; } }, null, { timeout:60000 });
22 return page;
23 }
24 const invoke = (page, name, args=[]) => page.evaluate(({name,args}) => window.reasonixDesktop.invoke(name,args),{name,args});
25 const installClipboardFixture = shell => shell.evaluate(async ({clipboard,ClipboardItem}) => {
26 globalThis.__draftSmokeClipboard = await Promise.all((await clipboard.read()).map(async item => new ClipboardItem(Object.fromEntries(await Promise.all(item.types.map(async type => [type, await item.getType(type)]))))));
27 const png = new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aY1sAAAAASUVORK5CYII=','base64')],{type:'image/png'});
28 // Explicit PNG matches the OS screenshot flavor consumed by the service.
29 await clipboard.write([new ClipboardItem({'electron application/osclipboard;format="public.png"': png})]);
30 });
31 const restoreClipboard = shell => shell.evaluate(async ({clipboard}) => {
32 await clipboard.write(globalThis.__draftSmokeClipboard);
33 delete globalThis.__draftSmokeClipboard;
34 });
35 let shell;
36 try {
37 shell = await launch();
38 let page = await ready(shell);
39 const before = await invoke(page,'ListTabs');
40 const drafts = await Promise.all(Array.from({length:20}, () => invoke(page,'OpenSessionDraftForTarget',['project',workspace])));
41 assert.equal(new Set(drafts.map(draft=>draft.id)).size,1);
42 assert.equal((await invoke(page,'ListTabs')).length,before.length);
43 check('20 native RPC opens reuse one draft without creating a runtime tab');
44 let draft = drafts[0];
45 // Exercise the native clipboard service, keeping the explicit draft target.
46 await installClipboardFixture(shell);
47 let attachment;
48 try { attachment = await invoke(page,'SaveClipboardImageForTarget',[{kind:'draft',draftId:draft.id}]); }
49 finally { await restoreClipboard(shell); }
50 assert.ok(attachment);
51 const content = { text:'native restart fixture', attachments:[{path:attachment}], invocations:[], workspaceRefs:[], sessionRefs:[], selectedTextRefs:[], pastedBlocks:[], openPastedLabels:[] };
52 const saved = await invoke(page,'SaveSessionDraft',[{draftId:draft.id,revision:draft.revision,contentJson:JSON.stringify(content),settings:draft.settings}]);
53 assert.equal(saved.outcome,'saved');
54 assert.ok(saved.draft.snapshotDigest);
55 draft = saved.draft;
56 await invoke(page,'SetSessionDraftRestoreTarget',[draft.id]);
57 check('native clipboard attachment is saved to the captured workspace with a confirmed digest');
58 await page.evaluate(() => window.__reasonixFlushSessionDraft?.());
59 await shell.close();
60 shell = await launch();
61 page = await ready(shell);
62 const restored = await invoke(page,'GetSessionDraftState',[draft.id]);
63 assert.equal(restored.draft.id,draft.id);
64 assert.equal(JSON.parse(restored.draft.contentJson).text,content.text);
65 assert.equal(JSON.parse(restored.draft.contentJson).attachments[0].path,attachment);
66 const preview = await invoke(page,'AttachmentDataURLForTarget',[{kind:'draft',draftId:draft.id},attachment]);
67 assert.match(preview,/^data:image\//);
68 assert.equal((await invoke(page,'ListTabs')).length,before.length);
69 check('normal Electron restart restores text and native attachment without creating a session');
70 const composer = page.locator('textarea.composer__input:not([aria-hidden=true])');
71 await composer.waitFor({state:'visible'});
72 const droppedPath = join(home, 'native-drop-fixture.txt');
73 writeFileSync(droppedPath, 'disposable native file drop fixture');
74 const bounds = await page.locator('[data-native-drop-target]').first().boundingBox();
75 assert.ok(bounds);
76 const cdp = await page.context().newCDPSession(page);
77 const drag = {x:bounds.x+bounds.width/2,y:bounds.y+bounds.height/2,data:{items:[],files:[droppedPath],dragOperationsMask:1}};
78 await cdp.send('Input.dispatchDragEvent',{type:'dragEnter',...drag});
79 await cdp.send('Input.dispatchDragEvent',{type:'dragOver',...drag});
80 await cdp.send('Input.dispatchDragEvent',{type:'drop',...drag});
81 await page.waitForFunction(async id => {
82 const state = await window.reasonixDesktop.invoke('GetSessionDraftState',[id]);
83 const content = JSON.parse(state.draft.contentJson);
84 return content.attachments.length > 1 || content.workspaceRefs.length > 0;
85 }, draft.id);
86 await cdp.detach();
87 check('native file drop crosses the preload boundary and persists in its draft');
88 const beforePaste = JSON.parse((await invoke(page,'GetSessionDraftState',[draft.id])).draft.contentJson).attachments.length;
89 await installClipboardFixture(shell);
90 try {
91 await composer.focus();
92 await page.keyboard.press('Meta+v');
93 await page.waitForFunction(async ({id,count}) => {
94 const state = await window.reasonixDesktop.invoke('GetSessionDraftState',[id]);
95 return JSON.parse(state.draft.contentJson).attachments.length > count;
96 }, {id:draft.id,count:beforePaste});
97 } finally { await restoreClipboard(shell); }
98 check('native keyboard paste persists through the Composer task owner');
99 await composer.fill('native close flushes the latest editor version');
100 // Close immediately, without waiting for the debounce or manually flushing.
101 await shell.close();
102 shell = await launch();
103 page = await ready(shell);
104 draft = (await invoke(page,'GetSessionDraftState',[draft.id])).draft;
105 assert.equal(JSON.parse(draft.contentJson).text,'native close flushes the latest editor version');
106 check('native renderer edit survives immediate normal close through the shutdown barrier');
107 // Kill the actual Electron process after confirmed persistence. The isolated
108 // service must recover through its ordinary parent/pipe shutdown path.
109 const stopped = shell.waitForEvent('close');
110 shell.process().kill('SIGKILL');
111 await stopped;
112 shell = await launch();
113 page = await ready(shell);
114 const recovered = await invoke(page,'GetSessionDraftState',[draft.id]);
115 assert.equal(recovered.draft.contentJson,draft.contentJson);
116 check('confirmed data survives Electron SIGKILL and service restart without replay');
117 } finally {
118 if(shell) await shell.close().catch(()=>{});
119 writeFileSync(join(home,'draft-smoke-results.json'),JSON.stringify(report,null,2));
120 console.log(`Evidence: ${join(home,'draft-smoke-results.json')}`);
121 }
122
122 lines Plain Text