| 1 | import assert from "node:assert/strict"; |
| 2 | import { execFileSync } from "node:child_process"; |
| 3 | import { createRequire } from "node:module"; |
| 4 | import { mkdtempSync, readFileSync, rmSync } from "node:fs"; |
| 5 | import { tmpdir } from "node:os"; |
| 6 | import { dirname, join, resolve } from "node:path"; |
| 7 | import { fileURLToPath } from "node:url"; |
| 8 | import { build } from "esbuild"; |
| 9 | |
| 10 | const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); |
| 11 | const temp = mkdtempSync(join(tmpdir(), "reasonix-graphics-smoke-")); |
| 12 | const entry = join(temp, "main.cjs"); |
| 13 | const native = process.argv.includes("--native-dialog"); |
| 14 | const electron = createRequire(import.meta.url)("electron"); |
| 15 | await build({ entryPoints: [join(root, "scripts/fixtures/graphics-recovery-main.ts")], outfile: entry, bundle: true, platform: "node", format: "cjs", external: ["electron"] }); |
| 16 | function launch(profile, mode, args = []) { |
| 17 | const env = { ...process.env, REASONIX_RECOVERY_TEST_HOME: profile, REASONIX_RECOVERY_TEST_MODE: mode, REASONIX_RECOVERY_NATIVE_DIALOG: native ? "1" : "0" }; |
| 18 | delete env.ELECTRON_RUN_AS_NODE; |
| 19 | execFileSync(electron, [entry, ...args], { env, stdio: "inherit", timeout: native ? 190_000 : 30_000, killSignal: "SIGKILL" }); |
| 20 | return JSON.parse(readFileSync(join(profile, "result.json"), "utf8")); |
| 21 | } |
| 22 | try { |
| 23 | const profile = join(temp, "gpu"); |
| 24 | const recovery = launch(profile, native ? "native" : "gpu"); |
| 25 | assert.equal(recovery.phase, "completed"); |
| 26 | assert.equal(recovery.relaunchArgs.includes("--reasonix-graphics-recovery"), recovery.actual); |
| 27 | assert.ok(recovery.events.indexOf("service-shutdown") < recovery.events.indexOf("close-allowed")); |
| 28 | if (native) console.log("PASS native recovery and draft-loss confirmation dialogs"); |
| 29 | else { |
| 30 | assert.equal(recovery.prompts.length, 2); |
| 31 | assert.equal(recovery.prompts[0].buttons[0].includes("compatibility"), recovery.actual); |
| 32 | assert.match(recovery.prompts[1].title, /Draft not saved/); |
| 33 | const software = launch(profile, "software", ["--reasonix-graphics-recovery"]); |
| 34 | assert.equal(software.startup, false); assert.equal(software.actual, false); |
| 35 | assert.equal(software.argv.includes("--reasonix-graphics-recovery"), false); |
| 36 | assert.equal(launch(profile, "plain").startup, true); |
| 37 | console.log("PASS GPU events → confirmed draft loss → ordered restart → actual software rendering → next launch restores preference"); |
| 38 | const renderer = launch(join(temp, "renderer"), "renderer"); |
| 39 | assert.equal(renderer.reloads, 2); |
| 40 | assert.equal(renderer.prompts[0].buttons[0], "Restart / 重启"); |
| 41 | console.log("PASS real renderer termination reloads once, then presents recovery without misattributing GPU"); |
| 42 | launch(profile, "keep", ["--reasonix-graphics-recovery"]); |
| 43 | assert.equal(JSON.parse(readFileSync(join(profile, "graphics.json"), "utf8")).hardwareAcceleration, false); |
| 44 | assert.equal(launch(profile, "plain").startup, false); |
| 45 | console.log("PASS explicit keep-disabled choice persists across a fresh launch"); |
| 46 | const pendingProfile = join(temp, "pending"); |
| 47 | assert.equal(launch(pendingProfile, "record").prompts.length, 0); |
| 48 | assert.equal(launch(pendingProfile, "previous").prompts.length, 1); |
| 49 | assert.equal(launch(pendingProfile, "plain").prompts.length, 0); |
| 50 | console.log("PASS explicit GPU evidence offers recovery on the next launch and acknowledgement stops repetition"); |
| 51 | for (const mode of ["during-dialog", "during-keep"]) { |
| 52 | const racingProfile = join(temp, mode); |
| 53 | launch(racingProfile, mode, mode === "during-keep" ? ["--reasonix-graphics-recovery"] : []); |
| 54 | assert.equal(JSON.parse(readFileSync(join(racingProfile, "graphics-fault.json"), "utf8")).pending, true); |
| 55 | assert.equal(launch(racingProfile, "previous").prompts.length, 1); |
| 56 | } |
| 57 | console.log("PASS both native notice paths preserve GPU failures that arrive during the dialog"); |
| 58 | } |
| 59 | } finally { |
| 60 | rmSync(temp, { recursive: true, force: true }); |
| 61 | } |
| 62 |