import assert from "node:assert/strict"; import { test } from "bun:test"; import { classifyWorkbenchEmptySessionGcCandidate, runWorkbenchEmptySessionGc } from "./workbench-empty-session-gc.ts"; test("workbench empty session GC archives only stale empty idle sessions", async () => { const sessions = new Map([ ["ses_empty_old", session({ id: "ses_empty_old", status: "idle", updatedAt: "2026-06-18T00:00:00.000Z" })], ["ses_empty_fresh", session({ id: "ses_empty_fresh", status: "idle", updatedAt: "2026-06-18T00:15:30.000Z" })], ["ses_with_user", session({ id: "ses_with_user", status: "idle", updatedAt: "2026-06-18T00:00:00.000Z", messages: [{ role: "user", text: "keep" }] })], ["ses_running", session({ id: "ses_running", status: "running", updatedAt: "2026-06-18T00:00:00.000Z" })], ["ses_trace", session({ id: "ses_trace", status: "idle", updatedAt: "2026-06-18T00:00:00.000Z", lastTraceId: "trc_keep" })], ["ses_completed", session({ id: "ses_completed", status: "completed", updatedAt: "2026-06-18T00:00:00.000Z" })] ]); const store = { async listAgentSessionsForUser(input = {}) { assert.equal(input.ownerScoped, false); assert.equal(input.actorRole, "admin"); return [...sessions.values()].filter((item) => item.status !== "archived"); }, async archiveAgentConversation(input = {}) { assert.equal(input.ownerScoped, false); assert.equal(input.actorRole, "admin"); const current = sessions.get(input.sessionId); if (!current || current.status === "archived") return { count: 0 }; sessions.set(input.sessionId, { ...current, status: "archived", endedAt: input.now, updatedAt: input.now }); return { count: 1 }; } }; const result = await runWorkbenchEmptySessionGc({ store, ttlMs: 10 * 60 * 1000, now: "2026-06-18T00:20:00.000Z" }); assert.equal(result.ok, true); assert.deepEqual(result.archivedSessionIds, ["ses_empty_old"]); assert.equal(sessions.get("ses_empty_old")?.status, "archived"); assert.equal(sessions.get("ses_empty_fresh")?.status, "idle"); assert.equal(sessions.get("ses_with_user")?.status, "idle"); assert.equal(sessions.get("ses_running")?.status, "running"); assert.equal(sessions.get("ses_trace")?.status, "idle"); assert.equal(sessions.get("ses_completed")?.status, "completed"); assert.equal(result.keptByReason.ttl_not_elapsed, 1); assert.equal(result.keptByReason.has_messages, 1); assert.equal(result.keptByReason.running_or_admitting, 1); assert.equal(result.keptByReason.has_trace_or_terminal_evidence, 1); assert.equal(result.keptByReason.terminal_result_status, 1); }); test("workbench empty session GC classifier uses canonical activity timestamp", () => { const candidate = session({ id: "ses_touched", status: "idle", updatedAt: "2026-06-18T00:00:00.000Z", session: { createdAt: "2026-06-18T00:00:00.000Z", lastTouchedAt: "2026-06-18T00:19:00.000Z", messageCount: 0 } }); assert.deepEqual( classifyWorkbenchEmptySessionGcCandidate(candidate, { nowMs: Date.parse("2026-06-18T00:20:00.000Z"), ttlMs: 10 * 60 * 1000 }), { eligible: false, reason: "ttl_not_elapsed" } ); }); function session(input: any) { const messages = input.messages ?? []; return { id: input.id, projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: input.status, startedAt: input.startedAt ?? input.updatedAt, endedAt: null, ownerUserId: "usr_gc", conversationId: `cnv_${input.id.slice("ses_".length)}`, threadId: null, lastTraceId: input.lastTraceId ?? null, updatedAt: input.updatedAt, session: { source: "test", createdAt: input.startedAt ?? input.updatedAt, updatedAt: input.updatedAt, messageCount: messages.length, messages, ...(input.session ?? {}), valuesRedacted: true, secretMaterialStored: false } }; }