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,