102 lines
5.8 KiB
TypeScript
102 lines
5.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { test } from "bun:test";
|
|
|
|
import { nodeWebObserveCollectViewNodeScript, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
|
|
|
function shellQuote(value: string): string {
|
|
return `\'${value.replace(/\'/g, `\'\\\'\'`)}\'`;
|
|
}
|
|
|
|
async function writeJsonl(path: string, rows: Array<Record<string, unknown>>): Promise<void> {
|
|
await writeFile(path, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : ""));
|
|
}
|
|
|
|
async function fixtureState(): Promise<string> {
|
|
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-turn-visibility-"));
|
|
await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-turn-visibility" }) + "\n");
|
|
await writeJsonl(join(stateDir, "control.jsonl"), [
|
|
{ type: "sendPrompt", phase: "started", commandId: "cmd-first", sessionId: "ses-fixture", ts: "2026-07-10T01:00:00.000Z", input: { textPreview: "FIRST_UNRELATED", textHash: "sha256:first", textBytes: 15 } },
|
|
{ type: "sendPrompt", phase: "completed", commandId: "cmd-first", sessionId: "ses-fixture", ts: "2026-07-10T01:00:01.000Z", detail: { chatSubmit: { status: 202, traceId: "trc_first", otelTraceId: "otel-first" } } },
|
|
{ type: "sendPrompt", phase: "started", commandId: "cmd-second", sessionId: "ses-fixture", ts: "2026-07-10T01:01:00.000Z", input: { textPreview: "SECOND_TARGET", textHash: "sha256:second", textBytes: 13 } },
|
|
{ type: "sendPrompt", phase: "completed", commandId: "cmd-second", sessionId: "ses-fixture", ts: "2026-07-10T01:01:01.000Z", detail: { chatSubmit: { status: 202, traceId: "trc_second", otelTraceId: "otel-second" } } },
|
|
]);
|
|
await writeJsonl(join(stateDir, "samples.jsonl"), [
|
|
{ seq: 1, ts: "2026-07-10T01:00:02.000Z", routeSessionId: "ses-fixture", turns: [{ traceId: "trc_first", status: "completed", text: "completed" }], messages: [{ traceId: "trc_first", role: "assistant", status: "completed", text: "FIRST_FINAL" }], traceRows: [] },
|
|
{ seq: 2, ts: "2026-07-10T01:01:02.000Z", routeSessionId: "ses-fixture", turns: [{ traceId: "trc_second", status: "completed", text: "completed" }], messages: [{ traceId: "trc_second", role: "assistant", status: "completed", text: "SECOND_FINAL" }], traceRows: [] },
|
|
]);
|
|
await writeJsonl(join(stateDir, "network.jsonl"), []);
|
|
await writeJsonl(join(stateDir, "browser-process.jsonl"), []);
|
|
return stateDir;
|
|
}
|
|
|
|
function collect(stateDir: string, view: NodeWebProbeObserveCollectView, input: { commandId?: string; traceId?: string } = {}): Record<string, unknown> {
|
|
const script = nodeWebObserveCollectViewNodeScript({
|
|
maxFiles: 100,
|
|
view,
|
|
traceId: input.traceId ?? null,
|
|
sampleSeq: null,
|
|
timestamp: null,
|
|
turn: null,
|
|
commandId: input.commandId ?? null,
|
|
windowMs: null,
|
|
});
|
|
const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
|
|
cwd: join(import.meta.dir, "../../.."),
|
|
encoding: "utf8",
|
|
});
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
return JSON.parse(result.stdout) as Record<string, unknown>;
|
|
}
|
|
|
|
test("turn-summary command-id selects exactly one turn and separates control from async state", async () => {
|
|
const output = collect(await fixtureState(), "turn-summary", { commandId: "cmd-second" });
|
|
assert.equal(output.ok, true);
|
|
assert.equal(output.status, "matched");
|
|
assert.equal(output.turnCount, 1);
|
|
assert.equal(output.availableTurnCount, 2);
|
|
const filter = output.filter as Record<string, unknown>;
|
|
assert.equal(filter.mode, "exact-command-id");
|
|
assert.equal(filter.matchedCount, 1);
|
|
const rows = output.rows as Array<Record<string, unknown>>;
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(rows[0].round, 2);
|
|
assert.equal(rows[0].commandId, "cmd-second");
|
|
assert.equal(rows[0].controlExecutionStatus, "completed");
|
|
assert.equal(rows[0].turnSubmissionStatus, "accepted");
|
|
assert.equal(rows[0].turnStatus, "completed");
|
|
assert.equal(rows[0].turnTerminalObserved, true);
|
|
assert.doesNotMatch(String(output.renderedText), /FIRST_UNRELATED|FIRST_FINAL/u);
|
|
assert.match(String(output.renderedText), /Control.*Submit.*Async turn/u);
|
|
});
|
|
|
|
test("turn-summary missing command-id is structured not-found without all-turn fallback", async () => {
|
|
const output = collect(await fixtureState(), "turn-summary", { commandId: "cmd-missing" });
|
|
assert.equal(output.ok, false);
|
|
assert.equal(output.status, "not-found");
|
|
assert.equal(output.turnCount, 0);
|
|
assert.deepEqual(output.rows, []);
|
|
const error = output.error as Record<string, unknown>;
|
|
assert.equal(error.code, "turn-summary-command-not-found");
|
|
assert.doesNotMatch(String(output.renderedText), /FIRST_UNRELATED|SECOND_TARGET|FIRST_FINAL|SECOND_FINAL/u);
|
|
assert.match(String(output.renderedText), /unrelated turns were not returned/u);
|
|
});
|
|
|
|
test("workbench-triad explains missing local API artifact and keeps OTel as independent evidence", async () => {
|
|
const output = collect(await fixtureState(), "workbench-triad", { traceId: "trc_second" });
|
|
const reconciliation = output.evidenceReconciliation as Record<string, unknown>;
|
|
assert.equal(reconciliation.status, "artifact-evidence-missing");
|
|
assert.equal(reconciliation.businessApiAbsenceProven, false);
|
|
const localApi = reconciliation.localApiArtifact as Record<string, unknown>;
|
|
assert.equal(localApi.status, "missing");
|
|
const otel = reconciliation.otel as Record<string, unknown>;
|
|
assert.equal(otel.status, "trace-reference-present");
|
|
assert.equal(otel.otelTraceId, "otel-second");
|
|
assert.equal(otel.coverageStatus, "not-evaluated-by-offline-artifact-view");
|
|
assert.match(String(output.renderedText), /artifact evidence missing/u);
|
|
assert.match(String(output.renderedText), /does not mean the business API did not occur/u);
|
|
});
|