/** * @file Tests for POST /api/settings/cleanup's input rules and dry run: * `purge_days` must be a whole number of days ≥ 1 (a fraction such as 0.1 * would purge everything older than ~2 hours) and `dry_run: true` reports what * would be abandoned/purged without changing a row. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const path = require("path"); const os = require("os"); const fs = require("fs"); const http = require("http"); const TEST_DB = path.join(os.tmpdir(), `dashboard-cleanup-test-${Date.now()}-${process.pid}.db`); process.env.DASHBOARD_DB_PATH = TEST_DB; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); const { db } = require("../db"); let server; let BASE; function post(urlPath, body) { return new Promise((resolve, reject) => { const url = new URL(urlPath, BASE); const req = http.request( { hostname: url.hostname, port: url.port, path: url.pathname, method: "POST", headers: { "Content-Type": "application/json" }, }, (res) => { let text = ""; res.on("data", (c) => (text += c)); res.on("end", () => resolve({ status: res.statusCode, body: JSON.parse(text) })); } ); req.on("error", reject); req.write(JSON.stringify(body)); req.end(); }); } const OLD = new Date(Date.now() - 400 * 86400 * 1000).toISOString(); before(async () => { server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; db.prepare( "INSERT INTO sessions (id, name, status, cwd, started_at, updated_at) VALUES (?, ?, 'completed', '/tmp', ?, ?)" ).run("old-done", "old", OLD, OLD); }); after(() => { if (server) server.close(); db.close(); for (const s of ["", "-wal", "-shm"]) { try { fs.unlinkSync(TEST_DB + s); } catch { /* ignore */ } } }); describe("POST /api/settings/cleanup", () => { for (const bad of [0.1, 0, -3, 1.5, "30"]) { it(`refuses purge_days=${JSON.stringify(bad)} with 400`, async () => { const r = await post("/api/settings/cleanup", { purge_days: bad }); assert.equal(r.status, 400); assert.equal(r.body.error.code, "INVALID_PURGE_DAYS"); }); } it("dry_run counts what would be purged and deletes nothing", async () => { const r = await post("/api/settings/cleanup", { purge_days: 30, dry_run: true }); assert.equal(r.status, 200); assert.equal(r.body.dry_run, true); assert.equal(r.body.purged_sessions, 1); assert.ok(db.prepare("SELECT 1 FROM sessions WHERE id = 'old-done'").get()); }); it("a real run purges", async () => { const r = await post("/api/settings/cleanup", { purge_days: 30 }); assert.equal(r.status, 200); assert.equal(r.body.purged_sessions, 1); assert.equal(db.prepare("SELECT 1 FROM sessions WHERE id = 'old-done'").get(), undefined); }); it("with abandon AND purge, the dry run counts the sessions the real run abandons then purges", async () => { // Still 'active' but silent for 400 days: a real run abandons it first, // then the purge (completed/error/abandoned) deletes it. db.prepare( "INSERT INTO sessions (id, name, status, cwd, started_at, updated_at) VALUES (?, ?, 'active', '/tmp', ?, ?)" ).run("old-stale", "stale", OLD, OLD); db.prepare( "INSERT INTO agents (id, session_id, name, status, started_at) VALUES ('old-stale-a', 'old-stale', 'main', 'working', ?)" ).run(OLD); db.prepare( "INSERT INTO events (session_id, event_type, created_at) VALUES ('old-stale', 'PreToolUse', ?)" ).run(OLD); const body = { abandon_hours: 1, purge_days: 30 }; const dry = await post("/api/settings/cleanup", { ...body, dry_run: true }); assert.equal(dry.status, 200); const row = db.prepare("SELECT status FROM sessions WHERE id = 'old-stale'").get(); assert.equal(row.status, "active"); const real = await post("/api/settings/cleanup", body); assert.equal(real.status, 200); assert.equal(real.body.purged_sessions, 1); for (const k of ["abandoned", "purged_sessions", "purged_events", "purged_agents"]) { assert.equal(dry.body[k], real.body[k], k); } }); });