chore: v2.8.21 — update progress steps, clone in background, extra SSH keys inline, today's spend

- Plugin update reports its running step (plugin-update-progress.json →
  plugin_update_progress); a hidden update or clone leaves a progress chip.
- Clone here can run in the background.
- Git identity: extra SSH keys are added inline per host (no More keys tab).
- Users: spend today (viewer's local day, ?tz_offset=).
- Prettier on the files touched since v2.8.20.
This commit is contained in:
2026-10-23 10:44:11 +07:00
parent 62b4520507
commit 9dd97a369c
33 changed files with 906 additions and 393 deletions
+57 -1
View File
@@ -19,7 +19,12 @@ const os = require("os");
const path = require("path");
const { execFileSync } = require("child_process");
const { readLastPluginUpdate, schedulePluginUpdate, runtimeDir } = require("../lib/plugin-update");
const {
readLastPluginUpdate,
readPluginUpdateProgress,
schedulePluginUpdate,
runtimeDir,
} = require("../lib/plugin-update");
const HELPER = path.join(__dirname, "..", "..", "scripts", "plugin-update-helper.js");
@@ -84,6 +89,43 @@ describe("readLastPluginUpdate", () => {
});
});
describe("readPluginUpdateProgress", () => {
const write = (name, body) => {
const rt = path.join(tmpDir, name);
fs.mkdirSync(rt, { recursive: true });
if (body !== undefined) fs.writeFileSync(path.join(rt, "plugin-update-progress.json"), body);
return rt;
};
it("is null with no file and for malformed JSON", () => {
assert.equal(readPluginUpdateProgress(write("prog-none")), null);
assert.equal(readPluginUpdateProgress(write("prog-bad", "{nope")), null);
});
it("reads the running step", () => {
const started_at = new Date().toISOString();
const rt = write(
"prog-ok",
JSON.stringify({ step: "plugin_update", index: 2, total: 3, started_at })
);
assert.deepEqual(readPluginUpdateProgress(rt), {
step: "plugin_update",
index: 2,
total: 3,
started_at,
});
});
it("ignores a file left by a helper that died long ago", () => {
const old = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const rt = write(
"prog-stale",
JSON.stringify({ step: "bootstrap", index: 3, total: 3, started_at: old })
);
assert.equal(readPluginUpdateProgress(rt), null);
});
});
describe("schedulePluginUpdate", () => {
it("clears a stale verdict before spawning, so an old success can't be read as this run's result", () => {
// Redirect the runtime dir into the temp tree — this must not touch the
@@ -152,6 +194,20 @@ describe("plugin-update-helper name guard", () => {
assert.equal(v.step, "validate");
});
it("removes a progress record with its final verdict", () => {
const rt = path.join(tmpDir, "guard-progress");
fs.mkdirSync(rt, { recursive: true });
fs.writeFileSync(
path.join(rt, "plugin-update-progress.json"),
JSON.stringify({ step: "plugin_update", started_at: new Date().toISOString() })
);
execFileSync(process.execPath, [HELPER, "../evil", "agenthub", "/nonexistent/root", rt], {
stdio: "ignore",
timeout: 30_000,
});
assert.equal(fs.existsSync(path.join(rt, "plugin-update-progress.json")), false);
});
it("stamps every verdict with a finish time", () => {
// The failure mode this file exists to prevent is a helper that dies
// silently: the tab would poll a server that never returns and say nothing.