fix: improve hwlab probe diagnostics
This commit is contained in:
@@ -74,6 +74,7 @@ JS
|
||||
|
||||
- `web-probe script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file <path>` 提供脚本;只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`。
|
||||
- 自定义 `web-probe script` 仍运行在 UniDesk `trans` 60s 最外层短连接约束内;能在一轮内完成的 P4 验收优先把 `--command-timeout-seconds` 控制在 55 秒以内,并减少无界 selector/network 等待。确需等待更久时,改用 `web-probe run` 的异步 job/status 语义,或把动作拆成“提交/采样/截图/状态读取”多次短 probe。若输出出现 `UNIDESK_SSH_RUNTIME_TIMEOUT` 但同时恢复了 `reportPath`、`reportSha256`、screenshots 或 DOM steps,先按远端报告判断脚本/页面实际状态;最终关闭证据仍优先用一次未触发短连接超时的 bounded rerun。
|
||||
- issue closeout 优先引用 `web-probe script` 输出的顶层 `issueEvidence` 或 `summary.issueEvidence`;只有需要展开调查时才粘贴 `probe.script.result`、`probe.steps` 或完整 `reportPath`,避免 stdout、summary 和 report 多层重复同一证据。
|
||||
- stdin heredoc 与 `--script-file` 都按 ES module 加载,脚本必须导出 `export default async ({ page, gotoStable, recordStep, ... }) => { ... }`;不要在模块顶层直接写 `return`。失败为 `Illegal return statement`、`does not provide an export named default` 或 finalUrl 仍是 `about:blank` 且 stepCount=0 时,先按 probe 脚本入口误用处理,不要归因成 Cloud Web 行为失败。
|
||||
- web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`;脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。
|
||||
- 脚本构造 URL 时使用 `new URL(path, baseUrl).toString()`;不要拼出 `//v1/...`。
|
||||
|
||||
@@ -24,12 +24,16 @@ G14/D601 v03 的 bootstrap admin password 是 HWLAB runtime Secret 生命周期
|
||||
|
||||
`hwlab nodes web-probe run|script --node <node> --lane <lane>` 是 HWLAB Cloud Web 线上 DOM/Playwright 验收的受控入口;CLI 负责从 YAML 解析 workspace、public URL 和 bootstrap admin sourceRef,并只输出 redacted 凭据状态、artifact path/hash、readiness、`probe.summary` 和失败分类。`run` 使用 repo-owned 标准 DOM probe;`script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file <path>` 提供调用者脚本。`run --message ...` 未显式设置 trace 参数时会做轻量 trace 采样,`script` helper 可用 `recordStep` / `safeFetchJson` / `fetchApiMatrix` 保留失败前的结构化 partial evidence,完整 redacted 报告通过 `reportPath`/`reportSha256` 展开。具体 Web 开发、fake-server Playwright、fixture 脱敏、`web-probe script` helper、截图和 Workbench/Performance 判定口径统一见 `$unidesk-webdev`,本 CLI 参考不再维护第二套操作面。
|
||||
|
||||
`web-probe script` 的 issue closeout 优先引用顶层 `issueEvidence` 或 `summary.issueEvidence`,其中包含 redacted `result`、最近 steps、lastStep、script/report SHA 和截图摘要;完整调查才展开 `probe.script.result`、`probe.steps` 或 `reportPath`,避免把同一证据在 stdout、summary 和 report 多层重复粘贴。
|
||||
|
||||
`hwlab nodes control-plane infra plan|status|apply --node D601 --lane v03` 是 D601 HWLAB v03 节点本地 k3s、CI/CD 与 git-mirror 前置控制面的 YAML 驱动入口,配置真相源是 `config/hwlab-node-control-plane.yaml`。`plan` 只读展示 YAML target、host k3s node config 摘要和将渲染的 control-plane 对象;`status` 只读观察 k3s systemd drop-in 与 node `capacity/allocatable.pods`、D601 Tekton、CI namespace、git-mirror、Argo、node-local registry 和 tools image readiness;`apply --dry-run` 只输出 manifest 与 host config 摘要;`apply --confirm` 按 YAML 收敛 D601 host k3s drop-in 和 control-plane bootstrap 对象,只有 host k3s 配置或 live pod capacity 未收敛时才重启 k3s,不触发 HWLAB runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。D601 host 侧 k3s pre-start 修正也必须写成 YAML `execStartPre` argv,不做手工 systemd 热改;当 kube API 已不可用时,`apply` 可用同一 YAML 渲染出的 host 脚本经 node-local tools image/Docker fallback 恢复 systemd drop-in,输出仍只给对象名、SHA、exit code 和摘要。k3s pod capacity 等可调数值只以 YAML 为准,长期参考不复制具体数值;tools image 的 node-local registry 地址只能作为输出 artifact,输入 base image 必须由 YAML 声明为公开 registry 来源,缺少 output image 时应在 `status.next.blockers` 中体现,而不是把现有 node-local image 当成输入基础镜像。
|
||||
|
||||
`hwlab nodes git-mirror status|sync|flush --node <node> --lane <lane>` 是 node-scoped runtime lane 的 Git mirror 维护入口。`status` 的 `githubSource` / `githubGitops` 来自本地 mirror cache 的 `refs/mirror-stage/...`,不是实时 GitHub API;输出中的 `refSources.githubFieldsAreMirrorStageCache=true` 和 `refSources.cacheRefresh` 给出这一来源和刷新命令。`sync --confirm --wait` 的 k3s Job 遇到 GitHub SSH transient 时,应通过目标 workspace fallback 拉取 GitHub source/gitops 并写回 node-local mirror,输出只披露 commit、mirror write URL 和 fallback 状态。`flush --confirm --wait` 如果已经把 GitOps ref push 到 GitHub,但 post-push fetch/recheck 因 transient SSH 失败而无法刷新 mirror-stage,会标记 `partialSuccess=push-succeeded-fetch-failed`;CLI 应自动执行一次受控 sync 刷新 mirror-stage,若恢复后 `pendingFlush=false` 且 `githubInSync=true`,结果应为 `ok=true` 并输出 `partialSuccessRecovered` / `postPushRecovery`,否则才保留 `degradedReason=node-runtime-git-mirror-flush-post-push-fetch-failed` 和下一步 `sync --confirm --wait`。不要把这种 partial success 解读为需要连续盲目 flush。`hwlab nodes control-plane trigger-current --node <node> --lane <lane> --confirm --wait` 会在 source sync 后自动执行必要的 pre-flush,在 PipelineRun terminal 后自动执行必要的 post-flush;progress 事件必须显式输出 `git-mirror-pre-flush` / `git-mirror-post-flush` 的 executed/skipped、jobName、local/github source、local/github GitOps、`pendingFlush` 和 `githubInSync`,且已恢复的 partial success 不能让顶层 trigger-current false-fail。`control-plane status` 仍是只读入口,只暴露 compact `gitMirror` 摘要和下一步 flush 命令,不隐式执行写操作。
|
||||
|
||||
PR 合并后触发 node-scoped runtime lane 时,`control-plane status --pipeline-run <name>` 是某次 PipelineRun 的定点观察入口,但同一输出中的 `sourceHead` / `summary.sourceCommit` 仍可能反映当前分支最新 head;如果触发后又有后续 PR 合并,当前 head 可能已经不是该 PipelineRun 名称中的短 SHA。closeout 证据必须同时写明:PR merge commit、定点 PipelineRun 名称和状态、最终 runtime/GitOps revision、当前 branch tip,以及当前 branch tip 是否包含本次 PR merge commit。不要只凭 `summary.sourceCommit` 反推某个旧 PipelineRun 的源码身份。
|
||||
|
||||
`hwlab nodes control-plane status` 的 `publicProbe.ready` 表示控制面从公网用户入口访问 YAML 声明 public Web/API 成功;`publicProbe.targetHost` 只表示目标节点 host 自己访问同一公网 URL 的诊断结果。若 `publicProbe.ready=true` 且 `publicProbe.diagnostic.kind=target-host-public-egress-mismatch`,closeout 仍以 `publicProbe` 和 `web-probe` 用户入口证据为准,host 侧 `hwlab-cli` 访问公网失败应单独按目标 host egress/hairpin 问题跟踪。
|
||||
|
||||
PipelineRun 失败或长时间未完成时,先按定点 `control-plane status --pipeline-run <name>` 和 bounded 只读 k3s 诊断定位失败 TaskRun/Pod/container。env-reuse service build 常见失败点是 `build-<service>` 的 `step-publish` 日志,apt、npm、Go module 等外部依赖下载可能通过 lane YAML 注入的 egress proxy 出现瞬时 502、reset 或超时;先用 `platform-infra sub2api status|validate` 区分共享 proxy 整体故障和单个上游 transient。proxy 健康但单个依赖下载 transient 时,可以受控 `trigger-current --rerun`;重复失败应把对应 `artifact-publish`/envRecipe 下载步骤补成有限重试后重新合并发布。不要用原生 `kubectl delete/patch`、pod 内热补或盲目全量重跑替代持久化 recipe 修复。
|
||||
|
||||
`hwlab nodes control-plane infra tools-image status|build|logs --node D601 --lane v03` 是 D601 tools image 的受控入口。Dockerfile 必须由 `config/hwlab-node-control-plane.yaml` 的 `tekton.toolsImage.dockerfileInline` 声明,输入镜像必须列在 `publicBaseImages`,构建参数和网络模式也来自 YAML;confirmed build 只在 D601 后台异步构建并推送到 node-local registry,返回 status/logs 轮询命令。`hwlab nodes control-plane infra argo status|apply|logs --node D601 --lane v03` 是 D601 Argo CD 的声明式安装入口。Argo 版本、官方 manifest URL、镜像 rewrite/preload、field manager、imagePullPolicy、CRD 列表、期望 Deployment/StatefulSet 以及生成的 AppProject/Application 都必须来自同一个 YAML;`argo apply --confirm` 只执行可重复 server-side apply 和后台轮询,不把原生 `kubectl apply`、手工 Argo CLI 或临时 manifest 作为正式安装路径。
|
||||
|
||||
@@ -75,7 +75,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
},
|
||||
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.",
|
||||
"Issue-ready evidence is available under issueEvidence and summary.issueEvidence; 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.",
|
||||
"Use reloadStable(), gotoCurrentStable(), or safeReload() for bounded retries around page reload/current-URL navigation jitter such as ERR_NETWORK_CHANGED.",
|
||||
"Playwright page.evaluate accepts one serializable argument; use page.evaluate(({ a, b }) => ..., { a, b }) or safeEvaluate(fn, { a, b }).",
|
||||
|
||||
@@ -3349,10 +3349,14 @@ function nullableInteger(value: string): number | null {
|
||||
function nodeRuntimePublicProbeStatus(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
||||
const web = publicHttpProbe("web", spec.publicWebUrl);
|
||||
const apiHealth = publicHttpProbe("apiHealth", joinUrlPath(spec.publicApiUrl, "/health/live"));
|
||||
const ready = web.ok === true && apiHealth.ok === true;
|
||||
const targetHost = nodeRuntimeTargetHostPublicProbeStatus(spec);
|
||||
return {
|
||||
ready: web.ok === true && apiHealth.ok === true,
|
||||
ready,
|
||||
web,
|
||||
apiHealth,
|
||||
targetHost,
|
||||
diagnostic: nodeRuntimePublicProbeDiagnostic(ready, targetHost),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3368,6 +3372,103 @@ function publicHttpProbe(kind: string, url: string): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function nodeRuntimeTargetHostPublicProbeStatus(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
||||
const webUrl = spec.publicWebUrl;
|
||||
const apiHealthUrl = joinUrlPath(spec.publicApiUrl, "/health/live");
|
||||
const script = [
|
||||
"set -eu",
|
||||
`web_url=${shellQuote(webUrl)}`,
|
||||
`api_url=${shellQuote(apiHealthUrl)}`,
|
||||
"probe() {",
|
||||
" name=\"$1\"",
|
||||
" url=\"$2\"",
|
||||
" err_file=$(mktemp)",
|
||||
" set +e",
|
||||
" http_status=$(curl -k -sS --connect-timeout 5 --max-time 12 -o /dev/null -w '%{http_code}' \"$url\" 2>\"$err_file\")",
|
||||
" rc=$?",
|
||||
" set -e",
|
||||
" error_text=$(tr '\\r\\n\\t' ' ' <\"$err_file\" | tail -c 600)",
|
||||
" rm -f \"$err_file\"",
|
||||
" printf '%sUrl\\t%s\\n' \"$name\" \"$url\"",
|
||||
" printf '%sExitCode\\t%s\\n' \"$name\" \"$rc\"",
|
||||
" printf '%sHttpStatus\\t%s\\n' \"$name\" \"$http_status\"",
|
||||
" printf '%sError\\t%s\\n' \"$name\" \"$error_text\"",
|
||||
"}",
|
||||
"probe web \"$web_url\"",
|
||||
"probe apiHealth \"$api_url\"",
|
||||
].join("\n");
|
||||
const result = runTransHostScript(spec.nodeId, script, "", 35);
|
||||
const fields = keyValueLinesFromText(result.stdout);
|
||||
const web = targetHostPublicHttpProbeFromFields("web", fields, webUrl, result.exitCode === 0);
|
||||
const apiHealth = targetHostPublicHttpProbeFromFields("apiHealth", fields, apiHealthUrl, result.exitCode === 0);
|
||||
return {
|
||||
node: spec.nodeId,
|
||||
ready: web.ok === true && apiHealth.ok === true,
|
||||
probeAvailable: result.exitCode === 0 && result.timedOut !== true,
|
||||
web,
|
||||
apiHealth,
|
||||
result: compactRuntimeCommand(result),
|
||||
};
|
||||
}
|
||||
|
||||
function targetHostPublicHttpProbeFromFields(kind: string, fields: Record<string, string>, fallbackUrl: string, transportOk: boolean): Record<string, unknown> {
|
||||
const exitCode = numericField(fields[`${kind}ExitCode`]);
|
||||
const httpStatus = numericField(fields[`${kind}HttpStatus`]);
|
||||
const error = fields[`${kind}Error`] ?? "";
|
||||
return {
|
||||
kind,
|
||||
url: fields[`${kind}Url`] ?? fallbackUrl,
|
||||
ok: transportOk && exitCode === 0 && httpStatus !== null && httpStatus >= 200 && httpStatus < 400,
|
||||
httpStatus,
|
||||
exitCode,
|
||||
error: error.length > 0 ? error.slice(0, 600) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function nodeRuntimePublicProbeDiagnostic(publicReady: boolean, targetHost: Record<string, unknown>): Record<string, unknown> {
|
||||
const targetWeb = record(targetHost.web);
|
||||
const targetApiHealth = record(targetHost.apiHealth);
|
||||
const targetReady = targetHost.ready === true;
|
||||
if (!publicReady) {
|
||||
return {
|
||||
kind: "public-entry-probe-failed",
|
||||
affectsUserEntry: true,
|
||||
targetHostReady: targetReady,
|
||||
message: "control-plane public probe failed; treat this as a public endpoint readiness failure before using web-probe closeout evidence.",
|
||||
nextAction: "run hwlab nodes web-probe run for the same node/lane after checking publicProbe.web and publicProbe.apiHealth",
|
||||
};
|
||||
}
|
||||
if (targetHost.probeAvailable !== true) {
|
||||
return {
|
||||
kind: "target-host-public-probe-unavailable",
|
||||
affectsUserEntry: false,
|
||||
targetHostReady: false,
|
||||
message: "control-plane public probe passed, but the target host diagnostic probe could not run; this does not invalidate user-entry evidence.",
|
||||
nextAction: "use control-plane publicProbe and web-probe evidence for closeout; inspect target host SSH/trans health separately if host-side CLI must call the public URL",
|
||||
};
|
||||
}
|
||||
if (!targetReady) {
|
||||
return {
|
||||
kind: "target-host-public-egress-mismatch",
|
||||
affectsUserEntry: false,
|
||||
targetHostReady: false,
|
||||
failed: {
|
||||
web: targetWeb.ok === true ? null : { httpStatus: targetWeb.httpStatus ?? null, exitCode: targetWeb.exitCode ?? null, error: targetWeb.error ?? null },
|
||||
apiHealth: targetApiHealth.ok === true ? null : { httpStatus: targetApiHealth.httpStatus ?? null, exitCode: targetApiHealth.exitCode ?? null, error: targetApiHealth.error ?? null },
|
||||
},
|
||||
message: "control-plane public probe passed, but the target host cannot reach the same public URLs; classify this as target-host egress/hairpin diagnostics, not a public endpoint failure.",
|
||||
nextAction: "use control-plane publicProbe and web-probe evidence for issue closeout; track host-side public URL access separately if hwlab-cli must run on that host",
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "public-entry-and-target-host-ok",
|
||||
affectsUserEntry: false,
|
||||
targetHostReady: true,
|
||||
message: "control-plane public probe and target-host public URL probe both passed.",
|
||||
nextAction: null,
|
||||
};
|
||||
}
|
||||
|
||||
function joinUrlPath(baseUrl: string, suffix: string): string {
|
||||
return `${baseUrl.replace(/\/+$/u, "")}/${suffix.replace(/^\/+/u, "")}`;
|
||||
}
|
||||
@@ -3385,6 +3486,10 @@ function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, unknown>,
|
||||
const workloadCount = typeof runtime.workloadCount === "number" ? runtime.workloadCount : workloadReadiness.length;
|
||||
const webProbe = record(publicProbes.web);
|
||||
const apiProbe = record(publicProbes.apiHealth);
|
||||
const targetHostProbe = record(publicProbes.targetHost);
|
||||
const targetHostWebProbe = record(targetHostProbe.web);
|
||||
const targetHostApiProbe = record(targetHostProbe.apiHealth);
|
||||
const publicProbeDiagnostic = record(publicProbes.diagnostic);
|
||||
return {
|
||||
ok: status.ok === true,
|
||||
command: status.command,
|
||||
@@ -3437,6 +3542,26 @@ function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, unknown>,
|
||||
ready: publicProbes.ready === true,
|
||||
web: { url: webProbe.url ?? null, ok: webProbe.ok === true, httpStatus: webProbe.httpStatus ?? null },
|
||||
apiHealth: { url: apiProbe.url ?? null, ok: apiProbe.ok === true, httpStatus: apiProbe.httpStatus ?? null },
|
||||
targetHost: Object.keys(targetHostProbe).length === 0 ? null : {
|
||||
node: targetHostProbe.node ?? status.node ?? null,
|
||||
ready: targetHostProbe.ready === true,
|
||||
probeAvailable: targetHostProbe.probeAvailable === true,
|
||||
web: {
|
||||
url: targetHostWebProbe.url ?? null,
|
||||
ok: targetHostWebProbe.ok === true,
|
||||
httpStatus: targetHostWebProbe.httpStatus ?? null,
|
||||
exitCode: targetHostWebProbe.exitCode ?? null,
|
||||
error: targetHostWebProbe.error ?? null,
|
||||
},
|
||||
apiHealth: {
|
||||
url: targetHostApiProbe.url ?? null,
|
||||
ok: targetHostApiProbe.ok === true,
|
||||
httpStatus: targetHostApiProbe.httpStatus ?? null,
|
||||
exitCode: targetHostApiProbe.exitCode ?? null,
|
||||
error: targetHostApiProbe.error ?? null,
|
||||
},
|
||||
},
|
||||
diagnostic: Object.keys(publicProbeDiagnostic).length === 0 ? null : publicProbeDiagnostic,
|
||||
},
|
||||
gitMirror: {
|
||||
ready: gitMirror.ready === true,
|
||||
@@ -6212,6 +6337,7 @@ function runNodeWebProbeScript(
|
||||
stderrTail: result.stderr.trim().slice(-2000),
|
||||
valuesRedacted: true,
|
||||
});
|
||||
const issueEvidence = nullableRecord(report?.issueEvidence) ?? nullableRecord(effectiveSummary?.issueEvidence);
|
||||
const compactResult = compactCommandResultRedacted(result, [material.password ?? ""]);
|
||||
if (outputFailureKind !== null) {
|
||||
compactResult.stdoutTail = redactKnownSecrets(result.stdout.slice(-2000), [material.password ?? ""]);
|
||||
@@ -6230,6 +6356,7 @@ function runNodeWebProbeScript(
|
||||
degradedReason,
|
||||
failureKind,
|
||||
summary: effectiveSummary,
|
||||
issueEvidence,
|
||||
probe: report,
|
||||
reportLoad: stdoutReport !== null ? { source: "stdout", path: report?.reportPath ?? null, degradedReason: null } : recoveredReport === null ? null : {
|
||||
source: recoveredReport.source,
|
||||
|
||||
@@ -1773,7 +1773,7 @@ function sanitize(value, depth = 0, seen = new WeakSet()) {
|
||||
if (typeof value === "number" || typeof value === "boolean") return value;
|
||||
if (typeof value === "bigint") return String(value);
|
||||
if (typeof value === "function" || typeof value === "symbol") return "[" + typeof value + "]";
|
||||
if (depth > 8) return "[max-depth]";
|
||||
if (depth > 12) return "[max-depth]";
|
||||
if (Array.isArray(value)) return value.slice(0, 200).map((item) => sanitize(item, depth + 1, seen));
|
||||
if (typeof value === "object") {
|
||||
if (seen.has(value)) return "[circular]";
|
||||
@@ -1836,6 +1836,7 @@ async function emit(payload) {
|
||||
}
|
||||
|
||||
function compactStdoutPayload(payload) {
|
||||
const issueEvidence = compactIssueEvidenceForStdout(payload?.summary?.issueEvidence ?? issueEvidenceFromPayload(payload));
|
||||
return {
|
||||
ok: payload?.ok === true,
|
||||
status: payload?.status ?? null,
|
||||
@@ -1859,6 +1860,7 @@ function compactStdoutPayload(payload) {
|
||||
lastScreenshot: compactArtifactForStdout(payload?.lastScreenshot),
|
||||
readiness: compactJsonForStdout(payload?.readiness),
|
||||
artifacts: compactArtifactsForStdout(payload?.artifacts),
|
||||
issueEvidence,
|
||||
summary: compactSummaryForStdout(payload?.summary),
|
||||
safety: {
|
||||
valuesRedacted: true,
|
||||
@@ -1874,7 +1876,7 @@ function compactScriptForStdout(script) {
|
||||
const steps = Array.isArray(value.steps) ? value.steps : [];
|
||||
return {
|
||||
ok: value.ok === true,
|
||||
result: compactJsonForStdout(value.result),
|
||||
result: compactJsonForEvidence(value.result),
|
||||
stepCount: steps.length,
|
||||
};
|
||||
}
|
||||
@@ -1932,6 +1934,7 @@ function compactSummaryForStdout(summary) {
|
||||
apiMatrix: compactApiMatrixForStdout(value.apiMatrix),
|
||||
stepCount: Number.isFinite(value.stepCount) ? value.stepCount : null,
|
||||
lastStep: compactStepForStdout(value.lastStep),
|
||||
issueEvidence: compactIssueEvidenceForStdout(value.issueEvidence),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
@@ -2000,6 +2003,98 @@ function compactJsonForStdout(value, depth = 0) {
|
||||
return compactText(String(value), 180);
|
||||
}
|
||||
|
||||
function issueEvidenceFromPayload(payload) {
|
||||
const steps = publicSteps();
|
||||
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) => compactArtifactForStdout(item))
|
||||
.filter(Boolean);
|
||||
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);
|
||||
const failedCondition = ok ? null : payload?.errorMessage ?? payload?.error ?? payload?.failureKind ?? "script did not pass";
|
||||
const script = payload?.script && typeof payload.script === "object" ? payload.script : {};
|
||||
return {
|
||||
ok,
|
||||
status: payload?.status ?? null,
|
||||
degradedReason,
|
||||
failureKind,
|
||||
failedCondition,
|
||||
nextAction: ok ? null : issueNextAction(failureKind, payload),
|
||||
baseUrl: payload?.baseUrl ?? null,
|
||||
finalUrl: payload?.finalUrl ?? payload?.lastUrl ?? null,
|
||||
lastUrl: payload?.lastUrl ?? payload?.finalUrl ?? null,
|
||||
scriptSha256: payload?.scriptSha256 ?? null,
|
||||
runDir,
|
||||
reportPath: payload?.reportPath ?? null,
|
||||
reportSha256: payload?.reportSha256 ?? null,
|
||||
result: compactJsonForEvidence(script.result),
|
||||
apiMatrix: compactApiMatrixForStdout(latestApiMatrixFromSteps(steps)),
|
||||
lastStep: steps.length > 0 ? compactStepForEvidence(steps[steps.length - 1]) : null,
|
||||
steps: steps.slice(-3).map((step) => compactStepForEvidence(step)),
|
||||
lastScreenshot: compactArtifactForStdout(payload?.lastScreenshot),
|
||||
screenshots,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactIssueEvidenceForStdout(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
return {
|
||||
ok: value.ok === true,
|
||||
status: value.status ?? null,
|
||||
degradedReason: typeof value.degradedReason === "string" ? compactText(value.degradedReason, 600) : value.degradedReason ?? null,
|
||||
failureKind: typeof value.failureKind === "string" ? compactText(value.failureKind, 300) : value.failureKind ?? null,
|
||||
failedCondition: typeof value.failedCondition === "string" ? compactText(value.failedCondition, 1200) : value.failedCondition ?? null,
|
||||
nextAction: typeof value.nextAction === "string" ? compactText(value.nextAction, 1200) : value.nextAction ?? null,
|
||||
baseUrl: value.baseUrl ?? null,
|
||||
finalUrl: value.finalUrl ?? null,
|
||||
lastUrl: value.lastUrl ?? null,
|
||||
scriptSha256: value.scriptSha256 ?? null,
|
||||
runDir: value.runDir ?? runDir,
|
||||
reportPath: value.reportPath ?? null,
|
||||
reportSha256: value.reportSha256 ?? null,
|
||||
result: compactJsonForEvidence(value.result),
|
||||
apiMatrix: compactApiMatrixForStdout(value.apiMatrix),
|
||||
lastStep: compactStepForEvidence(value.lastStep),
|
||||
steps: Array.isArray(value.steps) ? value.steps.slice(-3).map((step) => compactStepForEvidence(step)) : [],
|
||||
lastScreenshot: compactArtifactForStdout(value.lastScreenshot),
|
||||
screenshots: Array.isArray(value.screenshots) ? value.screenshots.slice(-5).map((item) => compactArtifactForStdout(item)).filter(Boolean) : [],
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactStepForEvidence(step) {
|
||||
if (!step || typeof step !== "object" || Array.isArray(step)) return null;
|
||||
return {
|
||||
index: Number.isFinite(step.index) ? step.index : null,
|
||||
name: typeof step.name === "string" ? step.name : null,
|
||||
ok: typeof step.ok === "boolean" ? step.ok : null,
|
||||
atMs: Number.isFinite(step.atMs) ? step.atMs : null,
|
||||
data: compactJsonForEvidence(step.data),
|
||||
};
|
||||
}
|
||||
|
||||
function compactJsonForEvidence(value, depth = 0) {
|
||||
if (value === null || value === undefined) return value ?? null;
|
||||
if (typeof value === "string") return compactText(value, 600);
|
||||
if (typeof value === "number" || typeof value === "boolean") return value;
|
||||
if (typeof value === "bigint") return String(value);
|
||||
if (typeof value === "function" || typeof value === "symbol") return "[" + typeof value + "]";
|
||||
if (depth >= 8) return "[max-depth]";
|
||||
if (Array.isArray(value)) return value.slice(0, 16).map((item) => compactJsonForEvidence(item, depth + 1));
|
||||
if (typeof value === "object") {
|
||||
const out = {};
|
||||
for (const [key, nested] of Object.entries(value).slice(0, 32)) {
|
||||
out[key] = compactJsonForEvidence(nested, depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return compactText(String(value), 600);
|
||||
}
|
||||
|
||||
function compactText(value, maxChars) {
|
||||
return redactString(String(value)).replace(/\s+/gu, " ").trim().slice(0, maxChars);
|
||||
}
|
||||
@@ -2037,6 +2132,7 @@ function scriptIssueSummary(payload) {
|
||||
apiMatrix,
|
||||
stepCount: stepRecords.length,
|
||||
lastStep: stepRecords.length > 0 ? deepClonePlain(stepRecords[stepRecords.length - 1]) : null,
|
||||
issueEvidence: issueEvidenceFromPayload(payload),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ 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 issueEvidence = compactIssueEvidence(report.issueEvidence ?? summary.issueEvidence ?? fallbackIssueEvidence(report, summary));
|
||||
return {
|
||||
ok: report.ok === true,
|
||||
status: typeof report.status === "string" ? report.status : null,
|
||||
summary,
|
||||
issueEvidence,
|
||||
baseUrl: typeof report.baseUrl === "string" ? report.baseUrl : null,
|
||||
finalUrl: typeof report.finalUrl === "string" ? report.finalUrl : null,
|
||||
lastUrl: typeof report.lastUrl === "string" ? report.lastUrl : null,
|
||||
@@ -41,7 +43,7 @@ function compactWebProbeScriptBlock(value: unknown): Record<string, unknown> {
|
||||
const script = record(value);
|
||||
return {
|
||||
ok: script.ok === true,
|
||||
result: compactJsonForIssue(script.result),
|
||||
result: compactJsonForEvidence(script.result),
|
||||
stepCount: Array.isArray(script.steps) ? script.steps.length : null,
|
||||
};
|
||||
}
|
||||
@@ -80,6 +82,7 @@ function compactIssueSummary(value: Record<string, unknown>): Record<string, unk
|
||||
apiMatrix: compactApiMatrixSummary(value.apiMatrix),
|
||||
stepCount: typeof value.stepCount === "number" ? value.stepCount : null,
|
||||
lastStep: compactStepForIssue(value.lastStep),
|
||||
issueEvidence: compactIssueEvidence(value.issueEvidence),
|
||||
valuesRedacted: value.valuesRedacted === true,
|
||||
};
|
||||
}
|
||||
@@ -100,6 +103,87 @@ function compactJsonForIssue(value: unknown, depth = 0): unknown {
|
||||
return String(value).slice(0, 240);
|
||||
}
|
||||
|
||||
function fallbackIssueEvidence(report: Record<string, unknown>, summary: Record<string, unknown>): Record<string, unknown> {
|
||||
const script = record(report.script);
|
||||
return {
|
||||
ok: report.ok === true,
|
||||
status: typeof report.status === "string" ? report.status : summary.status ?? null,
|
||||
degradedReason: summary.degradedReason ?? null,
|
||||
failureKind: summary.failureKind ?? (typeof report.failureKind === "string" ? report.failureKind : null),
|
||||
failedCondition: summary.failedCondition ?? (typeof report.errorMessage === "string" ? report.errorMessage : null),
|
||||
nextAction: summary.nextAction ?? null,
|
||||
baseUrl: typeof report.baseUrl === "string" ? report.baseUrl : null,
|
||||
finalUrl: typeof report.finalUrl === "string" ? report.finalUrl : summary.finalUrl ?? null,
|
||||
lastUrl: typeof report.lastUrl === "string" ? report.lastUrl : summary.lastUrl ?? null,
|
||||
scriptSha256: typeof report.scriptSha256 === "string" ? report.scriptSha256 : summary.scriptSha256 ?? null,
|
||||
runDir: typeof report.runDir === "string" ? report.runDir : summary.runDir ?? null,
|
||||
reportPath: typeof report.reportPath === "string" ? report.reportPath : summary.reportPath ?? null,
|
||||
reportSha256: typeof report.reportSha256 === "string" ? report.reportSha256 : summary.reportSha256 ?? null,
|
||||
result: script.result,
|
||||
apiMatrix: summary.apiMatrix ?? null,
|
||||
lastStep: summary.lastStep ?? null,
|
||||
steps: Array.isArray(report.steps) ? report.steps.slice(-3) : [],
|
||||
lastScreenshot: report.lastScreenshot ?? summary.lastScreenshot ?? null,
|
||||
screenshots: summary.screenshots ?? [],
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactIssueEvidence(value: unknown): Record<string, unknown> | null {
|
||||
const evidence = nullableRecord(value);
|
||||
if (evidence === null) return null;
|
||||
return {
|
||||
ok: evidence.ok === true,
|
||||
status: typeof evidence.status === "string" ? evidence.status : null,
|
||||
degradedReason: typeof evidence.degradedReason === "string" ? evidence.degradedReason : null,
|
||||
failureKind: typeof evidence.failureKind === "string" ? evidence.failureKind : null,
|
||||
failedCondition: typeof evidence.failedCondition === "string" ? evidence.failedCondition : null,
|
||||
nextAction: typeof evidence.nextAction === "string" ? evidence.nextAction : null,
|
||||
baseUrl: typeof evidence.baseUrl === "string" ? evidence.baseUrl : null,
|
||||
finalUrl: typeof evidence.finalUrl === "string" ? evidence.finalUrl : null,
|
||||
lastUrl: typeof evidence.lastUrl === "string" ? evidence.lastUrl : null,
|
||||
scriptSha256: typeof evidence.scriptSha256 === "string" ? evidence.scriptSha256 : null,
|
||||
runDir: typeof evidence.runDir === "string" ? evidence.runDir : null,
|
||||
reportPath: typeof evidence.reportPath === "string" ? evidence.reportPath : null,
|
||||
reportSha256: typeof evidence.reportSha256 === "string" ? evidence.reportSha256 : null,
|
||||
result: compactJsonForEvidence(evidence.result),
|
||||
apiMatrix: compactApiMatrixSummary(evidence.apiMatrix),
|
||||
lastStep: compactStepForEvidence(evidence.lastStep),
|
||||
steps: Array.isArray(evidence.steps) ? evidence.steps.slice(-3).map(compactStepForEvidence).filter((item): item is Record<string, unknown> => item !== null) : [],
|
||||
lastScreenshot: nullableRecord(evidence.lastScreenshot),
|
||||
screenshots: Array.isArray(evidence.screenshots) ? evidence.screenshots.slice(-5).map(nullableRecord).filter((item): item is Record<string, unknown> => item !== null) : [],
|
||||
valuesRedacted: evidence.valuesRedacted === true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactStepForEvidence(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: compactJsonForEvidence(step.data),
|
||||
};
|
||||
}
|
||||
|
||||
function compactJsonForEvidence(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 >= 8) return "[max-depth]";
|
||||
if (Array.isArray(value)) return value.slice(0, 16).map((item) => compactJsonForEvidence(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, 32)) {
|
||||
out[key] = compactJsonForEvidence(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;
|
||||
|
||||
Reference in New Issue
Block a user