From 73016e35e5e8b14f3d10156d0fba6eeb33cccbb3 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 16 Jul 2026 19:41:54 +0200 Subject: [PATCH] fix: classify pre-request WebProbe browser closure --- .../unidesk-webdev/references/web-probe.md | 4 + scripts/src/hwlab-node-web-probe-summary.ts | 95 +++++++++++++++++- .../src/hwlab-node/web-observe-render.test.ts | 49 ++++++++++ scripts/src/web-probe-product-smoke.test.ts | 98 +++++++++++++++++++ 4 files changed, 242 insertions(+), 4 deletions(-) diff --git a/.agents/skills/unidesk-webdev/references/web-probe.md b/.agents/skills/unidesk-webdev/references/web-probe.md index be780948..a22fe8a7 100644 --- a/.agents/skills/unidesk-webdev/references/web-probe.md +++ b/.agents/skills/unidesk-webdev/references/web-probe.md @@ -28,6 +28,10 @@ - 默认文本输出必须在独立 `COMMAND_OUTPUT` 段完整展示已经脱敏并限长的 `stderrTail` 与 `stdoutTail`; - 摘要表可以继续提供单行预览,但不得用表格宽度裁剪替代完整的有界错误证据; - `exitCode` 非零且命令输出为空时按可观测性故障处理,先修复探针输出合同,再判断浏览器或业务根因。 + - 产品 smoke 已解析凭据,但 UI 登录的首次导航因浏览器关闭而失败时: + - 仅在 `status=0`、登录响应为空、步骤数为零且认证请求与 API 响应均未到达时分类为 `browser-runtime-closed`; + - 默认有界输出同时披露 `credentialResolved`、`requestReached`、`apiResponseReached`、错误摘要及报告路径和 SHA; + - 已发出登录请求、已有 API 响应或收到真实认证状态码时继续按认证失败处理,不得降级门禁。 - Workbench 与 Performance 调查规则: - 先使用专用 collect/analyze view; - 不要先检索原始 JSONL artifact。 diff --git a/scripts/src/hwlab-node-web-probe-summary.ts b/scripts/src/hwlab-node-web-probe-summary.ts index 53dc43d9..fbb285f6 100644 --- a/scripts/src/hwlab-node-web-probe-summary.ts +++ b/scripts/src/hwlab-node-web-probe-summary.ts @@ -12,11 +12,15 @@ function nullableRecord(value: unknown): Record | null { export function compactWebProbeScriptResult(report: Record | null): Record | null { if (report === null) return null; - const summary = compactIssueSummary(record(report.summary)); + const browserRuntimeBlocker = classifyWebProbeBrowserRuntimeAuthFailure(report); + const summary = withBrowserRuntimeBlockerEvidence(compactIssueSummary(record(report.summary)), browserRuntimeBlocker); const scriptResult = record(record(report.script).result); const scriptReport = record(scriptResult.report); const scriptScreenshot = nullableRecord(scriptResult.screenshot); - const issueEvidence = compactIssueEvidence(report.issueEvidence ?? scriptResult.issueEvidence ?? summary.issueEvidence ?? fallbackIssueEvidence(report, summary)); + const issueEvidence = withBrowserRuntimeBlockerEvidence( + compactIssueEvidence(report.issueEvidence ?? scriptResult.issueEvidence ?? summary.issueEvidence ?? fallbackIssueEvidence(report, summary)), + browserRuntimeBlocker, + ); const reportPath = typeof report.reportPath === "string" ? report.reportPath : typeof issueEvidence?.reportPath === "string" @@ -42,17 +46,92 @@ export function compactWebProbeScriptResult(report: Record | nu auth: record(report.auth), script: compactWebProbeScriptBlock(report.script), steps: compactWebProbeSteps(report.steps), - failureKind: typeof report.failureKind === "string" ? report.failureKind : null, + failureKind: browserRuntimeBlocker?.failureKind ?? (typeof report.failureKind === "string" ? report.failureKind : null), guidance: typeof report.guidance === "string" ? report.guidance : null, lastScreenshot: nullableRecord(report.lastScreenshot) ?? scriptScreenshot, readiness: record(report.readiness), artifacts: record(report.artifacts), - error: typeof report.error === "string" ? report.error : null, + error: browserRuntimeBlocker?.code ?? (typeof report.error === "string" ? report.error : null), errorMessage: typeof report.errorMessage === "string" ? report.errorMessage : null, safety: record(report.safety), }; } +export function classifyWebProbeBrowserRuntimeAuthFailure(report: Record): Record | null { + const auth = nullableRecord(report.auth); + if (auth === null || auth.method !== "ui-form-memory") return null; + const attempts = Array.isArray(auth.attempts) ? auth.attempts.map(nullableRecord).filter((item): item is Record => item !== null) : []; + const firstAttempt = attempts[0] ?? null; + const script = record(report.script); + const stepCount = Array.isArray(script.steps) + ? script.steps.length + : Array.isArray(report.steps) ? report.steps.length : null; + const authError = nullableRecord(auth.errorSummary); + const message = typeof authError?.message === "string" + ? authError.message + : typeof report.errorMessage === "string" ? report.errorMessage : ""; + const credentialResolved = auth.credentialResolved === true || auth.usernamePresent === true; + const explicitRequestReached = auth.requestReached === true || firstAttempt?.requestReached === true; + const response = firstAttempt?.response ?? null; + const apiResponseReached = auth.apiResponseReached === true + || firstAttempt?.apiResponseReached === true + || response !== null + || (typeof auth.status === "number" && auth.status > 0); + const firstNavigationClosed = /Target page, context or browser has been closed/iu.test(message); + if (!credentialResolved || !firstNavigationClosed || auth.status !== 0 || response !== null || stepCount !== 0 || explicitRequestReached || apiResponseReached) { + return null; + } + return { + code: "browser-runtime-closed", + failureKind: "browser-runtime-blocker", + credentialResolved: true, + requestReached: false, + apiResponseReached: false, + errorSummary: { + message: message.replace(/\s+/gu, " ").trim().slice(0, 600), + status: 0, + response: null, + stepCount: 0, + valuesRedacted: true, + }, + valuesRedacted: true, + }; +} + +function withBrowserRuntimeBlockerEvidence( + evidence: Record | null, + blocker: Record | null, +): Record | null { + if (blocker === null) return evidence; + const source = evidence ?? {}; + const { + ok: _ok, + status: _status, + credentialResolved: _credentialResolved, + requestReached: _requestReached, + apiResponseReached: _apiResponseReached, + errorSummary: _errorSummary, + degradedReason: _degradedReason, + failureKind: _failureKind, + failedCondition: _failedCondition, + valuesRedacted: _valuesRedacted, + ...rest + } = source; + return { + ok: false, + status: typeof source.status === "string" ? source.status : "blocked", + credentialResolved: blocker.credentialResolved, + requestReached: blocker.requestReached, + apiResponseReached: blocker.apiResponseReached, + errorSummary: blocker.errorSummary, + ...rest, + degradedReason: blocker.code, + failureKind: blocker.failureKind, + failedCondition: record(blocker.errorSummary).message, + valuesRedacted: true, + }; +} + function compactWebProbeScriptBlock(value: unknown): Record { const script = record(value); return { @@ -81,6 +160,10 @@ function compactIssueSummary(value: Record): Record | null { return { ok: evidence.ok === true, status: typeof evidence.status === "string" ? evidence.status : null, + credentialResolved: typeof evidence.credentialResolved === "boolean" ? evidence.credentialResolved : null, + requestReached: typeof evidence.requestReached === "boolean" ? evidence.requestReached : null, + apiResponseReached: typeof evidence.apiResponseReached === "boolean" ? evidence.apiResponseReached : null, + errorSummary: compactJsonForEvidence(evidence.errorSummary), degradedReason: typeof evidence.degradedReason === "string" ? evidence.degradedReason : null, failureKind: typeof evidence.failureKind === "string" ? evidence.failureKind : null, failedCondition: typeof evidence.failedCondition === "string" ? evidence.failedCondition : null, diff --git a/scripts/src/hwlab-node/web-observe-render.test.ts b/scripts/src/hwlab-node/web-observe-render.test.ts index 39f1f606..6eea276c 100644 --- a/scripts/src/hwlab-node/web-observe-render.test.ts +++ b/scripts/src/hwlab-node/web-observe-render.test.ts @@ -495,6 +495,55 @@ test("web-probe script success promotes typed report and screenshot hashes witho assert.match(text, /ARTIFACTS[\s\S]*screenshot[\s\S]*json/u); }); +test("web-probe script renders browser runtime auth reachability without Secret drill-down", () => { + const reportPath = ".state/web-probe-script/run.fixture/web-probe-script-report.json"; + const reportSha = `sha256:${"d".repeat(64)}`; + const probe = compactWebProbeScriptResult({ + ok: false, + status: "blocked", + error: "auth-login-failed", + errorMessage: "auth-login-failed", + reportPath, + reportSha256: reportSha, + auth: { + ok: false, + method: "ui-form-memory", + status: 0, + usernamePresent: true, + attempts: [{ method: "ui-form-memory", response: null, finalPath: null }], + errorSummary: { message: "page.goto: Target page, context or browser has been closed" }, + valuesRedacted: true, + }, + script: { ok: false, steps: [] }, + steps: [], + }); + assert.notEqual(probe, null); + const rendered = renderWebProbeScriptResult({ + ok: false, + status: "blocked", + command: "web-probe product-smoke --product fixture --target test --profile admin", + node: "fixture-node", + lane: "fixture-lane", + url: "https://fixture.example.test", + degradedReason: "browser-runtime-closed", + failureKind: probe?.failureKind, + summary: probe?.summary, + issueEvidence: probe?.issueEvidence, + probe, + reportLoad: { source: "stdout", path: reportPath }, + result: { exitCode: 2, timedOut: false }, + }); + const text = String(rendered.renderedText ?? ""); + assert.match(text, /browser-runtime-closed/u); + assert.match(text, /browser-runtime-blocker/u); + assert.match(text, /credentialResolved\s+true/u); + assert.match(text, /requestReached\s+false/u); + assert.match(text, /apiResponseReached\s+false/u); + assert.match(text, /Target page, context or browser has been closed/u); + assert.match(text, new RegExp(reportPath.replaceAll(".", "\\."), "u")); + assert.match(text, new RegExp(reportSha, "u")); +}); + test("web-probe script renders bounded command tails when report output is missing", () => { const rendered = renderWebProbeScriptResult({ ok: false, diff --git a/scripts/src/web-probe-product-smoke.test.ts b/scripts/src/web-probe-product-smoke.test.ts index 96de8226..6f5475f2 100644 --- a/scripts/src/web-probe-product-smoke.test.ts +++ b/scripts/src/web-probe-product-smoke.test.ts @@ -8,6 +8,10 @@ import { readDeclaredSecretValues } from "./secrets"; import { resolveSelfMediaDeliveryTarget } from "./selfmedia-config"; import { pikaoaDevelopmentRuntimeConfigRef, resolvePikaoaDevelopmentRuntimeTargetByConfigRef } from "./pikaoa-runtime"; import { webProbeScriptCommandGovernance } from "./hwlab-node/web-observe-scripts"; +import { + classifyWebProbeBrowserRuntimeAuthFailure, + compactWebProbeScriptResult, +} from "./hwlab-node-web-probe-summary"; import { parseWebProbeProductSmokeOptions, resolveWebProbeProductSmoke, @@ -162,3 +166,97 @@ test("managed runner 的登录路径、字段和 cookie 都来自显式认证 pr assert.match(runner, /ui-form-memory/u); assert.match(runner, /page = await context\.newPage\(\)/u); }); + +test("首次 UI 导航关闭且未发出认证请求时分类为浏览器运行时 blocker", () => { + const report = { + ok: false, + status: "blocked", + error: "auth-login-failed", + errorMessage: "auth-login-failed", + reportPath: ".state/web-probe-script/run.fixture/web-probe-script-report.json", + reportSha256: `sha256:${"a".repeat(64)}`, + auth: { + ok: false, + method: "ui-form-memory", + status: 0, + usernamePresent: true, + attempts: [{ method: "ui-form-memory", response: null, finalPath: null }], + errorSummary: { message: "page.goto: Target page, context or browser has been closed" }, + valuesRedacted: true, + }, + script: { ok: false, steps: [] }, + steps: [], + }; + const blocker = classifyWebProbeBrowserRuntimeAuthFailure(report); + assert.deepEqual(blocker, { + code: "browser-runtime-closed", + failureKind: "browser-runtime-blocker", + credentialResolved: true, + requestReached: false, + apiResponseReached: false, + errorSummary: { + message: "page.goto: Target page, context or browser has been closed", + status: 0, + response: null, + stepCount: 0, + valuesRedacted: true, + }, + valuesRedacted: true, + }); + const compact = compactWebProbeScriptResult(report); + assert.equal(compact?.failureKind, "browser-runtime-blocker"); + assert.equal(compact?.error, "browser-runtime-closed"); + assert.deepEqual(compact?.issueEvidence, { + ok: false, + status: "blocked", + credentialResolved: true, + requestReached: false, + apiResponseReached: false, + errorSummary: { + message: "page.goto: Target page, context or browser has been closed", + status: 0, + response: null, + stepCount: 0, + valuesRedacted: true, + }, + degradedReason: "browser-runtime-closed", + failureKind: "browser-runtime-blocker", + failedCondition: "page.goto: Target page, context or browser has been closed", + nextAction: null, + baseUrl: null, + finalUrl: null, + lastUrl: null, + scriptSha256: null, + runDir: null, + reportPath: ".state/web-probe-script/run.fixture/web-probe-script-report.json", + reportSha256: `sha256:${"a".repeat(64)}`, + result: null, + apiMatrix: null, + lastStep: null, + steps: [], + lastScreenshot: null, + screenshots: [], + valuesRedacted: true, + }); +}); + +test("真实认证响应或已发出的登录请求不降级为浏览器运行时 blocker", () => { + const base = { + auth: { + method: "ui-form-memory", + status: 0, + usernamePresent: true, + attempts: [{ response: null }], + errorSummary: { message: "page.goto: Target page, context or browser has been closed" }, + }, + script: { steps: [] }, + }; + assert.equal(classifyWebProbeBrowserRuntimeAuthFailure({ + ...base, + auth: { ...base.auth, status: 401, attempts: [{ response: { status: 401 } }] }, + }), null); + assert.equal(classifyWebProbeBrowserRuntimeAuthFailure({ + ...base, + auth: { ...base.auth, requestReached: true }, + }), null); +});