feat: 沉淀 Workbench Kafka 隔离重放探针

This commit is contained in:
Codex
2026-07-10 13:51:05 +02:00
parent ea7bc0f444
commit 93abd9ba88
18 changed files with 490 additions and 10 deletions
+1
View File
@@ -139,6 +139,7 @@ export type NodeWebProbeObserveCommandType =
| "newSession"
| "sendPrompt"
| "validateRealtimeFanout"
| "validateWorkbenchKafkaDebugReplay"
| "steer"
| "cancel"
| "selectProvider"
@@ -66,6 +66,48 @@ test("observe status renderer exposes the exact command phase and failed report
assert.match(text, /terminal event arrived before subscriber disconnect/u);
});
test("observe status renderer exposes bounded Workbench Kafka debug replay evidence", () => {
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: 3, updatedAt: "2026-07-10T01:00:01.000Z" },
diagnostics: { effectiveLiveness: "alive", heartbeatStale: false },
commands: { pendingCount: 0, processingCount: 0, abandonedCount: 0, failedCount: 0 },
exactCommand: {
commandId: "cmd-debug-replay",
type: "validateWorkbenchKafkaDebugReplay",
phase: "done",
ok: true,
result: {
status: "passed",
traceId: "trc_fixture",
topic: "hwlab.event.debug.v1",
groupId: "hwlab-v03-workbench-isolated-debug-123-fixture",
receivedCount: 35,
appliedCount: 35,
terminalStatus: "completed",
reportSha256: "sha256:report",
screenshot: { sha256: "sha256:image" },
},
},
tails: { samples: [], control: [], network: [] },
},
next: {},
});
const text = String(rendered.renderedText);
assert.match(text, /Workbench Kafka debug replay:/u);
assert.match(text, /trc_fixture.*hwlab\.event\.debug\.v1/u);
assert.match(text, /35.*35.*completed.*sha256:image/u);
});
test("web-probe script render warns to promote repeated scripts into commands", () => {
const commandPromotionHint = {
schemaVersion: "unidesk.web-probe.command-promotion.v1",
@@ -68,3 +68,39 @@ test("observe status keeps exact command not-found structured", async () => {
valuesRedacted: true,
});
});
test("observe status preserves bounded Workbench Kafka debug replay evidence", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-debug-replay-status-"));
const commandId = "cmd-debug-replay";
await mkdir(join(stateDir, "commands", "done"), { recursive: true });
await writeFile(join(stateDir, "commands", "done", `${commandId}.json`), JSON.stringify({
ok: true,
commandId,
type: "validateWorkbenchKafkaDebugReplay",
completedAt: "2026-07-10T12:00:00.000Z",
result: {
ok: true,
status: "passed",
sessionId: "ses_fixture",
traceId: "trc_fixture",
topic: "hwlab.event.debug.v1",
groupId: "hwlab-v03-workbench-isolated-debug-123-fixture",
groupPrefix: "hwlab-v03-workbench-isolated-debug",
receivedCount: 35,
appliedCount: 35,
terminalStatus: "completed",
reportPath: "/tmp/report.json",
reportSha256: "sha256:report",
screenshot: { path: "/tmp/replay.png", sha256: "sha256:image" },
},
}) + "\n");
const status = await runStatusScript(stateDir, commandId);
assert.equal(status.exactCommand.type, "validateWorkbenchKafkaDebugReplay");
assert.equal(status.exactCommand.result.traceId, "trc_fixture");
assert.equal(status.exactCommand.result.topic, "hwlab.event.debug.v1");
assert.equal(status.exactCommand.result.groupPrefix, "hwlab-v03-workbench-isolated-debug");
assert.equal(status.exactCommand.result.receivedCount, 35);
assert.equal(status.exactCommand.result.appliedCount, 35);
assert.equal(status.exactCommand.result.screenshot.sha256, "sha256:image");
});
@@ -74,7 +74,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,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):[],warmRunnerReused:result.warmRunnerReused??null,forbiddenRequestCount:result.forbiddenRequestCount??null,replayedEventCount:result.replayedEventCount??null,reportPath:result.reportPath||null,reportSha256:result.reportSha256||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,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,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,reportPath:error.details.reportPath||null,reportSha256:error.details.reportSha256||null,valuesRedacted:true}:null}:null,valuesRedacted:true};
}
return {commandId:exactCommandId,phase:'not-found',ok:false,valuesRedacted:true};
};
@@ -5,7 +5,7 @@ import { join } from "node:path";
import { test } from "bun:test";
import { hwlabRuntimeLaneSpecForNode } from "../hwlab-node-lanes";
import { buildWebObserveCommandVisibility, nodeWebProbeRealtimeFanoutProfiles, resolveWebObserveActionJson } from "./web-probe-observe-actions";
import { buildWebObserveCommandVisibility, nodeWebProbeRealtimeFanoutProfiles, nodeWebProbeWorkbenchKafkaDebugReplay, 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"));
@@ -29,6 +29,16 @@ test("realtime fanout runner contract derives topics, groups, and independent ca
});
});
test("Workbench Kafka debug replay runner contract is derived from owning YAML", () => {
assert.deepEqual(nodeWebProbeWorkbenchKafkaDebugReplay(hwlabRuntimeLaneSpecForNode("v03", "NC01")), {
enabled: true,
topic: "hwlab.event.debug.v1",
groupPrefix: "hwlab-v03-workbench-isolated-debug",
completionTimeoutMs: 30_000,
valuesRedacted: true,
});
});
test("web observe command visibility does not equate control completion with async turn completion", () => {
const visibility = buildWebObserveCommandVisibility({
commandType: "sendPrompt",
@@ -48,6 +48,12 @@ export function nodeWebProbeRealtimeFanoutProfiles(spec: HwlabRuntimeLaneSpec):
}));
}
export function nodeWebProbeWorkbenchKafkaDebugReplay(spec: HwlabRuntimeLaneSpec): Record<string, unknown> | null {
const config = spec.webProbe?.workbenchKafkaDebugReplay;
if (config === undefined) return null;
return { ...config, valuesRedacted: true };
}
export function resolveWebObserveActionJson(
result: { stdout: string; stderr?: string; exitCode?: number | null; timedOut?: boolean },
contract: WebObserveActionJsonContract,
@@ -475,6 +481,7 @@ export function runNodeWebProbeObserveStart(
`UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON=${shellQuote(JSON.stringify(browserFreezePolicy))}`,
`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)))}`,
...(authLogin === null
? []
: [
@@ -726,6 +733,9 @@ export function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOption
if (!options.commandText?.trim()) throw new Error("validateRealtimeFanout requires --text or --text-stdin for the first turn");
if (!options.commandSecondText?.trim()) throw new Error("validateRealtimeFanout requires --second-text for the follow-up turn");
}
if (type === "validateWorkbenchKafkaDebugReplay" && spec.webProbe?.workbenchKafkaDebugReplay?.enabled !== true) {
throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
}
const commandId = `cmd-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
const payload = {
id: commandId,
@@ -58,6 +58,25 @@ test("validateRealtimeFanout rejects profiles absent from the owning YAML", () =
);
});
test("validateWorkbenchKafkaDebugReplay is a YAML-enabled argument-free typed command", () => {
const options = parseNodeWebProbeObserveOptions(
["command", "--type", "validateWorkbenchKafkaDebugReplay"],
"NC01",
"v03",
spec,
"webobs-fixture",
null,
);
assert.equal(options.commandType, "validateWorkbenchKafkaDebugReplay");
assert.deepEqual(spec.webProbe?.workbenchKafkaDebugReplay, {
enabled: true,
topic: "hwlab.event.debug.v1",
groupPrefix: "hwlab-v03-workbench-isolated-debug",
completionTimeoutMs: 30_000,
});
});
test("observe start selects the YAML public origin semantically", () => {
const options = parseNodeWebProbeObserveOptions(
["start", "--origin", "public"],
+7 -1
View File
@@ -600,6 +600,11 @@ export function parseNodeWebProbeObserveOptions(
if (!commandText?.trim()) throw new Error("validateRealtimeFanout requires --text or --text-stdin for the first turn");
if (!commandSecondText?.trim()) throw new Error("validateRealtimeFanout requires --second-text for the follow-up turn");
}
if (observeActionRaw === "command" && commandType === "validateWorkbenchKafkaDebugReplay") {
if (spec.webProbe?.workbenchKafkaDebugReplay?.enabled !== true) {
throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
}
}
return {
action: "observe",
observeAction: observeActionRaw,
@@ -695,6 +700,7 @@ export function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbe
|| value === "newSession"
|| value === "sendPrompt"
|| value === "validateRealtimeFanout"
|| value === "validateWorkbenchKafkaDebugReplay"
|| value === "steer"
|| value === "cancel"
|| value === "selectProvider"
@@ -730,7 +736,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, 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, 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 {