6715fbd971
- EditorModal loads once per path (a parent re-render no longer reloads the file over the user's typing), refuses Save after a failed load, opens a truncated (>256 KB) file read-only, and asks before overwriting a file that changed on disk or could not be re-read. - Settings/Hooks editors unset only keys from the snapshot taken at Edit, warn when the file changed underneath, keep a renamed key's value, and refuse a new key named like a redacted one (env) or a read-only hook event, which used to replace the secret/hooks. - Project scope shows its resolved folder and is disabled inside the plugin cache. Account-profile, pricing-row and pet deletes confirm; two-click actions disarm after 3 s; unsaved drafts ask before closing. - Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and scrolls to + highlights the row; the active config tab is in the URL. - General splits into General / Data / About; Pricing moves to a Cost group with one shared load; config tabs group as Content / Extensions / Config with arrow-key roving; Promise.allSettled per tab. - Data: browser-preferences export/import (allowlisted keys), per-section reset, housekeeping dry-run first, clear-all behind a typed phrase. Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and dry_run returns exactly what the real run would delete. - Alerts can be edited; webhooks/alerts/remote-source failures toast; copies go through copyText; modals get dialog roles and useDialog. - Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of row controls, arrow keys move it, reduced motion honoured; the dead /run launch link goes to the workspace.
122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|