fix: improve web-probe evidence summaries (#466)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-18 11:33:57 +08:00
committed by GitHub
parent 343c13aa98
commit 384cb9dfb8
3 changed files with 526 additions and 18 deletions
+517 -15
View File
@@ -285,14 +285,16 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000",
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --url https://hwlab.pikapython.com --fresh-session --message 'ping'",
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 --script-file .state/probes/workbench.mjs",
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS'\nexport default async ({ waitWorkbenchReady, fetchJson, collectText, safeEvaluate, screenshot }) => {\n const ready = await waitWorkbenchReady();\n const workspace = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_workbench');\n const workspaceText = await collectText('#workspace');\n const evaluated = await safeEvaluate(({ a, b }) => ({ sum: a + b }), { a: 1, b: 2 });\n await screenshot('workbench.png');\n return { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, workspaceText, evaluated };\n};\nJS",
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS'\nexport default async ({ waitWorkbenchReady, fetchJson, fetchApiMatrix, recordStep, collectText, safeEvaluate, screenshot }) => {\n const ready = await waitWorkbenchReady();\n const workspace = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_workbench');\n const apiMatrix = await fetchApiMatrix(['/v1/workbench/workspace?projectId=prj_hwpod_workbench', '/auth/session']);\n const workspaceText = await collectText('#workspace');\n const evaluated = await safeEvaluate(({ a, b }) => ({ sum: a + b }), { a: 1, b: 2 });\n await screenshot('workbench.png');\n recordStep('workbench-summary', { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, apiMatrixOk: apiMatrix.ok });\n return { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, workspaceText, evaluated };\n};\nJS",
],
actions: {
run: "Run the repo-owned scripts/web-live-dom-probe.mjs helper.",
script: "Run caller-provided Playwright JS after CLI-managed /auth/login; the script receives authenticated browser/context/page plus fetchJson/collectText/safeEvaluate/waitWorkbenchReady/screenshotOnError/summarizeWorkspace/summarizeConversation helpers and must not handle secrets itself.",
script: "Run caller-provided Playwright JS after CLI-managed /auth/login; the script receives authenticated browser/context/page plus fetchJson/safeFetchJson/fetchApiMatrix/recordStep/collectText/safeEvaluate/waitWorkbenchReady/screenshotOnError/summarizeWorkspace/summarizeConversation helpers and must not handle secrets itself.",
},
notes: [
"Prefer --script-file for reusable probes; stdin heredocs remain supported for one-off probes.",
"Issue-ready summary is available under probe.summary; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.",
"Use recordStep(name, data) or fetchApiMatrix(paths) to keep structured partial evidence when a later step fails.",
"Playwright page.evaluate accepts one serializable argument; use page.evaluate(({ a, b }) => ..., { a, b }) or safeEvaluate(fn, { a, b }).",
"Failures include failureKind, errorMessage, scriptSha256, runDir, lastUrl, and lastScreenshot when a screenshot can be captured.",
],
@@ -4957,8 +4959,12 @@ function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
const waitAfterSubmitMs = positiveIntegerOption(args, "--wait-after-submit-ms", 1500, 60000);
const waitMessagesMs = positiveIntegerOption(args, "--wait-messages-ms", 2500, 60000);
const waitAgentTerminalMs = positiveIntegerOption(args, "--wait-agent-terminal-ms", 0, 600000);
const traceSampleCount = positiveIntegerOption(args, "--trace-sample-count", 0, 200);
const traceSampleIntervalMs = positiveIntegerOption(args, "--trace-sample-interval-ms", 0, 60000);
const message = optionValue(args, "--message") ?? null;
const hasMessage = message !== null;
const traceSampleCountDefault = hasMessage ? 2 : 0;
const traceSampleIntervalDefault = hasMessage ? 1500 : 0;
const traceSampleCount = positiveIntegerOption(args, "--trace-sample-count", traceSampleCountDefault, 200);
const traceSampleIntervalMs = positiveIntegerOption(args, "--trace-sample-interval-ms", traceSampleIntervalDefault, 60000);
const commandTimeoutAutoSeconds = nodeWebProbeAutoCommandTimeoutSeconds({
timeoutMs,
waitAfterSubmitMs,
@@ -4967,7 +4973,7 @@ function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
traceSampleCount,
traceSampleIntervalMs,
freshSession: args.includes("--fresh-session"),
hasMessage: optionValue(args, "--message") !== undefined,
hasMessage,
});
const commandTimeoutRaw = optionValue(args, "--command-timeout-seconds");
const commandTimeoutUserProvided = commandTimeoutRaw !== undefined;
@@ -4985,7 +4991,7 @@ function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
waitAgentTerminalMs,
traceSampleCount,
traceSampleIntervalMs,
message: optionValue(args, "--message") ?? null,
message,
conversationId: optionValue(args, "--conversation-id") ?? null,
freshSession: args.includes("--fresh-session"),
cancelRunning: !args.includes("--no-cancel-running"),
@@ -5060,6 +5066,7 @@ function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, unknown>
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const probe = compactWebProbeResult(parseJsonObject(result.stdout));
const passed = result.exitCode === 0 && probe?.status === "pass";
const summary = nullableRecord(probe?.summary);
const degradedReason = result.timedOut
? "web-probe-command-timeout"
: typeof probe?.degradedReason === "string"
@@ -5081,6 +5088,8 @@ function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, unknown>
timedOut: result.timedOut,
},
degradedReason,
failureKind: typeof summary?.failureKind === "string" ? summary.failureKind : null,
summary,
probe,
result: compactCommandResult(result),
valuesRedacted: true,
@@ -5143,6 +5152,7 @@ function runNodeWebProbeAsync(
const report = record(record(poll.status).report);
const probe = compactWebProbeResult(Object.keys(report).length > 0 ? report : null);
const passed = probe?.status === "pass";
const summary = nullableRecord(probe?.summary);
const degradedReason = poll.timedOut
? "web-probe-command-timeout"
: typeof probe?.degradedReason === "string"
@@ -5162,6 +5172,8 @@ function runNodeWebProbeAsync(
mode: "async",
commandTimeout: webProbeCommandTimeoutSummary(options, poll.timedOut),
degradedReason,
failureKind: typeof summary?.failureKind === "string" ? summary.failureKind : null,
summary,
job: {
jobId,
startedAt: start.startedAt ?? null,
@@ -5244,6 +5256,14 @@ function runNodeWebProbeScript(
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const report = compactWebProbeScriptResult(parseJsonObject(result.stdout));
const passed = result.exitCode === 0 && report?.ok === true;
const summary = nullableRecord(report?.summary);
const degradedReason = typeof summary?.degradedReason === "string"
? summary.degradedReason
: typeof report?.failureKind === "string"
? report.failureKind
: result.timedOut
? "web-probe-command-timeout"
: null;
return {
ok: passed,
status: passed ? "pass" : "blocked",
@@ -5254,6 +5274,9 @@ function runNodeWebProbeScript(
url: options.url,
credential,
scriptSource: options.scriptSource,
degradedReason,
failureKind: typeof summary?.failureKind === "string" ? summary.failureKind : typeof report?.failureKind === "string" ? report.failureKind : null,
summary,
probe: report,
result: compactCommandResultRedacted(result, [material.password ?? ""]),
valuesRedacted: true,
@@ -5293,7 +5316,8 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const startedAt = new Date().toISOString();
const startedAtMs = Date.now();
const startedAt = new Date(startedAtMs).toISOString();
const baseUrl = normalizeBaseUrl(process.env.HWLAB_WEB_BASE_URL);
const username = process.env.HWLAB_WEB_USER || "admin";
const password = process.env.HWLAB_WEB_PASS || "";
@@ -5303,6 +5327,7 @@ const timeoutMs = positiveInteger(process.env.UNIDESK_WEB_PROBE_TIMEOUT_MS, 3000
const viewport = parseViewport(process.env.UNIDESK_WEB_PROBE_VIEWPORT || "1440x900");
const artifactRecords = [];
const readinessRecords = [];
const stepRecords = [];
let browser;
let context;
let page;
@@ -5343,7 +5368,8 @@ try {
scriptSha256: userScriptSha256,
runDir,
auth: publicAuth(auth),
script: { ok: scriptOk, result: safeResult },
script: { ok: scriptOk, result: safeResult, steps: publicSteps() },
steps: publicSteps(),
failureKind: failure ? failure.failureKind : null,
error: failure ? failure.code : null,
errorMessage: failure ? sanitize(failure.message) : null,
@@ -5370,6 +5396,8 @@ try {
scriptSha256: userScriptSha256,
runDir,
auth: auth === null ? null : publicAuth(auth),
script: { ok: false, steps: publicSteps() },
steps: publicSteps(),
failureKind: failure.failureKind,
error: failure.code,
errorMessage: sanitize(failure.message),
@@ -5608,6 +5636,9 @@ function scriptHelpers() {
waitWorkbenchReady,
waitForReady,
fetchJson,
safeFetchJson,
fetchApiMatrix,
recordStep,
collectText,
safeEvaluate,
summarizeWorkspace,
@@ -5622,6 +5653,99 @@ function scriptHelpers() {
return helpers;
}
function recordStep(name, data = {}, options = {}) {
const normalizedName = String(name || "step").replace(/[^A-Za-z0-9_.:-]/gu, "-").slice(0, 80) || "step";
const item = {
index: stepRecords.length,
name: normalizedName,
ok: data && typeof data === "object" && typeof data.ok === "boolean" ? data.ok : null,
atMs: Date.now() - startedAtMs,
data: sanitize(data),
};
stepRecords.push(item);
while (stepRecords.length > boundedInteger(options.maxSteps, 80, 1, 200)) stepRecords.shift();
return deepClonePlain(item);
}
function publicSteps() {
return stepRecords.slice(-50).map((item) => deepClonePlain(item));
}
async function safeFetchJson(target, options = {}) {
try {
return await fetchJson(target, { ...normalizeHelperOptions(options), throwOnError: false });
} catch (error) {
const failure = classifiedProbeError(error);
return {
ok: false,
path: typeof target === "string" ? urlPath(target) : String(target ?? ""),
status: 0,
statusText: "helper-error",
failureKind: failure.failureKind,
error: sanitize(failure.message),
guidance: failure.guidance,
};
}
}
async function fetchApiMatrix(paths, options = {}) {
options = normalizeHelperOptions(options);
const items = normalizeApiMatrixItems(paths);
const rows = [];
for (const item of items) {
const response = await safeFetchJson(item.path, {
headers: item.headers ?? options.headers,
method: item.method ?? options.method,
body: item.body ?? undefined,
});
rows.push(compactApiMatrixResponse(item, response));
}
const failed = rows.filter((row) => row.ok !== true);
const matrix = {
ok: failed.length === 0,
count: rows.length,
okCount: rows.length - failed.length,
failedCount: failed.length,
items: rows,
};
if (options.record !== false) recordStep(typeof options.name === "string" ? options.name : "api-matrix", matrix);
return matrix;
}
function normalizeApiMatrixItems(value) {
const list = Array.isArray(value) ? value : [value];
return list.slice(0, 20).map((item, index) => {
if (typeof item === "string") return { name: item, path: item, method: "GET" };
if (item && typeof item === "object") {
const path = typeof item.path === "string" ? item.path : typeof item.url === "string" ? item.url : "/";
return {
name: typeof item.name === "string" ? item.name : path,
path,
method: typeof item.method === "string" ? item.method : "GET",
headers: item.headers,
body: item.body,
};
}
return { name: "item-" + index, path: "/", method: "GET" };
});
}
function compactApiMatrixResponse(item, response) {
const body = response && typeof response.body === "object" && response.body !== null ? response.body : null;
return {
name: item.name,
path: response?.path ?? item.path,
method: item.method ?? "GET",
ok: response?.ok === true,
status: response?.status ?? null,
statusText: response?.statusText ?? null,
error: response?.error ?? response?.parseError ?? null,
failureKind: response?.failureKind ?? null,
bodyKeys: body && !Array.isArray(body) ? Object.keys(body).slice(0, 20) : [],
bodyPreview: body ? normalizeTextPreview(JSON.stringify(sanitize(body)), 240) : response?.textPreview ?? null,
};
}
async function safeEvaluate(fn, arg = undefined, options = {}) {
options = normalizeHelperOptions(options);
if (typeof fn !== "function") {
@@ -6651,23 +6775,125 @@ function redactString(text) {
}
async function emit(payload) {
process.stdout.write(JSON.stringify(sanitize(payload), null, 2) + "\n");
const enriched = {
...payload,
steps: publicSteps(),
artifacts: { runDir, items: artifactRecords },
};
enriched.summary = scriptIssueSummary(enriched);
const reportFile = artifactPath("web-probe-script-report.json");
let reportArtifact = null;
try {
await writeFile(reportFile, JSON.stringify(sanitize(enriched), null, 2) + "\n", "utf8");
const fileStat = await stat(reportFile);
reportArtifact = {
kind: "json",
path: reportFile,
byteCount: fileStat.size,
sha256: await sha256File(reportFile),
};
artifactRecords.push({ ...reportArtifact });
} catch (error) {
reportArtifact = {
kind: "json",
path: reportFile,
error: error instanceof Error ? error.message : String(error),
};
}
const finalPayload = {
...enriched,
reportPath: reportArtifact && typeof reportArtifact.path === "string" ? reportArtifact.path : null,
reportSha256: reportArtifact && typeof reportArtifact.sha256 === "string" ? reportArtifact.sha256 : null,
artifacts: { runDir, items: artifactRecords },
};
finalPayload.summary = scriptIssueSummary(finalPayload);
process.stdout.write(JSON.stringify(sanitize(finalPayload), null, 2) + "\n");
}
function scriptIssueSummary(payload) {
const artifacts = Array.isArray(payload?.artifacts?.items) ? payload.artifacts.items : artifactRecords;
const screenshots = artifacts
.filter((item) => item && typeof item === "object" && item.kind === "screenshot")
.slice(-5)
.map((item) => ({
path: item.path ?? null,
sha256: item.sha256 ?? null,
byteCount: item.byteCount ?? null,
}));
const apiMatrix = latestApiMatrixFromSteps(publicSteps());
const ok = payload?.ok === true;
const degradedReason = ok ? null : payload?.error ?? payload?.failureKind ?? "web-probe-script-failed";
const failureKind = ok ? null : classifyIssueFailureKind(payload?.failureKind ?? payload?.error ?? degradedReason, payload?.errorMessage);
return {
ok,
status: payload?.status ?? null,
degradedReason,
failureKind,
failedCondition: ok ? null : payload?.errorMessage ?? payload?.error ?? payload?.failureKind ?? "script did not pass",
nextAction: ok ? null : issueNextAction(failureKind, payload),
baseUrl: payload?.baseUrl ?? null,
finalUrl: payload?.finalUrl ?? payload?.lastUrl ?? null,
scriptSha256: payload?.scriptSha256 ?? null,
runDir,
reportPath: payload?.reportPath ?? null,
reportSha256: payload?.reportSha256 ?? null,
lastScreenshot: payload?.lastScreenshot ?? null,
screenshots,
apiMatrix,
stepCount: stepRecords.length,
lastStep: stepRecords.length > 0 ? deepClonePlain(stepRecords[stepRecords.length - 1]) : null,
valuesRedacted: true,
};
}
function latestApiMatrixFromSteps(steps) {
for (let index = steps.length - 1; index >= 0; index -= 1) {
const step = steps[index];
const data = step && typeof step === "object" ? step.data : null;
if (data && typeof data === "object" && Array.isArray(data.items) && typeof data.failedCount === "number") return data;
}
return null;
}
function classifyIssueFailureKind(kind, message = "") {
const text = String(kind ?? "") + " " + String(message ?? "");
if (/api|fetch|network|Failed to fetch|HTTP|status/iu.test(text)) return "network-or-api-fetch-bug";
if (/script-api-misuse|page\.evaluate|safeEvaluate|Too many arguments/iu.test(text)) return "script-bug";
if (/auth|login|credential/iu.test(text)) return "target-auth-bug";
if (/browser|chromium|playwright|executable/iu.test(text)) return "browser-environment-bug";
if (/assert|expect|ok:false|validation|final-response|message/iu.test(text)) return "unmet-expectation";
if (/script|ReferenceError|TypeError|SyntaxError/iu.test(text)) return "script-bug";
return "user-facing-web-bug";
}
function issueNextAction(failureKind, payload) {
if (failureKind === "network-or-api-fetch-bug") return "Inspect summary.apiMatrix failed rows and retry the same node/lane after checking API availability.";
if (failureKind === "script-bug") return "Fix the probe script or use safeEvaluate/safeFetchJson/fetchApiMatrix helpers, then rerun the same command.";
if (failureKind === "target-auth-bug") return "Inspect credential sourceRef/fingerprint and target /auth/login state; do not print secrets.";
if (failureKind === "browser-environment-bug") return "Inspect Playwright/browser-launcher availability in the target workspace.";
if (failureKind === "unmet-expectation") return "Inspect summary.failedCondition, DOM/API steps, and screenshots to decide whether the Web behavior or the assertion is stale.";
return payload?.guidance ?? "Inspect reportPath for full redacted details and rerun the same node/lane entry after the root cause is fixed.";
}
`;
}
function compactWebProbeScriptResult(report: Record<string, unknown> | null): Record<string, unknown> | null {
if (report === null) return null;
const summary = compactIssueSummary(record(report.summary));
return {
ok: report.ok === true,
status: typeof report.status === "string" ? report.status : null,
summary,
baseUrl: typeof report.baseUrl === "string" ? report.baseUrl : null,
finalUrl: typeof report.finalUrl === "string" ? report.finalUrl : null,
lastUrl: typeof report.lastUrl === "string" ? report.lastUrl : null,
scriptSha256: typeof report.scriptSha256 === "string" ? report.scriptSha256 : null,
runDir: typeof report.runDir === "string" ? report.runDir : null,
reportPath: typeof report.reportPath === "string" ? report.reportPath : null,
reportSha256: typeof report.reportSha256 === "string" ? report.reportSha256 : null,
auth: record(report.auth),
script: record(report.script),
script: compactWebProbeScriptBlock(report.script),
steps: compactWebProbeSteps(report.steps),
failureKind: typeof report.failureKind === "string" ? report.failureKind : null,
guidance: typeof report.guidance === "string" ? report.guidance : null,
lastScreenshot: nullableRecord(report.lastScreenshot),
@@ -6679,6 +6905,113 @@ function compactWebProbeScriptResult(report: Record<string, unknown> | null): Re
};
}
function compactWebProbeScriptBlock(value: unknown): Record<string, unknown> {
const script = record(value);
return {
ok: script.ok === true,
result: compactJsonForIssue(script.result),
stepCount: Array.isArray(script.steps) ? script.steps.length : null,
};
}
function compactWebProbeSteps(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) return [];
return value.slice(-30).map((item) => {
const step = record(item);
return {
index: typeof step.index === "number" ? step.index : null,
name: typeof step.name === "string" ? step.name : null,
ok: typeof step.ok === "boolean" ? step.ok : null,
atMs: typeof step.atMs === "number" ? step.atMs : null,
data: compactStepDataForIssue(step.data),
};
});
}
function compactIssueSummary(value: Record<string, unknown>): Record<string, unknown> {
if (Object.keys(value).length === 0) return {};
return {
ok: value.ok === true,
status: typeof value.status === "string" ? value.status : null,
degradedReason: typeof value.degradedReason === "string" ? value.degradedReason : null,
failureKind: typeof value.failureKind === "string" ? value.failureKind : null,
failedCondition: typeof value.failedCondition === "string" ? value.failedCondition : null,
nextAction: typeof value.nextAction === "string" ? value.nextAction : null,
finalUrl: typeof value.finalUrl === "string" ? value.finalUrl : null,
scriptSha256: typeof value.scriptSha256 === "string" ? value.scriptSha256 : null,
runDir: typeof value.runDir === "string" ? value.runDir : null,
reportPath: typeof value.reportPath === "string" ? value.reportPath : null,
reportSha256: typeof value.reportSha256 === "string" ? value.reportSha256 : null,
lastScreenshot: nullableRecord(value.lastScreenshot),
screenshots: Array.isArray(value.screenshots) ? value.screenshots.slice(-5).map(nullableRecord).filter((item): item is Record<string, unknown> => item !== null) : [],
apiMatrix: compactApiMatrixSummary(value.apiMatrix),
stepCount: typeof value.stepCount === "number" ? value.stepCount : null,
lastStep: compactStepForIssue(value.lastStep),
valuesRedacted: value.valuesRedacted === true,
};
}
function compactJsonForIssue(value: unknown, depth = 0): unknown {
if (value === null || value === undefined) return value ?? null;
if (typeof value === "string") return value.replace(/\s+/gu, " ").trim().slice(0, 600);
if (typeof value === "number" || typeof value === "boolean") return value;
if (depth >= 5) return "[max-depth]";
if (Array.isArray(value)) return value.slice(0, 30).map((item) => compactJsonForIssue(item, depth + 1));
if (typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [key, nested] of Object.entries(value as Record<string, unknown>).slice(0, 40)) {
out[key] = compactJsonForIssue(nested, depth + 1);
}
return out;
}
return String(value).slice(0, 600);
}
function compactStepForIssue(value: unknown): Record<string, unknown> | null {
const step = nullableRecord(value);
if (step === null) return null;
return {
index: typeof step.index === "number" ? step.index : null,
name: typeof step.name === "string" ? step.name : null,
ok: typeof step.ok === "boolean" ? step.ok : null,
atMs: typeof step.atMs === "number" ? step.atMs : null,
data: compactStepDataForIssue(step.data),
};
}
function compactStepDataForIssue(value: unknown): unknown {
const data = nullableRecord(value);
if (data === null) return compactJsonForIssue(value);
if (Array.isArray(data.items) && typeof data.failedCount === "number") return compactApiMatrixSummary(data);
return compactJsonForIssue(data);
}
function compactApiMatrixSummary(value: unknown): Record<string, unknown> | null {
const matrix = nullableRecord(value);
if (matrix === null) return null;
return {
ok: matrix.ok === true,
count: typeof matrix.count === "number" ? matrix.count : null,
okCount: typeof matrix.okCount === "number" ? matrix.okCount : null,
failedCount: typeof matrix.failedCount === "number" ? matrix.failedCount : null,
items: Array.isArray(matrix.items)
? matrix.items.slice(0, 12).map((item) => {
const row = record(item);
return {
name: typeof row.name === "string" ? row.name : null,
path: typeof row.path === "string" ? row.path : null,
method: typeof row.method === "string" ? row.method : null,
ok: row.ok === true,
status: typeof row.status === "number" ? row.status : null,
error: typeof row.error === "string" ? row.error : null,
failureKind: typeof row.failureKind === "string" ? row.failureKind : null,
bodyKeys: Array.isArray(row.bodyKeys) ? row.bodyKeys.filter((key) => typeof key === "string").slice(0, 12) : [],
};
})
: [],
};
}
function parseSecretOptions(args: string[]): NodeSecretOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "cleanup-owned-postgres" && actionRaw !== "cleanup-obsolete") {
@@ -9522,18 +9855,26 @@ function parseJsonObject(text: string): Record<string, unknown> | null {
function compactWebProbeResult(report: Record<string, unknown> | null): Record<string, unknown> | null {
if (report === null) return null;
const dom = record(report.dom);
const performance = record(report.performance);
const trace = record(report.trace);
const session = record(report.session);
const performance = compactWebProbePerformance(report.performance);
const trace = compactWebProbeTrace(report.trace);
const session = compactWebProbeSession(report.session);
const artifacts = record(report.artifacts);
const promptValidation = compactPromptValidation(report.promptValidation);
const degradedReason = typeof report.degradedReason === "string" ? report.degradedReason : null;
const summary = webProbeRunIssueSummary({ report, session, trace, promptValidation, artifacts, degradedReason });
return {
ok: report.ok === true,
status: typeof report.status === "string" ? report.status : null,
summary,
finalUrl: typeof report.finalUrl === "string" ? report.finalUrl : null,
error: typeof report.error === "string" ? report.error : null,
degradedReason: typeof report.degradedReason === "string" ? report.degradedReason : null,
degradedReason,
failureKind: typeof summary.failureKind === "string" ? summary.failureKind : null,
nextAction: typeof summary.nextAction === "string" ? summary.nextAction : null,
actions: compactWebProbeActions(report.actions),
session,
trace,
promptValidation,
performance,
traceSamples: compactWebProbeTraceSamples(report.traceSamples),
dom: {
@@ -9543,11 +9884,172 @@ function compactWebProbeResult(report: Record<string, unknown> | null): Record<s
sessionRail: record(dom.sessionRail),
},
failureDom: record(report.failureDom),
artifacts: record(report.artifacts),
artifacts,
safety: record(report.safety),
};
}
function compactWebProbeSession(value: unknown): Record<string, unknown> {
const session = record(value);
const freshSession = record(session.freshSession);
return {
freshSessionRequested: session.freshSessionRequested === true,
freshSessionAligned: typeof session.freshSessionAligned === "boolean" ? session.freshSessionAligned : null,
conversationId: session.conversationId ?? null,
routeConversationId: session.routeConversationId ?? null,
selectedConversationId: session.selectedConversationId ?? null,
sessionId: session.sessionId ?? null,
revision: session.revision ?? null,
messageCount: session.messageCount ?? null,
degradedReason: session.degradedReason ?? null,
freshSession: Object.keys(freshSession).length === 0 ? null : {
settled: freshSession.settled === true,
aligned: freshSession.aligned === true,
reason: freshSession.reason ?? null,
},
};
}
function compactWebProbeTrace(value: unknown): Record<string, unknown> {
const trace = record(value);
const latestFetch = record(trace.latestFetch);
return {
requested: trace.requested === true,
sampleCount: trace.sampleCount ?? null,
conversationId: trace.conversationId ?? null,
sessionId: trace.sessionId ?? null,
traceId: trace.traceId ?? null,
finalAgentStatus: trace.finalAgentStatus ?? null,
finalTraceStatus: trace.finalTraceStatus ?? null,
finalDomRowCount: trace.finalDomRowCount ?? null,
firstDomRowVisibleAt: trace.firstDomRowVisibleAt ?? null,
firstDomRowPreview: typeof trace.firstDomRowPreview === "string" ? trace.firstDomRowPreview.slice(0, 240) : null,
firstRestEventVisibleAt: trace.firstRestEventVisibleAt ?? null,
firstRestEventCount: trace.firstRestEventCount ?? null,
restTraceOk: trace.restTraceOk === true,
failedFetchCount: trace.failedFetchCount ?? null,
latestFetch: Object.keys(latestFetch).length === 0 ? null : {
ok: latestFetch.ok === true,
httpStatus: latestFetch.httpStatus ?? null,
eventCount: latestFetch.eventCount ?? null,
traceStatus: latestFetch.traceStatus ?? null,
degradedReason: latestFetch.degradedReason ?? null,
attempts: Array.isArray(latestFetch.attempts) ? latestFetch.attempts.length : null,
latestEventPreview: typeof latestFetch.latestEventPreview === "string" ? latestFetch.latestEventPreview.slice(0, 240) : null,
},
degradedReason: trace.degradedReason ?? null,
};
}
function compactWebProbePerformance(value: unknown): Record<string, unknown> {
const performance = record(value);
return {
startedAt: performance.startedAt ?? null,
submittedAt: performance.submittedAt ?? null,
firstDomTraceRowAt: performance.firstDomTraceRowAt ?? null,
firstRestTraceEventAt: performance.firstRestTraceEventAt ?? null,
submitToFirstDomTraceRowMs: performance.submitToFirstDomTraceRowMs ?? null,
submitToFirstRestTraceEventMs: performance.submitToFirstRestTraceEventMs ?? null,
totalSampleWindowMs: performance.totalSampleWindowMs ?? null,
};
}
function compactPromptValidation(value: unknown): Record<string, unknown> {
const prompt = record(value);
const finalResponse = record(prompt.finalResponse);
return {
ok: prompt.ok === true,
applicable: prompt.applicable === true,
reason: typeof prompt.reason === "string" ? prompt.reason : null,
failures: Array.isArray(prompt.failures) ? prompt.failures.filter((item) => typeof item === "string").slice(0, 20) : [],
finalResponse: Object.keys(finalResponse).length === 0 ? null : {
textChars: typeof finalResponse.textChars === "number" ? finalResponse.textChars : null,
textPreview: typeof finalResponse.textPreview === "string" ? finalResponse.textPreview.slice(0, 500) : null,
markdown: nullableRecord(finalResponse.markdown),
},
};
}
function webProbeRunIssueSummary(input: {
report: Record<string, unknown>;
session: Record<string, unknown>;
trace: Record<string, unknown>;
promptValidation: Record<string, unknown>;
artifacts: Record<string, unknown>;
degradedReason: string | null;
}): Record<string, unknown> {
const failureKind = webProbeRunFailureKind(input.degradedReason, input.promptValidation);
const failedCondition = webProbeRunFailedCondition(input.degradedReason, input.promptValidation);
const promptSubmitted = record(input.report.safety).promptSubmitted === true;
const traceRequested = input.trace.requested === true || record(input.report.safety).traceSamplingRequested === true;
return {
ok: input.report.status === "pass" && !input.degradedReason,
status: typeof input.report.status === "string" ? input.report.status : null,
degradedReason: input.degradedReason,
failureKind,
failedCondition,
nextAction: webProbeRunNextAction(failureKind, input.degradedReason, input.promptValidation, promptSubmitted, traceRequested),
finalUrl: typeof input.report.finalUrl === "string" ? input.report.finalUrl : null,
conversationId: input.session.conversationId ?? input.trace.conversationId ?? null,
sessionId: input.session.sessionId ?? input.trace.sessionId ?? null,
traceId: input.trace.traceId ?? null,
traceRequested,
traceStatus: input.trace.finalTraceStatus ?? null,
agentStatus: input.trace.finalAgentStatus ?? null,
messageCount: input.session.messageCount ?? record(input.report.dom).messageCount ?? null,
promptValidation: input.promptValidation,
reportPath: typeof input.artifacts.reportPath === "string" ? input.artifacts.reportPath : null,
reportSha256: typeof input.artifacts.reportSha256 === "string" ? input.artifacts.reportSha256 : null,
screenshotPath: typeof input.artifacts.screenshotPath === "string" ? input.artifacts.screenshotPath : null,
screenshotSha256: typeof input.artifacts.screenshotSha256 === "string" ? input.artifacts.screenshotSha256 : null,
traceRecommendation: promptSubmitted && !traceRequested
? "rerun with --trace-sample-count 2 --trace-sample-interval-ms 1500, or rely on the new default for prompt probes"
: null,
valuesRedacted: true,
};
}
function webProbeRunFailureKind(degradedReason: string | null, promptValidation: Record<string, unknown>): string | null {
if (degradedReason === null) return null;
if (degradedReason === "prompt-validation-failed") return "unmet-expectation";
if (/trace-fetch|api|fetch|http|network/iu.test(degradedReason)) return "network-or-api-fetch-bug";
if (/auth|login|credential/iu.test(degradedReason)) return "target-auth-bug";
if (/browser|timeout|playwright|chromium/iu.test(degradedReason)) return "browser-environment-bug";
const failures = Array.isArray(promptValidation.failures) ? promptValidation.failures.join(" ") : "";
if (/final-response|agent-message|markdown|completed/iu.test(failures)) return "unmet-expectation";
return "user-facing-web-bug";
}
function webProbeRunFailedCondition(degradedReason: string | null, promptValidation: Record<string, unknown>): string | null {
if (degradedReason === "prompt-validation-failed") {
const failures = Array.isArray(promptValidation.failures) ? promptValidation.failures.filter((item) => typeof item === "string") : [];
return failures.length > 0 ? failures.join(",") : "prompt final response validation failed";
}
return degradedReason;
}
function webProbeRunNextAction(
failureKind: string | null,
degradedReason: string | null,
promptValidation: Record<string, unknown>,
promptSubmitted: boolean,
traceRequested: boolean,
): string | null {
if (failureKind === null) return null;
if (failureKind === "unmet-expectation") {
const failures = Array.isArray(promptValidation.failures) ? promptValidation.failures.join(",") : "";
if (/agent-not-completed/iu.test(failures)) return "Increase --wait-agent-terminal-ms or inspect the Code Agent terminal state for the conversation/trace ids in summary.";
if (/final-response/iu.test(failures)) return "Inspect Workbench message projection and conversation detail; terminal messages must expose a final response.";
if (/markdown/iu.test(failures)) return "Check whether the prompt expectation still requires markdown structure; stale assertions should be removed instead of preserved.";
return "Inspect promptValidation.failures, screenshots, and reportPath to decide whether the Web behavior or the assertion is stale.";
}
if (failureKind === "network-or-api-fetch-bug") return "Inspect trace/session API fetch fields in reportPath and retry after checking target API availability.";
if (failureKind === "target-auth-bug") return "Inspect credential sourceRef/fingerprint and /auth/login status; do not print secrets.";
if (failureKind === "browser-environment-bug") return "Inspect browser launcher and Playwright availability on the target workspace.";
if (promptSubmitted && !traceRequested) return "Rerun with trace sampling if trace evidence is required.";
return degradedReason === null ? null : "Inspect reportPath for full redacted details, then rerun the same node/lane entry.";
}
function compactWebProbeTraceSamples(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) return [];
return value.map(record).map((sample) => {