837da6e5de
- v2.3.0's tree reported 2.2.1, so every install saw an update forever; package.json, the lockfile, openapi.yaml, the CLAUDE.md marker and the snapshot now read 2.3.1 - A failed plugin-update step records the tail of the command's stdout and stderr plus its exit code or signal (failureText); plugin-bootstrap.js prints its refusal on stdout, which the verdict used to drop. Update all uses the same text - updates.test.js never applies an update: it skips when the checkout is behind a release (apply ran npm run setup and pruned dev dependencies) - Files tree lists gitignored files and folders, named in orange (addIgnored in git-diff.js); change-tree rows indent in whole steps
127 lines
4.2 KiB
JavaScript
127 lines
4.2 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 (t) => {
|
|
// Never let this test APPLY anything. When this checkout really is behind
|
|
// a release (or reports a version older than the newest tag), the route
|
|
// fast-forwards the repo, runs `npm run setup` — which under the shell's
|
|
// NODE_ENV=production prunes every dev dependency — and SIGTERMs the
|
|
// process to restart it. That happened once; ask first and stand down.
|
|
const status = await httpFetch("/api/updates/status");
|
|
if (status.body.update_available) {
|
|
t.skip(`this checkout is behind ${status.body.latest_release}; not applying it in a test`);
|
|
return;
|
|
}
|
|
// 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.
|
|
//
|
|
// The `plugin_managed` branch cannot fire from a checkout: it additionally
|
|
// requires plugin_marketplace/plugin_name, which only a path under
|
|
// ~/.claude/plugins/cache/ produces. A suite run from such a copy would get
|
|
// 200 + mode:"plugin" and spawn the update chain, so guard on the shape
|
|
// rather than the status code alone.
|
|
const res = await httpFetch("/api/updates/apply", {
|
|
method: "POST",
|
|
body: "{}",
|
|
});
|
|
assert.equal(res.body.install_kind, "checkout", "suite must run from a git checkout");
|
|
assert.equal(res.status, 409);
|
|
assert.equal(res.body.applied, false);
|
|
assert.ok(
|
|
["up_to_date", "not_fast_forwardable", "plugin_managed"].includes(res.body.reason),
|
|
`unexpected reason: ${res.body.reason}`
|
|
);
|
|
});
|
|
});
|