95 lines
3.8 KiB
TypeScript
95 lines
3.8 KiB
TypeScript
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
|
|
}
|
|
};
|
|
}
|