import assert from "node:assert/strict"; export const ACTOR = { id: "usr_workbench_reader", username: "reader", displayName: "Reader", role: "user", status: "active" }; export async function getJson(port, path) { const response = await fetch(`http://127.0.0.1:${port}${path}`); return { status: response.status, body: await response.json() }; } export async function waitForCondition(predicate, timeoutMs = 500) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 5)); } assert.fail("condition was not met before timeout"); } export function createDurableFactsRuntimeStore({ sessions = [], queryError = null, queries = [] } = {}) { const facts = emptyFacts(); for (const item of sessions) mergeFacts(facts, buildDurableFactsForSession(item)); return createRuntimeStoreFromFacts(facts, { queryError, queries }); } export function createRuntimeStoreFromFacts(facts, { queryError = null, queries = [] } = {}) { return { queries, async queryWorkbenchFacts(params = {}) { queries.push(params); if (queryError) throw queryError; const filtered = filterFacts(facts, params); return { facts: filtered, count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0), persistence: { adapter: "test-durable-workbench-facts", durable: true } }; } }; } export function buildDurableFactsForSession({ session, events = [], status = null, finalText = null, assistantText = null, runId = null, commandId = null, projectionStatus = null, projectionHealth = "healthy", lastProjectedSeq = null, omitSessions = false } = {}) { const traceId = session.lastTraceId ?? session.session?.lastTraceId; const projectedStatus = normalizeTestStatus(status ?? session.status); const terminal = testTerminalStatuses.has(projectedStatus); const normalizedEvents = normalizeTestEvents(events, session, { traceId }); const timing = testTimingProjection({ session, events: normalizedEvents, status: projectedStatus, terminal }); const normalizedMessages = normalizeTestMessages(session, { traceId, status: projectedStatus, finalText, terminal, timing }); const explicitLastProjectedSeq = lastProjectedSeq !== null && lastProjectedSeq !== undefined ? Number(lastProjectedSeq) : NaN; const projectedSeq = Number.isFinite(explicitLastProjectedSeq) ? explicitLastProjectedSeq : Math.max(normalizedEvents.length, normalizedMessages.length, 0); const checkpointStatus = projectionStatus ?? (terminal ? "caught_up" : "projecting"); return { sessions: omitSessions ? [] : [{ sessionId: session.id, ownerUserId: session.ownerUserId, ownerRole: session.ownerRole ?? null, projectId: session.projectId ?? null, conversationId: session.conversationId ?? null, threadId: session.threadId ?? null, agentId: session.agentId ?? "hwlab-code-agent", status: projectedStatus, lastTraceId: traceId, providerProfile: session.session?.providerProfile ?? null, projectedSeq, sourceSeq: projectedSeq, sourceEventId: traceId, terminal, sealed: terminal, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, createdAt: session.startedAt ?? session.createdAt ?? session.updatedAt ?? null, updatedAt: session.updatedAt ?? session.endedAt ?? session.startedAt ?? null, valuesRedacted: true }], messages: normalizedMessages, parts: normalizedMessages.filter((message) => message.text).map((message, index) => { const partType = message.role !== "user" && message.terminal ? "final_response" : "text"; return { partId: `prt_${session.id}_${index}`, messageId: message.messageId, sessionId: session.id, turnId: message.turnId, traceId: message.traceId, partIndex: 0, partType, status: message.status, text: message.text, projectedSeq: message.projectedSeq, sourceSeq: message.sourceSeq, sourceEventId: message.sourceEventId, terminal: message.terminal, sealed: partType === "final_response" ? true : message.sealed, updatedAt: message.updatedAt, valuesRedacted: true }; }), turns: traceId ? [{ turnId: traceId, sessionId: session.id, traceId, messageId: normalizedMessages.find((message) => message.role !== "user")?.messageId ?? null, status: projectedStatus, projectedSeq, sourceSeq: projectedSeq, sourceEventId: traceId, terminal, sealed: terminal, assistantText: assistantText ?? null, finalResponse: finalText ? { text: finalText, status: projectedStatus, traceId, valuesPrinted: false } : null, diagnostic: { projectionStatus: checkpointStatus, projectionHealth, valuesRedacted: true }, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, createdAt: session.startedAt ?? session.updatedAt ?? null, updatedAt: session.updatedAt ?? null, valuesRedacted: true }] : [], traceEvents: normalizedEvents, checkpoints: traceId ? [{ traceId, sessionId: session.id, turnId: traceId, runId, commandId, projectedSeq, sourceSeq: projectedSeq, sourceEventId: traceId, projectionStatus: checkpointStatus, projectionHealth, terminal, sealed: terminal, diagnostic: { projectionStatus: checkpointStatus, projectionHealth, valuesRedacted: true }, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, createdAt: session.startedAt ?? session.updatedAt ?? null, updatedAt: session.updatedAt ?? null, valuesRedacted: true }] : [] }; } export function normalizeTestMessages(session, { traceId, status, finalText, terminal, timing }) { const raw = Array.isArray(session.session?.messages) ? session.session.messages : []; return raw.map((message, index) => { const role = message.role ?? (index % 2 === 0 ? "user" : "agent"); const isAssistant = role === "agent" || role === "assistant"; const text = isAssistant && finalText ? finalText : String(message.text ?? message.content ?? ""); const messageStatus = isAssistant && finalText ? status : normalizeTestStatus(message.status ?? (role === "user" ? "sent" : status)); const messageTraceId = message.traceId ?? traceId; const projectedSeq = Number.isFinite(Number(message.projectedSeq)) ? Number(message.projectedSeq) : index + 1; const sourceSeq = Number.isFinite(Number(message.sourceSeq)) ? Number(message.sourceSeq) : projectedSeq; return { messageId: message.messageId ?? `msg_${session.id}_${index}`, sessionId: session.id, turnId: message.turnId ?? messageTraceId, traceId: messageTraceId, role, status: messageStatus, projectedSeq, sourceSeq, sourceEventId: `${session.id}:message:${index}`, terminal: isAssistant && terminal, sealed: isAssistant && terminal, text, ...(isAssistant ? { timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs } : {}), createdAt: message.createdAt ?? session.startedAt ?? session.updatedAt ?? null, updatedAt: message.updatedAt ?? session.updatedAt ?? null, valuesRedacted: true }; }); } export function testTimingProjection({ session, events = [], status, terminal }) { const startedAt = timestampIso(session.startedAt ?? session.createdAt ?? events[0]?.createdAt ?? events[0]?.occurredAt); const lastEvent = events.at(-1) ?? null; const lastEventAt = timestampIso(lastEvent?.updatedAt ?? lastEvent?.createdAt ?? lastEvent?.occurredAt ?? session.updatedAt); const finishedAt = terminal ? timestampIso(session.endedAt ?? lastEventAt ?? session.updatedAt) : null; const durationMs = terminal ? elapsedMs(startedAt, finishedAt ?? lastEventAt) : null; return { startedAt, lastEventAt, finishedAt, durationMs, status, valuesRedacted: true }; } export function timestampIso(value) { const ms = Date.parse(String(value ?? "")); return Number.isFinite(ms) ? new Date(ms).toISOString() : null; } export function elapsedMs(startedAt, endedAt) { const start = Date.parse(String(startedAt ?? "")); const end = Date.parse(String(endedAt ?? "")); if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null; return end - start; } export function normalizeTestEvents(events, session, { traceId }) { return events.map((event, index) => { const projectedSeq = Number.isFinite(Number(event.projectedSeq)) ? Number(event.projectedSeq) : Number.isFinite(Number(event.seq)) ? Number(event.seq) : index + 1; const sourceSeq = Number.isFinite(Number(event.sourceSeq)) ? Number(event.sourceSeq) : null; return { ...event, id: event.id ?? `wte_${session.id}_${index}`, traceId: event.traceId ?? traceId, sessionId: event.sessionId ?? session.id, turnId: event.turnId ?? traceId, messageId: event.messageId ?? null, sourceSeq, sourceEventId: event.sourceEventId ?? `${traceId}:${sourceSeq ?? projectedSeq}:${index}`, projectedSeq, eventType: event.eventType ?? event.type ?? event.label ?? "event", terminal: event.terminal === true, sealed: event.terminal === true, occurredAt: event.occurredAt ?? event.createdAt ?? session.updatedAt ?? null, updatedAt: event.updatedAt ?? event.createdAt ?? session.updatedAt ?? null, valuesRedacted: true }; }); } export function filterFacts(facts, params = {}) { const families = testFactFamilySet(params); const afterProjectedSeq = Number.parseInt(params.afterProjectedSeq ?? "", 10); const filtered = { sessions: families.has("sessions") ? facts.sessions.filter((record) => matchesFact(record, params, ["sessionId", "ownerUserId", "projectId", "conversationId", "threadId", "status"]) && matchesTraceFact(record.lastTraceId, params)) : [], messages: families.has("messages") ? facts.messages.filter((record) => matchesFact(record, params, ["messageId", "sessionId", "turnId", "traceId", "role", "status"])) : [], parts: families.has("parts") ? facts.parts.filter((record) => matchesFact(record, params, ["partId", "messageId", "sessionId", "turnId", "traceId", "partType", "status"])) : [], turns: families.has("turns") ? facts.turns.filter((record) => matchesFact(record, params, ["turnId", "sessionId", "traceId", "messageId", "status"])) : [], traceEvents: families.has("traceEvents") ? facts.traceEvents .filter((record) => matchesFact(record, params, ["id", "traceId", "sessionId", "turnId", "messageId", "eventType"])) .filter((record) => !Number.isInteger(afterProjectedSeq) || afterProjectedSeq <= 0 || Number(record.projectedSeq) > afterProjectedSeq) .sort((left, right) => Number(left.projectedSeq) - Number(right.projectedSeq)) : [], checkpoints: families.has("checkpoints") ? facts.checkpoints.filter((record) => matchesFact(record, params, ["traceId", "sessionId", "turnId", "runId", "commandId", "projectionStatus", "projectionHealth"])) : [] }; if (workbenchTestFactOrder(params, "sessions") === "updated_desc") { filtered.sessions.sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")) || String(right.sessionId ?? "").localeCompare(String(left.sessionId ?? ""))); } const limit = Number.parseInt(params.limit ?? "", 10); if (!Number.isInteger(limit) || limit <= 0) return filtered; return Object.fromEntries(Object.entries(filtered).map(([key, value]) => [key, value.slice(0, limit)])); } export function workbenchTestFactOrder(params = {}, family = "") { const value = params[`${family}Order`] ?? params.order; return value === "updated_desc" ? "updated_desc" : "updated_asc"; } export function matchesFact(record, params, fields) { return fields.every((field) => { if (params[field] !== undefined && params[field] !== null && record[field] !== params[field]) return false; const values = Array.isArray(params[`${field}s`]) ? params[`${field}s`].map((item) => String(item ?? "")).filter(Boolean) : []; return values.length === 0 || values.includes(String(record[field] ?? "")); }); } export function matchesTraceFact(value, params) { if (params.traceId && value !== params.traceId) return false; const traceIds = Array.isArray(params.traceIds) ? params.traceIds.map((item) => String(item ?? "")).filter(Boolean) : []; return traceIds.length === 0 || traceIds.includes(String(value ?? "")); } export function testFactFamilySet(params = {}) { const all = ["sessions", "messages", "parts", "turns", "traceEvents", "checkpoints"]; const raw = params.families ?? params.factFamilies; if (!raw) return new Set(all); const values = Array.isArray(raw) ? raw : String(raw).split(","); const selected = values.map((item) => String(item ?? "").trim()).filter((item) => all.includes(item)); return new Set(selected.length ? selected : all); } export function mergeFacts(target, source) { for (const key of Object.keys(target)) target[key].push(...source[key]); return target; } export function emptyFacts() { return { sessions: [], messages: [], parts: [], turns: [], traceEvents: [], checkpoints: [] }; } export function normalizeTestStatus(value) { const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); if (status === "active" || status === "busy" || status === "processing") return "running"; if (status === "cancelled") return "canceled"; return status || "unknown"; } const testTerminalStatuses = new Set(["completed", "failed", "blocked", "timeout", "canceled", "idle"]); export async function getSseEvents(port, path, count) { const controller = new AbortController(); const response = await fetch(`http://127.0.0.1:${port}${path}`, { signal: controller.signal }); assert.equal(response.status, 200); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const events = []; try { while (events.length < count) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); for (;;) { const index = buffer.indexOf("\n\n"); if (index < 0) break; const block = buffer.slice(0, index); buffer = buffer.slice(index + 2); events.push(parseSseBlock(block)); if (events.length >= count) break; } } } finally { controller.abort(); } return events; } export function parseSseBlock(block) { const lines = block.split("\n"); const event = lines.find((line) => line.startsWith("event:"))?.slice(6).trim() ?? "message"; const id = lines.find((line) => line.startsWith("id:"))?.slice(3).trim() ?? null; const data = lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n"); return { event, id, data: JSON.parse(data) }; } export function createFakeKafkaFactory() { let eachMessage = null; let offset = 0; let subscribedTopic = "hwlab.event.v1"; const consumer = { connect: async () => undefined, subscribe: async (input = {}) => { subscribedTopic = String(input.topic ?? subscribedTopic); }, run: async (input = {}) => { eachMessage = input.eachMessage; }, stop: async () => undefined, disconnect: async () => undefined }; return { factory: () => ({ consumer: () => consumer }), emit: async (value = {}) => { await waitFor(() => typeof eachMessage === "function"); await eachMessage({ topic: subscribedTopic, partition: 0, message: { offset: String(offset++), key: Buffer.from(value.traceId ?? value.sessionId ?? "fake"), timestamp: String(Date.now()), value: Buffer.from(JSON.stringify(value)) } }); } }; } export async function waitFor(predicate, timeoutMs = 1000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error("waitFor timed out"); }