fix: 强化 Workbench Kafka 重放探针
This commit is contained in:
@@ -41,6 +41,7 @@ function renderWebObserveStatusTable(value: Record<string, unknown>): string {
|
||||
const exactResult = record(exactCommand?.result);
|
||||
const exactError = record(exactCommand?.error);
|
||||
const exactErrorDetails = record(exactError?.details);
|
||||
const exactReplayEvidence = exactResult ?? exactErrorDetails;
|
||||
const tails = record(observer?.tails);
|
||||
const result = record(value.result);
|
||||
const samples = Array.isArray(tails?.samples) ? tails.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-5) : [];
|
||||
@@ -175,17 +176,12 @@ function renderWebObserveStatusTable(value: Record<string, unknown>): string {
|
||||
]]),
|
||||
"",
|
||||
] : []),
|
||||
...(exactCommand.type === "validateWorkbenchKafkaDebugReplay" && exactResult !== null ? [
|
||||
"Workbench Kafka debug replay:",
|
||||
webObserveTable(["TRACE", "TOPIC", "GROUP", "RECEIVED", "APPLIED", "TERMINAL", "SCREENSHOT_SHA"], [[
|
||||
webObserveShort(webObserveText(exactResult.traceId), 28),
|
||||
webObserveShort(webObserveText(exactResult.topic), 32),
|
||||
webObserveShort(webObserveText(exactResult.groupId), 48),
|
||||
exactResult.receivedCount,
|
||||
exactResult.appliedCount,
|
||||
exactResult.terminalStatus,
|
||||
webObserveShort(webObserveText(record(exactResult.screenshot)?.sha256), 32),
|
||||
]]),
|
||||
...(exactCommand.type === "validateWorkbenchKafkaDebugReplay" && exactReplayEvidence !== null ? [
|
||||
...renderWorkbenchKafkaDebugReplayEvidence(
|
||||
exactReplayEvidence,
|
||||
exactResult?.reportSha256 ?? exactErrorDetails?.reportSha256,
|
||||
record(exactResult?.screenshot)?.sha256,
|
||||
),
|
||||
"",
|
||||
] : []),
|
||||
"Recent samples:",
|
||||
@@ -232,6 +228,58 @@ function renderWebObserveStatusTable(value: Record<string, unknown>): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderWorkbenchKafkaDebugReplayEvidence(
|
||||
evidence: Record<string, unknown>,
|
||||
reportSha: unknown,
|
||||
screenshotSha: unknown,
|
||||
): string[] {
|
||||
const layers = record(evidence.layerCounts);
|
||||
const server = record(layers?.server);
|
||||
const client = record(layers?.client);
|
||||
const trace = record(evidence.traceTimeline);
|
||||
const raw = record(evidence.rawHwlabEventWindow);
|
||||
const rawCounts = record(raw?.counts);
|
||||
return [
|
||||
"Workbench Kafka debug replay:",
|
||||
webObserveTable(["TRACE", "TOPIC", "GROUP", "PHASE", "CODE", "RECEIVED", "APPLIED", "TERMINAL", "REPORT_SHA", "SCREENSHOT_SHA"], [[
|
||||
webObserveShort(webObserveText(evidence.traceId), 28),
|
||||
webObserveShort(webObserveText(evidence.topic), 32),
|
||||
webObserveShort(webObserveText(evidence.groupId), 48),
|
||||
evidence.phase,
|
||||
evidence.code,
|
||||
evidence.receivedCount,
|
||||
evidence.appliedCount,
|
||||
evidence.terminalStatus,
|
||||
webObserveShort(webObserveText(reportSha), 32),
|
||||
webObserveShort(webObserveText(screenshotSha), 32),
|
||||
]]),
|
||||
"Replay layers:",
|
||||
webObserveTable(["SERVER_SCANNED", "SERVER_MATCHED", "SERVER_DELIVERED", "CLIENT_RECEIVED", "CLIENT_DECODED", "CLIENT_APPLIED"], [[
|
||||
server?.scanned,
|
||||
server?.matched,
|
||||
server?.delivered,
|
||||
client?.received,
|
||||
client?.decoded,
|
||||
client?.applied,
|
||||
]]),
|
||||
"Trace and raw HWLAB Event:",
|
||||
webObserveTable(["AUTHORITY", "SOURCE_ROWS", "READABLE_ROWS", "RAW_AVAILABLE", "SAME_PAGE", "UNFILTERED", "RAW_RECEIVED", "RAW_RETAINED", "RAW_REJECTED", "RAW_EVICTED", "RAW_BYTES", "EXTRA_PRODUCT_SSE"], [[
|
||||
trace?.contract,
|
||||
trace?.sourceAuthorityRowCount,
|
||||
trace?.readableSourceRowCount,
|
||||
raw?.available,
|
||||
raw?.samePage,
|
||||
raw?.unfilteredContractPresent,
|
||||
rawCounts?.received,
|
||||
rawCounts?.retained,
|
||||
rawCounts?.rejected,
|
||||
rawCounts?.evicted,
|
||||
rawCounts?.bytes,
|
||||
raw?.productEventSourceRequestCount,
|
||||
]]),
|
||||
];
|
||||
}
|
||||
|
||||
function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||||
const observerCommand = record(value.observerCommand);
|
||||
const observer = record(value.observer);
|
||||
@@ -241,6 +289,7 @@ function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||||
const resultScreenshot = record(result?.screenshot);
|
||||
const error = record(observer?.error);
|
||||
const details = record(error?.details);
|
||||
const replayEvidence = result ?? details;
|
||||
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);
|
||||
@@ -259,19 +308,13 @@ function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||||
webObserveShort(webObserveText(asyncTurn?.traceId), 24),
|
||||
webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80),
|
||||
]]),
|
||||
...(observerCommand?.type === "validateWorkbenchKafkaDebugReplay" && result !== null ? [
|
||||
...(observerCommand?.type === "validateWorkbenchKafkaDebugReplay" && replayEvidence !== null ? [
|
||||
"",
|
||||
"Workbench Kafka debug replay:",
|
||||
webObserveTable(["TRACE", "TOPIC", "GROUP", "RECEIVED", "APPLIED", "TERMINAL", "REPORT_SHA", "SCREENSHOT_SHA"], [[
|
||||
webObserveShort(webObserveText(result.traceId), 28),
|
||||
webObserveShort(webObserveText(result.topic), 32),
|
||||
webObserveShort(webObserveText(result.groupId), 48),
|
||||
result.receivedCount,
|
||||
result.appliedCount,
|
||||
result.terminalStatus,
|
||||
webObserveShort(webObserveText(result.reportSha256), 32),
|
||||
webObserveShort(webObserveText(resultScreenshot?.sha256), 32),
|
||||
]]),
|
||||
...renderWorkbenchKafkaDebugReplayEvidence(
|
||||
replayEvidence,
|
||||
result?.reportSha256 ?? details?.reportSha256,
|
||||
resultScreenshot?.sha256,
|
||||
),
|
||||
] : []),
|
||||
...(readiness !== null ? [
|
||||
"",
|
||||
|
||||
@@ -6,9 +6,15 @@ import { nodeWebObserveRunnerSource } from "./hwlab-node-web-observe-runner-sour
|
||||
|
||||
test("Workbench Kafka debug replay source parses the YAML contract and reducer counts", () => {
|
||||
const source = nodeWebObserveRunnerKafkaDebugReplaySource();
|
||||
const build = new Function(`${source}\nreturn { parseWorkbenchKafkaDebugReplayConfig, workbenchKafkaDebugReplayCounts };`) as () => {
|
||||
const build = new Function(`${source}\nreturn { parseWorkbenchKafkaDebugReplayConfig, workbenchKafkaDebugReplayCounts, workbenchKafkaDebugReplayLayerCounts, workbenchKafkaDebugReplayRawCounts, workbenchKafkaDebugReplayTypedOutcome, workbenchKafkaDebugReplayValidateLayerCounts, workbenchKafkaDebugReplayValidateTraceTimeline, workbenchKafkaDebugReplayValidateRawWindow };`) as () => {
|
||||
parseWorkbenchKafkaDebugReplayConfig: (raw: string) => Record<string, unknown>;
|
||||
workbenchKafkaDebugReplayCounts: (text: string) => Record<string, number> | null;
|
||||
workbenchKafkaDebugReplayLayerCounts: (text: string) => Record<string, unknown> | null;
|
||||
workbenchKafkaDebugReplayRawCounts: (text: string) => Record<string, unknown> | null;
|
||||
workbenchKafkaDebugReplayTypedOutcome: (input: Record<string, unknown>) => Record<string, unknown>;
|
||||
workbenchKafkaDebugReplayValidateLayerCounts: (counts: Record<string, unknown> | null, evidence: Record<string, unknown>) => void;
|
||||
workbenchKafkaDebugReplayValidateTraceTimeline: (timeline: Record<string, unknown>, evidence: Record<string, unknown>) => void;
|
||||
workbenchKafkaDebugReplayValidateRawWindow: (window: Record<string, unknown>, evidence: Record<string, unknown>) => void;
|
||||
};
|
||||
const helpers = build();
|
||||
assert.deepEqual(helpers.parseWorkbenchKafkaDebugReplayConfig(JSON.stringify({
|
||||
@@ -16,15 +22,101 @@ test("Workbench Kafka debug replay source parses the YAML contract and reducer c
|
||||
topic: "hwlab.event.debug.v1",
|
||||
groupPrefix: "hwlab-v03-workbench-isolated-debug",
|
||||
completionTimeoutMs: 30_000,
|
||||
productEventsPath: "/v1/workbench/events",
|
||||
debugEventsPath: "/v1/workbench/debug/kafka-sse/events",
|
||||
requestObservationQuietMs: 3_000,
|
||||
})), {
|
||||
enabled: true,
|
||||
topic: "hwlab.event.debug.v1",
|
||||
groupPrefix: "hwlab-v03-workbench-isolated-debug",
|
||||
completionTimeoutMs: 30_000,
|
||||
productEventsPath: "/v1/workbench/events",
|
||||
debugEventsPath: "/v1/workbench/debug/kafka-sse/events",
|
||||
requestObservationQuietMs: 3_000,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
assert.deepEqual(helpers.workbenchKafkaDebugReplayCounts("35/35 applied"), { appliedCount: 35, receivedCount: 35 });
|
||||
assert.equal(helpers.workbenchKafkaDebugReplayCounts("等待重放"), null);
|
||||
assert.deepEqual(helpers.workbenchKafkaDebugReplayLayerCounts(
|
||||
"server scanned=27 · matched=2 · delivered=2 client local overlay received=2 · decoded=2 · applied=2",
|
||||
), {
|
||||
server: { scanned: 27, matched: 2, delivered: 2 },
|
||||
client: { received: 2, decoded: 2, applied: 2 },
|
||||
valuesRedacted: true,
|
||||
});
|
||||
assert.deepEqual(helpers.workbenchKafkaDebugReplayRawCounts(
|
||||
"received=34 · retained=30 · decoded=33 · rejected=1 · evicted=4 · bytes=8192",
|
||||
), {
|
||||
received: 34,
|
||||
retained: 30,
|
||||
decoded: 33,
|
||||
rejected: 1,
|
||||
evicted: 4,
|
||||
bytes: 8192,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
assert.deepEqual(helpers.workbenchKafkaDebugReplayTypedOutcome({ status: "completed", layerCounts: { server: {} } }), {
|
||||
phase: "terminal",
|
||||
code: "terminal_complete",
|
||||
source: "typed-dom-v2",
|
||||
valuesRedacted: true,
|
||||
});
|
||||
assert.deepEqual(helpers.workbenchKafkaDebugReplayTypedOutcome({ status: "incomplete", errorText: "records_scanned_but_filter_mismatch: no replayId match" }), {
|
||||
phase: "server",
|
||||
code: "records_scanned_but_filter_mismatch",
|
||||
source: "legacy-dom",
|
||||
valuesRedacted: true,
|
||||
});
|
||||
const evidence = {
|
||||
phase: "terminal",
|
||||
code: "terminal_complete",
|
||||
traceId: "trc_fixture",
|
||||
legacyCounts: { receivedCount: 2, appliedCount: 2 },
|
||||
};
|
||||
assert.throws(
|
||||
() => helpers.workbenchKafkaDebugReplayValidateLayerCounts(null, evidence),
|
||||
/typed server\/client count contract is missing/u,
|
||||
);
|
||||
assert.throws(
|
||||
() => helpers.workbenchKafkaDebugReplayValidateTraceTimeline({ authorityAvailable: false }, evidence),
|
||||
/source-authority contract is missing/u,
|
||||
);
|
||||
assert.throws(
|
||||
() => helpers.workbenchKafkaDebugReplayValidateRawWindow({ available: false }, evidence),
|
||||
/raw HWLAB Event window contract is missing/u,
|
||||
);
|
||||
assert.throws(
|
||||
() => helpers.workbenchKafkaDebugReplayValidateRawWindow({
|
||||
available: true,
|
||||
samePage: true,
|
||||
sourceContractPresent: true,
|
||||
unfilteredContractPresent: true,
|
||||
productEventSourceRequestCount: 0,
|
||||
textPresent: false,
|
||||
textBytes: 0,
|
||||
counts: { received: 0, retained: 0, decoded: 0, rejected: 0, evicted: 0, bytes: 0 },
|
||||
}, evidence),
|
||||
/did not retain a readable product SSE frame/u,
|
||||
);
|
||||
assert.doesNotThrow(() => helpers.workbenchKafkaDebugReplayValidateLayerCounts({
|
||||
server: { scanned: 27, matched: 2, delivered: 2 },
|
||||
client: { received: 2, decoded: 2, applied: 2 },
|
||||
}, evidence));
|
||||
assert.doesNotThrow(() => helpers.workbenchKafkaDebugReplayValidateTraceTimeline({
|
||||
authorityAvailable: true,
|
||||
sourceAuthorityRowCount: 2,
|
||||
readableSourceRowCount: 2,
|
||||
}, evidence));
|
||||
assert.doesNotThrow(() => helpers.workbenchKafkaDebugReplayValidateRawWindow({
|
||||
available: true,
|
||||
samePage: true,
|
||||
sourceContractPresent: true,
|
||||
unfilteredContractPresent: true,
|
||||
productEventSourceRequestCount: 0,
|
||||
textPresent: true,
|
||||
textBytes: 256,
|
||||
counts: { received: 2, retained: 2, decoded: 2, rejected: 0, evicted: 0, bytes: 256 },
|
||||
}, evidence));
|
||||
});
|
||||
|
||||
test("generated observer runner contains the typed debug replay command and passes node syntax check", async () => {
|
||||
@@ -33,6 +125,10 @@ test("generated observer runner contains the typed debug replay command and pass
|
||||
assert.match(source, /workbench-kafka-debug-toggle/u);
|
||||
assert.match(source, /workbench-kafka-debug-replay/u);
|
||||
assert.match(source, /workbench-kafka-debug-counts/u);
|
||||
assert.match(source, /workbench-kafka-debug-layer-counts/u);
|
||||
assert.match(source, /workbench-kafka-debug-tab-raw/u);
|
||||
assert.match(source, /workbench-raw-hwlab-counts/u);
|
||||
assert.match(source, /data-event-seq-authority/u);
|
||||
const child = Bun.spawn(["node", "--input-type=module", "--check", "-"], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
||||
child.stdin.write(source);
|
||||
child.stdin.end();
|
||||
|
||||
@@ -16,13 +16,34 @@ function parseWorkbenchKafkaDebugReplayConfig(raw) {
|
||||
}
|
||||
const topic = String(parsed.topic || "").trim();
|
||||
const groupPrefix = String(parsed.groupPrefix || "").trim();
|
||||
const productEventsPath = String(parsed.productEventsPath || "").trim();
|
||||
const debugEventsPath = String(parsed.debugEventsPath || "").trim();
|
||||
const completionTimeoutMs = Number(parsed.completionTimeoutMs);
|
||||
const requestObservationQuietMs = Number(parsed.requestObservationQuietMs);
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(topic)) throw new Error("Workbench Kafka debug replay topic is missing or invalid in the owning YAML");
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(groupPrefix)) throw new Error("Workbench Kafka debug replay groupPrefix is missing or invalid in the owning YAML");
|
||||
if (!productEventsPath.startsWith("/") || productEventsPath.includes("?") || productEventsPath.includes("#")) {
|
||||
throw new Error("Workbench Kafka debug replay productEventsPath must be an absolute path without query or fragment in the owning YAML");
|
||||
}
|
||||
if (!debugEventsPath.startsWith("/") || debugEventsPath.includes("?") || debugEventsPath.includes("#")) {
|
||||
throw new Error("Workbench Kafka debug replay debugEventsPath must be an absolute path without query or fragment in the owning YAML");
|
||||
}
|
||||
if (!Number.isInteger(completionTimeoutMs) || completionTimeoutMs < 1000 || completionTimeoutMs > 120000) {
|
||||
throw new Error("Workbench Kafka debug replay completionTimeoutMs must be 1000-120000 in the owning YAML");
|
||||
}
|
||||
return { enabled: parsed.enabled === true, topic, groupPrefix, completionTimeoutMs, valuesRedacted: true };
|
||||
if (!Number.isInteger(requestObservationQuietMs) || requestObservationQuietMs < 100 || requestObservationQuietMs > 30000) {
|
||||
throw new Error("Workbench Kafka debug replay requestObservationQuietMs must be 100-30000 in the owning YAML");
|
||||
}
|
||||
return {
|
||||
enabled: parsed.enabled === true,
|
||||
topic,
|
||||
groupPrefix,
|
||||
productEventsPath,
|
||||
debugEventsPath,
|
||||
completionTimeoutMs,
|
||||
requestObservationQuietMs,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayCounts(value) {
|
||||
@@ -30,32 +51,114 @@ function workbenchKafkaDebugReplayCounts(value) {
|
||||
return match ? { appliedCount: Number(match[1]), receivedCount: Number(match[2]) } : null;
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayNamedCount(value, name) {
|
||||
const match = String(value || "").match(new RegExp("(?:^|\\s|·)" + name + "\\s*=\\s*(-|\\d+)", "iu"));
|
||||
if (!match) return undefined;
|
||||
return match[1] === "-" ? null : Number(match[1]);
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayLayerCounts(value) {
|
||||
const text = String(value || "").replace(/\s+/gu, " ").trim();
|
||||
if (!/\bserver\b/iu.test(text) || !/\bclient\b/iu.test(text)) return null;
|
||||
return {
|
||||
server: {
|
||||
scanned: workbenchKafkaDebugReplayNamedCount(text, "scanned"),
|
||||
matched: workbenchKafkaDebugReplayNamedCount(text, "matched"),
|
||||
delivered: workbenchKafkaDebugReplayNamedCount(text, "delivered")
|
||||
},
|
||||
client: {
|
||||
received: workbenchKafkaDebugReplayNamedCount(text, "received"),
|
||||
decoded: workbenchKafkaDebugReplayNamedCount(text, "decoded"),
|
||||
applied: workbenchKafkaDebugReplayNamedCount(text, "applied")
|
||||
},
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayRawCounts(value) {
|
||||
const text = String(value || "").replace(/\s+/gu, " ").trim();
|
||||
if (!/\breceived\s*=/iu.test(text) || !/\bretained\s*=/iu.test(text)) return null;
|
||||
return {
|
||||
received: workbenchKafkaDebugReplayNamedCount(text, "received"),
|
||||
retained: workbenchKafkaDebugReplayNamedCount(text, "retained"),
|
||||
decoded: workbenchKafkaDebugReplayNamedCount(text, "decoded"),
|
||||
rejected: workbenchKafkaDebugReplayNamedCount(text, "rejected"),
|
||||
evicted: workbenchKafkaDebugReplayNamedCount(text, "evicted"),
|
||||
bytes: workbenchKafkaDebugReplayNamedCount(text, "bytes"),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayTypedOutcome(input) {
|
||||
const status = String(input?.status || "unknown").trim() || "unknown";
|
||||
const errorText = String(input?.errorText || "").trim();
|
||||
const source = input?.layerCounts ? "typed-dom-v2" : "legacy-dom";
|
||||
const codeMatch = errorText.match(/\b(consumer_not_ready|source_trace_missing|producer_not_invoked|topic_no_append_for_replay|record_parse_rejected|records_scanned_but_filter_mismatch|sse_write_failed|transport_failed|decoder_rejected|reducer_rejected|terminal_missing)\b/u);
|
||||
const phaseByCode = {
|
||||
consumer_not_ready: "consumer",
|
||||
source_trace_missing: "producer",
|
||||
producer_not_invoked: "producer",
|
||||
topic_no_append_for_replay: "kafka",
|
||||
record_parse_rejected: "server",
|
||||
records_scanned_but_filter_mismatch: "server",
|
||||
sse_write_failed: "server",
|
||||
transport_failed: "client",
|
||||
decoder_rejected: "client",
|
||||
reducer_rejected: "client",
|
||||
terminal_missing: "terminal"
|
||||
};
|
||||
if (codeMatch) return { phase: phaseByCode[codeMatch[1]], code: codeMatch[1], source, valuesRedacted: true };
|
||||
if (status === "completed") return { phase: "terminal", code: "terminal_complete", source, valuesRedacted: true };
|
||||
if (status === "incomplete" && /terminal|终态|未观测/iu.test(errorText)) {
|
||||
return { phase: "terminal", code: "terminal_missing", source, valuesRedacted: true };
|
||||
}
|
||||
if (status === "incomplete") return { phase: "ui", code: "replay_incomplete", source, valuesRedacted: true };
|
||||
if (status === "error") return { phase: "ui", code: "replay_error", source, valuesRedacted: true };
|
||||
if (status === "closed") return { phase: "client", code: "stream_closed", source, valuesRedacted: true };
|
||||
return { phase: "ui", code: "replay_" + status.replace(/[^a-z0-9_-]+/giu, "_"), source, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function validateWorkbenchKafkaDebugReplay(command) {
|
||||
const config = workbenchKafkaDebugReplay;
|
||||
if (!config || config.enabled !== true) throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
|
||||
const reportDir = path.join(stateDir, "artifacts", "workbench-kafka-debug-replay", safeId(command.id));
|
||||
const report = {
|
||||
ok: false,
|
||||
contract: "web-probe-workbench-kafka-debug-replay-v1",
|
||||
contract: "web-probe-workbench-kafka-debug-replay-v2",
|
||||
commandId: command.id,
|
||||
expected: { topic: config.topic, groupPrefix: config.groupPrefix, completionTimeoutMs: config.completionTimeoutMs, valuesRedacted: true },
|
||||
expected: {
|
||||
topic: config.topic,
|
||||
groupPrefix: config.groupPrefix,
|
||||
productEventsPath: config.productEventsPath,
|
||||
debugEventsPath: config.debugEventsPath,
|
||||
completionTimeoutMs: config.completionTimeoutMs,
|
||||
requestObservationQuietMs: config.requestObservationQuietMs,
|
||||
valuesRedacted: true
|
||||
},
|
||||
origin,
|
||||
startedAt: new Date().toISOString(),
|
||||
valuesRedacted: true,
|
||||
valuesRedacted: true
|
||||
};
|
||||
const requests = [];
|
||||
const productEventSourceRequests = [];
|
||||
let requestPage = null;
|
||||
let requestPhase = "setup";
|
||||
const requestListener = (request) => {
|
||||
let url;
|
||||
try { url = new URL(request.url()); } catch { return; }
|
||||
if (url.pathname !== "/v1/workbench/debug/kafka-sse/events") return;
|
||||
if (url.pathname === config.productEventsPath) {
|
||||
productEventSourceRequests.push({ method: request.method(), path: url.pathname, phase: requestPhase, valuesRedacted: true });
|
||||
return;
|
||||
}
|
||||
if (url.pathname !== config.debugEventsPath) return;
|
||||
requests.push({
|
||||
method: request.method(),
|
||||
path: url.pathname,
|
||||
stream: url.searchParams.get("stream"),
|
||||
traceId: url.searchParams.get("traceId"),
|
||||
fromBeginning: url.searchParams.get("fromBeginning"),
|
||||
valuesRedacted: true,
|
||||
phase: requestPhase,
|
||||
valuesRedacted: true
|
||||
});
|
||||
};
|
||||
try {
|
||||
@@ -66,6 +169,8 @@ async function validateWorkbenchKafkaDebugReplay(command) {
|
||||
const sessionId = snapshot?.activeSessionId || snapshot?.routeSessionId || routeSessionIdFromUrl(currentPageUrl());
|
||||
if (!sessionId) throw new Error("validateWorkbenchKafkaDebugReplay requires an existing Workbench session");
|
||||
|
||||
requestPage = page;
|
||||
requestPage.on("request", requestListener);
|
||||
const toggle = page.locator('[data-testid="workbench-kafka-debug-toggle"]').first();
|
||||
const toggleVisible = await toggle.waitFor({ state: "visible", timeout: 10000 }).then(() => true).catch(() => false);
|
||||
if (!toggleVisible) throw new Error("Workbench Kafka debug toggle is not visible; verify the owning YAML capability and admin session");
|
||||
@@ -88,45 +193,91 @@ async function validateWorkbenchKafkaDebugReplay(command) {
|
||||
if (!/^trc_[A-Za-z0-9_-]+$/u.test(before.traceId || "")) throw new Error("Workbench Kafka debug replay requires the current traceId");
|
||||
if (await replayButton.isDisabled()) throw new Error("Workbench Kafka debug replay button is disabled for traceId " + before.traceId);
|
||||
|
||||
requestPage = page;
|
||||
requestPage.on("request", requestListener);
|
||||
requestPhase = "debug-replay";
|
||||
await replayButton.click({ timeout: 10000 });
|
||||
const terminalStatus = await workbenchKafkaDebugReplayWaitForTerminal(panel, config.completionTimeoutMs);
|
||||
if (terminalStatus !== "completed") throw new Error("Workbench Kafka debug replay ended with status " + terminalStatus);
|
||||
const after = await workbenchKafkaDebugReplayPanelSnapshot(panel, countsNode);
|
||||
Object.assign(report, {
|
||||
sessionId,
|
||||
traceId: after.traceId,
|
||||
topic: after.topic,
|
||||
groupId: after.groupId,
|
||||
terminalStatus,
|
||||
phase: after.typedOutcome.phase,
|
||||
code: after.typedOutcome.code,
|
||||
typedOutcome: after.typedOutcome,
|
||||
layerCounts: after.layerCounts,
|
||||
legacyCounts: after.counts,
|
||||
valuesRedacted: true
|
||||
});
|
||||
const traceTimeline = await workbenchKafkaDebugReplayTraceTimelineSnapshot(panel);
|
||||
report.traceTimeline = traceTimeline;
|
||||
const rawHwlabEventWindow = await workbenchKafkaDebugReplayInspectRawWindow({
|
||||
panel,
|
||||
config,
|
||||
pathBefore: currentPath,
|
||||
productEventSourceRequests,
|
||||
setRequestPhase: (value) => { requestPhase = value; }
|
||||
});
|
||||
report.rawHwlabEventWindow = rawHwlabEventWindow;
|
||||
const matchingRequest = requests.find((item) => item.stream === "hwlab-debug" && item.traceId === before.traceId && item.fromBeginning === "true") || null;
|
||||
if (!matchingRequest) throw new Error("Workbench Kafka debug replay did not issue the expected trace-scoped fromBeginning request");
|
||||
if (after.traceId !== before.traceId) throw new Error("Workbench Kafka debug replay traceId changed during replay");
|
||||
if (after.topic !== config.topic) throw new Error("Workbench Kafka debug replay topic mismatch: " + (after.topic || "missing"));
|
||||
const replayEvidence = {
|
||||
sessionId,
|
||||
traceId: after.traceId,
|
||||
topic: after.topic,
|
||||
groupId: after.groupId,
|
||||
terminalStatus,
|
||||
phase: after.typedOutcome.phase,
|
||||
code: after.typedOutcome.code,
|
||||
typedOutcome: after.typedOutcome,
|
||||
layerCounts: after.layerCounts,
|
||||
legacyCounts: after.counts,
|
||||
traceTimeline,
|
||||
rawHwlabEventWindow,
|
||||
productEventSource: {
|
||||
path: config.productEventsPath,
|
||||
additionalRequestCountDuringTabSwitch: rawHwlabEventWindow.productEventSourceRequestCount,
|
||||
requests: productEventSourceRequests.slice(-10),
|
||||
valuesRedacted: true
|
||||
},
|
||||
valuesRedacted: true
|
||||
};
|
||||
Object.assign(report, replayEvidence);
|
||||
|
||||
if (terminalStatus !== "completed") throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay ended with status " + terminalStatus, replayEvidence);
|
||||
if (!matchingRequest) throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay did not issue the expected trace-scoped fromBeginning request", replayEvidence);
|
||||
if (after.traceId !== before.traceId) throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay traceId changed during replay", replayEvidence);
|
||||
if (after.topic !== config.topic) throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay topic mismatch: " + (after.topic || "missing"), replayEvidence);
|
||||
if (!after.groupId || !after.groupId.startsWith(config.groupPrefix + "-")) {
|
||||
throw new Error("Workbench Kafka debug replay groupId does not use the YAML group prefix");
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay groupId does not use the YAML group prefix", replayEvidence);
|
||||
}
|
||||
if (!after.deliveryText.includes("debug-replay") || !after.deliveryText.includes("fromBeginning=true")) {
|
||||
throw new Error("Workbench Kafka debug replay delivery contract is missing");
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay delivery contract is missing", replayEvidence);
|
||||
}
|
||||
if (!after.counts || after.counts.receivedCount <= 0 || after.counts.appliedCount <= 0) {
|
||||
throw new Error("Workbench Kafka debug replay received/applied counts must both be greater than zero");
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay received/applied counts must both be greater than zero", replayEvidence);
|
||||
}
|
||||
if (after.typedOutcome.phase !== "terminal" || after.typedOutcome.code !== "terminal_complete") {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay typed terminal contract is not terminal_complete", replayEvidence);
|
||||
}
|
||||
workbenchKafkaDebugReplayValidateLayerCounts(after.layerCounts, replayEvidence);
|
||||
workbenchKafkaDebugReplayValidateTraceTimeline(traceTimeline, replayEvidence);
|
||||
workbenchKafkaDebugReplayValidateRawWindow(rawHwlabEventWindow, replayEvidence);
|
||||
|
||||
const screenshot = await captureScreenshot("workbench-kafka-debug-replay-" + safeId(command.id));
|
||||
Object.assign(report, {
|
||||
ok: true,
|
||||
status: "passed",
|
||||
completedAt: new Date().toISOString(),
|
||||
sessionId,
|
||||
traceId: after.traceId,
|
||||
topic: after.topic,
|
||||
groupId: after.groupId,
|
||||
groupPrefix: config.groupPrefix,
|
||||
receivedCount: after.counts.receivedCount,
|
||||
appliedCount: after.counts.appliedCount,
|
||||
terminalStatus,
|
||||
toggle: { available: true, enabled: true, checkedBefore: toggleCheckedBefore, checkedAfter: await toggle.isChecked(), valuesRedacted: true },
|
||||
clear: cleared,
|
||||
request: matchingRequest,
|
||||
screenshot: { path: screenshot.path, sha256: screenshot.sha256, valuesRedacted: true },
|
||||
controlRecovery,
|
||||
valuesRedacted: true,
|
||||
valuesRedacted: true
|
||||
});
|
||||
const reportArtifact = await workbenchKafkaDebugReplayWriteReport(reportDir, report);
|
||||
report.reportPath = reportArtifact.path;
|
||||
@@ -136,12 +287,17 @@ async function validateWorkbenchKafkaDebugReplay(command) {
|
||||
traceId: report.traceId,
|
||||
topic: report.topic,
|
||||
groupId: report.groupId,
|
||||
phase: report.phase,
|
||||
code: report.code,
|
||||
receivedCount: report.receivedCount,
|
||||
appliedCount: report.appliedCount,
|
||||
layerCounts: report.layerCounts,
|
||||
traceTimeline: report.traceTimeline,
|
||||
rawHwlabEventWindow: report.rawHwlabEventWindow,
|
||||
reportPath: report.reportPath,
|
||||
reportSha256: report.reportSha256,
|
||||
screenshot: report.screenshot,
|
||||
valuesRedacted: true,
|
||||
valuesRedacted: true
|
||||
}));
|
||||
return {
|
||||
ok: true,
|
||||
@@ -151,13 +307,19 @@ async function validateWorkbenchKafkaDebugReplay(command) {
|
||||
topic: report.topic,
|
||||
groupId: report.groupId,
|
||||
groupPrefix: report.groupPrefix,
|
||||
phase: report.phase,
|
||||
code: report.code,
|
||||
receivedCount: report.receivedCount,
|
||||
appliedCount: report.appliedCount,
|
||||
layerCounts: report.layerCounts,
|
||||
traceTimeline: report.traceTimeline,
|
||||
rawHwlabEventWindow: report.rawHwlabEventWindow,
|
||||
productEventSource: report.productEventSource,
|
||||
terminalStatus,
|
||||
reportPath: report.reportPath,
|
||||
reportSha256: report.reportSha256,
|
||||
screenshot: report.screenshot,
|
||||
valuesRedacted: true,
|
||||
valuesRedacted: true
|
||||
};
|
||||
} catch (error) {
|
||||
Object.assign(report, {
|
||||
@@ -166,15 +328,26 @@ async function validateWorkbenchKafkaDebugReplay(command) {
|
||||
completedAt: new Date().toISOString(),
|
||||
failure: errorSummary(error),
|
||||
requests,
|
||||
valuesRedacted: true,
|
||||
valuesRedacted: true
|
||||
});
|
||||
const reportArtifact = await workbenchKafkaDebugReplayWriteReport(reportDir, report).catch(() => null);
|
||||
const wrapped = error instanceof Error ? error : new Error(String(error));
|
||||
wrapped.details = {
|
||||
...(wrapped.details || {}),
|
||||
traceId: report.traceId || wrapped.details?.traceId || null,
|
||||
topic: report.topic || wrapped.details?.topic || null,
|
||||
groupId: report.groupId || wrapped.details?.groupId || null,
|
||||
terminalStatus: report.terminalStatus || wrapped.details?.terminalStatus || null,
|
||||
receivedCount: report.receivedCount ?? report.legacyCounts?.receivedCount ?? wrapped.details?.receivedCount ?? null,
|
||||
appliedCount: report.appliedCount ?? report.legacyCounts?.appliedCount ?? wrapped.details?.appliedCount ?? null,
|
||||
phase: report.phase || wrapped.details?.phase || null,
|
||||
code: report.code || wrapped.details?.code || null,
|
||||
layerCounts: report.layerCounts || wrapped.details?.layerCounts || null,
|
||||
traceTimeline: report.traceTimeline || wrapped.details?.traceTimeline || null,
|
||||
rawHwlabEventWindow: report.rawHwlabEventWindow || wrapped.details?.rawHwlabEventWindow || null,
|
||||
reportPath: reportArtifact?.path || null,
|
||||
reportSha256: reportArtifact?.sha256 || null,
|
||||
valuesRedacted: true,
|
||||
valuesRedacted: true
|
||||
};
|
||||
throw wrapped;
|
||||
} finally {
|
||||
@@ -206,6 +379,15 @@ async function workbenchKafkaDebugReplayWaitForTerminal(panel, timeoutMs) {
|
||||
throw new Error("Workbench Kafka debug replay did not reach a terminal panel status within " + timeoutMs + "ms");
|
||||
}
|
||||
|
||||
async function workbenchKafkaDebugReplayWaitForTabSelected(tab, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() <= deadline) {
|
||||
if (await tab.getAttribute("aria-selected") === "true") return true;
|
||||
await sleep(50);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function workbenchKafkaDebugReplayPanelSnapshot(panel, countsNode) {
|
||||
const contract = await panel.locator('[data-testid="workbench-isolated-debug-contract"]').evaluate((root) => {
|
||||
const values = {};
|
||||
@@ -216,17 +398,194 @@ async function workbenchKafkaDebugReplayPanelSnapshot(panel, countsNode) {
|
||||
}
|
||||
return { values, text: root.textContent?.replace(/\s+/gu, " ").trim() || "" };
|
||||
});
|
||||
const status = await panel.getAttribute("data-replay-status");
|
||||
const layerNode = panel.locator('[data-testid="workbench-kafka-debug-layer-counts"]').first();
|
||||
const layerText = await layerNode.count() > 0 ? await layerNode.textContent() : "";
|
||||
const errorNode = panel.locator(".isolated-debug-error").first();
|
||||
const errorText = await errorNode.count() > 0 ? String(await errorNode.textContent() || "").trim() : "";
|
||||
const layerCounts = workbenchKafkaDebugReplayLayerCounts(layerText);
|
||||
return {
|
||||
status: await panel.getAttribute("data-replay-status"),
|
||||
status,
|
||||
traceId: contract.values.traceId || null,
|
||||
topic: contract.values.topic || null,
|
||||
groupId: contract.values.group || null,
|
||||
deliveryText: contract.text,
|
||||
counts: workbenchKafkaDebugReplayCounts(await countsNode.textContent()),
|
||||
valuesRedacted: true,
|
||||
layerCounts,
|
||||
error: errorText ? { textHash: sha256Text(errorText), textPreview: truncate(errorText, 160), textBytes: Buffer.byteLength(errorText), valuesRedacted: true } : null,
|
||||
typedOutcome: workbenchKafkaDebugReplayTypedOutcome({ status, errorText, layerCounts }),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function workbenchKafkaDebugReplayTraceTimelineSnapshot(panel) {
|
||||
return panel.evaluate((root) => {
|
||||
const rows = Array.from(root.querySelectorAll('[data-testid="trace-render-row"]'));
|
||||
const authorityRows = rows.filter((row) => row.hasAttribute("data-event-seq-authority"));
|
||||
const sourceRows = authorityRows.filter((row) => row.getAttribute("data-event-seq-authority") === "source");
|
||||
const projectedRows = authorityRows.filter((row) => row.getAttribute("data-event-seq-authority") === "projected");
|
||||
const readableSourceRows = sourceRows.filter((row) => Boolean(row.textContent?.replace(/\s+/gu, " ").trim()));
|
||||
return {
|
||||
contract: authorityRows.length > 0 ? "trace-sequence-authority-v1" : "legacy-ui",
|
||||
authorityAvailable: authorityRows.length > 0,
|
||||
rowCount: rows.length,
|
||||
sourceAuthorityRowCount: sourceRows.length,
|
||||
projectedAuthorityRowCount: projectedRows.length,
|
||||
readableSourceRowCount: readableSourceRows.length,
|
||||
samples: readableSourceRows.slice(0, 8).map((row) => ({
|
||||
seq: row.getAttribute("data-event-seq"),
|
||||
authority: row.getAttribute("data-event-seq-authority"),
|
||||
kind: row.getAttribute("data-event-kind"),
|
||||
status: row.getAttribute("data-event-status"),
|
||||
terminal: row.getAttribute("data-terminal") === "true",
|
||||
textBytes: new TextEncoder().encode(row.textContent?.replace(/\s+/gu, " ").trim() || "").byteLength
|
||||
})),
|
||||
valuesRedacted: true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function workbenchKafkaDebugReplayInspectRawWindow(input) {
|
||||
const rawTabLocator = input.panel.locator('[data-testid="workbench-kafka-debug-tab-raw"]');
|
||||
const rawTabCount = await rawTabLocator.count();
|
||||
if (rawTabCount === 0) {
|
||||
return {
|
||||
available: false,
|
||||
contract: "legacy-ui",
|
||||
reason: "raw-hwlab-event-tab-not-present",
|
||||
productEventsPath: input.config.productEventsPath,
|
||||
productEventSourceRequestCount: 0,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
if (rawTabCount !== 1) throw new Error("Workbench raw HWLAB Event tab count is " + rawTabCount + ", expected 1");
|
||||
const traceTabLocator = input.panel.locator('[data-testid="workbench-kafka-debug-tab-trace"]');
|
||||
const traceTabCount = await traceTabLocator.count();
|
||||
if (traceTabCount !== 1) throw new Error("Workbench Trace debug tab count is " + traceTabCount + ", expected 1");
|
||||
const rawTab = rawTabLocator.first();
|
||||
const traceTab = traceTabLocator.first();
|
||||
const requestStart = input.productEventSourceRequests.length;
|
||||
const pageUrlBefore = currentPageUrl();
|
||||
input.setRequestPhase("raw-tab-switch");
|
||||
await rawTab.click({ timeout: 10000 });
|
||||
if (!await workbenchKafkaDebugReplayWaitForTabSelected(rawTab, 5000)) throw new Error("Workbench raw HWLAB Event tab did not become selected");
|
||||
const rawCountsNode = input.panel.locator('[data-testid="workbench-raw-hwlab-counts"]').first();
|
||||
const rawTextNode = input.panel.locator('[data-testid="workbench-raw-hwlab-text"]').first();
|
||||
const rawContractNode = input.panel.locator('[data-testid="workbench-raw-hwlab-contract"]').first();
|
||||
await rawCountsNode.waitFor({ state: "visible", timeout: 5000 });
|
||||
await rawTextNode.waitFor({ state: "visible", timeout: 5000 });
|
||||
await rawContractNode.waitFor({ state: "visible", timeout: 5000 });
|
||||
await sleep(input.config.requestObservationQuietMs);
|
||||
const counts = workbenchKafkaDebugReplayRawCounts(await rawCountsNode.textContent());
|
||||
const rawText = await rawTextNode.inputValue();
|
||||
const contractText = String(await rawContractNode.textContent() || "").replace(/\s+/gu, " ").trim();
|
||||
input.setRequestPhase("trace-tab-restore");
|
||||
await traceTab.click({ timeout: 10000 });
|
||||
if (!await workbenchKafkaDebugReplayWaitForTabSelected(traceTab, 5000)) throw new Error("Workbench Trace debug tab did not restore after raw inspection");
|
||||
await sleep(input.config.requestObservationQuietMs);
|
||||
input.setRequestPhase("complete");
|
||||
const pageUrlAfter = currentPageUrl();
|
||||
const productRequests = input.productEventSourceRequests.slice(requestStart);
|
||||
return {
|
||||
available: true,
|
||||
contract: "raw-hwlab-event-window-v1",
|
||||
pageId,
|
||||
samePage: safeUrlPath(pageUrlBefore) === input.pathBefore && safeUrlPath(pageUrlAfter) === input.pathBefore,
|
||||
pathBefore: input.pathBefore,
|
||||
pathAfter: safeUrlPath(pageUrlAfter),
|
||||
productEventsPath: input.config.productEventsPath,
|
||||
productEventSourceRequestCount: productRequests.length,
|
||||
productEventSourceRequests: productRequests.slice(-10),
|
||||
sourceContractPresent: contractText.includes("EventSource") && contractText.includes("hwlab.event.v1"),
|
||||
unfilteredContractPresent: contractText.includes("no client event filter"),
|
||||
counts,
|
||||
textBytes: Buffer.byteLength(rawText),
|
||||
textHash: sha256Text(rawText),
|
||||
textPresent: rawText.length > 0,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayValidateLayerCounts(layerCounts, evidence) {
|
||||
if (!layerCounts) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay typed server/client count contract is missing", evidence);
|
||||
}
|
||||
const values = [
|
||||
layerCounts.server.scanned,
|
||||
layerCounts.server.matched,
|
||||
layerCounts.server.delivered,
|
||||
layerCounts.client.received,
|
||||
layerCounts.client.decoded,
|
||||
layerCounts.client.applied
|
||||
];
|
||||
if (values.some((value) => !Number.isInteger(value) || value <= 0)) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay typed server/client counts must all be greater than zero", evidence);
|
||||
}
|
||||
if (layerCounts.server.delivered !== layerCounts.client.received) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay server delivered and client received counts differ", evidence);
|
||||
}
|
||||
if (layerCounts.server.scanned < layerCounts.server.matched || layerCounts.server.matched !== layerCounts.server.delivered) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay server scanned/matched/delivered counts are inconsistent", evidence);
|
||||
}
|
||||
if (layerCounts.client.received !== layerCounts.client.decoded || layerCounts.client.decoded !== layerCounts.client.applied) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay client received/decoded/applied counts differ", evidence);
|
||||
}
|
||||
if (evidence?.legacyCounts && (evidence.legacyCounts.receivedCount !== layerCounts.client.received || evidence.legacyCounts.appliedCount !== layerCounts.client.applied)) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Kafka debug replay legacy reducer counts differ from the typed client layer", evidence);
|
||||
}
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayValidateTraceTimeline(traceTimeline, evidence) {
|
||||
if (!traceTimeline.authorityAvailable) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Trace timeline source-authority contract is missing", evidence);
|
||||
}
|
||||
if (traceTimeline.sourceAuthorityRowCount <= 0 || traceTimeline.readableSourceRowCount <= 0) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench Trace timeline did not expose a readable source-authority row", evidence);
|
||||
}
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayValidateRawWindow(rawWindow, evidence) {
|
||||
if (!rawWindow.available) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event window contract is missing", evidence);
|
||||
}
|
||||
if (!rawWindow.samePage) throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event inspection navigated away from the original page", evidence);
|
||||
if (!rawWindow.sourceContractPresent) throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event window does not declare the existing product EventSource contract", evidence);
|
||||
if (!rawWindow.unfilteredContractPresent) throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event window does not declare the unfiltered ingress contract", evidence);
|
||||
if (!rawWindow.counts) throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event counts are unreadable", evidence);
|
||||
const counts = rawWindow.counts;
|
||||
if ([counts.received, counts.retained, counts.decoded, counts.rejected, counts.evicted, counts.bytes].some((value) => !Number.isInteger(value) || value < 0)) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event counts are incomplete", evidence);
|
||||
}
|
||||
if (counts.received !== counts.decoded + counts.rejected || counts.received !== counts.retained + counts.evicted) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event bounded-buffer counts are inconsistent", evidence);
|
||||
}
|
||||
if (counts.received <= 0 || counts.retained <= 0 || counts.bytes <= 0 || rawWindow.textPresent !== true || rawWindow.textBytes <= 0) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event window did not retain a readable product SSE frame", evidence);
|
||||
}
|
||||
if (rawWindow.productEventSourceRequestCount !== 0) {
|
||||
throw workbenchKafkaDebugReplayEvidenceError("Workbench raw HWLAB Event tab opened an additional product EventSource", evidence);
|
||||
}
|
||||
}
|
||||
|
||||
function workbenchKafkaDebugReplayEvidenceError(message, evidence) {
|
||||
const error = new Error(message);
|
||||
error.details = {
|
||||
traceId: evidence?.traceId || null,
|
||||
topic: evidence?.topic || null,
|
||||
groupId: evidence?.groupId || null,
|
||||
terminalStatus: evidence?.terminalStatus || null,
|
||||
receivedCount: evidence?.legacyCounts?.receivedCount ?? null,
|
||||
appliedCount: evidence?.legacyCounts?.appliedCount ?? null,
|
||||
phase: evidence?.phase || null,
|
||||
code: evidence?.code || null,
|
||||
layerCounts: evidence?.layerCounts || null,
|
||||
traceTimeline: evidence?.traceTimeline || null,
|
||||
rawHwlabEventWindow: evidence?.rawHwlabEventWindow || null,
|
||||
valuesRedacted: true
|
||||
};
|
||||
return error;
|
||||
}
|
||||
|
||||
async function workbenchKafkaDebugReplayWriteReport(reportDir, report) {
|
||||
await mkdir(reportDir, { recursive: true, mode: 0o700 });
|
||||
const file = path.join(reportDir, "report.json");
|
||||
|
||||
@@ -90,8 +90,26 @@ test("observe status renderer exposes bounded Workbench Kafka debug replay evide
|
||||
traceId: "trc_fixture",
|
||||
topic: "hwlab.event.debug.v1",
|
||||
groupId: "hwlab-v03-workbench-isolated-debug-123-fixture",
|
||||
phase: "terminal",
|
||||
code: "terminal_complete",
|
||||
receivedCount: 35,
|
||||
appliedCount: 35,
|
||||
layerCounts: {
|
||||
server: { scanned: 35, matched: 35, delivered: 35 },
|
||||
client: { received: 35, decoded: 35, applied: 35 },
|
||||
},
|
||||
traceTimeline: {
|
||||
contract: "trace-sequence-authority-v1",
|
||||
sourceAuthorityRowCount: 35,
|
||||
readableSourceRowCount: 35,
|
||||
},
|
||||
rawHwlabEventWindow: {
|
||||
available: true,
|
||||
samePage: true,
|
||||
unfilteredContractPresent: true,
|
||||
productEventSourceRequestCount: 0,
|
||||
counts: { received: 35, retained: 30, rejected: 0, evicted: 5, bytes: 8192 },
|
||||
},
|
||||
terminalStatus: "completed",
|
||||
reportSha256: "sha256:report",
|
||||
screenshot: { sha256: "sha256:image" },
|
||||
@@ -105,7 +123,10 @@ test("observe status renderer exposes bounded Workbench Kafka debug replay evide
|
||||
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, /terminal.*terminal_complete/u);
|
||||
assert.match(text, /35.*35.*completed.*sha256:image/u);
|
||||
assert.match(text, /SERVER_SCANNED.*CLIENT_APPLIED/u);
|
||||
assert.match(text, /trace-sequence-authority-v1.*35.*35.*true.*true.*true.*35.*30.*0.*5.*8192.*0/u);
|
||||
});
|
||||
|
||||
test("web-probe script render warns to promote repeated scripts into commands", () => {
|
||||
|
||||
@@ -86,8 +86,26 @@ test("observe status preserves bounded Workbench Kafka debug replay evidence", a
|
||||
topic: "hwlab.event.debug.v1",
|
||||
groupId: "hwlab-v03-workbench-isolated-debug-123-fixture",
|
||||
groupPrefix: "hwlab-v03-workbench-isolated-debug",
|
||||
phase: "terminal",
|
||||
code: "terminal_complete",
|
||||
receivedCount: 35,
|
||||
appliedCount: 35,
|
||||
layerCounts: {
|
||||
server: { scanned: 35, matched: 35, delivered: 35 },
|
||||
client: { received: 35, decoded: 35, applied: 35 },
|
||||
},
|
||||
traceTimeline: {
|
||||
contract: "trace-sequence-authority-v1",
|
||||
sourceAuthorityRowCount: 35,
|
||||
readableSourceRowCount: 35,
|
||||
},
|
||||
rawHwlabEventWindow: {
|
||||
available: true,
|
||||
samePage: true,
|
||||
unfilteredContractPresent: true,
|
||||
productEventSourceRequestCount: 0,
|
||||
counts: { received: 35, retained: 30, rejected: 0, evicted: 5, bytes: 8192 },
|
||||
},
|
||||
terminalStatus: "completed",
|
||||
reportPath: "/tmp/report.json",
|
||||
reportSha256: "sha256:report",
|
||||
@@ -102,5 +120,10 @@ test("observe status preserves bounded Workbench Kafka debug replay evidence", a
|
||||
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.phase, "terminal");
|
||||
assert.equal(status.exactCommand.result.code, "terminal_complete");
|
||||
assert.equal(status.exactCommand.result.layerCounts.server.scanned, 35);
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ 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 commandSummary=(name)=>{
|
||||
const root=commandDir(name); let entries=[];
|
||||
try{entries=fs.readdirSync(root).filter((item)=>item.endsWith('.json')).sort();}catch{}
|
||||
@@ -74,7 +75,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,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: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:'not-found',ok:false,valuesRedacted:true};
|
||||
};
|
||||
|
||||
@@ -35,6 +35,9 @@ test("Workbench Kafka debug replay runner contract is derived from owning YAML",
|
||||
topic: "hwlab.event.debug.v1",
|
||||
groupPrefix: "hwlab-v03-workbench-isolated-debug",
|
||||
completionTimeoutMs: 30_000,
|
||||
productEventsPath: "/v1/workbench/events",
|
||||
debugEventsPath: "/v1/workbench/debug/kafka-sse/events",
|
||||
requestObservationQuietMs: 3_000,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,17 @@ 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 };
|
||||
const pureKafkaLive = spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"];
|
||||
if (pureKafkaLive === undefined) {
|
||||
throw new Error("workbenchKafkaDebugReplay requires the pure-kafka-live realtimeFanoutProfile in the owning YAML");
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
productEventsPath: pureKafkaLive.productEventsPath,
|
||||
debugEventsPath: pureKafkaLive.debugEventsPath,
|
||||
requestObservationQuietMs: pureKafkaLive.reconnectQuietMs,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveWebObserveActionJson(
|
||||
|
||||
Reference in New Issue
Block a user