fix: detect workbench recovery authority fanout
This commit is contained in:
@@ -65,6 +65,27 @@ const browserFreezePolicy = {
|
||||
},
|
||||
};
|
||||
|
||||
async function runObserveAnalyzer(files: Record<string, string>): Promise<Record<string, any>> {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-"));
|
||||
const analyzerPath = join(stateDir, "analyze.mjs");
|
||||
await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 });
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
await writeFile(join(stateDir, name), content.endsWith("\n") ? content : content + "\n");
|
||||
}
|
||||
const result = spawnSync("bun", [analyzerPath, stateDir], {
|
||||
cwd: join(import.meta.dir, "../../.."),
|
||||
env: {
|
||||
...process.env,
|
||||
UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES: "0",
|
||||
UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON: JSON.stringify(alertThresholds),
|
||||
UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON: JSON.stringify(browserFreezePolicy),
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8"));
|
||||
}
|
||||
|
||||
test("observe analyzer classifies stale completed session rail when a newer turn is running", async () => {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-"));
|
||||
const analyzerPath = join(stateDir, "analyze.mjs");
|
||||
@@ -163,3 +184,105 @@ test("observe analyzer classifies stale completed session rail when a newer turn
|
||||
assert.equal(report.sampleMetrics.workbenchTurnStateTriad.summary.invalidRowCount, 1);
|
||||
assert.equal(report.sampleMetrics.workbenchTurnStateTriad.summary.cardFinalResponseMismatchCount, 0);
|
||||
}, 20_000);
|
||||
|
||||
test("observe analyzer flags automatic recovery legacy fanout authority", async () => {
|
||||
const samples = [
|
||||
JSON.stringify({
|
||||
seq: 1,
|
||||
ts: "2026-07-01T15:21:25.000Z",
|
||||
path: "/workbench/sessions/ses_auto",
|
||||
url: "https://hwlab.example.test/workbench/sessions/ses_auto",
|
||||
pageRole: "control",
|
||||
pageId: "control-test",
|
||||
routeSessionId: "ses_auto",
|
||||
activeSessionId: "ses_auto",
|
||||
turns: []
|
||||
})
|
||||
].join("\n") + "\n";
|
||||
const network = [
|
||||
JSON.stringify({
|
||||
ts: "2026-07-01T15:21:26.000Z",
|
||||
type: "response",
|
||||
method: "POST",
|
||||
status: 202,
|
||||
url: "https://hwlab.example.test/v1/web-performance",
|
||||
webPerformancePayload: {
|
||||
parseStatus: "parsed",
|
||||
schemaVersion: "hwlab-web-performance-v2",
|
||||
events: [{
|
||||
kind: "workbench_ui_event",
|
||||
eventType: "runtime_diagnostic",
|
||||
module: "workbench-stream-transport",
|
||||
diagnosticCode: "workbench_recovery_action",
|
||||
recoveryAction: "hydrate-trace-events",
|
||||
rootCause: "sse-gap",
|
||||
eventCount: 0
|
||||
}]
|
||||
}
|
||||
}),
|
||||
JSON.stringify({ ts: "2026-07-01T15:21:27.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/turns/trc_auto", bodySummary: { pathKind: "workbench-turn", terminalEvidenceCount: 1, traceIds: ["trc_auto"] } }),
|
||||
JSON.stringify({ ts: "2026-07-01T15:21:28.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/sessions/ses_auto/messages", bodySummary: { pathKind: "workbench-session-messages", terminalEvidenceCount: 1, sessionIds: ["ses_auto"] } }),
|
||||
JSON.stringify({ ts: "2026-07-01T15:21:29.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/traces/trc_auto/events", bodySummary: { pathKind: "workbench-trace-events", traceEventLikeCount: 3, traceIds: ["trc_auto"] } }),
|
||||
JSON.stringify({ ts: "2026-07-01T15:21:30.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/sync?sessionId=ses_auto&traceId=trc_auto&since=7", bodySummary: { pathKind: "workbench-sync-replay", eventCount: 1, cursorOutboxSeq: 12, realtimeAuthority: "workbench-realtime-authority-v2", terminalSeal: true } })
|
||||
].join("\n") + "\n";
|
||||
|
||||
const report = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network });
|
||||
const finding = report.findings.find((item: Record<string, unknown>) => item.id === "workbench-automatic-recovery-fanout-authority");
|
||||
|
||||
assert.equal(finding?.rootCause, "workbench_automatic_recovery_legacy_fanout");
|
||||
assert.equal(finding?.recoveryAuthority.summary.forbiddenAutomaticRecoveryCount, 1);
|
||||
assert.equal(finding?.recoveryAuthority.summary.legacyFanoutWindowCount, 1);
|
||||
assert.equal(finding?.recoveryAuthority.summary.syncReplayEvidenceCount, 1);
|
||||
assert.equal(report.runtimeAlerts.webPerformanceRuntimeDiagnostics[0].recoveryAction, "hydrate-trace-events");
|
||||
}, 20_000);
|
||||
|
||||
test("observe analyzer allows explicit detail trace reads when sync authority evidence exists", async () => {
|
||||
const samples = JSON.stringify({
|
||||
seq: 1,
|
||||
ts: "2026-07-01T15:31:25.000Z",
|
||||
path: "/workbench/sessions/ses_detail",
|
||||
url: "https://hwlab.example.test/workbench/sessions/ses_detail",
|
||||
pageRole: "control",
|
||||
pageId: "control-test",
|
||||
routeSessionId: "ses_detail",
|
||||
activeSessionId: "ses_detail",
|
||||
turns: []
|
||||
}) + "\n";
|
||||
const network = [
|
||||
JSON.stringify({ ts: "2026-07-01T15:31:26.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/traces/trc_detail/events", bodySummary: { pathKind: "workbench-trace-events", traceEventLikeCount: 2, traceIds: ["trc_detail"], authority: "trace-detail-only", detailProjection: true } }),
|
||||
JSON.stringify({
|
||||
ts: "2026-07-01T15:31:27.000Z",
|
||||
type: "response",
|
||||
method: "POST",
|
||||
status: 202,
|
||||
url: "https://hwlab.example.test/v1/web-performance",
|
||||
webPerformancePayload: {
|
||||
parseStatus: "parsed",
|
||||
schemaVersion: "hwlab-web-performance-v2",
|
||||
events: [{
|
||||
kind: "workbench_ui_event",
|
||||
eventType: "runtime_diagnostic",
|
||||
module: "workbench-sync-replay",
|
||||
diagnosticCode: "workbench_sync_replay_applied",
|
||||
rootCause: "sse-gap",
|
||||
eventCount: 1,
|
||||
sinceOutboxSeq: 7,
|
||||
syncCursorOutboxSeq: 12,
|
||||
realtimeAuthority: "workbench-realtime-authority-v2",
|
||||
entityFamilyCount: 1,
|
||||
entityFamilies: "turn",
|
||||
maxEntityVersion: 3,
|
||||
projectionRevision: "rev_detail",
|
||||
terminalSeal: true,
|
||||
detailProjection: false
|
||||
}]
|
||||
}
|
||||
})
|
||||
].join("\n") + "\n";
|
||||
|
||||
const report = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network });
|
||||
const finding = report.findings.find((item: Record<string, unknown>) => item.id === "workbench-automatic-recovery-fanout-authority");
|
||||
|
||||
assert.equal(finding, undefined);
|
||||
assert.equal(report.runtimeAlerts.webPerformanceRuntimeDiagnostics[0].syncCursorOutboxSeq, 12);
|
||||
}, 20_000);
|
||||
|
||||
Reference in New Issue
Block a user