fix: 收敛性能采集控制面输出
This commit is contained in:
@@ -165,14 +165,15 @@ function renderWebObserveStatusTable(value: Record<string, unknown>): string {
|
||||
] : []),
|
||||
...(Object.keys(exactCommand).length > 0 ? [
|
||||
"Exact command:",
|
||||
webObserveTable(["COMMAND", "TYPE", "PHASE", "OK", "STATUS", "REPORT_SHA", "DETAIL"], [[
|
||||
webObserveTable(["COMMAND", "TYPE", "PHASE", "OK", "STATUS", "PROFILE_SHA", "REPORT_SHA", "DETAIL"], [[
|
||||
webObserveShort(webObserveText(exactCommand.commandId), 40),
|
||||
exactCommand.type,
|
||||
exactCommand.phase,
|
||||
exactCommand.ok,
|
||||
exactResult?.status,
|
||||
webObserveShort(webObserveText(exactResult?.profileSha256 ?? exactErrorDetails?.profileSha256), 32),
|
||||
webObserveShort(webObserveText(exactResult?.reportSha256 ?? exactErrorDetails?.reportSha256), 32),
|
||||
webObserveShort(webObserveText(exactError.message ?? exactResult?.reportPath ?? exactErrorDetails?.reportPath), 96),
|
||||
webObserveShort(webObserveText(exactError.message ?? exactResult?.reportPath ?? exactErrorDetails?.reportPath ?? exactResult?.profilePath ?? exactErrorDetails?.profilePath), 96),
|
||||
]]),
|
||||
"",
|
||||
] : []),
|
||||
@@ -436,6 +437,7 @@ function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||||
const details = nullableRecord(error?.details);
|
||||
const replayEvidence = result ?? details;
|
||||
const replayScreenshot = record(result?.screenshot ?? details?.screenshot);
|
||||
const performanceCapture = observerCommand?.type === "performanceCapture" ? result ?? details : null;
|
||||
const rawReadiness = record(error?.navigationReadiness) ?? record(details?.readiness) ?? record(details?.readinessAfterWait) ?? record(details?.readinessBeforeClick);
|
||||
const readiness = record(rawReadiness?.snapshot) ?? rawReadiness;
|
||||
const id = webObserveText(value.id);
|
||||
@@ -454,6 +456,17 @@ function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||||
webObserveShort(webObserveText(asyncTurn?.traceId), 24),
|
||||
webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80),
|
||||
]]),
|
||||
...(performanceCapture !== null ? [
|
||||
"",
|
||||
"Performance capture:",
|
||||
webObserveTable(["CAPTURE", "DURATION_MS", "PROFILE_SHA", "REPORT_SHA", "COLLECT"], [[
|
||||
performanceCapture.captureId,
|
||||
performanceCapture.durationMs,
|
||||
webObserveShort(webObserveText(performanceCapture.profileSha256), 32),
|
||||
webObserveShort(webObserveText(performanceCapture.reportSha256), 32),
|
||||
`observe collect ${id} --file ${webObserveText(performanceCapture.reportPath)}`,
|
||||
]]),
|
||||
] : []),
|
||||
...(observerCommand?.type === "validateWorkbenchKafkaDebugReplay" && replayEvidence !== null ? [
|
||||
"",
|
||||
...renderWorkbenchKafkaDebugReplayEvidence(
|
||||
|
||||
@@ -25,6 +25,36 @@ test("observe command renderer separates control completion from async turn term
|
||||
assert.match(text, /--view turn-summary --command-id cmd-fixture/u);
|
||||
});
|
||||
|
||||
test("observe command renderer exposes bounded performance capture artifact drill-down", () => {
|
||||
const rendered = withWebObserveCommandRendered({
|
||||
ok: true,
|
||||
status: "control-completed",
|
||||
command: "web-probe observe command",
|
||||
id: "webobs-fixture",
|
||||
commandId: "cmd-performance",
|
||||
observerCommand: { type: "performanceCapture" },
|
||||
observer: {
|
||||
ok: true,
|
||||
completedAt: "2026-07-13T12:00:00.000Z",
|
||||
result: {
|
||||
captureId: "cpu-fixture",
|
||||
durationMs: 1200,
|
||||
profileSha256: "sha256:profile",
|
||||
reportPath: ".state/web-observe/performance/cpu-fixture/summary.json",
|
||||
reportSha256: "sha256:report",
|
||||
},
|
||||
},
|
||||
control: { executionStatus: "completed", businessTurnTerminalImplied: false },
|
||||
asyncTurn: { applicable: false, submissionStatus: "not-applicable", terminalStatus: null, terminalObserved: false },
|
||||
});
|
||||
const text = String(rendered.renderedText);
|
||||
assert.match(text, /Performance capture:/u);
|
||||
assert.match(text, /sha256:profile/u);
|
||||
assert.match(text, /sha256:report/u);
|
||||
assert.match(text, /observe collect webobs-fixture --file .*summary\.json/u);
|
||||
assert.doesNotMatch(text, /nodes.*callFrame/u);
|
||||
});
|
||||
|
||||
test("observe status renderer exposes the exact command phase and failed report evidence", () => {
|
||||
const rendered = withWebObserveStatusRendered({
|
||||
ok: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { nodeWebObserveStatusNodeScript } from "./web-observe-scripts";
|
||||
import { nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell } from "./web-observe-scripts";
|
||||
|
||||
async function runStatusScript(stateDir: string, commandId: string): Promise<Record<string, any>> {
|
||||
const child = Bun.spawn(["bash", "-lc", nodeWebObserveStatusNodeScript(1, "NC01", "v03", commandId)], {
|
||||
@@ -58,6 +58,77 @@ test("observe status returns one exact completed command without prompt payloads
|
||||
assert.equal(status.exactCommand.valuesRedacted, true);
|
||||
});
|
||||
|
||||
test("observe status summarizes performance capture artifact without embedding CPU profile", async () => {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-status-"));
|
||||
const commandId = "cmd-performance";
|
||||
await mkdir(join(stateDir, "commands", "done"), { recursive: true });
|
||||
await writeFile(join(stateDir, "commands", "done", `${commandId}.json`), JSON.stringify({
|
||||
ok: true,
|
||||
commandId,
|
||||
type: "performanceCapture",
|
||||
completedAt: "2026-07-13T12:00:00.000Z",
|
||||
result: {
|
||||
ok: true,
|
||||
captureId: "cpu-fixture",
|
||||
durationMs: 1200,
|
||||
profile: { nodes: [{ id: 1, callFrame: { functionName: "large-profile-payload" } }] },
|
||||
artifact: {
|
||||
path: ".state/web-observe/performance/cpu-fixture/profile.cpuprofile",
|
||||
sha256: "sha256:profile",
|
||||
summaryPath: ".state/web-observe/performance/cpu-fixture/summary.json",
|
||||
summarySha256: "sha256:report",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const status = await runStatusScript(stateDir, commandId);
|
||||
assert.equal(status.exactCommand.result.captureId, "cpu-fixture");
|
||||
assert.equal(status.exactCommand.result.profileSha256, "sha256:profile");
|
||||
assert.equal(status.exactCommand.result.reportSha256, "sha256:report");
|
||||
assert.equal(JSON.stringify(status).includes("large-profile-payload"), false);
|
||||
});
|
||||
|
||||
test("observe command wait emits bounded performance capture summary", async () => {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-wait-"));
|
||||
const commandId = "cmd-performance";
|
||||
await mkdir(join(stateDir, "commands", "done"), { recursive: true });
|
||||
await writeFile(join(stateDir, "commands", "done", `${commandId}.json`), JSON.stringify({
|
||||
ok: true,
|
||||
commandId,
|
||||
type: "performanceCapture",
|
||||
completedAt: "2026-07-13T12:00:00.000Z",
|
||||
result: {
|
||||
ok: true,
|
||||
captureId: "cpu-fixture",
|
||||
durationMs: 1200,
|
||||
profile: { nodes: [{ id: 1, callFrame: { functionName: "large-profile-payload" } }] },
|
||||
artifact: {
|
||||
path: ".state/web-observe/performance/cpu-fixture/profile.cpuprofile",
|
||||
sha256: "sha256:profile",
|
||||
summaryPath: ".state/web-observe/performance/cpu-fixture/summary.json",
|
||||
summarySha256: "sha256:report",
|
||||
},
|
||||
},
|
||||
}));
|
||||
const child = Bun.spawn(["bash", "-lc", nodeWebObserveWaitCommandShell(commandId, 1000)], {
|
||||
env: { ...process.env, state_dir: stateDir },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
]);
|
||||
assert.equal(exitCode, 0, stderr);
|
||||
const summary = JSON.parse(stdout);
|
||||
assert.equal(summary.status, "done");
|
||||
assert.equal(summary.result.profileSha256, "sha256:profile");
|
||||
assert.equal(summary.result.reportSha256, "sha256:report");
|
||||
assert.equal(stdout.includes("large-profile-payload"), false);
|
||||
assert.ok(Buffer.byteLength(stdout) < 2048);
|
||||
});
|
||||
|
||||
test("observe status preserves bounded existing-session refresh evidence", async () => {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-existing-refresh-status-"));
|
||||
const commandId = "cmd-existing-refresh";
|
||||
|
||||
@@ -101,6 +101,7 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string,
|
||||
const compactTracePhase=(value)=>{const item=value&&typeof value==='object'?value:null;return item?{observed:item.observed??null,stable:item.stable??null,reason:item.reason||null,status:item.status||null,elapsedMs:item.elapsedMs??null,sampleCount:item.sampleCount??null,snapshot:compactTraceSnapshot(item.snapshot),valuesRedacted:true}:null;};
|
||||
const compactTraceReadabilityEvidence=(value)=>{const item=value&&typeof value==='object'?value:null;if(!item)return{};const scope=item.scope&&typeof item.scope==='object'?item.scope:null;const disclosure=item.disclosure&&typeof item.disclosure==='object'?item.disclosure:null;const retention=item.runningRetention&&typeof item.runningRetention==='object'?item.runningRetention:null;return{startedDuringRunning:item.startedDuringRunning??null,forcedExpansion:item.forcedExpansion??null,scope:scope?{selector:short(scope.selector),conversationCount:scope.conversationCount??null,productCardCount:scope.productCardCount??null,debugPanelCountInsideConversation:scope.debugPanelCountInsideConversation??null,isolatedDebugExcluded:scope.isolatedDebugExcluded??null,valuesRedacted:true}:null,disclosure:disclosure?{openBefore:disclosure.openBefore??null,openAfterExpansion:disclosure.openAfterExpansion??null,openAfterTerminal:disclosure.openAfterTerminal??null,autoReadable:disclosure.autoReadable??null,valuesRedacted:true}:null,running:compactTracePhase(item.running),terminalArrival:compactTracePhase(item.terminalArrival),terminal:compactTracePhase(item.terminal),runningRetention:retention?{mode:retention.mode||null,passed:retention.passed??null,retainedRowCount:retention.retainedRowCount??null,runningEventCount:retention.runningEventCount??null,terminalEventCount:retention.terminalEventCount??null,runningSourceSeqMin:retention.runningSourceSeqMin??null,runningSourceSeqMax:retention.runningSourceSeqMax??null,terminalSourceSeqMin:retention.terminalSourceSeqMin??null,terminalSourceSeqMax:retention.terminalSourceSeqMax??null,valuesRedacted:true}:null,retainedRunningRowCount:item.retainedRunningRowCount??null,failures:Array.isArray(item.failures)?item.failures.slice(0,8).map((failure)=>({code:failure&&failure.code||null,message:short(failure&&failure.message),valuesRedacted:true})):null};};
|
||||
const compactScreenshot=(value)=>value&&typeof value==='object'?{path:value.path||null,sha256:value.sha256||null,available:value.available??null,valuesRedacted:true}:null;
|
||||
const compactPerformanceCapture=(value)=>{const item=value&&typeof value==='object'?value:null;const artifact=item&&item.artifact&&typeof item.artifact==='object'?item.artifact:null;if(!item&&!artifact)return{};return{captureId:item&&item.captureId||artifact&&artifact.captureId||null,durationMs:item&&item.durationMs!==undefined?item.durationMs:artifact&&artifact.durationMs!==undefined?artifact.durationMs:null,profilePath:artifact&&artifact.path||null,profileSha256:artifact&&artifact.sha256||null,reportPath:artifact&&artifact.summaryPath||null,reportSha256:artifact&&artifact.summarySha256||null,valuesRedacted:true};};
|
||||
const commandSummary=(name)=>{
|
||||
const root=commandDir(name); let entries=[];
|
||||
try{entries=fs.readdirSync(root).filter((item)=>item.endsWith('.json')).sort();}catch{}
|
||||
@@ -115,7 +116,7 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string,
|
||||
if(!parsed)continue;
|
||||
const result=parsed.result&&typeof parsed.result==='object'?parsed.result:null;
|
||||
const error=parsed.error&&typeof parsed.error==='object'?parsed.error:null;
|
||||
return {commandId:exactCommandId,phase:bucket,ok:parsed.ok??null,type:parsed.type||null,createdAt:parsed.createdAt||null,completedAt:parsed.completedAt||null,failedAt:parsed.failedAt||null,result:result?{ok:result.ok??null,status:result.status||null,profile:result.profile||null,sessionId:result.sessionId||null,traceId:result.traceId||null,traceIds:Array.isArray(result.traceIds)?result.traceIds.slice(0,4):[],runIds:Array.isArray(result.runIds)?result.runIds.slice(0,4):[],commandIds:Array.isArray(result.commandIds)?result.commandIds.slice(0,4):[],topic:result.topic||null,groupId:result.groupId||null,groupPrefix:result.groupPrefix||null,receivedCount:result.receivedCount??null,appliedCount:result.appliedCount??null,terminalStatus:result.terminalStatus||null,...compactReplayEvidence(result),...(parsed.type==='validateExistingSessionRefresh'?compactExistingSessionRefreshEvidence(result):{}),...compactTraceReadabilityEvidence(result),warmRunnerReused:result.warmRunnerReused??null,forbiddenRequestCount:result.forbiddenRequestCount??null,replayedEventCount:result.replayedEventCount??null,reportPath:result.reportPath||null,reportSha256:result.reportSha256||null,screenshot:compactScreenshot(result.screenshot),valuesRedacted:true}:null,error:error?{name:error.name||null,message:short(error.message,200),details:error.details&&typeof error.details==='object'?{profile:error.details.profile||null,...compactReplayEvidence(error.details),...(parsed.type==='validateExistingSessionRefresh'?compactExistingSessionRefreshEvidence(error.details):{}),...compactTraceReadabilityEvidence(error.details),sessionId:error.details.sessionId||null,traceId:error.details.traceId||null,traceIds:Array.isArray(error.details.traceIds)?error.details.traceIds.slice(0,4):[],topic:error.details.topic||null,groupId:error.details.groupId||null,receivedCount:error.details.receivedCount??null,appliedCount:error.details.appliedCount??null,terminalStatus:error.details.terminalStatus||null,forbiddenRequestCount:error.details.forbiddenRequestCount??null,reportPath:error.details.reportPath||null,reportSha256:error.details.reportSha256||null,screenshot:compactScreenshot(error.details.screenshot),valuesRedacted:true}:null}:null,valuesRedacted:true};
|
||||
return {commandId:exactCommandId,phase:bucket,ok:parsed.ok??null,type:parsed.type||null,createdAt:parsed.createdAt||null,completedAt:parsed.completedAt||null,failedAt:parsed.failedAt||null,result:result?{ok:result.ok??null,status:result.status||null,sessionId:result.sessionId||null,traceId:result.traceId||null,traceIds:Array.isArray(result.traceIds)?result.traceIds.slice(0,4):[],runIds:Array.isArray(result.runIds)?result.runIds.slice(0,4):[],commandIds:Array.isArray(result.commandIds)?result.commandIds.slice(0,4):[],topic:result.topic||null,groupId:result.groupId||null,groupPrefix:result.groupPrefix||null,receivedCount:result.receivedCount??null,appliedCount:result.appliedCount??null,terminalStatus:result.terminalStatus||null,...compactReplayEvidence(result),...(parsed.type==='validateExistingSessionRefresh'?compactExistingSessionRefreshEvidence(result):{}),...compactTraceReadabilityEvidence(result),warmRunnerReused:result.warmRunnerReused??null,forbiddenRequestCount:result.forbiddenRequestCount??null,replayedEventCount:result.replayedEventCount??null,reportPath:result.reportPath||null,reportSha256:result.reportSha256||null,screenshot:compactScreenshot(result.screenshot),...(parsed.type==='performanceCapture'?compactPerformanceCapture(result):{profile:result.profile||null}),valuesRedacted:true}:null,error:error?{name:error.name||null,message:short(error.message,200),details:error.details&&typeof error.details==='object'?{...compactReplayEvidence(error.details),...(parsed.type==='validateExistingSessionRefresh'?compactExistingSessionRefreshEvidence(error.details):{}),...compactTraceReadabilityEvidence(error.details),sessionId:error.details.sessionId||null,traceId:error.details.traceId||null,traceIds:Array.isArray(error.details.traceIds)?error.details.traceIds.slice(0,4):[],topic:error.details.topic||null,groupId:error.details.groupId||null,receivedCount:error.details.receivedCount??null,appliedCount:error.details.appliedCount??null,terminalStatus:error.details.terminalStatus||null,forbiddenRequestCount:error.details.forbiddenRequestCount??null,reportPath:error.details.reportPath||null,reportSha256:error.details.reportSha256||null,screenshot:compactScreenshot(error.details.screenshot),...(parsed.type==='performanceCapture'?compactPerformanceCapture(error.details):{profile:error.details.profile||null}),valuesRedacted:true}:null}:null,valuesRedacted:true};
|
||||
}
|
||||
return {commandId:exactCommandId,phase:'not-found',ok:false,valuesRedacted:true};
|
||||
};
|
||||
@@ -425,12 +426,22 @@ export function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number
|
||||
].join("\n");
|
||||
}
|
||||
const waitSeconds = Math.max(1, Math.ceil(waitMs / 1000));
|
||||
const printCommandSummary = `node -e ${shellQuote(`
|
||||
const fs=require('fs');
|
||||
const file=process.argv[1];
|
||||
const phase=process.argv[2];
|
||||
const parsed=JSON.parse(fs.readFileSync(file,'utf8'));
|
||||
const result=parsed&&parsed.result&&typeof parsed.result==='object'?parsed.result:null;
|
||||
const artifact=result&&result.artifact&&typeof result.artifact==='object'?result.artifact:null;
|
||||
const summary={ok:parsed.ok!==false,queued:false,commandId:parsed.commandId||parsed.id||null,type:parsed.type||null,status:phase,completedAt:parsed.completedAt||null,failedAt:parsed.failedAt||null,result:result?{ok:result.ok??null,status:result.status||null,captureId:result.captureId||artifact&&artifact.captureId||null,durationMs:result.durationMs!==undefined?result.durationMs:artifact&&artifact.durationMs!==undefined?artifact.durationMs:null,profilePath:artifact&&artifact.path||null,profileSha256:artifact&&artifact.sha256||null,reportPath:artifact&&artifact.summaryPath||result.reportPath||null,reportSha256:artifact&&artifact.summarySha256||result.reportSha256||null,valuesRedacted:true}:null,error:parsed.error?{name:parsed.error.name||null,message:String(parsed.error.message||'').replace(/\\s+/g,' ').slice(0,200),valuesRedacted:true}:null,valuesRedacted:true};
|
||||
console.log(JSON.stringify(summary));
|
||||
`)}`;
|
||||
return [
|
||||
`command_id=${shellQuote(commandId)}`,
|
||||
`deadline=$(( $(date +%s) + ${waitSeconds} ))`,
|
||||
"while [ \"$(date +%s)\" -le \"$deadline\" ]; do",
|
||||
" if [ -f \"$state_dir/commands/done/${command_id}.json\" ]; then cat \"$state_dir/commands/done/${command_id}.json\"; exit 0; fi",
|
||||
" if [ -f \"$state_dir/commands/failed/${command_id}.json\" ]; then cat \"$state_dir/commands/failed/${command_id}.json\"; exit 2; fi",
|
||||
` if [ -f "$state_dir/commands/done/\${command_id}.json" ]; then ${printCommandSummary} "$state_dir/commands/done/\${command_id}.json" done; exit 0; fi`,
|
||||
` if [ -f "$state_dir/commands/failed/\${command_id}.json" ]; then ${printCommandSummary} "$state_dir/commands/failed/\${command_id}.json" failed; exit 2; fi`,
|
||||
" sleep 1",
|
||||
"done",
|
||||
"printf '{\"ok\":true,\"queued\":true,\"waitTimedOut\":true,\"commandId\":\"%s\",\"stateDir\":\"%s\"}\\n' \"$command_id\" \"$state_dir\"",
|
||||
|
||||
@@ -110,6 +110,32 @@ test("web observe action JSON recovery accepts concrete start contract", () => {
|
||||
assert.equal(resolved.diagnostics.stdoutContractAccepted, true);
|
||||
});
|
||||
|
||||
test("web observe action JSON recovery accepts bounded performance capture command summary", () => {
|
||||
const resolved = resolveWebObserveActionJson({
|
||||
stdout: JSON.stringify({
|
||||
ok: true,
|
||||
queued: false,
|
||||
commandId: "cmd-performance",
|
||||
type: "performanceCapture",
|
||||
status: "done",
|
||||
result: {
|
||||
captureId: "cpu-fixture",
|
||||
profileSha256: "sha256:profile",
|
||||
reportSha256: "sha256:report",
|
||||
valuesRedacted: true,
|
||||
},
|
||||
valuesRedacted: true,
|
||||
}),
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
timedOut: false,
|
||||
}, "command");
|
||||
|
||||
assert.equal(resolved.source, "stdout");
|
||||
assert.equal(resolved.parsed?.commandId, "cmd-performance");
|
||||
assert.equal(resolved.diagnostics.stdoutContractAccepted, true);
|
||||
});
|
||||
|
||||
test("web observe action JSON recovery reads dump wrapper for status contract", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-status-dump-"));
|
||||
const dumpPath = join(dir, "status.json");
|
||||
|
||||
Reference in New Issue
Block a user