Merge pull request #1468 from pikasTech/feat/1467-child-json-recovery-migration
feat(cli): migrate high-risk child JSON recovery callers
This commit is contained in:
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { test } from "bun:test";
|
import { test } from "bun:test";
|
||||||
|
|
||||||
import { resolveCliChildJsonObject } from "./cli-child-json-recovery";
|
import { resolveCliChildJsonCommandResult, resolveCliChildJsonObject } from "./cli-child-json-recovery";
|
||||||
|
|
||||||
test("child JSON recovery reads full JSON from CLI stdout dump wrapper", async () => {
|
test("child JSON recovery reads full JSON from CLI stdout dump wrapper", async () => {
|
||||||
const dir = await mkdtemp(join(tmpdir(), "unidesk-cli-child-json-dump-"));
|
const dir = await mkdtemp(join(tmpdir(), "unidesk-cli-child-json-dump-"));
|
||||||
@@ -34,6 +34,34 @@ test("child JSON recovery reads full JSON from CLI stdout dump wrapper", async (
|
|||||||
assert.equal(resolved.diagnostics.dumpJsonOk, true);
|
assert.equal(resolved.diagnostics.dumpJsonOk, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("child JSON recovery command-result helper keeps dump diagnostics", async () => {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), "unidesk-cli-child-json-result-dump-"));
|
||||||
|
const dumpPath = join(dir, "dump.json");
|
||||||
|
await writeFile(dumpPath, JSON.stringify({ ok: true, verified: true, bytes: 123 }) + "\n");
|
||||||
|
const resolved = resolveCliChildJsonCommandResult({
|
||||||
|
result: {
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
outputTruncated: true,
|
||||||
|
reason: "stdout-json-bytes-exceeded-threshold",
|
||||||
|
dump: { path: dumpPath },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: "",
|
||||||
|
exitCode: 0,
|
||||||
|
timedOut: false,
|
||||||
|
},
|
||||||
|
requestedStdoutType: "trans download verification JSON",
|
||||||
|
acceptParsed: (value) => value.ok === true && value.verified === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolved.source, "dump");
|
||||||
|
assert.equal(resolved.parsed?.verified, true);
|
||||||
|
assert.equal(resolved.diagnostics.requestedStdoutType, "trans download verification JSON");
|
||||||
|
assert.equal(resolved.diagnostics.dumpPath, dumpPath);
|
||||||
|
});
|
||||||
|
|
||||||
test("child JSON recovery falls back to artifact after trans truncation summary", () => {
|
test("child JSON recovery falls back to artifact after trans truncation summary", () => {
|
||||||
const summary = {
|
const summary = {
|
||||||
code: "ssh-truncation-summary",
|
code: "ssh-truncation-summary",
|
||||||
|
|||||||
@@ -19,6 +19,23 @@ export interface CliChildJsonResolution {
|
|||||||
diagnostics: Record<string, unknown>;
|
diagnostics: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveCliChildJsonCommandResult(options: {
|
||||||
|
result: { stdout: string; stderr?: string; exitCode?: number | null; timedOut?: boolean };
|
||||||
|
requestedStdoutType: string;
|
||||||
|
acceptParsed?: (value: Record<string, unknown>) => boolean;
|
||||||
|
artifactFallback?: CliChildJsonArtifactFallback | null;
|
||||||
|
forceArtifactFallbackReason?: string | null;
|
||||||
|
}): CliChildJsonResolution {
|
||||||
|
return resolveCliChildJsonObject({
|
||||||
|
stdout: options.result.stdout,
|
||||||
|
stderr: options.result.stderr ?? "",
|
||||||
|
requestedStdoutType: options.requestedStdoutType,
|
||||||
|
acceptParsed: options.acceptParsed,
|
||||||
|
artifactFallback: options.artifactFallback,
|
||||||
|
forceArtifactFallbackReason: options.forceArtifactFallbackReason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveCliChildJsonObject(options: {
|
export function resolveCliChildJsonObject(options: {
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr?: string;
|
stderr?: string;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { repoRoot, rootPath, type Config } from "./config";
|
import { repoRoot, rootPath, type Config } from "./config";
|
||||||
import { runCommand, type CommandResult } from "./command";
|
import { runCommand, type CommandResult } from "./command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
|
||||||
import { resolveAgentRunLaneTarget, type AgentRunLaneSpec } from "./agentrun-lanes";
|
import { resolveAgentRunLaneTarget, type AgentRunLaneSpec } from "./agentrun-lanes";
|
||||||
import { resolveSecretSourceRoot } from "./agentrun/secrets";
|
import { resolveSecretSourceRoot } from "./agentrun/secrets";
|
||||||
import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||||
@@ -353,7 +354,8 @@ function applyFakeModelProvider(state: FakeModelProviderState): Record<string, u
|
|||||||
const materialized = materializeFakeModelProviderSources(state);
|
const materialized = materializeFakeModelProviderSources(state);
|
||||||
const rendered = renderFakeModelProviderManifests(state, materialized.providerApiKey);
|
const rendered = renderFakeModelProviderManifests(state, materialized.providerApiKey);
|
||||||
const applyResult = runRemoteApply(state, rendered.yaml);
|
const applyResult = runRemoteApply(state, rendered.yaml);
|
||||||
const applyPayload = parseJsonObject(applyResult.stdout);
|
const applyResolution = resolveFakeModelProviderRemotePayload(applyResult, "fake-model-provider apply JSON");
|
||||||
|
const applyPayload = applyResolution.parsed ?? {};
|
||||||
const status = remoteFakeModelProviderStatus(state);
|
const status = remoteFakeModelProviderStatus(state);
|
||||||
return {
|
return {
|
||||||
ok: applyResult.exitCode === 0 && applyPayload.ok === true && status.ok === true,
|
ok: applyResult.exitCode === 0 && applyPayload.ok === true && status.ok === true,
|
||||||
@@ -364,7 +366,7 @@ function applyFakeModelProvider(state: FakeModelProviderState): Record<string, u
|
|||||||
mutation: true,
|
mutation: true,
|
||||||
materialized: redactMaterialization(materialized),
|
materialized: redactMaterialization(materialized),
|
||||||
manifest: rendered.summary,
|
manifest: rendered.summary,
|
||||||
apply: Object.keys(applyPayload).length > 0 ? applyPayload : compactCommand(applyResult, state.options.full),
|
apply: Object.keys(applyPayload).length > 0 ? { ...applyPayload, stdoutRecovery: applyResolution.diagnostics, valuesRedacted: true } : { ...compactCommand(applyResult, state.options.full), stdoutRecovery: applyResolution.diagnostics },
|
||||||
status,
|
status,
|
||||||
next: {
|
next: {
|
||||||
smoke: `bun scripts/cli.ts hwlab nodes fake-model-provider smoke --node ${state.options.node} --lane ${state.options.lane} --provider ${state.options.provider}`,
|
smoke: `bun scripts/cli.ts hwlab nodes fake-model-provider smoke --node ${state.options.node} --lane ${state.options.lane} --provider ${state.options.provider}`,
|
||||||
@@ -548,7 +550,8 @@ function remoteFakeModelProviderStatus(state: FakeModelProviderState): Record<st
|
|||||||
const runtime = state.runtime;
|
const runtime = state.runtime;
|
||||||
const script = remoteStatusScript(runtime);
|
const script = remoteStatusScript(runtime);
|
||||||
const result = runCommand([transPath(), state.spec.nodeKubeRoute, "sh", "--", script], repoRoot, { timeoutMs: state.options.timeoutSeconds * 1000 });
|
const result = runCommand([transPath(), state.spec.nodeKubeRoute, "sh", "--", script], repoRoot, { timeoutMs: state.options.timeoutSeconds * 1000 });
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveFakeModelProviderRemotePayload(result, "fake-model-provider status JSON");
|
||||||
|
const payload = payloadResolution.parsed ?? {};
|
||||||
return {
|
return {
|
||||||
ok: result.exitCode === 0 && payload.ok === true,
|
ok: result.exitCode === 0 && payload.ok === true,
|
||||||
command: "hwlab nodes fake-model-provider status",
|
command: "hwlab nodes fake-model-provider status",
|
||||||
@@ -562,6 +565,7 @@ function remoteFakeModelProviderStatus(state: FakeModelProviderState): Record<st
|
|||||||
service: stringAt(runtime, "serviceName"),
|
service: stringAt(runtime, "serviceName"),
|
||||||
},
|
},
|
||||||
...(Object.keys(payload).length > 0 ? payload : { result: compactCommand(result, state.options.full) }),
|
...(Object.keys(payload).length > 0 ? payload : { result: compactCommand(result, state.options.full) }),
|
||||||
|
stdoutRecovery: payloadResolution.diagnostics,
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -570,7 +574,8 @@ function remoteFakeModelProviderSmoke(state: FakeModelProviderState): Record<str
|
|||||||
const runtime = state.runtime;
|
const runtime = state.runtime;
|
||||||
const script = remoteSmokeScript(runtime);
|
const script = remoteSmokeScript(runtime);
|
||||||
const result = runCommand([transPath(), state.spec.nodeKubeRoute, "sh", "--", script], repoRoot, { timeoutMs: state.options.timeoutSeconds * 1000 });
|
const result = runCommand([transPath(), state.spec.nodeKubeRoute, "sh", "--", script], repoRoot, { timeoutMs: state.options.timeoutSeconds * 1000 });
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveFakeModelProviderRemotePayload(result, "fake-model-provider smoke JSON");
|
||||||
|
const payload = payloadResolution.parsed ?? {};
|
||||||
return {
|
return {
|
||||||
ok: result.exitCode === 0 && payload.ok === true,
|
ok: result.exitCode === 0 && payload.ok === true,
|
||||||
command: "hwlab nodes fake-model-provider smoke",
|
command: "hwlab nodes fake-model-provider smoke",
|
||||||
@@ -584,6 +589,7 @@ function remoteFakeModelProviderSmoke(state: FakeModelProviderState): Record<str
|
|||||||
service: stringAt(runtime, "serviceName"),
|
service: stringAt(runtime, "serviceName"),
|
||||||
},
|
},
|
||||||
...(Object.keys(payload).length > 0 ? payload : { result: compactCommand(result, state.options.full) }),
|
...(Object.keys(payload).length > 0 ? payload : { result: compactCommand(result, state.options.full) }),
|
||||||
|
stdoutRecovery: payloadResolution.diagnostics,
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -905,22 +911,13 @@ function tomlString(value: string): string {
|
|||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonObject(text: string): Record<string, unknown> {
|
function resolveFakeModelProviderRemotePayload(result: CommandResult, requestedStdoutType: string): { parsed: Record<string, unknown> | null; diagnostics: Record<string, unknown> } {
|
||||||
const trimmed = text.trim();
|
const resolved = resolveCliChildJsonCommandResult({
|
||||||
if (trimmed.length > 0) {
|
result,
|
||||||
try {
|
requestedStdoutType,
|
||||||
return record(JSON.parse(trimmed) as unknown, "json");
|
acceptParsed: (value) => typeof value.ok === "boolean" || typeof value.error === "string" || typeof value.failureKind === "string" || Object.keys(value).length > 1,
|
||||||
} catch {
|
});
|
||||||
const start = trimmed.indexOf("{");
|
return { parsed: resolved.parsed, diagnostics: resolved.diagnostics };
|
||||||
const end = trimmed.lastIndexOf("}");
|
|
||||||
if (start >= 0 && end > start) {
|
|
||||||
try {
|
|
||||||
return record(JSON.parse(trimmed.slice(start, end + 1)) as unknown, "json");
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compactCommand(result: CommandResult, full = false): Record<string, unknown> {
|
function compactCommand(result: CommandResult, full = false): Record<string, unknown> {
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ function renderWebObserveCollectTable(value: Record<string, unknown>): string {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (collect === null) {
|
if (collect === null) {
|
||||||
|
const stdoutRecovery = record(value.stdoutRecovery) ?? record(result.stdoutRecovery);
|
||||||
|
const artifact = record(stdoutRecovery?.artifact);
|
||||||
lines.push(
|
lines.push(
|
||||||
"Collect command:",
|
"Collect command:",
|
||||||
webObserveTable(["REQUESTED", "REASON", "EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDOUT_TAIL", "STDERR"], [[
|
webObserveTable(["REQUESTED", "REASON", "EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDOUT_TAIL", "STDERR"], [[
|
||||||
@@ -329,8 +331,19 @@ function renderWebObserveCollectTable(value: Record<string, unknown>): string {
|
|||||||
webObserveShort(webObserveText(result.stderr), 160),
|
webObserveShort(webObserveText(result.stderr), 160),
|
||||||
]]),
|
]]),
|
||||||
"",
|
"",
|
||||||
|
"Stdout recovery:",
|
||||||
|
webObserveTable(["REQUESTED_TYPE", "STDOUT_KIND", "SOURCE", "FALLBACK_REASON", "DUMP_PATH", "ARTIFACT_PATH", "NEXT"], [[
|
||||||
|
webObserveShort(webObserveText(stdoutRecovery?.requestedStdoutType), 42),
|
||||||
|
webObserveShort(webObserveText(stdoutRecovery?.stdoutKind), 28),
|
||||||
|
webObserveShort(webObserveText(stdoutRecovery?.source), 20),
|
||||||
|
webObserveShort(webObserveText(stdoutRecovery?.fallbackReason), 40),
|
||||||
|
webObserveShort(webObserveText(stdoutRecovery?.dumpPath), 72),
|
||||||
|
webObserveShort(webObserveText(artifact?.path ?? artifact?.requestedPath), 72),
|
||||||
|
webObserveShort(webObserveText(artifact?.nextCommand), 96),
|
||||||
|
]]),
|
||||||
|
"",
|
||||||
"Disclosure:",
|
"Disclosure:",
|
||||||
" collect stdout was not valid JSON; fix the collect command or rerun with a narrower --file after the root cause is visible.",
|
" collect stdout could not be recovered as a valid collect contract; inspect dumpPath or artifact path when present, otherwise rerun with a narrower --file.",
|
||||||
);
|
);
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { CommandResult } from "./command";
|
import type { CommandResult } from "./command";
|
||||||
import { runCommand } from "./command";
|
import { runCommand } from "./command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
|
||||||
import { repoRoot, rootPath } from "./config";
|
import { repoRoot, rootPath } from "./config";
|
||||||
import { readWebProbeSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-config-ref";
|
import { readWebProbeSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-config-ref";
|
||||||
import type { ChildCliResult, CompactCommandResult, SentinelCicdState } from "./hwlab-node-web-sentinel-cicd";
|
import type { ChildCliResult, CompactCommandResult, SentinelCicdState } from "./hwlab-node-web-sentinel-cicd";
|
||||||
@@ -809,10 +810,13 @@ function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", p
|
|||||||
let parsed: Record<string, unknown> | null = null;
|
let parsed: Record<string, unknown> | null = null;
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: attemptTimeoutSeconds * 1000 });
|
result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: attemptTimeoutSeconds * 1000 });
|
||||||
parsed = parseJsonObject(result.stdout);
|
const parsedResolution = resolveCliChildJsonCommandResult({
|
||||||
const recovered = parsed === null ? recoverTruncatedSshStdoutJson(result) : null;
|
result,
|
||||||
if (recovered !== null) parsed = recovered.parsed;
|
requestedStdoutType: "web-probe sentinel service proxy response JSON",
|
||||||
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, parsedFromDump: recovered !== null, dumpPath: recovered?.dumpPath ?? null, valuesRedacted: true });
|
acceptParsed: isSentinelServiceResponseContract,
|
||||||
|
});
|
||||||
|
parsed = parsedResolution.parsed;
|
||||||
|
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, stdoutRecovery: parsedResolution.diagnostics, valuesRedacted: true });
|
||||||
if (result.exitCode === 0) break;
|
if (result.exitCode === 0) break;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -832,27 +836,15 @@ function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", p
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function recoverTruncatedSshStdoutJson(result: CommandResult): { parsed: Record<string, unknown>; dumpPath: string } | null {
|
function isSentinelServiceResponseContract(value: Record<string, unknown>): boolean {
|
||||||
const dumpPath = sshStdoutDumpPathFromStderr(result.stderr);
|
return value.ok === false
|
||||||
if (dumpPath === null || !existsSync(dumpPath)) return null;
|
|| typeof value.view === "string"
|
||||||
const parsed = parseJsonObject(readFileSync(dumpPath, "utf8"));
|
|| typeof value.renderedText === "string"
|
||||||
return parsed === null ? null : { parsed, dumpPath };
|
|| typeof value.error === "string"
|
||||||
}
|
|| Array.isArray(value.availableViews)
|
||||||
|
|| Object.keys(record(value.run)).length > 0
|
||||||
function sshStdoutDumpPathFromStderr(value: string): string | null {
|
|| Object.keys(record(value.summary)).length > 0
|
||||||
for (const rawLine of value.split(/\r?\n/u)) {
|
|| Array.isArray(value.findings);
|
||||||
const line = rawLine.trim();
|
|
||||||
const prefix = "UNIDESK_SSH_STDOUT_TRUNCATED ";
|
|
||||||
if (!line.startsWith(prefix)) continue;
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(line.slice(prefix.length)) as unknown;
|
|
||||||
const dumpPath = stringAtNullable(record(payload), "dumpPath");
|
|
||||||
if (dumpPath !== null) return dumpPath;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compactSentinelServiceBodyJson(value: Record<string, unknown> | null): unknown {
|
function compactSentinelServiceBodyJson(value: Record<string, unknown> | null): unknown {
|
||||||
@@ -1392,7 +1384,12 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
|
|||||||
"NODE",
|
"NODE",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 });
|
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 });
|
||||||
const parsed = parseJsonObject(result.stdout);
|
const parsedResolution = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: "web-probe sentinel quick-verify analysis summary JSON",
|
||||||
|
acceptParsed: (value) => value.ok === true || value.ok === false || typeof value.reason === "string" || typeof value.reportJsonPath === "string" || Object.keys(record(value.counts)).length > 0,
|
||||||
|
});
|
||||||
|
const parsed = parsedResolution.parsed;
|
||||||
const parsedRecord = record(parsed);
|
const parsedRecord = record(parsed);
|
||||||
const reason = stringAtNullable(parsedRecord, "reason")
|
const reason = stringAtNullable(parsedRecord, "reason")
|
||||||
?? (result.timedOut ? "workspace-artifact-read-timeout"
|
?? (result.timedOut ? "workspace-artifact-read-timeout"
|
||||||
@@ -1406,6 +1403,7 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
|
|||||||
reason,
|
reason,
|
||||||
stateDir: stringAtNullable(parsedRecord, "stateDir") ?? stateDir,
|
stateDir: stringAtNullable(parsedRecord, "stateDir") ?? stateDir,
|
||||||
result: compactCommand(result),
|
result: compactCommand(result),
|
||||||
|
stdoutRecovery: parsedResolution.diagnostics,
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1426,7 +1424,12 @@ function waitForQuickVerifyObserverStartup(state: SentinelCicdState, observerId:
|
|||||||
const waitMs = Math.max(1000, Math.min(55_000, deadline - Date.now()));
|
const waitMs = Math.max(1000, Math.min(55_000, deadline - Date.now()));
|
||||||
const script = quickVerifyObserverStartupWaitScript(indexEntry.stateDir, waitMs, pollSleepMs);
|
const script = quickVerifyObserverStartupWaitScript(indexEntry.stateDir, waitMs, pollSleepMs);
|
||||||
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 5000 });
|
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 5000 });
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: "quick-verify observer startup wait JSON",
|
||||||
|
acceptParsed: (value) => typeof value.status === "string" || typeof value.heartbeatStatus === "string" || Array.isArray(value.observations) || Object.keys(record(value.startup)).length > 0,
|
||||||
|
});
|
||||||
|
const payload = payloadResolution.parsed;
|
||||||
if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record));
|
if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record));
|
||||||
const terminalPayload = {
|
const terminalPayload = {
|
||||||
observerId,
|
observerId,
|
||||||
@@ -1435,7 +1438,7 @@ function waitForQuickVerifyObserverStartup(state: SentinelCicdState, observerId:
|
|||||||
heartbeatStatus: typeof payload?.heartbeatStatus === "string" ? payload.heartbeatStatus : null,
|
heartbeatStatus: typeof payload?.heartbeatStatus === "string" ? payload.heartbeatStatus : null,
|
||||||
startup: record(payload?.startup),
|
startup: record(payload?.startup),
|
||||||
observations: observations.slice(-6),
|
observations: observations.slice(-6),
|
||||||
waitResult: compactCommand(result),
|
waitResult: { ...compactCommand(result), stdoutRecovery: payloadResolution.diagnostics },
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
};
|
};
|
||||||
if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-startup-wait-chunk-timeout") {
|
if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-startup-wait-chunk-timeout") {
|
||||||
@@ -1574,7 +1577,10 @@ export function runChildCli(args: string[], timeoutSeconds: number, input?: stri
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
ok: result.exitCode === 0 && !result.timedOut,
|
ok: result.exitCode === 0 && !result.timedOut,
|
||||||
parsed: parseJsonObject(result.stdout),
|
parsed: resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: `child CLI ${args.join(" ")} JSON`,
|
||||||
|
}).parsed,
|
||||||
result: compactCommandWithTail(result),
|
result: compactCommandWithTail(result),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1596,7 +1602,12 @@ function waitForQuickVerifyPromptTurn(state: SentinelCicdState, observerId: stri
|
|||||||
const waitMs = Math.max(1000, Math.min(Math.max(1000, Math.trunc(chunkSeconds * 1000)), deadline - Date.now()));
|
const waitMs = Math.max(1000, Math.min(Math.max(1000, Math.trunc(chunkSeconds * 1000)), deadline - Date.now()));
|
||||||
const script = quickVerifyPromptWaitScript(indexEntry.stateDir, promptIndex, waitMs, pollSleepMs);
|
const script = quickVerifyPromptWaitScript(indexEntry.stateDir, promptIndex, waitMs, pollSleepMs);
|
||||||
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 8000 });
|
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 8000 });
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: "quick-verify prompt wait JSON",
|
||||||
|
acceptParsed: (value) => typeof value.status === "string" || typeof value.traceId === "string" || Array.isArray(value.observations) || value.finalResponseEmpty === true || value.composerReadyForTurn === true,
|
||||||
|
});
|
||||||
|
const payload = payloadResolution.parsed;
|
||||||
if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record));
|
if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record));
|
||||||
const status = typeof payload?.status === "string" ? payload.status : null;
|
const status = typeof payload?.status === "string" ? payload.status : null;
|
||||||
const terminalPayload = {
|
const terminalPayload = {
|
||||||
@@ -1607,7 +1618,7 @@ function waitForQuickVerifyPromptTurn(state: SentinelCicdState, observerId: stri
|
|||||||
composerReadyForTurn: payload?.composerReadyForTurn === true,
|
composerReadyForTurn: payload?.composerReadyForTurn === true,
|
||||||
composerAction: typeof payload?.composerAction === "string" ? payload.composerAction : null,
|
composerAction: typeof payload?.composerAction === "string" ? payload.composerAction : null,
|
||||||
observations: observations.slice(-6),
|
observations: observations.slice(-6),
|
||||||
waitResult: compactCommand(result),
|
waitResult: { ...compactCommand(result), stdoutRecovery: payloadResolution.diagnostics },
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
};
|
};
|
||||||
if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-wait-chunk-timeout") {
|
if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-wait-chunk-timeout") {
|
||||||
@@ -1840,18 +1851,7 @@ function normalizeQuickVerifyStatus(value: string | null): string {
|
|||||||
|
|
||||||
function cliDataPayload(parsed: Record<string, unknown> | null): Record<string, unknown> {
|
function cliDataPayload(parsed: Record<string, unknown> | null): Record<string, unknown> {
|
||||||
const root = record(parsed);
|
const root = record(parsed);
|
||||||
const payload = isRecord(root.data) ? root.data : root;
|
return isRecord(root.data) ? root.data : root;
|
||||||
return cliDumpPayload(payload) ?? payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cliDumpPayload(payload: Record<string, unknown>): Record<string, unknown> | null {
|
|
||||||
if (payload.outputTruncated !== true) return null;
|
|
||||||
const dumpPath = stringAtNullable(record(payload.dump), "path");
|
|
||||||
if (dumpPath === null || !existsSync(dumpPath)) return null;
|
|
||||||
const dumped = parseJsonObject(readFileSync(dumpPath, "utf8"));
|
|
||||||
if (dumped === null) return null;
|
|
||||||
const dumpedRoot = record(dumped);
|
|
||||||
return isRecord(dumpedRoot.data) ? dumpedRoot.data : dumpedRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findScenario(state: SentinelCicdState, scenarioId: string): Record<string, unknown> | null {
|
function findScenario(state: SentinelCicdState, scenarioId: string): Record<string, unknown> | null {
|
||||||
@@ -2256,7 +2256,7 @@ function quickVerifyControlFindings(failure: string | null, promptIndex: number,
|
|||||||
return [quickVerifyControlFinding(
|
return [quickVerifyControlFinding(
|
||||||
"quick-verify-diagnostics-inconclusive",
|
"quick-verify-diagnostics-inconclusive",
|
||||||
"快速验证诊断信息不足",
|
"快速验证诊断信息不足",
|
||||||
"quick verify did not prove a durable completed turn, but structured diagnostics did not match any specific failure code.",
|
"quick verify did not prove a durable completed turn, but structured diagnostics did not match a specific failure code.",
|
||||||
`quick verify has rowCount=${rowCount} scopedRowCount=${scopedRowCount}, traceIdPresent=${traceDiagnostics.traceIdPresent === true}, finalResponseEmpty=${traceDiagnostics.finalResponseEmpty === true}.`,
|
`quick verify has rowCount=${rowCount} scopedRowCount=${scopedRowCount}, traceIdPresent=${traceDiagnostics.traceIdPresent === true}, finalResponseEmpty=${traceDiagnostics.finalResponseEmpty === true}.`,
|
||||||
"Improve turn-summary/trace-frame diagnostics before making a business recovery decision.",
|
"Improve turn-summary/trace-frame diagnostics before making a business recovery decision.",
|
||||||
promptIndex,
|
promptIndex,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p11-monitor-web-observability-dashboard.
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p11-monitor-web-observability-dashboard.
|
||||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
|
||||||
// Responsibility: P5 web-probe sentinel service validation, maintenance, report and dashboard commands.
|
// Responsibility: P5 web-probe sentinel service validation, maintenance, report and dashboard commands.
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
|
||||||
import type { CommandResult } from "./command";
|
import type { CommandResult } from "./command";
|
||||||
import { runCommand } from "./command";
|
import { runCommand } from "./command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
|
||||||
import { repoRoot } from "./config";
|
import { repoRoot } from "./config";
|
||||||
import { startJob } from "./jobs";
|
import { startJob } from "./jobs";
|
||||||
import type { RenderedCliResult } from "./output";
|
import type { RenderedCliResult } from "./output";
|
||||||
@@ -513,8 +513,13 @@ function readSentinelReportArtifactSummary(state: SentinelCicdState, body: Recor
|
|||||||
"NODE",
|
"NODE",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.max(5, Math.min(timeoutSeconds, 55)) * 1000 });
|
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.max(5, Math.min(timeoutSeconds, 55)) * 1000 });
|
||||||
const parsed = parseJsonObject(result.stdout);
|
const parsedResolution = resolveCliChildJsonCommandResult({
|
||||||
return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true };
|
result,
|
||||||
|
requestedStdoutType: "web-probe sentinel report summary JSON",
|
||||||
|
acceptParsed: (value) => value.ok === true || value.ok === false || typeof value.reason === "string" || typeof value.reportJsonPath === "string" || Object.keys(record(value.counts)).length > 0,
|
||||||
|
});
|
||||||
|
const parsed = parsedResolution.parsed;
|
||||||
|
return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), stdoutRecovery: parsedResolution.diagnostics, valuesRedacted: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeSentinelReportStateDir(value: string): boolean {
|
function isSafeSentinelReportStateDir(value: string): boolean {
|
||||||
@@ -1468,10 +1473,13 @@ function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", p
|
|||||||
let parsed: Record<string, unknown> | null = null;
|
let parsed: Record<string, unknown> | null = null;
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: attemptTimeoutSeconds * 1000 });
|
result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: attemptTimeoutSeconds * 1000 });
|
||||||
parsed = parseJsonObject(result.stdout);
|
const parsedResolution = resolveCliChildJsonCommandResult({
|
||||||
const recovered = parsed === null ? recoverTruncatedSshStdoutJson(result) : null;
|
result,
|
||||||
if (recovered !== null) parsed = recovered.parsed;
|
requestedStdoutType: "web-probe sentinel service proxy response JSON",
|
||||||
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, parsedFromDump: recovered !== null, dumpPath: recovered?.dumpPath ?? null, valuesRedacted: true });
|
acceptParsed: isSentinelServiceResponseContract,
|
||||||
|
});
|
||||||
|
parsed = parsedResolution.parsed;
|
||||||
|
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, stdoutRecovery: parsedResolution.diagnostics, valuesRedacted: true });
|
||||||
if (result.exitCode === 0) break;
|
if (result.exitCode === 0) break;
|
||||||
}
|
}
|
||||||
const compactBodyJson = compactSentinelServiceBodyJson(parsed);
|
const compactBodyJson = compactSentinelServiceBodyJson(parsed);
|
||||||
@@ -1492,27 +1500,15 @@ function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", p
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function recoverTruncatedSshStdoutJson(result: CommandResult): { parsed: Record<string, unknown>; dumpPath: string } | null {
|
function isSentinelServiceResponseContract(value: Record<string, unknown>): boolean {
|
||||||
const dumpPath = sshStdoutDumpPathFromStderr(result.stderr);
|
return value.ok === false
|
||||||
if (dumpPath === null || !existsSync(dumpPath)) return null;
|
|| typeof value.view === "string"
|
||||||
const parsed = parseJsonObject(readFileSync(dumpPath, "utf8"));
|
|| typeof value.renderedText === "string"
|
||||||
return parsed === null ? null : { parsed, dumpPath };
|
|| typeof value.error === "string"
|
||||||
}
|
|| Array.isArray(value.availableViews)
|
||||||
|
|| Object.keys(record(value.run)).length > 0
|
||||||
function sshStdoutDumpPathFromStderr(value: string): string | null {
|
|| Object.keys(record(value.summary)).length > 0
|
||||||
for (const rawLine of value.split(/\r?\n/u)) {
|
|| Array.isArray(value.findings);
|
||||||
const line = rawLine.trim();
|
|
||||||
const prefix = "UNIDESK_SSH_STDOUT_TRUNCATED ";
|
|
||||||
if (!line.startsWith(prefix)) continue;
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(line.slice(prefix.length)) as unknown;
|
|
||||||
const dumpPath = stringAtNullable(record(payload), "dumpPath");
|
|
||||||
if (dumpPath !== null) return dumpPath;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function callSentinelServiceDirect(method: "GET" | "POST", pathWithQuery: string, body: Record<string, unknown> | null, timeoutSeconds: number, url: string): Record<string, unknown> {
|
function callSentinelServiceDirect(method: "GET" | "POST", pathWithQuery: string, body: Record<string, unknown> | null, timeoutSeconds: number, url: string): Record<string, unknown> {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { repoRoot, rootPath, type Config } from "../config";
|
import { repoRoot, rootPath, type Config } from "../config";
|
||||||
import { runCommand, type CommandResult } from "../command";
|
import { runCommand, type CommandResult } from "../command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "../cli-child-json-recovery";
|
||||||
import { startJob } from "../jobs";
|
import { startJob } from "../jobs";
|
||||||
import { classifySshTcpPoolFailure } from "../ssh";
|
import { classifySshTcpPoolFailure } from "../ssh";
|
||||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
||||||
@@ -36,15 +37,6 @@ import { parseNodeScopedDelegatedOptions } from "./plan";
|
|||||||
import { compactRuntimeCommand, compactRuntimeCommandStats, nodeRuntimeUnsupportedAction, runNodeHostScript, transPath } from "./runtime-common";
|
import { compactRuntimeCommand, compactRuntimeCommandStats, nodeRuntimeUnsupportedAction, runNodeHostScript, transPath } from "./runtime-common";
|
||||||
import { optionValue, positiveIntegerOption, shellQuote, statusText } from "./utils";
|
import { optionValue, positiveIntegerOption, shellQuote, statusText } from "./utils";
|
||||||
|
|
||||||
export function parseJsonObject(text: string): Record<string, unknown> {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(text) as unknown;
|
|
||||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function commandResultFromAsync(spec: HwlabRuntimeLaneSpec, payload: Record<string, unknown>, statusPath: string, timedOut: boolean): CommandResult {
|
export function commandResultFromAsync(spec: HwlabRuntimeLaneSpec, payload: Record<string, unknown>, statusPath: string, timedOut: boolean): CommandResult {
|
||||||
return {
|
return {
|
||||||
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
||||||
@@ -102,7 +94,7 @@ export function nodeRuntimeGitopsRoot(spec: HwlabRuntimeLaneSpec): string {
|
|||||||
return spec.gitopsRoot;
|
return spec.gitopsRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveNodeRuntimeLaneHead(spec: HwlabRuntimeLaneSpec): { sourceCommit: string | null; result: CommandResult } {
|
export function resolveNodeRuntimeLaneHead(spec: HwlabRuntimeLaneSpec): { sourceCommit: string | null; result: CommandResult; stdoutRecovery?: Record<string, unknown> } {
|
||||||
const mirror = nodeRuntimeSourceMirrorTarget(spec);
|
const mirror = nodeRuntimeSourceMirrorTarget(spec);
|
||||||
const script = [
|
const script = [
|
||||||
"set +e",
|
"set +e",
|
||||||
@@ -135,10 +127,25 @@ export function resolveNodeRuntimeLaneHead(spec: HwlabRuntimeLaneSpec): { source
|
|||||||
"NODE",
|
"NODE",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const result = runNodeK3sScript(spec, script, 45);
|
const result = runNodeK3sScript(spec, script, 45);
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveRuntimeCleanupJson(result, "node runtime source snapshot JSON");
|
||||||
|
const payload = payloadResolution.parsed ?? {};
|
||||||
const payloadCommit = typeof payload.sourceCommit === "string" && /^[0-9a-f]{40}$/iu.test(payload.sourceCommit) ? payload.sourceCommit.toLowerCase() : null;
|
const payloadCommit = typeof payload.sourceCommit === "string" && /^[0-9a-f]{40}$/iu.test(payload.sourceCommit) ? payload.sourceCommit.toLowerCase() : null;
|
||||||
const match = payloadCommit ?? /[0-9a-f]{40}/iu.exec(statusText(result))?.[0].toLowerCase() ?? null;
|
const match = payloadCommit ?? /[0-9a-f]{40}/iu.exec(statusText(result))?.[0].toLowerCase() ?? null;
|
||||||
return { sourceCommit: result.exitCode === 0 ? match : null, result };
|
return { sourceCommit: result.exitCode === 0 ? match : null, result, stdoutRecovery: payloadResolution.diagnostics };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRuntimeCleanupJson(result: CommandResult, requestedStdoutType: string): { parsed: Record<string, unknown> | null; diagnostics: Record<string, unknown> } {
|
||||||
|
const resolved = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType,
|
||||||
|
acceptParsed: (value) => typeof value.ok === "boolean"
|
||||||
|
|| typeof value.mode === "string"
|
||||||
|
|| typeof value.sourceCommit === "string"
|
||||||
|
|| typeof value.degradedReason === "string"
|
||||||
|
|| typeof value.error === "string"
|
||||||
|
|| Object.keys(value).length > 1,
|
||||||
|
});
|
||||||
|
return { parsed: resolved.parsed, diagnostics: resolved.diagnostics };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodeRuntimeSourceMirrorTarget {
|
interface NodeRuntimeSourceMirrorTarget {
|
||||||
@@ -688,7 +695,8 @@ export function nodeRuntimeCleanupLegacyDockerImages(scoped: ReturnType<typeof p
|
|||||||
"UNIDESK_LEGACY_DOCKER_IMAGE_GC",
|
"UNIDESK_LEGACY_DOCKER_IMAGE_GC",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const result = runNodeHostScript(scoped.spec, script, scoped.timeoutSeconds);
|
const result = runNodeHostScript(scoped.spec, script, scoped.timeoutSeconds);
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveRuntimeCleanupJson(result, "legacy docker image cleanup JSON");
|
||||||
|
const payload = payloadResolution.parsed ?? {};
|
||||||
const ok = isCommandSuccess(result) && payload.ok !== false;
|
const ok = isCommandSuccess(result) && payload.ok !== false;
|
||||||
return {
|
return {
|
||||||
ok,
|
ok,
|
||||||
@@ -711,7 +719,7 @@ export function nodeRuntimeCleanupLegacyDockerImages(scoped: ReturnType<typeof p
|
|||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
},
|
},
|
||||||
cleanup: options.dryRun || ok ? compactNodeRuntimeLegacyDockerImageCleanupPayload(payload) : payload,
|
cleanup: options.dryRun || ok ? compactNodeRuntimeLegacyDockerImageCleanupPayload(payload) : payload,
|
||||||
result: ok ? compactRuntimeCommandStats(result) : compactRuntimeCommand(result),
|
result: { ...(ok ? compactRuntimeCommandStats(result) : compactRuntimeCommand(result)), stdoutRecovery: payloadResolution.diagnostics },
|
||||||
degradedReason: ok ? undefined : "legacy-docker-image-cleanup-failed",
|
degradedReason: ok ? undefined : "legacy-docker-image-cleanup-failed",
|
||||||
next: options.dryRun
|
next: options.dryRun
|
||||||
? { confirm: `${command} --min-age-hours ${options.minAgeHours} --keep-per-repository ${options.keepPerRepository} --limit ${options.limit} --confirm` }
|
? { confirm: `${command} --min-age-hours ${options.minAgeHours} --keep-per-repository ${options.keepPerRepository} --limit ${options.limit} --confirm` }
|
||||||
@@ -769,7 +777,8 @@ export function nodeRuntimeCleanupLegacyDockerRegistryVolume(scoped: ReturnType<
|
|||||||
"UNIDESK_LEGACY_DOCKER_REGISTRY_VOLUME_GC",
|
"UNIDESK_LEGACY_DOCKER_REGISTRY_VOLUME_GC",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const result = runNodeHostScript(scoped.spec, script, scoped.timeoutSeconds);
|
const result = runNodeHostScript(scoped.spec, script, scoped.timeoutSeconds);
|
||||||
const payload = parseJsonObject(result.stdout);
|
const payloadResolution = resolveRuntimeCleanupJson(result, "legacy docker registry volume cleanup JSON");
|
||||||
|
const payload = payloadResolution.parsed ?? {};
|
||||||
const ok = isCommandSuccess(result) && payload.ok !== false;
|
const ok = isCommandSuccess(result) && payload.ok !== false;
|
||||||
return {
|
return {
|
||||||
ok,
|
ok,
|
||||||
@@ -784,7 +793,7 @@ export function nodeRuntimeCleanupLegacyDockerRegistryVolume(scoped: ReturnType<
|
|||||||
},
|
},
|
||||||
target,
|
target,
|
||||||
cleanup: compactNodeRuntimeLegacyDockerRegistryVolumeCleanupPayload(payload),
|
cleanup: compactNodeRuntimeLegacyDockerRegistryVolumeCleanupPayload(payload),
|
||||||
result: ok ? compactRuntimeCommandStats(result) : compactRuntimeCommand(result),
|
result: { ...(ok ? compactRuntimeCommandStats(result) : compactRuntimeCommand(result)), stdoutRecovery: payloadResolution.diagnostics },
|
||||||
degradedReason: ok ? undefined : "legacy-docker-registry-volume-cleanup-failed",
|
degradedReason: ok ? undefined : "legacy-docker-registry-volume-cleanup-failed",
|
||||||
next: dryRun
|
next: dryRun
|
||||||
? { confirm: `${command} --confirm --wait` }
|
? { confirm: `${command} --confirm --wait` }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { repoRoot, rootPath, type Config } from "../config";
|
import { repoRoot, rootPath, type Config } from "../config";
|
||||||
import { runCommand, type CommandResult } from "../command";
|
import { runCommand, type CommandResult } from "../command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "../cli-child-json-recovery";
|
||||||
import { startJob } from "../jobs";
|
import { startJob } from "../jobs";
|
||||||
import { classifySshTcpPoolFailure } from "../ssh";
|
import { classifySshTcpPoolFailure } from "../ssh";
|
||||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
||||||
@@ -30,7 +31,7 @@ import type { RenderedCliResult } from "../output";
|
|||||||
|
|
||||||
import type { NodeWebProbeObserveAction, NodeWebProbeObserveOptions, WebObserveIndexEntry } from "./entry";
|
import type { NodeWebProbeObserveAction, NodeWebProbeObserveOptions, WebObserveIndexEntry } from "./entry";
|
||||||
import { runTransWorkspaceStdinScript } from "./public-exposure";
|
import { runTransWorkspaceStdinScript } from "./public-exposure";
|
||||||
import { parseJsonObject, record, shellQuote } from "./utils";
|
import { record, shellQuote } from "./utils";
|
||||||
import { isSafeWebObserveJobId, isSafeWebObserveStateDir, safeWebObserveSegment } from "./web-observe-scripts";
|
import { isSafeWebObserveJobId, isSafeWebObserveStateDir, safeWebObserveSegment } from "./web-observe-scripts";
|
||||||
import { webObserveShort, webObserveText } from "./web-probe-observe";
|
import { webObserveShort, webObserveText } from "./web-probe-observe";
|
||||||
|
|
||||||
@@ -68,9 +69,14 @@ export function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserve
|
|||||||
"console.log(JSON.stringify({ ok: true, rounds, turnColumns }));",
|
"console.log(JSON.stringify({ ok: true, rounds, turnColumns }));",
|
||||||
"UNIDESK_WEB_OBSERVE_RECOVER_TURN_DETAILS",
|
"UNIDESK_WEB_OBSERVE_RECOVER_TURN_DETAILS",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const recovered = parseJsonObject(runTransWorkspaceStdinScript(options.node, spec.workspace, recoverScript, Math.min(options.commandTimeoutSeconds, 30)).stdout);
|
const recoverResult = runTransWorkspaceStdinScript(options.node, spec.workspace, recoverScript, Math.min(options.commandTimeoutSeconds, 30));
|
||||||
const recoveredRounds = Array.isArray(recovered.rounds) ? recovered.rounds : [];
|
const recovered = resolveCliChildJsonCommandResult({
|
||||||
const recoveredColumns = Array.isArray(recovered.turnColumns) ? recovered.turnColumns : [];
|
result: recoverResult,
|
||||||
|
requestedStdoutType: "web-probe observe analyze turn-detail recovery JSON",
|
||||||
|
acceptParsed: (value) => value.ok === true && (Array.isArray(value.rounds) || Array.isArray(value.turnColumns)),
|
||||||
|
}).parsed;
|
||||||
|
const recoveredRounds = Array.isArray(recovered?.rounds) ? recovered.rounds : [];
|
||||||
|
const recoveredColumns = Array.isArray(recovered?.turnColumns) ? recovered.turnColumns : [];
|
||||||
if (recoveredRounds.length === 0 && recoveredColumns.length === 0) return analysis;
|
if (recoveredRounds.length === 0 && recoveredColumns.length === 0) return analysis;
|
||||||
const nextMetrics = { ...(sampleMetrics ?? {}) };
|
const nextMetrics = { ...(sampleMetrics ?? {}) };
|
||||||
if (!hasRounds && recoveredRounds.length > 0) {
|
if (!hasRounds && recoveredRounds.length > 0) {
|
||||||
@@ -1083,10 +1089,16 @@ export function discoverWebObserveIndexEntryOnTarget(id: string, node: string, l
|
|||||||
"NODE",
|
"NODE",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const result = runTransWorkspaceStdinScript(node, spec.workspace, script, 55);
|
const result = runTransWorkspaceStdinScript(node, spec.workspace, script, 55);
|
||||||
const payload = parseJsonObject(result.stdout);
|
const discovery = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: "web-probe observe index discovery JSON",
|
||||||
|
acceptParsed: (value) => value.ok === true && (value.found === true || value.found === false || value.ambiguous === true),
|
||||||
|
});
|
||||||
|
const payload = discovery.parsed;
|
||||||
if (result.exitCode !== 0 || result.timedOut || payload?.ok !== true) {
|
if (result.exitCode !== 0 || result.timedOut || payload?.ok !== true) {
|
||||||
const reason = result.timedOut ? "timeout" : payload?.ambiguous === true ? "ambiguous" : `exit=${result.exitCode}`;
|
const reason = result.timedOut ? "timeout" : payload?.ambiguous === true ? "ambiguous" : `exit=${result.exitCode}`;
|
||||||
throw new Error(`observer discovery failed (${reason}): ${result.stderr.trim().slice(-240) || result.stdout.trim().slice(-240)}`);
|
const recovery = JSON.stringify(discovery.diagnostics).slice(0, 360);
|
||||||
|
throw new Error(`observer discovery failed (${reason}): ${result.stderr.trim().slice(-240) || result.stdout.trim().slice(-240)} recovery=${recovery}`);
|
||||||
}
|
}
|
||||||
if (payload.found !== true) return null;
|
if (payload.found !== true) return null;
|
||||||
const entry = record(payload.entry);
|
const entry = record(payload.entry);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { repoRoot, rootPath, type Config } from "../config";
|
import { repoRoot, rootPath, type Config } from "../config";
|
||||||
import { runCommand, type CommandResult } from "../command";
|
import { runCommand, type CommandResult } from "../command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "../cli-child-json-recovery";
|
||||||
import { startJob } from "../jobs";
|
import { startJob } from "../jobs";
|
||||||
import { classifySshTcpPoolFailure } from "../ssh";
|
import { classifySshTcpPoolFailure } from "../ssh";
|
||||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
||||||
@@ -31,7 +32,7 @@ import type { RenderedCliResult } from "../output";
|
|||||||
import type { BootstrapAdminPasswordMaterial, NodeWebProbeScriptOptions, RuntimeSecretSpec } from "./entry";
|
import type { BootstrapAdminPasswordMaterial, NodeWebProbeScriptOptions, RuntimeSecretSpec } from "./entry";
|
||||||
import type { NodeWebProbeHostProxyEnv } from "./web-probe-observe";
|
import type { NodeWebProbeHostProxyEnv } from "./web-probe-observe";
|
||||||
import { isSafeWebProbeScriptArtifactPath, isSafeWebProbeScriptReportPath, isSafeWebProbeScriptRunDir, runTransWorkspaceStdinScript } from "./public-exposure";
|
import { isSafeWebProbeScriptArtifactPath, isSafeWebProbeScriptReportPath, isSafeWebProbeScriptRunDir, runTransWorkspaceStdinScript } from "./public-exposure";
|
||||||
import { compactCommandResultRedacted, nullableRecord, parseJsonObject, redactKnownSecrets, shellQuote } from "./utils";
|
import { compactCommandResultRedacted, nullableRecord, redactKnownSecrets, shellQuote } from "./utils";
|
||||||
import { renderWebProbeScriptResult } from "./web-observe-render";
|
import { renderWebProbeScriptResult } from "./web-observe-render";
|
||||||
import { nodeWebProbeHostProxyEnv } from "./web-probe-observe";
|
import { nodeWebProbeHostProxyEnv } from "./web-probe-observe";
|
||||||
|
|
||||||
@@ -481,11 +482,26 @@ export function runNodeWebProbeScript(
|
|||||||
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.username ?? secretSpec.bootstrapAdminUsername, material.password ?? "", webProbeProxy, spec.webProbe?.playwrightBrowsersPath);
|
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.username ?? secretSpec.bootstrapAdminUsername, material.password ?? "", webProbeProxy, spec.webProbe?.playwrightBrowsersPath);
|
||||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||||
const commandTimedOut = result.timedOut || result.exitCode === 124;
|
const commandTimedOut = result.timedOut || result.exitCode === 124;
|
||||||
const stdoutReport = parseJsonObject(result.stdout);
|
|
||||||
const runPaths = webProbeScriptRunPathsFromStderr(result.stderr);
|
const runPaths = webProbeScriptRunPathsFromStderr(result.stderr);
|
||||||
const recoveredReport = stdoutReport === null ? readNodeWebProbeScriptReport(options, spec, runPaths.reportPath) : null;
|
let recoveredReport: WebProbeScriptReportReadResult | null = null;
|
||||||
const recoveredArtifacts = stdoutReport === null || commandTimedOut ? readNodeWebProbeScriptArtifacts(options, spec, runPaths.runDir) : null;
|
const stdoutResolution = resolveCliChildJsonCommandResult({
|
||||||
const parsedReport = stdoutReport ?? recoveredReport?.report ?? null;
|
result,
|
||||||
|
requestedStdoutType: "web-probe script report JSON",
|
||||||
|
acceptParsed: isWebProbeScriptReportContract,
|
||||||
|
forceArtifactFallbackReason: commandTimedOut ? "remote-command-timeout" : null,
|
||||||
|
artifactFallback: {
|
||||||
|
path: runPaths.reportPath,
|
||||||
|
nextCommand: runPaths.reportPath === null ? null : `trans ${options.node}:${spec.workspace} cat ${shellQuote(runPaths.reportPath)}`,
|
||||||
|
read: () => {
|
||||||
|
recoveredReport = readNodeWebProbeScriptReport(options, spec, runPaths.reportPath);
|
||||||
|
if (recoveredReport?.report) return { ok: true, value: recoveredReport.report, path: recoveredReport.path, reason: recoveredReport.source };
|
||||||
|
return { ok: false, reason: recoveredReport?.degradedReason ?? "web-probe-report-artifact-missing", path: runPaths.reportPath };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const stdoutReport = stdoutResolution.source === "stdout" || stdoutResolution.source === "dump" ? stdoutResolution.parsed : null;
|
||||||
|
const recoveredArtifacts = stdoutResolution.parsed === null || commandTimedOut ? readNodeWebProbeScriptArtifacts(options, spec, runPaths.runDir) : null;
|
||||||
|
const parsedReport = stdoutResolution.parsed;
|
||||||
const report = compactWebProbeScriptResult(parsedReport);
|
const report = compactWebProbeScriptResult(parsedReport);
|
||||||
const passed = result.exitCode === 0 && report?.ok === true;
|
const passed = result.exitCode === 0 && report?.ok === true;
|
||||||
const summary = nullableRecord(report?.summary);
|
const summary = nullableRecord(report?.summary);
|
||||||
@@ -493,9 +509,10 @@ export function runNodeWebProbeScript(
|
|||||||
const outputFailureKind = parsedReport === null
|
const outputFailureKind = parsedReport === null
|
||||||
? commandTimedOut
|
? commandTimedOut
|
||||||
? "web-probe-command-timeout"
|
? "web-probe-command-timeout"
|
||||||
: stdoutBytes > 64 * 1024
|
: stringOrNullValue(stdoutResolution.diagnostics.fallbackReason)
|
||||||
? "web-probe-output-too-large"
|
?? (stdoutBytes > 64 * 1024
|
||||||
: "web-probe-report-parse-failed"
|
? "web-probe-output-too-large"
|
||||||
|
: "web-probe-report-parse-failed")
|
||||||
: null;
|
: null;
|
||||||
const degradedReason = commandTimedOut
|
const degradedReason = commandTimedOut
|
||||||
? "web-probe-command-timeout"
|
? "web-probe-command-timeout"
|
||||||
@@ -518,7 +535,8 @@ export function runNodeWebProbeScript(
|
|||||||
const effectiveSummary = summary !== null ? {
|
const effectiveSummary = summary !== null ? {
|
||||||
...summary,
|
...summary,
|
||||||
transportTimedOut: commandTimedOut,
|
transportTimedOut: commandTimedOut,
|
||||||
recoveredFrom: stdoutReport !== null ? "stdout" : recoveredReport?.source ?? null,
|
recoveredFrom: stdoutReport !== null ? stdoutResolution.source : recoveredReport?.source ?? null,
|
||||||
|
stdoutRecovery: stdoutResolution.diagnostics,
|
||||||
} : (outputFailureKind === null ? null : {
|
} : (outputFailureKind === null ? null : {
|
||||||
ok: false,
|
ok: false,
|
||||||
status: "blocked",
|
status: "blocked",
|
||||||
@@ -539,8 +557,9 @@ export function runNodeWebProbeScript(
|
|||||||
},
|
},
|
||||||
screenshots: recoveredArtifacts?.screenshots ?? [],
|
screenshots: recoveredArtifacts?.screenshots ?? [],
|
||||||
artifacts: recoveredArtifacts?.artifacts ?? null,
|
artifacts: recoveredArtifacts?.artifacts ?? null,
|
||||||
stdoutBytes,
|
stdoutBytes,
|
||||||
exitCode: result.exitCode,
|
stdoutRecovery: stdoutResolution.diagnostics,
|
||||||
|
exitCode: result.exitCode,
|
||||||
stderrTail: result.stderr.trim().slice(-2000),
|
stderrTail: result.stderr.trim().slice(-2000),
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
});
|
});
|
||||||
@@ -566,12 +585,14 @@ export function runNodeWebProbeScript(
|
|||||||
summary: effectiveSummary,
|
summary: effectiveSummary,
|
||||||
issueEvidence,
|
issueEvidence,
|
||||||
probe: report,
|
probe: report,
|
||||||
reportLoad: stdoutReport !== null ? { source: "stdout", path: report?.reportPath ?? null, degradedReason: null } : recoveredReport === null ? null : {
|
reportLoad: stdoutReport !== null ? { source: stdoutResolution.source, path: report?.reportPath ?? null, degradedReason: null } : recoveredReport === null ? null : {
|
||||||
source: recoveredReport.source,
|
source: recoveredReport.source,
|
||||||
path: recoveredReport.path,
|
path: recoveredReport.path,
|
||||||
degradedReason: recoveredReport.degradedReason,
|
degradedReason: recoveredReport.degradedReason,
|
||||||
result: recoveredReport.result === null ? null : compactCommandResultRedacted(recoveredReport.result, [material.password ?? ""]),
|
result: recoveredReport.result === null ? null : compactCommandResultRedacted(recoveredReport.result, [material.password ?? ""]),
|
||||||
|
stdoutRecovery: recoveredReport.stdoutRecovery ?? null,
|
||||||
},
|
},
|
||||||
|
stdoutRecovery: stdoutResolution.diagnostics,
|
||||||
warnings: webProbeScriptGovernanceWarnings(options),
|
warnings: webProbeScriptGovernanceWarnings(options),
|
||||||
hints: webProbeScriptGovernanceHints(options),
|
hints: webProbeScriptGovernanceHints(options),
|
||||||
preferredCommands: webProbeScriptPreferredCommands(options),
|
preferredCommands: webProbeScriptPreferredCommands(options),
|
||||||
@@ -667,11 +688,20 @@ export function lineValueFromText(text: string, name: string): string | null {
|
|||||||
return match ? match[1].trim() : null;
|
return match ? match[1].trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WebProbeScriptReportReadResult {
|
||||||
|
source: string;
|
||||||
|
report: Record<string, unknown> | null;
|
||||||
|
result: CommandResult | null;
|
||||||
|
degradedReason: string | null;
|
||||||
|
path: string | null;
|
||||||
|
stdoutRecovery?: Record<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
export function readNodeWebProbeScriptReport(
|
export function readNodeWebProbeScriptReport(
|
||||||
options: NodeWebProbeScriptOptions,
|
options: NodeWebProbeScriptOptions,
|
||||||
spec: HwlabRuntimeLaneSpec,
|
spec: HwlabRuntimeLaneSpec,
|
||||||
reportPath: string | null,
|
reportPath: string | null,
|
||||||
): { source: string; report: Record<string, unknown> | null; result: CommandResult | null; degradedReason: string | null; path: string | null } | null {
|
): WebProbeScriptReportReadResult | null {
|
||||||
if (!reportPath) return null;
|
if (!reportPath) return null;
|
||||||
if (!isSafeWebProbeScriptReportPath(reportPath)) return { source: "unsafe-path", report: null, result: null, degradedReason: "web-probe-report-path-invalid", path: reportPath };
|
if (!isSafeWebProbeScriptReportPath(reportPath)) return { source: "unsafe-path", report: null, result: null, degradedReason: "web-probe-report-path-invalid", path: reportPath };
|
||||||
const script = [
|
const script = [
|
||||||
@@ -753,16 +783,38 @@ export function readNodeWebProbeScriptReport(
|
|||||||
if (result.exitCode !== 0 || result.timedOut) {
|
if (result.exitCode !== 0 || result.timedOut) {
|
||||||
return { source: "report-file", report: null, result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-report-read-failed", path: reportPath };
|
return { source: "report-file", report: null, result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-report-read-failed", path: reportPath };
|
||||||
}
|
}
|
||||||
const report = parseJsonObject(result.stdout);
|
const reportResolution = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: "web-probe script report artifact compact JSON",
|
||||||
|
acceptParsed: isWebProbeScriptReportContract,
|
||||||
|
});
|
||||||
|
const report = reportResolution.parsed;
|
||||||
return {
|
return {
|
||||||
source: "report-file",
|
source: "report-file",
|
||||||
report,
|
report,
|
||||||
result,
|
result,
|
||||||
degradedReason: report === null ? "web-probe-report-parse-failed" : null,
|
degradedReason: report === null ? stringOrNullValue(reportResolution.diagnostics.fallbackReason) ?? "web-probe-report-parse-failed" : null,
|
||||||
path: reportPath,
|
path: reportPath,
|
||||||
|
stdoutRecovery: reportResolution.diagnostics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWebProbeScriptReportContract(value: Record<string, unknown>): boolean {
|
||||||
|
return value.ok === true
|
||||||
|
|| value.ok === false
|
||||||
|
|| typeof value.status === "string"
|
||||||
|
|| typeof value.reportPath === "string"
|
||||||
|
|| typeof value.error === "string"
|
||||||
|
|| typeof value.failureKind === "string"
|
||||||
|
|| Object.keys(nullableRecord(value.summary) ?? {}).length > 0
|
||||||
|
|| Object.keys(nullableRecord(value.script) ?? {}).length > 0
|
||||||
|
|| Array.isArray(value.steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringOrNullValue(value: unknown): string | null {
|
||||||
|
return typeof value === "string" && value.length > 0 ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function readNodeWebProbeScriptArtifacts(
|
export function readNodeWebProbeScriptArtifacts(
|
||||||
options: NodeWebProbeScriptOptions,
|
options: NodeWebProbeScriptOptions,
|
||||||
spec: HwlabRuntimeLaneSpec,
|
spec: HwlabRuntimeLaneSpec,
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import type { CommandResult } from "../command";
|
||||||
|
import type { NodeWebProbeObserveOptions } from "./entry";
|
||||||
|
import { buildNodeWebProbeObserveCollectPayload } from "./web-probe-observe-collect";
|
||||||
|
|
||||||
|
describe("web-probe observe collect child JSON recovery", () => {
|
||||||
|
test("recovers collect JSON from trans stdout dump", () => {
|
||||||
|
const dir = join(tmpdir(), `unidesk-collect-recovery-${Date.now()}-${process.pid}`);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const dumpPath = join(dir, "stdout.json");
|
||||||
|
writeFileSync(dumpPath, JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
command: "web-probe-observe collect",
|
||||||
|
view: "performance-summary",
|
||||||
|
stateDir: "/remote/state",
|
||||||
|
renderedText: "WEB PERFORMANCE SUMMARY\npayloads=2 events=3 groups=1",
|
||||||
|
valuesRedacted: true,
|
||||||
|
}));
|
||||||
|
const summary = {
|
||||||
|
stdout: {
|
||||||
|
stream: "stdout",
|
||||||
|
truncated: true,
|
||||||
|
dumpPath,
|
||||||
|
valuesRedacted: true,
|
||||||
|
},
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
const payload = buildNodeWebProbeObserveCollectPayload(collectOptions(), { workspace: "/workspace" }, {
|
||||||
|
command: ["trans", "JD01:/workspace", "sh"],
|
||||||
|
cwd: "/repo",
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: "tail only WEB PERFORMANCE SUMMARY",
|
||||||
|
stderr: `UNIDESK_SSH_TRUNCATION_SUMMARY ${JSON.stringify(summary)}\n`,
|
||||||
|
signal: null,
|
||||||
|
timedOut: false,
|
||||||
|
} satisfies CommandResult);
|
||||||
|
expect(payload.ok).toBe(true);
|
||||||
|
expect(payload.status).toBe("collected");
|
||||||
|
const collect = payload.collect as Record<string, unknown>;
|
||||||
|
expect(collect.view).toBe("performance-summary");
|
||||||
|
expect(collect.renderedText).toContain("payloads=2");
|
||||||
|
const recovery = payload.stdoutRecovery as Record<string, unknown>;
|
||||||
|
expect(recovery.stdoutKind).toBe("ssh-truncation-summary");
|
||||||
|
expect(recovery.dumpPath).toBe(dumpPath);
|
||||||
|
expect(recovery.source).toBe("dump");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function collectOptions(): NodeWebProbeObserveOptions {
|
||||||
|
return {
|
||||||
|
action: "observe",
|
||||||
|
observeAction: "collect",
|
||||||
|
id: "webobs-test",
|
||||||
|
node: "JD01",
|
||||||
|
lane: "v03",
|
||||||
|
url: "https://example.test",
|
||||||
|
targetPath: "/",
|
||||||
|
viewport: "1920x1080",
|
||||||
|
browserProxyMode: "auto",
|
||||||
|
sampleIntervalMs: 1000,
|
||||||
|
screenshotIntervalMs: 0,
|
||||||
|
observerRefreshIntervalMs: 1000,
|
||||||
|
maxSamples: 10,
|
||||||
|
maxRunSeconds: 60,
|
||||||
|
commandTimeoutSeconds: 30,
|
||||||
|
waitMs: 0,
|
||||||
|
tailLines: 20,
|
||||||
|
maxFiles: 100,
|
||||||
|
gcKeepHours: 24,
|
||||||
|
gcLimit: 10,
|
||||||
|
confirm: false,
|
||||||
|
dryRun: false,
|
||||||
|
collectView: "performance-summary",
|
||||||
|
collectFile: null,
|
||||||
|
collectFinding: null,
|
||||||
|
collectGrep: null,
|
||||||
|
collectTraceId: null,
|
||||||
|
collectSampleSeq: null,
|
||||||
|
collectTimestamp: null,
|
||||||
|
collectTurn: null,
|
||||||
|
collectCommandId: null,
|
||||||
|
collectWindowMs: null,
|
||||||
|
analyzeArchivePrefix: null,
|
||||||
|
analyzeTailSamples: null,
|
||||||
|
full: false,
|
||||||
|
raw: false,
|
||||||
|
compactRaw: false,
|
||||||
|
stateDir: null,
|
||||||
|
jobId: null,
|
||||||
|
force: false,
|
||||||
|
commandType: null,
|
||||||
|
commandText: null,
|
||||||
|
commandPath: null,
|
||||||
|
commandLabel: null,
|
||||||
|
commandSessionId: null,
|
||||||
|
commandProvider: null,
|
||||||
|
commandAfterRound: null,
|
||||||
|
commandSeverity: null,
|
||||||
|
commandAlternateSessionStrategy: null,
|
||||||
|
commandExpectedSentinelRange: null,
|
||||||
|
commandExpectedActionWaitMs: null,
|
||||||
|
commandDurationMs: null,
|
||||||
|
commandRequireComposerReady: false,
|
||||||
|
commandWaitProjectManagementReady: false,
|
||||||
|
commandFindingId: null,
|
||||||
|
commandBlocking: null,
|
||||||
|
commandAccountId: null,
|
||||||
|
commandFromAccountId: null,
|
||||||
|
commandToAccountId: null,
|
||||||
|
commandSourceId: null,
|
||||||
|
commandFileRef: null,
|
||||||
|
commandFilename: null,
|
||||||
|
commandTaskRef: null,
|
||||||
|
commandTaskId: null,
|
||||||
|
commandField: null,
|
||||||
|
commandLink: null,
|
||||||
|
commandTitle: null,
|
||||||
|
commandBody: null,
|
||||||
|
commandStatus: null,
|
||||||
|
commandHwpodId: null,
|
||||||
|
commandNodeId: null,
|
||||||
|
commandWorkspaceRoot: null,
|
||||||
|
commandRoot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// SPEC: PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer.
|
||||||
|
// Responsibility: web-probe observe collect action and child JSON recovery.
|
||||||
|
import type { CommandResult } from "../command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "../cli-child-json-recovery";
|
||||||
|
import { nodeWebObserveCollectViewNodeScript } from "../hwlab-node-web-observe-collect";
|
||||||
|
import { withWebObserveCollectRendered } from "../hwlab-node-web-observe-render";
|
||||||
|
import { buildWebObserveWrapperForObserveOptions } from "../hwlab-node-web-observe-wrapper";
|
||||||
|
import type { HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
|
||||||
|
import type { RenderedCliResult } from "../output";
|
||||||
|
import type { NodeWebProbeObserveOptions } from "./entry";
|
||||||
|
import { runTransWorkspaceStdinScript } from "./public-exposure";
|
||||||
|
import { compactCommandResult, compactCommandResultWithStdoutTail } from "./utils";
|
||||||
|
import { webObserveCommandLabel, webObserveIdFromOptions, nodeWebObserveResolveStateDirShell } from "./web-observe-render";
|
||||||
|
import { nodeWebObserveCollectNodeScript } from "./web-observe-scripts";
|
||||||
|
import { compactObserveCollectForRaw } from "./web-observe-collect-compact";
|
||||||
|
|
||||||
|
export function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
||||||
|
const collectScript = options.collectView === "files"
|
||||||
|
? nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep)
|
||||||
|
: nodeWebObserveCollectViewNodeScript({
|
||||||
|
maxFiles: options.maxFiles,
|
||||||
|
view: options.collectView,
|
||||||
|
traceId: options.collectTraceId,
|
||||||
|
sampleSeq: options.collectSampleSeq,
|
||||||
|
timestamp: options.collectTimestamp,
|
||||||
|
turn: options.collectTurn,
|
||||||
|
commandId: options.collectCommandId,
|
||||||
|
windowMs: options.collectWindowMs,
|
||||||
|
});
|
||||||
|
const script = [
|
||||||
|
"set -eu",
|
||||||
|
nodeWebObserveResolveStateDirShell(options),
|
||||||
|
collectScript,
|
||||||
|
].join("\n");
|
||||||
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||||
|
const payload = buildNodeWebProbeObserveCollectPayload(options, spec, result);
|
||||||
|
return options.raw ? payload : withWebObserveCollectRendered(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildNodeWebProbeObserveCollectPayload(
|
||||||
|
options: NodeWebProbeObserveOptions,
|
||||||
|
spec: Pick<HwlabRuntimeLaneSpec, "workspace">,
|
||||||
|
result: CommandResult,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const stdoutResolution = resolveCliChildJsonCommandResult({
|
||||||
|
result,
|
||||||
|
requestedStdoutType: "web-probe-observe-collect",
|
||||||
|
acceptParsed: isWebObserveCollectJsonContract,
|
||||||
|
});
|
||||||
|
const collect = stdoutResolution.parsed;
|
||||||
|
const compactRaw = options.raw && options.compactRaw;
|
||||||
|
const degradedReason = collect === null
|
||||||
|
? collectDegradedReason(stdoutResolution.diagnostics)
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
ok: result.exitCode === 0 && collect !== null && collect.ok !== false,
|
||||||
|
status: result.exitCode === 0 && collect !== null ? "collected" : "blocked",
|
||||||
|
command: webObserveCommandLabel("collect", options),
|
||||||
|
id: webObserveIdFromOptions(options),
|
||||||
|
node: options.node,
|
||||||
|
lane: options.lane,
|
||||||
|
workspace: spec.workspace,
|
||||||
|
view: options.collectView,
|
||||||
|
requestedFile: options.collectFile,
|
||||||
|
requestedGrep: options.collectGrep,
|
||||||
|
requestedCommandId: options.collectCommandId,
|
||||||
|
requestedWindowMs: options.collectWindowMs,
|
||||||
|
degradedReason,
|
||||||
|
stdoutRecovery: stdoutResolution.diagnostics,
|
||||||
|
collect: compactRaw ? compactObserveCollectForRaw(collect) : collect,
|
||||||
|
wrapper: compactRaw
|
||||||
|
? { mode: "wrapper-only", action: "collect", node: options.node, lane: options.lane, id: webObserveIdFromOptions(options), stateDir: options.stateDir, valuesRedacted: true }
|
||||||
|
: buildWebObserveWrapperForObserveOptions("collect", options, spec.workspace),
|
||||||
|
result: compactRaw
|
||||||
|
? { exitCode: result.exitCode, timedOut: result.timedOut, stdoutBytes: Buffer.byteLength(result.stdout), stderrBytes: Buffer.byteLength(result.stderr), stdoutRecovery: stdoutResolution.diagnostics }
|
||||||
|
: collect === null
|
||||||
|
? { ...compactCommandResultWithStdoutTail(result), stdoutRecovery: stdoutResolution.diagnostics }
|
||||||
|
: compactCommandResult(result),
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWebObserveCollectJsonContract(value: Record<string, unknown>): boolean {
|
||||||
|
const command = stringOrNull(value.command);
|
||||||
|
const view = stringOrNull(value.view);
|
||||||
|
const stateDir = stringOrNull(value.stateDir);
|
||||||
|
if (value.ok === false) {
|
||||||
|
return command === "web-probe-observe collect"
|
||||||
|
|| view !== null
|
||||||
|
|| stringOrNull(value.reason) !== null
|
||||||
|
|| stringOrNull(value.error) !== null;
|
||||||
|
}
|
||||||
|
return command === "web-probe-observe collect"
|
||||||
|
&& view !== null
|
||||||
|
&& stateDir !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDegradedReason(diagnostics: Record<string, unknown>): string {
|
||||||
|
const fallbackReason = stringOrNull(diagnostics.fallbackReason);
|
||||||
|
if (fallbackReason === "stdout-json-contract-invalid") return "collect-stdout-json-contract-invalid";
|
||||||
|
if (fallbackReason === "stdout-trans-truncated" || fallbackReason === "stdout-ssh-truncated") return "collect-stdout-dump-unavailable";
|
||||||
|
if (fallbackReason === "stdout-dump-wrapper-unreadable") return "collect-stdout-dump-unreadable";
|
||||||
|
if (fallbackReason === "stdout-empty") return "collect-stdout-empty";
|
||||||
|
return "collect-stdout-json-unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringOrNull(value: unknown): string | null {
|
||||||
|
return typeof value === "string" && value.length > 0 ? value : null;
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec,
|
|||||||
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
||||||
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
||||||
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
||||||
import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
import { parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
||||||
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
import { withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
||||||
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
||||||
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
||||||
import { runWebProbeSentinelCommand, type WebProbeSentinelDashboardAction, type WebProbeSentinelOptions, type WebProbeSentinelReportView } from "../hwlab-node-web-sentinel-cicd";
|
import { runWebProbeSentinelCommand, type WebProbeSentinelDashboardAction, type WebProbeSentinelOptions, type WebProbeSentinelReportView } from "../hwlab-node-web-sentinel-cicd";
|
||||||
@@ -35,9 +35,9 @@ import type { BootstrapAdminPasswordMaterial, NodeWebProbeObserveCommandType, No
|
|||||||
import { runTransWorkspaceStdinScript, runtimeSecretSpec } from "./public-exposure";
|
import { runTransWorkspaceStdinScript, runtimeSecretSpec } from "./public-exposure";
|
||||||
import { transPath } from "./runtime-common";
|
import { transPath } from "./runtime-common";
|
||||||
import { assertLane, assertNodeId, compactCommandResult, compactCommandResultRedacted, compactCommandResultWithStdoutTail, nullableRecord, optionValue, parseJsonObject, positiveIntegerOption, record, requiredOption, shellQuote } from "./utils";
|
import { assertLane, assertNodeId, compactCommandResult, compactCommandResultRedacted, compactCommandResultWithStdoutTail, nullableRecord, optionValue, parseJsonObject, positiveIntegerOption, record, requiredOption, shellQuote } from "./utils";
|
||||||
|
import { runNodeWebProbeObserveCollect } from "./web-probe-observe-collect";
|
||||||
import { nodeWebObserveResolveStateDirShell, recoverWebObserveAnalyzeTurnDetails, renderWebObserveStartResult, renderWebProbeRunResult, upsertWebObserveIndexEntry, webObserveCommandLabel, webObserveIdFromOptions, webObserveIdFromStatus, webObserveIndexEntryFromOptions, webObserveNextCommands, withWebObserveAnalyzeRendered, withWebObserveShortcuts } from "./web-observe-render";
|
import { nodeWebObserveResolveStateDirShell, recoverWebObserveAnalyzeTurnDetails, renderWebObserveStartResult, renderWebProbeRunResult, upsertWebObserveIndexEntry, webObserveCommandLabel, webObserveIdFromOptions, webObserveIdFromStatus, webObserveIndexEntryFromOptions, webObserveNextCommands, withWebObserveAnalyzeRendered, withWebObserveShortcuts } from "./web-observe-render";
|
||||||
import { commandSummaryForOutput, isSafeWebObserveArchivePrefix, isSafeWebObserveCollectFile, isSafeWebObserveFindingId, isSafeWebObserveJobId, isSafeWebObserveStateDir, isSafeWebObserveTraceId, nodeWebObserveCollectNodeScript, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, runNodeWebProbeScript, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";
|
import { commandSummaryForOutput, isSafeWebObserveArchivePrefix, isSafeWebObserveCollectFile, isSafeWebObserveFindingId, isSafeWebObserveJobId, isSafeWebObserveStateDir, isSafeWebObserveTraceId, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, runNodeWebProbeScript, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";
|
||||||
import { compactObserveCollectForRaw } from "./web-observe-collect-compact";
|
|
||||||
import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "./web-probe";
|
import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "./web-probe";
|
||||||
|
|
||||||
export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSentinelOptions {
|
export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSentinelOptions {
|
||||||
@@ -2022,51 +2022,6 @@ export function runNodeWebProbeObserveForceStop(
|
|||||||
return options.raw ? payloadResult : withWebObserveCommandRendered(payloadResult);
|
return options.raw ? payloadResult : withWebObserveCommandRendered(payloadResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
|
||||||
const collectScript = options.collectView === "files"
|
|
||||||
? nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep)
|
|
||||||
: nodeWebObserveCollectViewNodeScript({
|
|
||||||
maxFiles: options.maxFiles,
|
|
||||||
view: options.collectView,
|
|
||||||
traceId: options.collectTraceId,
|
|
||||||
sampleSeq: options.collectSampleSeq,
|
|
||||||
timestamp: options.collectTimestamp,
|
|
||||||
turn: options.collectTurn,
|
|
||||||
commandId: options.collectCommandId,
|
|
||||||
windowMs: options.collectWindowMs,
|
|
||||||
});
|
|
||||||
const script = [
|
|
||||||
"set -eu",
|
|
||||||
nodeWebObserveResolveStateDirShell(options),
|
|
||||||
collectScript,
|
|
||||||
].join("\n");
|
|
||||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
|
||||||
const collect = parseJsonObject(result.stdout);
|
|
||||||
const compactRaw = options.raw && options.compactRaw;
|
|
||||||
const payload = {
|
|
||||||
ok: result.exitCode === 0 && collect !== null && collect.ok !== false,
|
|
||||||
status: result.exitCode === 0 && collect !== null ? "collected" : "blocked",
|
|
||||||
command: webObserveCommandLabel("collect", options),
|
|
||||||
id: webObserveIdFromOptions(options),
|
|
||||||
node: options.node,
|
|
||||||
lane: options.lane,
|
|
||||||
workspace: spec.workspace,
|
|
||||||
view: options.collectView,
|
|
||||||
requestedFile: options.collectFile,
|
|
||||||
requestedGrep: options.collectGrep,
|
|
||||||
requestedCommandId: options.collectCommandId,
|
|
||||||
requestedWindowMs: options.collectWindowMs,
|
|
||||||
degradedReason: collect === null ? "collect-json-parse-failed" : null,
|
|
||||||
collect: compactRaw ? compactObserveCollectForRaw(collect) : collect,
|
|
||||||
wrapper: compactRaw
|
|
||||||
? { mode: "wrapper-only", action: "collect", node: options.node, lane: options.lane, id: webObserveIdFromOptions(options), stateDir: options.stateDir, valuesRedacted: true }
|
|
||||||
: buildWebObserveWrapperForObserveOptions("collect", options, spec.workspace),
|
|
||||||
result: compactRaw ? { exitCode: result.exitCode, timedOut: result.timedOut, stdoutBytes: Buffer.byteLength(result.stdout), stderrBytes: Buffer.byteLength(result.stderr) } : collect === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result),
|
|
||||||
valuesRedacted: true,
|
|
||||||
};
|
|
||||||
return options.raw ? payload : withWebObserveCollectRendered(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
||||||
const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64");
|
const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64");
|
||||||
const alertThresholds = nodeWebProbeAlertThresholds(spec);
|
const alertThresholds = nodeWebProbeAlertThresholds(spec);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|||||||
import { basename, join, resolve } from "node:path";
|
import { basename, join, resolve } from "node:path";
|
||||||
import { repoRoot } from "./config";
|
import { repoRoot } from "./config";
|
||||||
import { runCommand, type CommandResult } from "./command";
|
import { runCommand, type CommandResult } from "./command";
|
||||||
|
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
|
||||||
|
|
||||||
export interface WebProbeRemoteArtifactJobOptions {
|
export interface WebProbeRemoteArtifactJobOptions {
|
||||||
route: string;
|
route: string;
|
||||||
@@ -99,7 +100,12 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO
|
|||||||
const download = runCommand(downloadArgs, repoRoot, { timeoutMs: commandTimeoutMs });
|
const download = runCommand(downloadArgs, repoRoot, { timeoutMs: commandTimeoutMs });
|
||||||
commandResults.push(download);
|
commandResults.push(download);
|
||||||
lastDownload = download;
|
lastDownload = download;
|
||||||
const parsed = parseJsonObject(download.stdout);
|
const downloadJson = resolveCliChildJsonCommandResult({
|
||||||
|
result: download,
|
||||||
|
requestedStdoutType: "trans download verification JSON",
|
||||||
|
acceptParsed: (value) => typeof value.ok === "boolean" || typeof value.verified === "boolean" || typeof value.bytes === "number" || typeof value.sha256 === "string",
|
||||||
|
});
|
||||||
|
const parsed = downloadJson.parsed ?? {};
|
||||||
if (download.exitCode === 0 && parsed.ok === true && parsed.verified === true) {
|
if (download.exitCode === 0 && parsed.ok === true && parsed.verified === true) {
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
remotePath: artifact.remotePath,
|
remotePath: artifact.remotePath,
|
||||||
@@ -109,6 +115,7 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO
|
|||||||
verified: true,
|
verified: true,
|
||||||
verification: parsed.verification ?? null,
|
verification: parsed.verification ?? null,
|
||||||
transfer: parsed.transfer ?? null,
|
transfer: parsed.transfer ?? null,
|
||||||
|
stdoutRecovery: downloadJson.diagnostics,
|
||||||
manifestBytes: artifact.bytes,
|
manifestBytes: artifact.bytes,
|
||||||
manifestSha256: artifact.sha256,
|
manifestSha256: artifact.sha256,
|
||||||
});
|
});
|
||||||
@@ -124,6 +131,11 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO
|
|||||||
exitCode: lastDownload?.exitCode ?? null,
|
exitCode: lastDownload?.exitCode ?? null,
|
||||||
stdoutTail: lastDownload?.stdout.slice(-1000) ?? "",
|
stdoutTail: lastDownload?.stdout.slice(-1000) ?? "",
|
||||||
stderrTail: lastDownload?.stderr.slice(-1000) ?? "",
|
stderrTail: lastDownload?.stderr.slice(-1000) ?? "",
|
||||||
|
stdoutRecovery: lastDownload === null ? null : resolveCliChildJsonCommandResult({
|
||||||
|
result: lastDownload,
|
||||||
|
requestedStdoutType: "trans download verification JSON",
|
||||||
|
acceptParsed: (value) => typeof value.ok === "boolean" || typeof value.verified === "boolean" || typeof value.bytes === "number" || typeof value.sha256 === "string",
|
||||||
|
}).diagnostics,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -468,17 +480,6 @@ function wrapBase64(value: string): string {
|
|||||||
return value.replace(/.{1,76}/gu, "$&\n").trimEnd();
|
return value.replace(/.{1,76}/gu, "$&\n").trimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonObject(text: string): Record<string, unknown> {
|
|
||||||
const trimmed = text.trim();
|
|
||||||
if (trimmed.length === 0) return {};
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed) as unknown;
|
|
||||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTabStatus(text: string): Record<string, string> {
|
function parseTabStatus(text: string): Record<string, string> {
|
||||||
const out: Record<string, string> = {};
|
const out: Record<string, string> = {};
|
||||||
for (const rawLine of text.split(/\r?\n/u)) {
|
for (const rawLine of text.split(/\r?\n/u)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user