server/claude-update.test.ts
Rama main · réplica de lectura del repositorio original.
import type { ExecFileOptions } from "node:child_process";
import { describe, expect, it } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { CLAUDE_CODE_PACKAGE, isAppInstalledClaude, updateClaudeCli, type AppCopy } from "./claude-update.ts";
import { ClaudeDriver } from "./drivers/claude.ts";
import { npmPackageOf } from "./engine-install.ts";
import { registerPathDir, resetPathCacheForTests } from "./env-path.ts";
type Callback = (error: Error | null, stdout: string, stderr?: string) => void;
const noAppCopy: AppCopy = { owns: () => false, reinstall: async () => { throw new Error("not the app's copy"); } };
describe("updateClaudeCli", () => {
it("runs the official updater before verifying the installed version", async () => {
const calls: Array<{ cli: string; args: string[]; options: ExecFileOptions }> = [];
const execute = (cli: string, args: string[], options: ExecFileOptions, callback: Callback) => {
calls.push({ cli, args, options });
callback(null, args[0] === "--version" ? "2.1.257 (Claude Code)\n" : "updated\n");
};
await expect(updateClaudeCli("/opt/claude", { PATH: "/bin" }, execute, noAppCopy)).resolves.toEqual({
version: "2.1.257 (Claude Code)",
});
expect(calls.map((call) => call.args)).toEqual([["update"], ["--version"]]);
expect(calls[0].options).toMatchObject({ timeout: 180_000, killSignal: "SIGKILL" });
expect(calls[0].options.env).toEqual({ PATH: "/bin" });
});
it("does not probe the version after a failed update and gives a manual fallback", async () => {
let calls = 0;
const execute = (_cli: string, _args: string[], _options: ExecFileOptions, callback: Callback) => {
calls += 1;
const error = Object.assign(new Error("exit 1"), { code: 1, stderr: "permission denied by updater\nmore" });
callback(error, "", "permission denied by updater");
};
await expect(updateClaudeCli("claude", {}, execute, noAppCopy)).rejects.toThrow(
"Claude update failed: permission denied by updater. Run `claude update` in Terminal",
);
expect(calls).toBe(1);
});
it("reports when the update finishes but version verification fails", async () => {
const execute = (_cli: string, args: string[], _options: ExecFileOptions, callback: Callback) => {
if (args[0] === "update") callback(null, "updated");
else callback(Object.assign(new Error("spawn failed"), { code: "ENOENT" }), "");
};
await expect(updateClaudeCli("claude", {}, execute, noAppCopy)).rejects.toThrow(
"Claude finished updating, but OpenMausBot could not verify",
);
});
it("updates the copy the app installed through npm, not `claude update`, and reports that copy's version", async () => {
const calls: string[][] = [];
const reinstalled: Array<{ cli: string; env: NodeJS.ProcessEnv }> = [];
const execute = (_cli: string, args: string[], _options: ExecFileOptions, callback: Callback) => {
calls.push(args);
callback(null, "2.1.295 (Claude Code)\n");
};
const own: AppCopy = { owns: () => true, reinstall: async (cli, env) => { reinstalled.push({ cli, env }); } };
await expect(updateClaudeCli("claude", { PATH: "/engines" }, execute, own)).resolves.toEqual({ version: "2.1.295 (Claude Code)" });
expect(reinstalled).toEqual([{ cli: "claude", env: { PATH: "/engines" } }]);
expect(calls).toEqual([["--version"]]);
});
it("reports an npm failure on the app's copy without probing the version", async () => {
let calls = 0;
const execute = (_cli: string, _args: string[], _options: ExecFileOptions, callback: Callback) => {
calls += 1;
callback(null, "2.1.272 (Claude Code)\n");
};
const own: AppCopy = { owns: () => true, reinstall: async () => { throw new Error("npm could not install @anthropic-ai/claude-code on this server."); } };
await expect(updateClaudeCli("claude", {}, execute, own)).rejects.toThrow(
"Claude update failed: npm could not install @anthropic-ai/claude-code on this server.",
);
expect(calls).toBe(0);
});
});
describe("isAppInstalledClaude", () => {
it("is true only when the claude a turn runs is the one in the app's engines folder", () => {
const root = mkdtempSync(join(tmpdir(), "omb-claude-update-"));
try {
const engines = join(root, "tools", "npm", "bin");
const other = join(root, "elsewhere");
mkdirSync(engines, { recursive: true });
mkdirSync(other, { recursive: true });
// Windows discovery follows PATHEXT; an extensionless POSIX stub is not a runnable CLI there.
const name = process.platform === "win32" ? "claude.cmd" : "claude";
writeFileSync(join(engines, name), process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n", { mode: 0o755 });
resetPathCacheForTests();
registerPathDir(engines);
expect(isAppInstalledClaude("claude", engines)).toBe(true);
expect(isAppInstalledClaude("claude", other)).toBe(false);
expect(isAppInstalledClaude("/opt/homebrew/bin/claude", engines)).toBe(false);
expect(isAppInstalledClaude("/usr/local/bin/ag claude agp", engines)).toBe(false);
} finally {
resetPathCacheForTests();
rmSync(root, { recursive: true, force: true });
}
});
it("names the same package ClaudeDriver installs", () => {
expect(npmPackageOf(ClaudeDriver.install)).toBe(CLAUDE_CODE_PACKAGE);
});
});