Files
Claude-Code-Monitor/server/__tests__/updates.test.js
T
nntrivi2001 8e49d2b300 feat(updates): auto-apply and self-restart on new dashboard versions
Adds POST /api/updates/apply (pull, rebuild, restart) plus an "Update now"
button and hourly auto-check in the UI, for checkouts that are safely
fast-forwardable. Reintroduces self-restart (previously removed in edc25ca
for cross-environment reliability concerns) per explicit user request.
2026-08-18 16:04:27 +07:00

107 lines
3.1 KiB
JavaScript

/**
* @file Tests for dashboard self-update HTTP endpoints.
* @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 http = require("http");
const TEST_DB = path.join(os.tmpdir(), `dashboard-updates-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const { createApp, startServer } = require("../index");
const { db } = require("../db");
let server;
let BASE;
function httpFetch(urlPath, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(urlPath, BASE);
const opts = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: options.method || "GET",
headers: { "Content-Type": "application/json", ...options.headers },
};
const req = http.request(opts, (res) => {
let body = "";
res.on("data", (chunk) => {
body += chunk;
});
res.on("end", () => {
let parsed;
try {
parsed = JSON.parse(body);
} catch {
parsed = body;
}
resolve({ status: res.statusCode, body: parsed });
});
});
req.on("error", reject);
if (options.body) req.write(options.body);
req.end();
});
}
before(async () => {
const app = createApp();
server = await startServer(app, 0);
const addr = server.address();
BASE = `http://127.0.0.1:${addr.port}`;
});
after(() => {
if (server) server.close();
if (db) db.close();
try {
fs.unlinkSync(TEST_DB);
fs.unlinkSync(`${TEST_DB}-wal`);
fs.unlinkSync(`${TEST_DB}-shm`);
} catch {
// ignore
}
});
describe("GET /api/updates/status", () => {
it("returns update check payload", async () => {
const res = await httpFetch("/api/updates/status");
assert.equal(res.status, 200);
assert.equal(typeof res.body.git_repo, "boolean");
assert.equal(typeof res.body.update_available, "boolean");
if (res.body.git_repo) {
assert.ok(typeof res.body.repo_root === "string");
}
});
});
describe("POST /api/updates/check", () => {
it("returns a fresh update status payload", async () => {
const res = await httpFetch("/api/updates/check", { method: "POST", body: "{}" });
assert.equal(res.status, 200);
assert.equal(typeof res.body.git_repo, "boolean");
assert.equal(typeof res.body.update_available, "boolean");
});
});
describe("POST /api/updates/apply", () => {
it("declines with a reason instead of pulling/restarting when there is nothing to apply", async () => {
// The test repo checkout has no update pending (or isn't fast-forwardable
// from a bare test run), so this exercises the decline path only — it must
// never touch the working tree or process in a test run.
const res = await httpFetch("/api/updates/apply", {
method: "POST",
body: "{}",
});
assert.equal(res.status, 409);
assert.equal(res.body.applied, false);
assert.ok(["up_to_date", "not_fast_forwardable"].includes(res.body.reason));
});
});