110 行
6.9 KiB
JavaScript
110 行
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
import { atomicEnvironmentJson, namespaceFingerprint, verifyManagedEnvironment } from "./full-local-environment.mjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
export function guestProbeFailures(observed) {
|
|
const failures = [];
|
|
if (!observed.freshContext || observed.initialSessionHint !== false) failures.push("context_not_fresh");
|
|
if (!observed.listenerBeforeNavigation || !observed.noRouteMocks) failures.push("observation_setup_invalid");
|
|
if (!observed.loginFormVisible) failures.push("login_form_not_observed");
|
|
if (observed.refreshRequestCount !== 0) failures.push("refresh_attempt_observed");
|
|
const samples = [observed.nativeOnlineSamples?.beforeNavigation, observed.nativeOnlineSamples?.documentStart, observed.nativeOnlineSamples?.afterForm];
|
|
if (samples.some(value => value !== true && value !== false)) failures.push("native_online_observation_missing");
|
|
if (samples.includes(false)) failures.push("native_online_observed_false");
|
|
if (observed.loginRequestCount !== 0) failures.push("unexpected_login_request");
|
|
return failures;
|
|
}
|
|
export async function observeGuestEntry(context, baseURL, timeout = 30000) {
|
|
const target = new URL("/login", baseURL);
|
|
if (target.protocol !== "http:" || target.hostname !== "127.0.0.1") throw new Error("environment_probe_requires_loopback");
|
|
const state = await context.storageState();
|
|
const observed = {
|
|
scope: "environment-probe", observationPoint: "guest-entry-black-box",
|
|
freshContext: state.cookies.length === 0 && state.origins.length === 0,
|
|
initialSessionHint: state.cookies.some(cookie => cookie.name === "has_session" && cookie.value === "1"),
|
|
listenerBeforeNavigation: false, noRouteMocks: true, loginFormVisible: false,
|
|
refreshRequestCount: 0, loginRequestCount: 0, refreshFailedCount: 0,
|
|
nativeOnlineSamples: { beforeNavigation: null, documentStart: null, afterForm: null },
|
|
documentPath: "/login", startedAt: new Date().toISOString(), valid: false,
|
|
};
|
|
const page = await context.newPage();
|
|
try {
|
|
page.on("request", request => {
|
|
const pathname = new URL(request.url()).pathname;
|
|
if (pathname === "/api/auth/refresh") observed.refreshRequestCount += 1;
|
|
if (pathname === "/api/auth/login") observed.loginRequestCount += 1;
|
|
});
|
|
page.on("requestfailed", request => { if (new URL(request.url()).pathname === "/api/auth/refresh") observed.refreshFailedCount += 1; });
|
|
page.on("console", message => {
|
|
if (observed.nativeOnlineSamples.documentStart !== null) return;
|
|
if (message.text() === "QA_ENV_DOCUMENT_ONLINE:true") observed.nativeOnlineSamples.documentStart = true;
|
|
if (message.text() === "QA_ENV_DOCUMENT_ONLINE:false") observed.nativeOnlineSamples.documentStart = false;
|
|
});
|
|
await page.addInitScript(() => { if (window === window.top) console.info(`QA_ENV_DOCUMENT_ONLINE:${navigator.onLine}`); });
|
|
observed.listenerBeforeNavigation = true;
|
|
observed.nativeOnlineSamples.beforeNavigation = await page.evaluate(() => navigator.onLine);
|
|
if (!observed.freshContext || observed.nativeOnlineSamples.beforeNavigation !== true) throw new Error("environment_guest_precondition_invalid");
|
|
const response = await page.goto(target.href, { waitUntil: "load", timeout });
|
|
observed.documentStatus = response?.status() ?? null;
|
|
for (const testId of ["login-identifier", "login-password", "login-submit"]) await page.getByTestId(testId).waitFor({ state: "visible", timeout });
|
|
observed.loginFormVisible = true;
|
|
observed.nativeOnlineSamples.afterForm = await page.evaluate(() => navigator.onLine);
|
|
observed.stayedOnLogin = new URL(page.url()).pathname === "/login";
|
|
observed.failures = guestProbeFailures(observed);
|
|
if (observed.documentStatus !== 200 || !observed.stayedOnLogin) observed.failures.push("candidate_login_document_invalid");
|
|
observed.valid = observed.failures.length === 0;
|
|
} catch (error) {
|
|
observed.failures = guestProbeFailures(observed);
|
|
observed.errorKind = error.name === "TimeoutError" ? "probe_timeout" : "probe_precondition_or_navigation_failed";
|
|
} finally {
|
|
observed.endedAt = new Date().toISOString();
|
|
await page.close();
|
|
}
|
|
return observed;
|
|
}
|
|
|
|
async function main(mode, baseURL) {
|
|
if (!["native", "guest"].includes(mode)) throw new Error("environment_probe_mode_invalid");
|
|
const { context: owner } = verifyManagedEnvironment();
|
|
const require = createRequire(path.join(root, "client/package.json"));
|
|
const { chromium } = require("@playwright/test");
|
|
const server = await chromium.launchServer({ headless: true, host: "127.0.0.1" });
|
|
let browser;
|
|
let context;
|
|
let tracing = false;
|
|
let result = { valid: false, scope: "environment-probe", unknown: true };
|
|
const output = path.join(owner.environmentDir, mode === "native" ? "native-online.json" : "guest-entry.json");
|
|
try {
|
|
const browserNamespace = namespaceFingerprint(server.process().pid);
|
|
const browserMatchesOwnedChild = browserNamespace === owner.ownedNamespace;
|
|
if (!browserMatchesOwnedChild) throw new Error("environment_browser_namespace_mismatch");
|
|
browser = await chromium.connect(server.wsEndpoint());
|
|
context = await browser.newContext({ serviceWorkers: "block" });
|
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
|
tracing = true;
|
|
if (mode === "native") {
|
|
const page = await context.newPage();
|
|
const online = await page.evaluate(() => navigator.onLine);
|
|
result = { valid: online === true, scope: "blank-page-native-probe", online, errorKind: online === true ? null : online === false ? "native_online_observed_false" : "native_online_observation_missing", browserNamespace, browserMatchesOwnedChild, noNavigatorOverride: true };
|
|
} else result = { ...await observeGuestEntry(context, baseURL), browserNamespace, browserMatchesOwnedChild, noNavigatorOverride: true };
|
|
verifyManagedEnvironment();
|
|
} catch (error) {
|
|
result.valid = false;
|
|
result.errorKind = error.message.startsWith("environment_") ? error.message : "environment_probe_failed";
|
|
} finally {
|
|
if (tracing) { try { await context.tracing.stop({ path: path.join(owner.environmentDir, `${mode}-trace.zip`) }); } catch { result.valid = false; result.traceFailed = true; } }
|
|
await context?.close().catch(() => {});
|
|
await browser?.close().catch(() => {});
|
|
await server.close();
|
|
atomicEnvironmentJson(output, result);
|
|
}
|
|
console.log(`[environment] ${mode} probe ${result.valid ? "PASS" : "FAIL"}`);
|
|
if (!result.valid) process.exitCode = 1;
|
|
}
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
main(process.argv[2], process.argv[3]).catch(error => { console.error(`[environment] ${error.message.startsWith("environment_") ? error.message : "environment_probe_failed"}`); process.exitCode = 1; });
|
|
}
|