fix(cli): make logicsrc update actually check for updates

`update` was three hardcoded console.log lines: it printed 0.1.0 as both
current and latest, claimed "already up to date", and never checked or
installed anything. `--version` was hardcoded the same way.

A version comparison alone could not have worked either. install.sh ships
a tarball of the master branch, not a tagged release, and
packages/cli/package.json has been 0.1.0 since the repo began, so version
equality says "up to date" no matter how far master has moved. The commit
is the real signal.

- install.sh records ref/commit/version/installed_at to
  $LOGICSRC_HOME/install.json. The sha comes from GitHub's
  Accept: application/vnd.github.sha media type, so this needs no jq.
  It is resolved before the download on purpose: if master moves
  mid-install we under-report (a spurious update) rather than falsely
  claim to be current.
- update compares the installed commit against the remote ref head,
  falls back to version comparison for installs predating the manifest,
  and reports why it reached its verdict instead of just asserting one.
  --check reports without installing; otherwise it re-runs the installer.
- --version now reads the package's real version.

Verified against live GitHub in all three states: matching commit, stale
commit, and no manifest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-29 06:15:28 +00:00
parent cf475f0f4c
commit 8c3c0bb12a
4 changed files with 339 additions and 8 deletions

View file

@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import { compareVersions, parseManifest, trackedRef, updateStatus } from "./update.js";
describe("compareVersions", () => {
it("orders releases numerically, not lexically", () => {
expect(compareVersions("0.2.0", "0.1.0")).toBe(1);
expect(compareVersions("0.1.0", "0.2.0")).toBe(-1);
expect(compareVersions("0.1.0", "0.1.0")).toBe(0);
// "10" > "9" numerically but sorts lower as a string.
expect(compareVersions("0.10.0", "0.9.0")).toBe(1);
expect(compareVersions("1.0.0", "0.99.99")).toBe(1);
});
it("tolerates v-prefixes, prereleases, and short versions", () => {
expect(compareVersions("v1.2.3", "1.2.3")).toBe(0);
expect(compareVersions("1.2.0-beta.1", "1.2.0")).toBe(0);
expect(compareVersions("1.2", "1.2.0")).toBe(0);
expect(compareVersions("garbage", "0.0.0")).toBe(0);
});
});
describe("parseManifest", () => {
it("reads a manifest written by install.sh", () => {
const m = parseManifest(
JSON.stringify({ ref: "master", commit: "abc1234def", version: "0.1.0", installed_at: "2026-07-28T00:00:00Z" })
);
expect(m).toEqual({ ref: "master", commit: "abc1234def", version: "0.1.0", installed_at: "2026-07-28T00:00:00Z" });
});
it("defaults the ref and nulls empty or missing fields", () => {
// install.sh writes empty strings when the sha lookup or version read fails.
expect(parseManifest(JSON.stringify({ commit: "", version: "" }))).toEqual({
ref: "master",
commit: null,
version: null,
installed_at: null
});
});
it("returns null for junk rather than throwing", () => {
expect(parseManifest("not json")).toBeNull();
expect(parseManifest("[]")).toBeNull();
expect(parseManifest("null")).toBeNull();
});
});
describe("trackedRef", () => {
it("prefers the environment, then the manifest, then master", () => {
const manifest = { ref: "next", commit: null, version: null, installed_at: null };
expect(trackedRef(manifest, { LOGICSRC_REF: "experiment" })).toBe("experiment");
expect(trackedRef(manifest, {})).toBe("next");
expect(trackedRef(null, {})).toBe("master");
});
});
describe("updateStatus", () => {
const local = { version: "0.1.0", commit: "aaaaaaaaaaaa" };
it("reports up to date only when the commit actually matches", () => {
const s = updateStatus(local, { version: "0.1.0", commit: "aaaaaaaaaaaa" });
expect(s.upToDate).toBe(true);
expect(s.reason).toContain("current commit");
});
it("matches a short sha against a full one", () => {
expect(updateStatus({ version: "0.1.0", commit: "aaaaaaa" }, { version: "0.1.0", commit: "aaaaaaaaaaaa" }).upToDate).toBe(true);
});
it("detects a moved branch even when the version is unchanged", () => {
// The bug this replaces: version-only comparison called this "up to date"
// forever, because the installer ships a branch tarball, not a release.
const s = updateStatus(local, { version: "0.1.0", commit: "bbbbbbbbbbbb" });
expect(s.upToDate).toBe(false);
expect(s.reason).toContain("moved on");
});
it("detects a newer published version", () => {
const s = updateStatus(local, { version: "0.2.0", commit: "aaaaaaaaaaaa" });
expect(s.upToDate).toBe(false);
expect(s.reason).toContain("0.1.0 → 0.2.0");
});
it("never claims to be current when the local commit is unknown", () => {
const s = updateStatus({ version: "0.1.0", commit: null }, { version: "0.1.0", commit: "bbbbbbbbbbbb" });
expect(s.upToDate).toBe(false);
expect(s.reason).toContain("predates update tracking");
});
it("does not invent an update when GitHub is unreachable", () => {
const s = updateStatus(local, { version: null, commit: null });
expect(s.upToDate).toBe(true);
expect(s.reason).toContain("could not reach GitHub");
});
});