fix: 固化 Workbench Trace 可读性探针

This commit is contained in:
Codex
2026-07-10 18:00:24 +02:00
parent 92686ff213
commit b6f5e5efd4
21 changed files with 1378 additions and 16 deletions
+1
View File
@@ -140,6 +140,7 @@ export type NodeWebProbeObserveCommandType =
| "sendPrompt"
| "validateRealtimeFanout"
| "validateWorkbenchKafkaDebugReplay"
| "validateWorkbenchTraceReadability"
| "steer"
| "cancel"
| "selectProvider"
@@ -98,6 +98,19 @@ test("observe status renderer exposes bounded Workbench Kafka debug replay evide
server: { scanned: 35, matched: 35, delivered: 35 },
client: { received: 35, decoded: 35, applied: 35 },
},
layerDom: {
matchCount: 1,
visibleCount: 1,
connectedCount: 1,
textBytes: 128,
textHash: "sha256:layer-dom",
},
layerDomStabilization: {
stable: true,
timedOut: false,
sampleCount: 4,
elapsedMs: 400,
},
traceTimeline: {
contract: "trace-sequence-authority-v1",
sourceAuthorityRowCount: 35,
@@ -126,9 +139,141 @@ test("observe status renderer exposes bounded Workbench Kafka debug replay evide
assert.match(text, /terminal.*terminal_complete/u);
assert.match(text, /35.*35.*completed.*sha256:image/u);
assert.match(text, /SERVER_SCANNED.*CLIENT_APPLIED/u);
assert.match(text, / DOM /u);
assert.match(text, /1.*1.*1.*128.*sha256:layer-dom.*true.*false.*4.*400/u);
assert.match(text, /trace-sequence-authority-v1.*35.*35.*true.*true.*true.*35.*30.*0.*5.*8192.*0/u);
});
test("observe status renderer 展示限定作用域的产品 Trace 生命周期证据", () => {
const rendered = withWebObserveStatusRendered({
ok: true,
status: "running",
command: "web-probe observe status",
id: "webobs-fixture",
node: "NC01",
lane: "v03",
observer: {
processAlive: true,
manifest: { baseUrl: "http://internal.example.test", targetPath: "/workbench" },
heartbeat: { status: "running", sampleSeq: 12, commandSeq: 4, updatedAt: "2026-07-10T01:00:01.000Z" },
diagnostics: { effectiveLiveness: "alive", heartbeatStale: false },
commands: { pendingCount: 0, processingCount: 0, abandonedCount: 0, failedCount: 0 },
exactCommand: {
commandId: "cmd-trace-readable",
type: "validateWorkbenchTraceReadability",
phase: "done",
ok: true,
result: {
status: "passed",
traceId: "trc_fixture",
startedDuringRunning: true,
forcedExpansion: false,
scope: { conversationCount: 1, productCardCount: 2, debugPanelCountInsideConversation: 0 },
disclosure: { openBefore: true, autoReadable: true, openAfterTerminal: true },
running: { observed: true, snapshot: { status: "running", rowCount: 2, sourceAuthorityRowCount: 2 } },
terminal: { stable: true, snapshot: { status: "completed", rowCount: 3, sourceAuthorityRowCount: 3, projectedAuthorityRowCount: 0 } },
runningRetention: { mode: "identity-overlap", passed: true, retainedRowCount: 2 },
retainedRunningRowCount: 2,
reportSha256: "sha256:trace-report",
screenshot: { sha256: "sha256:trace-image" },
},
},
tails: { samples: [], control: [], network: [] },
},
next: {},
});
const text = String(rendered.renderedText);
assert.match(text, /Workbench Trace /u);
assert.match(text, /trc_fixture.*true.*true.*true.*true.*false.*true.*identity-overlap.*true.*2.*sha256:trace-report.*sha256:trace-image/u);
assert.match(text, /CONVERSATION.*PRODUCT_CARDS.*DEBUG_IN_SCOPE/u);
assert.match(text, /1.*2.*0.*running.*2.*2.*completed.*3.*3.*0.*true/u);
});
test("observe status renderer 在命令失败时保留产品 Trace typed evidence", () => {
const rendered = withWebObserveStatusRendered({
ok: true,
status: "running",
command: "web-probe observe status",
id: "webobs-fixture",
node: "NC01",
lane: "v03",
observer: {
processAlive: true,
manifest: {},
heartbeat: {},
diagnostics: {},
commands: {},
exactCommand: {
commandId: "cmd-trace-failed",
type: "validateWorkbenchTraceReadability",
phase: "failed",
ok: false,
error: {
message: "trace validation failed",
details: {
traceId: "trc_failed",
startedDuringRunning: true,
forcedExpansion: false,
scope: { conversationCount: 1, productCardCount: 1, debugPanelCountInsideConversation: 0 },
disclosure: { openBefore: false, autoReadable: false, openAfterTerminal: true },
running: { observed: true, snapshot: { status: "running", rowCount: 2, sourceAuthorityRowCount: 2 } },
terminal: { stable: true, snapshot: { status: "completed", rowCount: 3, sourceAuthorityRowCount: 3, projectedAuthorityRowCount: 0 } },
runningRetention: { mode: "identity-overlap", passed: true, retainedRowCount: 2 },
retainedRunningRowCount: 2,
failures: [{ code: "trace_not_auto_expanded" }],
reportSha256: "sha256:failed-trace-report",
},
},
},
tails: { samples: [], control: [], network: [] },
},
next: {},
});
const text = String(rendered.renderedText);
assert.match(text, /trc_failed.*identity-overlap.*true.*2.*sha256:failed-trace-report/u);
assert.match(text, /trace_not_auto_expanded/u);
});
test("observe command renderer 在隔离重放失败时保留分层 typed evidence", () => {
const rendered = withWebObserveCommandRendered({
ok: false,
status: "failed",
command: "web-probe observe command",
id: "webobs-fixture",
commandId: "cmd-debug-failed",
observerCommand: { type: "validateWorkbenchKafkaDebugReplay" },
observer: {
ok: false,
error: {
message: "debug trace missing",
details: {
traceId: "trc_debug_failed",
topic: "hwlab.event.debug.v1",
groupId: "hwlab-v03-workbench-isolated-debug-fixture",
phase: "filter",
code: "debug_trace_not_generated",
receivedCount: 0,
appliedCount: 0,
layerCounts: {
server: { scanned: 27, matched: 0, delivered: 0 },
client: { received: 0, decoded: 0, applied: 0 },
},
reportSha256: "sha256:failed-debug-report",
},
},
},
control: { executionStatus: "failed" },
asyncTurn: {},
});
const text = String(rendered.renderedText);
assert.match(text, /trc_debug_failed.*debug_trace_not_generated/u);
assert.match(text, /27.*0.*0.*0.*0.*0/u);
assert.match(text, /sha256:failed-debug-report/u);
});
test("web-probe script render warns to promote repeated scripts into commands", () => {
const commandPromotionHint = {
schemaVersion: "unidesk.web-probe.command-promotion.v1",
@@ -94,6 +94,22 @@ test("observe status preserves bounded Workbench Kafka debug replay evidence", a
server: { scanned: 35, matched: 35, delivered: 35 },
client: { received: 35, decoded: 35, applied: 35 },
},
layerDom: {
matchCount: 1,
visibleCount: 1,
connectedCount: 1,
textPresent: true,
textBytes: 128,
textHash: "sha256:layer",
textPreview: "server scanned=35 client received=35",
},
layerDomStabilization: {
stable: true,
timedOut: false,
sampleCount: 4,
elapsedMs: 400,
stableQuietMs: 250,
},
traceTimeline: {
contract: "trace-sequence-authority-v1",
sourceAuthorityRowCount: 35,
@@ -123,7 +139,107 @@ test("observe status preserves bounded Workbench Kafka debug replay evidence", a
assert.equal(status.exactCommand.result.phase, "terminal");
assert.equal(status.exactCommand.result.code, "terminal_complete");
assert.equal(status.exactCommand.result.layerCounts.server.scanned, 35);
assert.equal(status.exactCommand.result.layerDom.visibleCount, 1);
assert.equal(status.exactCommand.result.layerDom.textHash, "sha256:layer");
assert.equal(status.exactCommand.result.layerDomStabilization.stable, true);
assert.equal(status.exactCommand.result.traceTimeline.sourceAuthorityRowCount, 35);
assert.equal(status.exactCommand.result.rawHwlabEventWindow.counts.retained, 30);
assert.equal(status.exactCommand.result.screenshot.sha256, "sha256:image");
});
test("observe status preserves failed replay layer DOM and screenshot diagnostics", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-debug-replay-failed-status-"));
const commandId = "cmd-debug-replay-failed";
await mkdir(join(stateDir, "commands", "failed"), { recursive: true });
await writeFile(join(stateDir, "commands", "failed", `${commandId}.json`), JSON.stringify({
ok: false,
commandId,
type: "validateWorkbenchKafkaDebugReplay",
failedAt: "2026-07-10T12:00:00.000Z",
error: {
name: "Error",
message: "layer DOM did not stabilize",
details: {
traceId: "trc_fixture",
layerCounts: null,
layerDom: { matchCount: 1, visibleCount: 1, connectedCount: 1, textPresent: true, textBytes: 128, textHash: "sha256:failed-layer" },
layerDomStabilization: { stable: false, timedOut: true, sampleCount: 30, elapsedMs: 3000, stableQuietMs: 500 },
reportPath: "/tmp/failed-report.json",
reportSha256: "sha256:failed-report",
screenshot: { path: "/tmp/failed.png", sha256: "sha256:failed-image" },
},
},
}) + "\n");
const status = await runStatusScript(stateDir, commandId);
assert.equal(status.exactCommand.phase, "failed");
assert.equal(status.exactCommand.error.details.layerDom.visibleCount, 1);
assert.equal(status.exactCommand.error.details.layerDom.textHash, "sha256:failed-layer");
assert.equal(status.exactCommand.error.details.layerDomStabilization.timedOut, true);
assert.equal(status.exactCommand.error.details.reportSha256, "sha256:failed-report");
assert.equal(status.exactCommand.error.details.screenshot.sha256, "sha256:failed-image");
});
test("observe status preserves bounded scoped Workbench product Trace evidence", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-trace-readability-status-"));
const commandId = "cmd-trace-readable";
await mkdir(join(stateDir, "commands", "done"), { recursive: true });
await writeFile(join(stateDir, "commands", "done", `${commandId}.json`), JSON.stringify({
ok: true,
commandId,
type: "validateWorkbenchTraceReadability",
completedAt: "2026-07-10T12:00:00.000Z",
result: {
ok: true,
status: "passed",
sessionId: "ses_fixture",
traceId: "trc_fixture",
startedDuringRunning: true,
forcedExpansion: false,
scope: {
selector: "#conversation-list .message-card[data-role=agent][data-trace-id]",
conversationCount: 1,
productCardCount: 2,
debugPanelCountInsideConversation: 0,
isolatedDebugExcluded: true,
},
disclosure: {
openBefore: true,
openAfterExpansion: true,
openAfterTerminal: true,
autoReadable: true,
},
running: {
observed: true,
snapshot: { status: "running", traceId: "trc_fixture", rowCount: 2, sourceAuthorityRowCount: 2, projectedAuthorityRowCount: 0, rowSetHash: "sha256:running" },
},
terminal: {
observed: true,
stable: true,
snapshot: { status: "completed", traceId: "trc_fixture", disclosure: { open: true }, rowCount: 3, sourceAuthorityRowCount: 3, readableSourceRowCount: 3, projectedAuthorityRowCount: 0, rowSetHash: "sha256:terminal" },
},
runningRetention: {
mode: "identity-overlap",
passed: true,
retainedRowCount: 2,
},
retainedRunningRowCount: 2,
reportPath: "/tmp/trace-report.json",
reportSha256: "sha256:trace-report",
screenshot: { path: "/tmp/trace.png", sha256: "sha256:trace-image" },
},
}) + "\n");
const status = await runStatusScript(stateDir, commandId);
assert.equal(status.exactCommand.type, "validateWorkbenchTraceReadability");
assert.equal(status.exactCommand.result.scope.debugPanelCountInsideConversation, 0);
assert.equal(status.exactCommand.result.disclosure.openBefore, true);
assert.equal(status.exactCommand.result.running.snapshot.sourceAuthorityRowCount, 2);
assert.equal(status.exactCommand.result.terminal.snapshot.status, "completed");
assert.equal(status.exactCommand.result.terminal.snapshot.projectedAuthorityRowCount, 0);
assert.equal(status.exactCommand.result.runningRetention.mode, "identity-overlap");
assert.equal(status.exactCommand.result.runningRetention.passed, true);
assert.equal(status.exactCommand.result.retainedRunningRowCount, 2);
assert.equal(status.exactCommand.result.reportSha256, "sha256:trace-report");
assert.equal(status.exactCommand.result.screenshot.sha256, "sha256:trace-image");
});
+24 -2
View File
@@ -60,7 +60,29 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string,
const compactSample=(item)=>({seq:item.seq,ts:item.ts,path:item.path,routeSessionId:item.routeSessionId||null,activeSessionId:item.activeSessionId||null,messageCount:Array.isArray(item.messages)?item.messages.length:0,traceRowCount:Array.isArray(item.traceRows)?item.traceRows.length:0,loadingCount:Array.isArray(item.loadings)?item.loadings.length:0,loadingOwners:Array.isArray(item.loadings)?Array.from(new Set(item.loadings.map((loading)=>loading.ownerLabel||loading.ownerKind||loading.ownerKey||'loading'))).slice(0,4):[],sessionRail:item.sessionRail?{visibleCount:item.sessionRail.visibleCount??null,fallbackTitleCount:item.sessionRail.fallbackTitleCount??null,fallbackTitleRatio:item.sessionRail.fallbackTitleRatio??null,fallbackItems:Array.isArray(item.sessionRail.fallbackItems)?item.sessionRail.fallbackItems.slice(0,4).map((fallback)=>({sessionIdPrefix:fallback.sessionIdPrefix??null,titlePreview:fallback.titlePreview??null,active:fallback.active===true})):[]}:null});
const compactNetwork=(item)=>({ts:item.ts,type:item.type,method:item.method,url:short(item.url),status:item.status||null,failure:item.failure?short(item.failure):null});
const commandDir=(name)=>path.join(dir,'commands',name);
const compactReplayEvidence=(value)=>{const item=value&&typeof value==='object'?value:null;if(!item)return{};const layers=item.layerCounts&&typeof item.layerCounts==='object'?item.layerCounts:null;const server=layers&&layers.server&&typeof layers.server==='object'?layers.server:null;const client=layers&&layers.client&&typeof layers.client==='object'?layers.client:null;const trace=item.traceTimeline&&typeof item.traceTimeline==='object'?item.traceTimeline:null;const raw=item.rawHwlabEventWindow&&typeof item.rawHwlabEventWindow==='object'?item.rawHwlabEventWindow:null;const rawCounts=raw&&raw.counts&&typeof raw.counts==='object'?raw.counts:null;return{phase:item.phase||null,code:item.code||null,layerCounts:layers?{server:server?{scanned:server.scanned??null,matched:server.matched??null,delivered:server.delivered??null}:null,client:client?{received:client.received??null,decoded:client.decoded??null,applied:client.applied??null}:null,valuesRedacted:true}:null,traceTimeline:trace?{contract:trace.contract||null,authorityAvailable:trace.authorityAvailable??null,rowCount:trace.rowCount??null,sourceAuthorityRowCount:trace.sourceAuthorityRowCount??null,projectedAuthorityRowCount:trace.projectedAuthorityRowCount??null,readableSourceRowCount:trace.readableSourceRowCount??null,valuesRedacted:true}:null,rawHwlabEventWindow:raw?{available:raw.available??null,contract:raw.contract||null,reason:raw.reason||null,samePage:raw.samePage??null,productEventsPath:raw.productEventsPath||null,productEventSourceRequestCount:raw.productEventSourceRequestCount??null,sourceContractPresent:raw.sourceContractPresent??null,unfilteredContractPresent:raw.unfilteredContractPresent??null,textBytes:raw.textBytes??null,textHash:raw.textHash||null,textPresent:raw.textPresent??null,counts:rawCounts?{received:rawCounts.received??null,retained:rawCounts.retained??null,decoded:rawCounts.decoded??null,rejected:rawCounts.rejected??null,evicted:rawCounts.evicted??null,bytes:rawCounts.bytes??null}:null,valuesRedacted:true}:null}};
const compactReplayEvidence=(value)=>{
const item=value&&typeof value==='object'?value:null;if(!item)return{};
const layers=item.layerCounts&&typeof item.layerCounts==='object'?item.layerCounts:null;
const server=layers&&layers.server&&typeof layers.server==='object'?layers.server:null;
const client=layers&&layers.client&&typeof layers.client==='object'?layers.client:null;
const layerDom=item.layerDom&&typeof item.layerDom==='object'?item.layerDom:null;
const layerStable=item.layerDomStabilization&&typeof item.layerDomStabilization==='object'?item.layerDomStabilization:null;
const trace=item.traceTimeline&&typeof item.traceTimeline==='object'?item.traceTimeline:null;
const raw=item.rawHwlabEventWindow&&typeof item.rawHwlabEventWindow==='object'?item.rawHwlabEventWindow:null;
const rawCounts=raw&&raw.counts&&typeof raw.counts==='object'?raw.counts:null;
return{
phase:item.phase||null,code:item.code||null,
layerCounts:layers?{server:server?{scanned:server.scanned??null,matched:server.matched??null,delivered:server.delivered??null}:null,client:client?{received:client.received??null,decoded:client.decoded??null,applied:client.applied??null}:null,valuesRedacted:true}:null,
layerDom:layerDom?{matchCount:layerDom.matchCount??null,visibleCount:layerDom.visibleCount??null,connectedCount:layerDom.connectedCount??null,textPresent:layerDom.textPresent??null,textBytes:layerDom.textBytes??null,textHash:layerDom.textHash||null,textPreview:short(layerDom.textPreview),valuesRedacted:true}:null,
layerDomStabilization:layerStable?{stable:layerStable.stable??null,timedOut:layerStable.timedOut??null,sampleCount:layerStable.sampleCount??null,elapsedMs:layerStable.elapsedMs??null,stableQuietMs:layerStable.stableQuietMs??null,valuesRedacted:true}:null,
traceTimeline:trace?{contract:trace.contract||null,authorityAvailable:trace.authorityAvailable??null,rowCount:trace.rowCount??null,sourceAuthorityRowCount:trace.sourceAuthorityRowCount??null,projectedAuthorityRowCount:trace.projectedAuthorityRowCount??null,readableSourceRowCount:trace.readableSourceRowCount??null,valuesRedacted:true}:null,
rawHwlabEventWindow:raw?{available:raw.available??null,contract:raw.contract||null,reason:raw.reason||null,samePage:raw.samePage??null,productEventsPath:raw.productEventsPath||null,productEventSourceRequestCount:raw.productEventSourceRequestCount??null,sourceContractPresent:raw.sourceContractPresent??null,unfilteredContractPresent:raw.unfilteredContractPresent??null,textBytes:raw.textBytes??null,textHash:raw.textHash||null,textPresent:raw.textPresent??null,counts:rawCounts?{received:rawCounts.received??null,retained:rawCounts.retained??null,decoded:rawCounts.decoded??null,rejected:rawCounts.rejected??null,evicted:rawCounts.evicted??null,bytes:rawCounts.bytes??null}:null,valuesRedacted:true}:null
};
};
const compactTraceSnapshot=(value)=>{const item=value&&typeof value==='object'?value:null;if(!item)return null;const disclosure=item.disclosure&&typeof item.disclosure==='object'?item.disclosure:null;return{status:item.status||null,traceId:item.traceId||null,disclosure:disclosure?{available:disclosure.available??null,open:disclosure.open??null,eventCount:disclosure.eventCount??null,readableRowCount:disclosure.readableRowCount??null,renderedRowCount:disclosure.renderedRowCount??null,rowWindowed:disclosure.rowWindowed??null}:null,rowCount:item.rowCount??null,readableRowCount:item.readableRowCount??null,sourceAuthorityRowCount:item.sourceAuthorityRowCount??null,readableSourceRowCount:item.readableSourceRowCount??null,projectedAuthorityRowCount:item.projectedAuthorityRowCount??null,rowSetHash:item.rowSetHash||null,valuesRedacted:true};};
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 commandSummary=(name)=>{
const root=commandDir(name); let entries=[];
try{entries=fs.readdirSync(root).filter((item)=>item.endsWith('.json')).sort();}catch{}
@@ -75,7 +97,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),warmRunnerReused:result.warmRunnerReused??null,forbiddenRequestCount:result.forbiddenRequestCount??null,replayedEventCount:result.replayedEventCount??null,reportPath:result.reportPath||null,reportSha256:result.reportSha256||null,screenshot:result.screenshot&&typeof result.screenshot==='object'?{path:result.screenshot.path||null,sha256:result.screenshot.sha256||null,valuesRedacted:true}:null,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),traceId:error.details.traceId||null,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,reportPath:error.details.reportPath||null,reportSha256:error.details.reportSha256||null,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,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),...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),...compactTraceReadabilityEvidence(error.details),sessionId:error.details.sessionId||null,traceId:error.details.traceId||null,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,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:'not-found',ok:false,valuesRedacted:true};
};
@@ -0,0 +1,14 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { hwlabNodeWebProbeHelp } from "../hwlab-node-help";
test("web-probe help promotes reusable Workbench debug and product Trace commands", () => {
const help = hwlabNodeWebProbeHelp();
const examples = Array.isArray(help.examples) ? help.examples.join("\n") : "";
const notes = Array.isArray(help.notes) ? help.notes.join("\n") : "";
assert.match(examples, /--type validateWorkbenchKafkaDebugReplay/u);
assert.match(examples, /--type validateWorkbenchTraceReadability/u);
assert.match(notes, /validateWorkbenchTraceReadability/u);
assert.match(notes, //u);
});
@@ -5,7 +5,7 @@ import { join } from "node:path";
import { test } from "bun:test";
import { hwlabRuntimeLaneSpecForNode } from "../hwlab-node-lanes";
import { buildWebObserveCommandVisibility, nodeWebProbeRealtimeFanoutProfiles, nodeWebProbeWorkbenchKafkaDebugReplay, resolveWebObserveActionJson } from "./web-probe-observe-actions";
import { buildWebObserveCommandVisibility, nodeWebProbeRealtimeFanoutProfiles, nodeWebProbeWorkbenchKafkaDebugReplay, nodeWebProbeWorkbenchTraceReadability, resolveWebObserveActionJson } from "./web-probe-observe-actions";
test("realtime fanout runner contract derives topics, groups, and independent capabilities from owning YAML", () => {
const profiles = nodeWebProbeRealtimeFanoutProfiles(hwlabRuntimeLaneSpecForNode("v03", "NC01"));
@@ -42,6 +42,21 @@ test("Workbench Kafka debug replay runner contract is derived from owning YAML",
});
});
test("Workbench 产品 Trace 可读性 runner 合同完全来自独立 owning YAML", () => {
assert.deepEqual(nodeWebProbeWorkbenchTraceReadability(hwlabRuntimeLaneSpecForNode("v03", "NC01")), {
enabled: true,
runningCardTimeoutMs: 15_000,
disclosureActionTimeoutMs: 15_000,
disclosureOpenTimeoutMs: 10_000,
runningReadableTimeoutMs: 480_000,
terminalTimeoutMs: 480_000,
domStableQuietMs: 2_000,
domStableTimeoutMs: 6_000,
pollIntervalMs: 100,
valuesRedacted: true,
});
});
test("web observe command visibility does not equate control completion with async turn completion", () => {
const visibility = buildWebObserveCommandVisibility({
commandType: "sendPrompt",
@@ -64,6 +64,15 @@ export function nodeWebProbeWorkbenchKafkaDebugReplay(spec: HwlabRuntimeLaneSpec
};
}
export function nodeWebProbeWorkbenchTraceReadability(spec: HwlabRuntimeLaneSpec): Record<string, unknown> | null {
const config = spec.webProbe?.workbenchTraceReadability;
if (config === undefined) return null;
return {
...config,
valuesRedacted: true,
};
}
export function resolveWebObserveActionJson(
result: { stdout: string; stderr?: string; exitCode?: number | null; timedOut?: boolean },
contract: WebObserveActionJsonContract,
@@ -492,6 +501,7 @@ export function runNodeWebProbeObserveStart(
`UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON=${shellQuote(JSON.stringify(projectManagement))}`,
`UNIDESK_WEB_OBSERVE_REALTIME_FANOUT_PROFILES_JSON=${shellQuote(JSON.stringify(nodeWebProbeRealtimeFanoutProfiles(spec)))}`,
`UNIDESK_WEB_OBSERVE_WORKBENCH_KAFKA_DEBUG_REPLAY_JSON=${shellQuote(JSON.stringify(nodeWebProbeWorkbenchKafkaDebugReplay(spec)))}`,
`UNIDESK_WEB_OBSERVE_WORKBENCH_TRACE_READABILITY_JSON=${shellQuote(JSON.stringify(nodeWebProbeWorkbenchTraceReadability(spec)))}`,
...(authLogin === null
? []
: [
@@ -746,6 +756,9 @@ export function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOption
if (type === "validateWorkbenchKafkaDebugReplay" && spec.webProbe?.workbenchKafkaDebugReplay?.enabled !== true) {
throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
}
if (type === "validateWorkbenchTraceReadability" && nodeWebProbeWorkbenchTraceReadability(spec)?.enabled !== true) {
throw new Error("owning YAML 未启用 validateWorkbenchTraceReadability");
}
const commandId = `cmd-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
const payload = {
id: commandId,
@@ -77,6 +77,21 @@ test("validateWorkbenchKafkaDebugReplay is a YAML-enabled argument-free typed co
});
});
test("validateWorkbenchTraceReadability 是由独立 YAML 启用的无参数 typed command", () => {
const options = parseNodeWebProbeObserveOptions(
["command", "--type", "validateWorkbenchTraceReadability"],
"NC01",
"v03",
spec,
"webobs-fixture",
null,
);
assert.equal(options.commandType, "validateWorkbenchTraceReadability");
assert.equal(spec.webProbe?.workbenchTraceReadability?.terminalTimeoutMs, 480_000);
assert.equal(spec.webProbe?.workbenchTraceReadability?.runningCardTimeoutMs, 15_000);
});
test("observe start selects the YAML public origin semantically", () => {
const options = parseNodeWebProbeObserveOptions(
["start", "--origin", "public"],
+7 -1
View File
@@ -605,6 +605,11 @@ export function parseNodeWebProbeObserveOptions(
throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
}
}
if (observeActionRaw === "command" && commandType === "validateWorkbenchTraceReadability") {
if (spec.webProbe?.workbenchTraceReadability?.enabled !== true) {
throw new Error("owning YAML 未启用 validateWorkbenchTraceReadability");
}
}
return {
action: "observe",
observeAction: observeActionRaw,
@@ -701,6 +706,7 @@ export function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbe
|| value === "sendPrompt"
|| value === "validateRealtimeFanout"
|| value === "validateWorkbenchKafkaDebugReplay"
|| value === "validateWorkbenchTraceReadability"
|| value === "steer"
|| value === "cancel"
|| value === "selectProvider"
@@ -736,7 +742,7 @@ export function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbe
|| value === "mark"
|| value === "stop"
) return value;
throw new Error(`web-probe observe command --type must be login, loginAccount, logout, listSessions, switchSessions, preflight, goto, gotoProjectMdtodo, newSession, sendPrompt, validateRealtimeFanout, validateWorkbenchKafkaDebugReplay, steer, cancel, selectProvider, clickSession, refreshCurrentSession, switchAwayAndBack, assertSessionInvariant, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoReportPreview, toggleMdtodoReportFullscreen, openMdtodoSourceConfig, closeMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskInline, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, performanceCapture, screenshot, mark, or stop; got ${value}`);
throw new Error(`web-probe observe command --type must be login, loginAccount, logout, listSessions, switchSessions, preflight, goto, gotoProjectMdtodo, newSession, sendPrompt, validateRealtimeFanout, validateWorkbenchKafkaDebugReplay, validateWorkbenchTraceReadability, steer, cancel, selectProvider, clickSession, refreshCurrentSession, switchAwayAndBack, assertSessionInvariant, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoReportPreview, toggleMdtodoReportFullscreen, openMdtodoSourceConfig, closeMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskInline, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, performanceCapture, screenshot, mark, or stop; got ${value}`);
}
export function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrowserProxyMode {