fix: event-loop freezes, plugin-install boot crash, tmux attach/scrollbar, Codex transcripts
- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js, workflow-stats.js); idx_events_session restored, idx_events_tool added - compat-sqlite gains function/pluck/readonly so a plugin install without a better-sqlite3 build boots again; antigravity-usage uses db.js's Database - install-hooks adds no checkout hooks while the agenthub plugin is enabled (events were doubled) - terminal attach runs the live tmux server's own binary; the pane scrollbar gets a visible 2-column style, and claude panes start with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll - Codex sessions' conversation in GET /api/sessions/:id/transcript - Codex/Antigravity sessions claimed by their workspace without hooks - UI-audit follow-ups, docs and OpenAPI
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @file settings-import.test.js
|
||||
* @description Restoring a backup: `GET /api/settings/export` writes every table
|
||||
* the restore reads (with the format stamp), and `POST /api/settings/import`
|
||||
* takes that file as a multipart upload, restores it without overwriting
|
||||
* anything, refuses a file that is not an export without echoing its text, and
|
||||
* refuses a cross-origin caller.
|
||||
* @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 fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const TEST_DB = path.join(os.tmpdir(), `settings-import-${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 upload(text, headers = {}) {
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([text], { type: "application/json" }), "backup.json");
|
||||
return fetch(`${BASE}/api/settings/import`, { method: "POST", body: form, headers });
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
server = await startServer(createApp(), 0);
|
||||
BASE = `http://127.0.0.1:${server.address().port}`;
|
||||
db.prepare("INSERT INTO sessions (id, name, status, started_at) VALUES (?, ?, ?, ?)").run(
|
||||
"imp-1",
|
||||
"Backed up",
|
||||
"completed",
|
||||
"2026-01-10T10:00:00.000Z"
|
||||
);
|
||||
db.prepare("INSERT INTO events (session_id, event_type, data) VALUES (?, ?, ?)").run(
|
||||
"imp-1",
|
||||
"Stop",
|
||||
"{}"
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (server) server.close();
|
||||
if (db) db.close();
|
||||
for (const f of [TEST_DB, TEST_DB + "-wal", TEST_DB + "-shm"]) {
|
||||
try {
|
||||
fs.unlinkSync(f);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("backup and restore", () => {
|
||||
let exported;
|
||||
|
||||
it("the export carries the format stamp and every table the restore reads", async () => {
|
||||
const res = await fetch(`${BASE}/api/settings/export`);
|
||||
assert.equal(res.status, 200);
|
||||
exported = await res.text();
|
||||
const bundle = JSON.parse(exported);
|
||||
assert.equal(bundle.format, "agenthub-export");
|
||||
assert.equal(bundle.version, 1);
|
||||
for (const t of [
|
||||
"sessions",
|
||||
"agents",
|
||||
"events",
|
||||
"token_usage",
|
||||
"workflows",
|
||||
"dashboard_runs",
|
||||
"alert_rules",
|
||||
"model_pricing",
|
||||
]) {
|
||||
assert.ok(Array.isArray(bundle[t]), `missing ${t}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("restores an uploaded export, skipping what is already there", async () => {
|
||||
let res = await upload(exported);
|
||||
let body = await res.json();
|
||||
assert.equal(res.status, 200, JSON.stringify(body));
|
||||
assert.equal(body.sessions_imported, 0);
|
||||
assert.equal(body.sessions_skipped, 1);
|
||||
|
||||
db.prepare("DELETE FROM events WHERE session_id = 'imp-1'").run();
|
||||
db.prepare("DELETE FROM sessions WHERE id = 'imp-1'").run();
|
||||
res = await upload(exported);
|
||||
body = await res.json();
|
||||
assert.equal(res.status, 200, JSON.stringify(body));
|
||||
assert.equal(body.sessions_imported, 1);
|
||||
assert.equal(body.events, 1);
|
||||
assert.ok(db.prepare("SELECT 1 FROM sessions WHERE id = 'imp-1'").get());
|
||||
});
|
||||
|
||||
it("refuses a file that is not an export, without echoing its text", async () => {
|
||||
const res = await upload("root:x:0:0:secret-line");
|
||||
const body = await res.json();
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal(body.error.code, "INVALID_FORMAT");
|
||||
assert.doesNotMatch(JSON.stringify(body), /secret-line/);
|
||||
|
||||
const notBundle = await upload(JSON.stringify({ foo: 1 }));
|
||||
assert.equal(notBundle.status, 400);
|
||||
});
|
||||
|
||||
it("asks for a file", async () => {
|
||||
const res = await fetch(`${BASE}/api/settings/import`, { method: "POST" });
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal((await res.json()).error.code, "NO_FILE");
|
||||
});
|
||||
|
||||
it("refuses a cross-origin caller", async () => {
|
||||
const res = await upload(exported, { Origin: "https://evil.example" });
|
||||
assert.equal(res.status, 403);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user