fix: classify pre-request WebProbe browser closure

This commit is contained in:
Codex
2026-07-16 19:41:54 +02:00
parent c17106d897
commit 73016e35e5
4 changed files with 242 additions and 4 deletions
@@ -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。
+91 -4
View File
@@ -12,11 +12,15 @@ function nullableRecord(value: unknown): Record<string, unknown> | null {
export function compactWebProbeScriptResult(report: Record<string, unknown> | null): Record<string, unknown> | 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<string, unknown> | 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<string, unknown>): Record<string, unknown> | 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<string, unknown> => 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<string, unknown> | null,
blocker: Record<string, unknown> | null,
): Record<string, unknown> | 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<string, unknown> {
const script = record(value);
return {
@@ -81,6 +160,10 @@ function compactIssueSummary(value: Record<string, unknown>): Record<string, unk
return {
ok: value.ok === true,
status: typeof value.status === "string" ? value.status : null,
credentialResolved: typeof value.credentialResolved === "boolean" ? value.credentialResolved : null,
requestReached: typeof value.requestReached === "boolean" ? value.requestReached : null,
apiResponseReached: typeof value.apiResponseReached === "boolean" ? value.apiResponseReached : null,
errorSummary: compactJsonForIssue(value.errorSummary),
degradedReason: typeof value.degradedReason === "string" ? value.degradedReason : null,
failureKind: typeof value.failureKind === "string" ? value.failureKind : null,
failedCondition: typeof value.failedCondition === "string" ? value.failedCondition : null,
@@ -149,6 +232,10 @@ function compactIssueEvidence(value: unknown): Record<string, unknown> | 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,
@@ -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,
@@ -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);
});