import assert from "node:assert/strict"; import test from "node:test"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { observeGuestEntry, observeNativeEngine, observeNativeProbe, recordNativeProbe } from "../browser-environment-probe.mjs"; function fixture({ refresh = false, failed = false, online = true, documentOnline = true, cookies = [], status = 200 } = {}) { const handlers = {}; let navigated = false; const page = { on(event, handler) { handlers[event] = handler; }, async addInitScript() {}, async evaluate() { return online; }, async goto() { assert.ok(handlers.request); navigated = true; handlers.console({ text: () => `QA_ENV_DOCUMENT_ONLINE:${documentOnline}` }); handlers.console({ text: () => "QA_ENV_DOCUMENT_ONLINE:true" }); if (refresh) { const request = { url: () => "http://127.0.0.1/api/auth/refresh" }; handlers.request(request); if (failed) handlers.requestfailed(request); } return { status: () => status }; }, getByTestId() { return { async waitFor() {} }; }, url() { return "http://127.0.0.1/login"; }, async close() {}, }; return { context: { async storageState() { return { cookies, origins: [] }; }, async newPage() { return page; } }, navigated: () => navigated }; } test("在线新 context、表单及零 refresh 是独立事实", async () => { const owned = fixture(); const result = await observeGuestEntry(owned.context, "http://127.0.0.1"); assert.equal(result.valid, true); assert.equal(result.refreshRequestCount, 0); assert.equal(result.noRouteMocks, true); }); for (const failed of [false, true]) test(`表单可见但 refresh 已尝试必须拒绝,失败请求=${failed}`, async () => { const result = await observeGuestEntry(fixture({ refresh: true, failed }).context, "http://127.0.0.1"); assert.equal(result.valid, false); assert.equal(result.refreshRequestCount, 1); assert.equal(result.refreshFailedCount, Number(failed)); assert.ok(result.failures.includes("refresh_attempt_observed")); }); test("首次 document-start 离线不可被后续 true 覆盖", async () => { const result = await observeGuestEntry(fixture({ documentOnline: false }).context, "http://127.0.0.1"); assert.equal(result.nativeOnlineSamples.documentStart, false); assert.equal(result.valid, false); }); for (const options of [{ online: false }, { cookies: [{ name: "has_session", value: "1" }] }]) test(`前置无效不导航 ${JSON.stringify(options)}`, async () => { const owned = fixture(options); assert.equal((await observeGuestEntry(owned.context, "http://127.0.0.1")).valid, false); assert.equal(owned.navigated(), false); }); test("非候选 HTTP 200 不合格,禁止公网探针", async () => { assert.equal((await observeGuestEntry(fixture({ status: 500 }).context, "http://127.0.0.1")).valid, false); await assert.rejects(observeGuestEntry(fixture().context, "https://example.invalid"), /requires_loopback/); }); function nativeFixture({ faults = {}, online = true, events = [], name = "webkit" } = {}) { const step = stage => { events.push(`${name}:${stage}`); if (faults[stage]) throw faults[stage]; }; const context = { tracing: { async start() { step("trace_start"); }, async stop() { step("trace_stop"); } }, async newPage() { step("page"); return { async evaluate() { step("online"); return online; } }; }, async close() { step("context_close"); }, }; const browser = { version() { step("version"); return "26.0"; }, async newContext(options) { step("context"); assert.equal(options.serviceWorkers, "block"); return context; }, async close() { step("browser_close"); }, }; return { engine: { async launchServer(options) { step("launch"); assert.deepEqual(options, { headless: true, host: "127.0.0.1" }); return { process() { step("namespace"); return { pid: 123 }; }, wsEndpoint() { return "ws://127.0.0.1/synthetic-private-endpoint"; }, async close() { step("server_close"); }, async kill() { step("server_kill"); }, }; }, async connect() { step("connect"); return browser; }, }, events, }; } const nativeOwner = { ownedNamespace: "child", environmentDir: "synthetic-output" }; const nativeOptions = { fingerprint: () => "child", tracePath: "synthetic-trace.zip" }; for (const stage of ["launch", "namespace", "connect", "version", "context", "trace_start", "page", "online", "trace_stop", "context_close", "browser_close", "server_close"]) { test(`native ${stage} 失败保留引擎、阶段和安全原因,继续清理`, async () => { const error = Object.assign(new Error("synthetic-private-endpoint /private/synthetic/path secret=do-not-record"), { code: "EACCES" }); const fake = nativeFixture({ faults: { [stage]: error } }); const result = await observeNativeEngine(fake.engine, "webkit", nativeOwner, nativeOptions); assert.equal(result.name, "webkit"); assert.equal(result.valid, false); assert.equal(result.failureStage, stage); assert.equal(result.failures[0].systemCode, "EACCES"); assert.doesNotMatch(JSON.stringify(result), /private|secret|do-not-record|stack|ws:\/\//); assert.equal(fake.events.includes("webkit:server_close"), stage !== "launch"); assert.equal(fake.events.includes("webkit:server_kill"), stage === "server_close"); }); } test("清理失败不能覆盖首因,kill失败也有记录", async () => { const fault = new Error("synthetic failure"); const fake = nativeFixture({ faults: { online: fault, trace_stop: fault, context_close: fault, browser_close: fault, server_close: fault, server_kill: fault } }); const result = await observeNativeEngine(fake.engine, "webkit", nativeOwner, nativeOptions); assert.equal(result.failureStage, "online"); assert.deepEqual(result.failures.map(failure => failure.stage), ["online", "trace_stop", "context_close", "browser_close", "server_close", "server_kill"]); }); for (const [error, expected] of [ [Object.assign(new Error("private timeout"), { name: "TimeoutError" }), { errorKind: "probe_timeout" }], [new Error("/private/MiniBrowser: error while loading shared libraries: private.so"), { errorKind: "browser_dependency_missing" }], [new Error("Executable doesn't exist at /private/engine"), { errorKind: "browser_executable_missing" }], [new Error("Permission denied /private/engine"), { errorKind: "browser_permission_denied" }], [new Error("Target page, context or browser has been closed\n"), { errorKind: "browser_process_closed", processExitCode: null, processSignal: "SIGSEGV" }], ]) test(`native 安全错误分类 ${expected.errorKind}`, async () => { const result = await observeNativeEngine(nativeFixture({ faults: { launch: error } }).engine, "webkit", nativeOwner, nativeOptions); assert.deepEqual(result.failures, [{ stage: "launch", ...expected }]); }); test("错误namespace不连接;false和缺失online不通过", async () => { const fake = nativeFixture(); const mismatch = await observeNativeEngine(fake.engine, "webkit", nativeOwner, { fingerprint: () => "other" }); assert.equal(mismatch.valid, false); assert.equal(mismatch.errorKind, "environment_browser_namespace_mismatch"); assert.equal(fake.events.includes("webkit:connect"), false); for (const online of [false, null, "synthetic-sensitive-value"]) { const result = await observeNativeEngine(nativeFixture({ online }).engine, "webkit", nativeOwner, nativeOptions); assert.equal(result.valid, false); assert.equal(result.failureStage, "online"); assert.equal(result.online, online === false ? false : null); } }); test("两引擎通过且Chromium保持存活直到WebKit清理", async () => { const events = []; const chromium = nativeFixture({ name: "chromium", events }).engine; const webkit = nativeFixture({ events }).engine; const result = await observeNativeProbe({ chromium, webkit }, nativeOwner, nativeOptions); assert.equal(result.valid, true); assert.deepEqual(result.engines.map(engine => engine.name), ["chromium", "webkit"]); assert.ok(events.indexOf("chromium:online") < events.indexOf("webkit:launch")); assert.ok(events.indexOf("webkit:server_close") < events.indexOf("chromium:context_close")); }); for (const failedEngine of ["chromium", "webkit"]) test(`${failedEngine}启动失败仍落盘两个引擎,不虚报通过`, async t => { const directory = mkdtempSync(path.join(os.tmpdir(), "hyb-native-probe-test-")); t.after(() => rmSync(directory, { recursive: true, force: true })); const engines = Object.fromEntries(["chromium", "webkit"].map(name => [name, nativeFixture({ name, faults: name === failedEngine ? { launch: new Error("synthetic launch failure") } : {} }).engine])); let rechecked = false; const result = await recordNativeProbe(engines, { ...nativeOwner, environmentDir: directory }, { ...nativeOptions, verify() { rechecked = true; } }); assert.equal(rechecked, true); assert.equal(result.valid, false); assert.deepEqual(result.engines.map(engine => [engine.name, engine.valid]), ["chromium", "webkit"].map(name => [name, name !== failedEngine])); assert.deepEqual(JSON.parse(readFileSync(path.join(directory, "native-online.json"))), result); }); test("双引擎成功后环境重验失败仍落盘拒绝", async () => { let persisted; const result = await recordNativeProbe({ chromium: nativeFixture().engine, webkit: nativeFixture().engine }, nativeOwner, { ...nativeOptions, verify() { throw new Error("environment_private_detail"); }, write(file, value) { persisted = value; }, }); assert.equal(result.valid, false); assert.equal(result.engines.every(engine => engine.valid), true); assert.equal(persisted.environmentFailure.stage, "environment_recheck"); assert.equal(persisted.errorKind, "environment_recheck_failed"); assert.doesNotMatch(JSON.stringify(persisted), /private_detail/); });