返回 DeepSeek-Reasonix
attention-notifications.mjs
根目录 / desktop / frontend / bench / attention-notifications.mjs
1 #!/usr/bin/env node
2 // Real app UI and Web Audio, with controlled background runtime snapshots.
3 import assert from "node:assert/strict";
4 import path from "node:path";
5 import fs from "node:fs/promises";
6 import { existsSync } from "node:fs";
7 import { fileURLToPath } from "node:url";
8 import { createServer } from "vite";
9 import { selectSession, readActiveSessionLabel } from "./app-page-actions.mjs";
10
11 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12 if (process.env.PLAYWRIGHT_BROWSERS_PATH === ".pw-browsers" ||
13 (!process.env.PLAYWRIGHT_BROWSERS_PATH && existsSync(path.join(root, ".pw-browsers")))) {
14 process.env.PLAYWRIGHT_BROWSERS_PATH = path.join(root, ".pw-browsers");
15 }
16 const { chromium } = await import("playwright");
17 const server = await createServer({ root, logLevel: "error", server: { host: "127.0.0.1", port: 0 } });
18 await server.listen();
19 const url = server.resolvedUrls.local[0];
20 console.log("Attention browser fixture: " + url);
21 let browser;
22 let releaseNotificationModule;
23 try {
24 browser = await chromium.launch({ headless: true });
25 const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
26 const errors = [];
27 const notificationModuleGate = new Promise(resolve => { releaseNotificationModule = resolve; });
28 await page.route("**/src/lib/runtimeNotifications.ts", async route => {
29 await notificationModuleGate;
30 await route.continue();
31 });
32 page.on("pageerror", error => errors.push(error.message));
33 page.on("console", message => { if (message.type() === "error") errors.push(message.text()); });
34 await page.goto(url + "?mock=bench&bench=1", { waitUntil: "domcontentloaded" });
35 const input = page.locator("textarea.composer__input:not([aria-hidden=true])");
36 await input.waitFor();
37 await selectSession(page, "bench:small-6t");
38 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("ASYNC LAYOUT EXPANSION COMPLETE"));
39 await input.fill("B draft remains here while A asks");
40 await input.focus();
41 const selected = await readActiveSessionLabel(page);
42 await page.evaluate(async () => {
43 const { app } = await import("/src/lib/bridge.ts");
44 const { installDesktopHostStub } = await import("/src/__tests__/desktopHostStub.ts");
45 const { DESKTOP_COMMANDS } = await import("/src/generated/desktopContract.generated.ts");
46 const { runtimeStateStore } = await import("/src/lib/runtimeStateStore.ts");
47 const { setAttentionPreference } = await import("/src/lib/sound.ts");
48 const fallback = Object.fromEntries(DESKTOP_COMMANDS.map(key => [key, app[key]]));
49 const readRuntime = async () => { window.__attentionReads++; return runtimeStateStore.getSnapshot(); };
50 window.__attentionReads = 0;
51 // Focus and periodic reads must use the same producer as the injected
52 // frames, including while the notification module is held at its gate.
53 installDesktopHostStub({ ...fallback, SyncRuntimeState: readRuntime, GetRuntimeStateSnapshot: readRuntime });
54 setAttentionPreference("synth");
55 window.__attentionContexts = [];
56 const NativeAudioContext = window.AudioContext;
57 window.AudioContext = class extends NativeAudioContext {
58 constructor(...args) { super(...args); window.__attentionContexts.push(this); }
59 };
60 let revision = 0;
61 window.__publishAttention = (kind = "ask", turnId = "turn-background-A") => {
62 const state = { schemaVersion: 1, runtimeEpoch: "runtime-A", activityRevision: 1, revision: ++revision,
63 phase: "executing", running: true, turnId, turnStatus: "in_progress", turnEventSeq: 1, pendingPrompt: true,
64 pendingInteractions: [{ requestId: "1", kind, turnId, runtimeEpoch: "runtime-A", headId: "head-A" }],
65 cancelRequested: false, cancellable: true, backgroundJobs: 0, activity: "" };
66 runtimeStateStore.commit({ epoch: "attention-fixture", revision, sessions: [{ tabId: "detached:A", scope: "global",
67 workspaceRoot: "/fixture", topicId: "topic-A", sessionId: "session-A", sessionPath: "/fixture/a", sessionGeneration: 1,
68 open: false, remote: false, freshness: "synced", state }], topics: [{ scope: "global", node: {
69 key: "session-B", topicId: "topic-A", kind: "global_session", label: "Conversation B",
70 session: { hostId: "local", sessionId: "session-B" },
71 } }, { scope: "global", node: {
72 key: "topic-A", topicId: "topic-A", kind: "global_session", label: "Conversation A", status: "waiting_confirmation",
73 session: { hostId: "local", sessionId: "session-A" },
74 } }] });
75 };
76 window.__publishAttention();
77 });
78 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 0, "fixture holds the notification module until a background prompt is pending");
79 await page.evaluate(() => { window.__attentionReads = 0; window.dispatchEvent(new Event("focus")); });
80 await page.waitForFunction(() => window.__attentionReads > 0);
81 assert.equal(await page.evaluate(async () => (await import("/src/lib/runtimeStateStore.ts")).runtimeStateStore.getFailed()), false, "background prompt remains authoritative across focus synchronization");
82 releaseNotificationModule();
83 await page.getByText("Conversation A is waiting for your answer", { exact: true }).waitFor();
84 assert.equal(await input.inputValue(), "B draft remains here while A asks");
85 assert.equal(await readActiveSessionLabel(page), selected);
86 assert.equal(await input.evaluate(element => element === document.activeElement), true, "notification cannot steal focus");
87 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 1);
88 assert.equal(await page.evaluate(() => window.__attentionContexts[0].state === "suspended"), false, "Web Audio must not wait for a tab switch");
89 const evidence = process.env.REASONIX_ATTENTION_EVIDENCE ?? "/tmp/reasonix-attention-evidence";
90 await fs.mkdir(evidence, { recursive: true });
91 await page.screenshot({ path: path.join(evidence, "background-ask.png") });
92 await page.evaluate(() => window.__publishAttention());
93 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 1, "repeated snapshot is silent");
94 await page.evaluate(() => window.__publishAttention("approval", "turn-background-approval"));
95 await page.getByText("Conversation A is waiting for your approval", { exact: true }).waitFor();
96 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 2);
97 await selectSession(page, "bench:geometry");
98 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("Geometry contract fixture complete."));
99 await selectSession(page, "bench:small-6t");
100 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("ASYNC LAYOUT EXPANSION COMPLETE"));
101 assert.equal(await input.inputValue(), "B draft remains here while A asks", "draft survives real navigation");
102 await page.evaluate(() => window.__publishAttention("approval", "turn-background-approval"));
103 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 2, "navigation must not reset notification identity");
104 assert.deepEqual(errors, []);
105 console.log("PASS background Ask and approval, immediate Web Audio, visible toast, focus/draft preservation, navigation and dedupe");
106 } finally {
107 releaseNotificationModule?.();
108 await browser?.close();
109 await server.close();
110 }
111
111 lines Plain Text