fix(updates): report a plugin build installed beside the running one, never "up to date" on a failed check

A `claude plugin update` run from a terminal (or with only the Claude desktop
app open) unpacks the new build and starts nothing: no SessionStart fires, so
the dashboard kept running the old cache dir across reboots, and with the
forge unreachable the sidebar read "Up to date" (seen on a machine stuck at
1.8.10).

- pluginStatus asks supersededBy for its own root: a recorded sibling install
  sets `installed_not_running: {root, version}` and `update_available`, with
  no network needed
- the sidebar reads "<version> installed, not running"; the update modal keys
  on it, so Update now is offered even with no release tag or sha
- plugin-update-helper hands over to the recorded install first, the newest
  sibling dir only as a fallback
- a plugin install whose release lookup failed reads "Could not check for
  updates" instead of "Up to date"
- docs/PLUGINS.md: the case and the manual hand-over command
This commit is contained in:
2026-10-01 10:09:12 +07:00
parent 969b06253c
commit 058c574138
14 changed files with 208 additions and 27 deletions
@@ -5,10 +5,11 @@
*/
import { describe, it, expect } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { Sidebar } from "../Sidebar";
import { eventBus } from "../../lib/eventBus";
function renderSidebar(wsConnected: boolean, collapsed = false) {
return render(
@@ -103,4 +104,44 @@ describe("Sidebar", () => {
expect(screen.getByText("VI")).toBeInTheDocument();
});
});
describe("update status line", () => {
const plugin = {
git_repo: false,
install_kind: "plugin",
situation: "plugin_managed",
local_version: "1.8.10",
};
const publish = (data: object) =>
act(() => eventBus.publish({ type: "update_status", data } as never));
it("does not claim up to date when a plugin install could not read the release", () => {
renderSidebar(true);
publish({ ...plugin, update_available: false, release_check: "unreachable" });
expect(screen.queryByText("Up to date")).toBeNull();
expect(screen.getByText("Could not check for updates")).toBeInTheDocument();
});
it("names a build installed beside the running one", () => {
renderSidebar(true);
publish({
...plugin,
update_available: true,
release_check: "unreachable",
installed_not_running: { root: "/x", version: "1.8.12" },
});
expect(screen.getByText("1.8.12 installed, not running")).toBeInTheDocument();
});
it("still says up to date when the release was read and matches", () => {
renderSidebar(true);
publish({
...plugin,
update_available: false,
release_check: "ok",
latest_release: "v1.8.10",
});
expect(screen.getByText("Up to date")).toBeInTheDocument();
});
});
});