From 18614f378d68e5c6d79abcd7725e249252013c03 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 11 Sep 2026 15:54:03 +0700 Subject: [PATCH] fix(updates): give a plugin-cache install a real message, not null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agenthub update-check` prints `message` as its headline, so every plugin-cache install saw the literal "null" where the verdict belongs — and that is the one install kind with no commit count to fall back on, so the line carried nothing at all. The plugin branch of getUpdatesStatus compares versions exactly like the checkout branch does; it now builds the same four sentences from what it already computed (newer release / newest / no local version to compare / release lookup failed). The nogit regression test asserted `message === null` as a proxy for "did not fall through to the not-a-git-clone payload" — it now asserts that on the message's content instead, which is what it always meant. --- .../update-check-plugin-nogit.test.js | 36 +++++++++++++++++-- server/lib/update-check.js | 12 ++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/server/__tests__/update-check-plugin-nogit.test.js b/server/__tests__/update-check-plugin-nogit.test.js index 091e8dd7..4fe1a4c5 100644 --- a/server/__tests__/update-check-plugin-nogit.test.js +++ b/server/__tests__/update-check-plugin-nogit.test.js @@ -176,8 +176,15 @@ describe("getUpdatesStatus — plugin cache with no .git", () => { const s = await getUpdatesStatus(root, { latestRelease: release("v1.5.1") }); - // The old behaviour: message set, everything else absent. - assert.equal(s.message, null, "must not fall through to the not-a-git-clone payload"); + // The old behaviour: the not-a-git-clone message, everything else absent. + // Asserted on the message's CONTENT, not on it being null — a plugin + // install now carries its own sentence, and null was only ever a proxy for + // "did not fall through here". + assert.doesNotMatch( + s.message, + /not a git clone/i, + "must not fall through to the not-a-git-clone payload" + ); assert.equal(s.release_check, "ok", "the release layer must actually run"); assert.equal(s.install_kind, "plugin"); assert.equal(s.plugin_marketplace, "sg"); @@ -208,6 +215,31 @@ describe("getUpdatesStatus — plugin cache with no .git", () => { assert.ok(s.plugin_marketplace && s.plugin_name && s.repo_root); }); + it("carries a human-readable message in every outcome, never a null", async () => { + // `agenthub update-check` prints `message` as its headline. Null here made + // it print the literal "null" on every plugin-cache install — the one kind + // that has no commit count to fall back on. + const cases = [ + [makePluginInstall("msg-newer").root, { latestRelease: release("v1.6.0") }, /v1\.6\.0/], + [makePluginInstall("msg-current").root, { latestRelease: release("v1.5.0") }, /newest/i], + [ + makePluginInstall("msg-failed").root, + { latestRelease: { ok: false, reason: "unreachable" } }, + /unreachable/, + ], + [ + makePluginInstall("msg-noversion", { version: null }).root, + { latestRelease: release("v1.6.0") }, + /no version|cannot be compared/i, + ], + ]; + for (const [root, opts, expected] of cases) { + const s = await getUpdatesStatus(root, opts); + assert.equal(typeof s.message, "string", `message must be a string, got ${s.message}`); + assert.match(s.message, expected); + } + }); + it("does not treat an older published release as an update", async () => { const { root } = makePluginInstall("older"); const s = await getUpdatesStatus(root, { latestRelease: release("v1.4.0") }); diff --git a/server/lib/update-check.js b/server/lib/update-check.js index 5ea04702..10c5287d 100644 --- a/server/lib/update-check.js +++ b/server/lib/update-check.js @@ -402,7 +402,17 @@ async function pluginStatus(root, pluginInstall, options = {}) { `commands — "Update now" runs them for you and restarts the dashboard. An already-open ` + `Claude Code session still needs '/reload-plugins' to pick up new skills and commands.`, manual_command: manualParts.join(" && "), - message: null, + // A plugin install compares versions exactly like a checkout does, so it + // gets the same sentence. Left null, `agenthub update-check` printed the + // literal "null" as its headline on every plugin-cache install — the one + // install kind that cannot fall back to a commit count. + message: releaseIsNewer + ? `Release ${release.tag} is available (this install reports ${localVersion || "no version"}).` + : comparable + ? `Running the newest release (${release.tag}).` + : release.ok + ? `This install reports no version, so it cannot be compared with release ${release.tag}.` + : `Could not read the latest release (${release.reason}); version comparison unavailable.`, install_kind: "plugin", plugin_marketplace: pluginInstall.marketplace,