fix: classify pre-request WebProbe browser closure
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user