返回 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 { runtimeStateStore } = await import("/src/lib/runtimeStateStore.ts");
44 const { setAttentionPreference } = await import("/src/lib/sound.ts");
45 setAttentionPreference("synth");
46 window.__attentionContexts = [];
47 const NativeAudioContext = window.AudioContext;
48 window.AudioContext = class extends NativeAudioContext {
49 constructor(...args) { super(...args); window.__attentionContexts.push(this); }
50 };
51 let revision = 0;
52 window.__publishAttention = (kind = "ask", turnId = "turn-background-A") => {
53 const state = { schemaVersion: 1, runtimeEpoch: "runtime-A", activityRevision: 1, revision: ++revision,
54 phase: "executing", running: true, turnId, turnStatus: "in_progress", turnEventSeq: 1, pendingPrompt: true,
55 pendingInteractions: [{ requestId: "1", kind, turnId, runtimeEpoch: "runtime-A", headId: "head-A" }],
56 cancelRequested: false, cancellable: true, backgroundJobs: 0, activity: "" };
57 runtimeStateStore.commit({ epoch: "attention-fixture", revision, sessions: [{ tabId: "detached:A", scope: "global",
58 workspaceRoot: "/fixture", topicId: "topic-A", sessionId: "session-A", sessionPath: "/fixture/a", sessionGeneration: 1,
59 open: false, remote: false, freshness: "synced", state }], topics: [{ scope: "global", node: {
60 key: "session-B", topicId: "topic-A", kind: "global_session", label: "Conversation B",
61 session: { hostId: "local", sessionId: "session-B" },
62 } }, { scope: "global", node: {
63 key: "topic-A", topicId: "topic-A", kind: "global_session", label: "Conversation A", status: "waiting_confirmation",
64 session: { hostId: "local", sessionId: "session-A" },
65 } }] });
66 };
67 window.__publishAttention();
68 });
69 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 0, "fixture holds the notification module until a background prompt is pending");
70 releaseNotificationModule();
71 await page.getByText("Conversation A is waiting for your answer", { exact: true }).waitFor();
72 assert.equal(await input.inputValue(), "B draft remains here while A asks");
73 assert.equal(await readActiveSessionLabel(page), selected);
74 assert.equal(await input.evaluate(element => element === document.activeElement), true, "notification cannot steal focus");
75 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 1);
76 assert.equal(await page.evaluate(() => window.__attentionContexts[0].state === "suspended"), false, "Web Audio must not wait for a tab switch");
77 const evidence = process.env.REASONIX_ATTENTION_EVIDENCE ?? "/tmp/reasonix-attention-evidence";
78 await fs.mkdir(evidence, { recursive: true });
79 await page.screenshot({ path: path.join(evidence, "background-ask.png") });
80 await page.evaluate(() => window.__publishAttention());
81 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 1, "repeated snapshot is silent");
82 await page.evaluate(() => window.__publishAttention("approval", "turn-background-approval"));
83 await page.getByText("Conversation A is waiting for your approval", { exact: true }).waitFor();
84 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 2);
85 await selectSession(page, "bench:geometry");
86 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("Geometry contract fixture complete."));
87 await selectSession(page, "bench:small-6t");
88 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("ASYNC LAYOUT EXPANSION COMPLETE"));
89 assert.equal(await input.inputValue(), "B draft remains here while A asks", "draft survives real navigation");
90 await page.evaluate(() => window.__publishAttention("approval", "turn-background-approval"));
91 assert.equal(await page.evaluate(() => window.__attentionContexts.length), 2, "navigation must not reset notification identity");
92 assert.deepEqual(errors, []);
93 console.log("PASS background Ask and approval, immediate Web Audio, visible toast, focus/draft preservation, navigation and dedupe");
94 } finally {
95 releaseNotificationModule?.();
96 await browser?.close();
97 await server.close();
98 }
99
99 lines Plain Text