test: cover beta fallback update logic
This commit is contained in:
@@ -27,6 +27,7 @@ vi.mock("../infra/update-check.js", async () => {
|
||||
...actual,
|
||||
checkUpdateStatus: vi.fn(),
|
||||
fetchNpmTagVersion: vi.fn(),
|
||||
resolveNpmChannelTag: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -38,11 +39,6 @@ vi.mock("../commands/doctor.js", () => ({
|
||||
vi.mock("./daemon-cli.js", () => ({
|
||||
runDaemonRestart: vi.fn(),
|
||||
}));
|
||||
// Mock plugin update helpers
|
||||
vi.mock("../plugins/update.js", () => ({
|
||||
syncPluginsForUpdateChannel: vi.fn(),
|
||||
updateNpmInstalledPlugins: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the runtime
|
||||
vi.mock("../runtime.js", () => ({
|
||||
@@ -78,15 +74,18 @@ describe("update-cli", () => {
|
||||
vi.clearAllMocks();
|
||||
const { resolveClawdbotPackageRoot } = await import("../infra/clawdbot-root.js");
|
||||
const { readConfigFileSnapshot } = await import("../config/config.js");
|
||||
const { checkUpdateStatus, fetchNpmTagVersion } = await import("../infra/update-check.js");
|
||||
const { syncPluginsForUpdateChannel, updateNpmInstalledPlugins } =
|
||||
await import("../plugins/update.js");
|
||||
const { checkUpdateStatus, fetchNpmTagVersion, resolveNpmChannelTag } =
|
||||
await import("../infra/update-check.js");
|
||||
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue(process.cwd());
|
||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
|
||||
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "9999.0.0",
|
||||
});
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "9999.0.0",
|
||||
});
|
||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||
root: "/test/path",
|
||||
installKind: "git",
|
||||
@@ -112,16 +111,6 @@ describe("update-cli", () => {
|
||||
latestVersion: "1.2.3",
|
||||
},
|
||||
});
|
||||
vi.mocked(syncPluginsForUpdateChannel).mockResolvedValue({
|
||||
config: baseSnapshot.config,
|
||||
changed: false,
|
||||
summary: { switchedToBundled: [], switchedToNpm: [], warnings: [], errors: [] },
|
||||
});
|
||||
vi.mocked(updateNpmInstalledPlugins).mockResolvedValue({
|
||||
config: baseSnapshot.config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
setTty(false);
|
||||
setStdoutTty(false);
|
||||
});
|
||||
@@ -163,25 +152,6 @@ describe("update-cli", () => {
|
||||
expect(defaultRuntime.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updateCommand syncs plugins after a successful update", async () => {
|
||||
const { runGatewayUpdate } = await import("../infra/update-runner.js");
|
||||
const { syncPluginsForUpdateChannel, updateNpmInstalledPlugins } =
|
||||
await import("../plugins/update.js");
|
||||
const { updateCommand } = await import("./update-cli.js");
|
||||
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
||||
status: "ok",
|
||||
mode: "git",
|
||||
steps: [],
|
||||
durationMs: 100,
|
||||
});
|
||||
|
||||
await updateCommand({});
|
||||
|
||||
expect(syncPluginsForUpdateChannel).toHaveBeenCalled();
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updateStatusCommand prints table output", async () => {
|
||||
const { defaultRuntime } = await import("../runtime.js");
|
||||
const { updateStatusCommand } = await import("./update-cli.js");
|
||||
@@ -274,6 +244,47 @@ describe("update-cli", () => {
|
||||
expect(call?.channel).toBe("beta");
|
||||
});
|
||||
|
||||
it("falls back to latest when beta tag is older than release", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-update-"));
|
||||
try {
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, "package.json"),
|
||||
JSON.stringify({ name: "clawdbot", version: "2026.1.18-1" }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { resolveClawdbotPackageRoot } = await import("../infra/clawdbot-root.js");
|
||||
const { readConfigFileSnapshot } = await import("../config/config.js");
|
||||
const { resolveNpmChannelTag } = await import("../infra/update-check.js");
|
||||
const { runGatewayUpdate } = await import("../infra/update-runner.js");
|
||||
const { updateCommand } = await import("./update-cli.js");
|
||||
|
||||
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue(tempDir);
|
||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
config: { update: { channel: "beta" } },
|
||||
});
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.1.20-1",
|
||||
});
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
||||
status: "ok",
|
||||
mode: "npm",
|
||||
steps: [],
|
||||
durationMs: 100,
|
||||
});
|
||||
|
||||
await updateCommand({});
|
||||
|
||||
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||
expect(call?.channel).toBe("beta");
|
||||
expect(call?.tag).toBe("latest");
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("honors --tag override", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-update-"));
|
||||
try {
|
||||
@@ -443,13 +454,13 @@ describe("update-cli", () => {
|
||||
);
|
||||
|
||||
const { resolveClawdbotPackageRoot } = await import("../infra/clawdbot-root.js");
|
||||
const { fetchNpmTagVersion } = await import("../infra/update-check.js");
|
||||
const { resolveNpmChannelTag } = await import("../infra/update-check.js");
|
||||
const { runGatewayUpdate } = await import("../infra/update-runner.js");
|
||||
const { defaultRuntime } = await import("../runtime.js");
|
||||
const { updateCommand } = await import("./update-cli.js");
|
||||
|
||||
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue(tempDir);
|
||||
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
47
src/infra/update-check.test.ts
Normal file
47
src/infra/update-check.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { resolveNpmChannelTag } from "./update-check.js";
|
||||
|
||||
describe("resolveNpmChannelTag", () => {
|
||||
let versionByTag: Record<string, string | null>;
|
||||
|
||||
beforeEach(() => {
|
||||
versionByTag = {};
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const tag = decodeURIComponent(url.split("/").pop() ?? "");
|
||||
const version = versionByTag[tag] ?? null;
|
||||
return {
|
||||
ok: version != null,
|
||||
status: version != null ? 200 : 404,
|
||||
json: async () => ({ version }),
|
||||
} as Response;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("falls back to latest when beta is older", async () => {
|
||||
versionByTag.beta = "2026.1.19-beta.1";
|
||||
versionByTag.latest = "2026.1.20-1";
|
||||
|
||||
const resolved = await resolveNpmChannelTag({ channel: "beta", timeoutMs: 1000 });
|
||||
|
||||
expect(resolved).toEqual({ tag: "latest", version: "2026.1.20-1" });
|
||||
});
|
||||
|
||||
it("keeps beta when beta is not older", async () => {
|
||||
versionByTag.beta = "2026.1.20-beta.1";
|
||||
versionByTag.latest = "2026.1.20-1";
|
||||
|
||||
const resolved = await resolveNpmChannelTag({ channel: "beta", timeoutMs: 1000 });
|
||||
|
||||
expect(resolved).toEqual({ tag: "beta", version: "2026.1.20-beta.1" });
|
||||
});
|
||||
});
|
||||
@@ -89,6 +89,42 @@ describe("runGatewayUpdate", () => {
|
||||
expect(calls.some((call) => call.includes("rebase --abort"))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses stable tag when beta tag is older than release", async () => {
|
||||
await fs.mkdir(path.join(tempDir, ".git"));
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, "package.json"),
|
||||
JSON.stringify({ name: "clawdbot", version: "1.0.0", packageManager: "pnpm@8.0.0" }),
|
||||
"utf-8",
|
||||
);
|
||||
const stableTag = "v2026.1.20-1";
|
||||
const betaTag = "v2026.1.19-beta.2";
|
||||
const { runner, calls } = createRunner({
|
||||
[`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir },
|
||||
[`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" },
|
||||
[`git -C ${tempDir} status --porcelain`]: { stdout: "" },
|
||||
[`git -C ${tempDir} fetch --all --prune --tags`]: { stdout: "" },
|
||||
[`git -C ${tempDir} tag --list v* --sort=-v:refname`]: {
|
||||
stdout: `${stableTag}\n${betaTag}\n`,
|
||||
},
|
||||
[`git -C ${tempDir} checkout --detach ${stableTag}`]: { stdout: "" },
|
||||
"pnpm install": { stdout: "" },
|
||||
"pnpm build": { stdout: "" },
|
||||
"pnpm ui:build": { stdout: "" },
|
||||
"pnpm clawdbot doctor --non-interactive": { stdout: "" },
|
||||
});
|
||||
|
||||
const result = await runGatewayUpdate({
|
||||
cwd: tempDir,
|
||||
runCommand: async (argv, _options) => runner(argv),
|
||||
timeoutMs: 5000,
|
||||
channel: "beta",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(calls).toContain(`git -C ${tempDir} checkout --detach ${stableTag}`);
|
||||
expect(calls).not.toContain(`git -C ${tempDir} checkout --detach ${betaTag}`);
|
||||
});
|
||||
|
||||
it("skips update when no git root", async () => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, "package.json"),
|
||||
|
||||
@@ -15,6 +15,7 @@ vi.mock("./update-check.js", async () => {
|
||||
...actual,
|
||||
checkUpdateStatus: vi.fn(),
|
||||
fetchNpmTagVersion: vi.fn(),
|
||||
resolveNpmChannelTag: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -43,7 +44,7 @@ describe("update-startup", () => {
|
||||
|
||||
it("logs update hint for npm installs when newer tag exists", async () => {
|
||||
const { resolveClawdbotPackageRoot } = await import("./clawdbot-root.js");
|
||||
const { checkUpdateStatus, fetchNpmTagVersion } = await import("./update-check.js");
|
||||
const { checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js");
|
||||
const { runGatewayUpdateCheck } = await import("./update-startup.js");
|
||||
|
||||
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue("/opt/clawdbot");
|
||||
@@ -52,7 +53,7 @@ describe("update-startup", () => {
|
||||
installKind: "package",
|
||||
packageManager: "npm",
|
||||
} satisfies UpdateCheckResult);
|
||||
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2.0.0",
|
||||
});
|
||||
@@ -75,6 +76,40 @@ describe("update-startup", () => {
|
||||
expect(parsed.lastNotifiedVersion).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("uses latest when beta tag is older than release", async () => {
|
||||
const { resolveClawdbotPackageRoot } = await import("./clawdbot-root.js");
|
||||
const { checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js");
|
||||
const { runGatewayUpdateCheck } = await import("./update-startup.js");
|
||||
|
||||
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue("/opt/clawdbot");
|
||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||
root: "/opt/clawdbot",
|
||||
installKind: "package",
|
||||
packageManager: "npm",
|
||||
} satisfies UpdateCheckResult);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
const log = { info: vi.fn() };
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: { update: { channel: "beta" } },
|
||||
log,
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
});
|
||||
|
||||
expect(log.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining("update available (latest): v2.0.0"),
|
||||
);
|
||||
|
||||
const statePath = path.join(tempDir, "update-check.json");
|
||||
const raw = await fs.readFile(statePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { lastNotifiedTag?: string };
|
||||
expect(parsed.lastNotifiedTag).toBe("latest");
|
||||
});
|
||||
|
||||
it("skips update check when disabled in config", async () => {
|
||||
const { runGatewayUpdateCheck } = await import("./update-startup.js");
|
||||
const log = { info: vi.fn() };
|
||||
|
||||
Reference in New Issue
Block a user