mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
test(cli): isolate update backup state
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ba65687bc1
commit
3fb1ce83b5
2 files changed
+9
-25
No files matched your search
@@ -19,10 +19,6 @@ function createPayload(payloadPath: string, version: string): string {
|
||||
fs.writeFileSync(entrypoint, version);
|
||||
return entrypoint;
|
||||
}
|
||||
function markInstanceOnboarded(): void {
|
||||
fs.mkdirSync(path.join(process.env.PAPERCLIP_HOME!, "instances", "default"), { recursive: true });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-update-"));
|
||||
previousHome = process.env.HOME;
|
||||
@@ -66,7 +62,6 @@ describe("update command", () => {
|
||||
});
|
||||
|
||||
it("re-resolves a moving git branch and activates the new SHA payload", async () => {
|
||||
markInstanceOnboarded();
|
||||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const oldSha = "1".repeat(40); const newSha = "2".repeat(40);
|
||||
const oldPayload = payloadPathFor(paths, "git", oldSha.slice(0, 12));
|
||||
@@ -81,7 +76,7 @@ describe("update command", () => {
|
||||
const confirm = vi.fn(async () => true);
|
||||
const restartActiveService = vi.fn(async () => true);
|
||||
const runCommand = vi.fn(async (file: string) => file === "curl" ? { stdout: JSON.stringify({ sha: newSha }), stderr: "" } : { stdout: "0.3.1\n", stderr: "" });
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, confirm, restartActiveService, now: () => new Date("2026-07-22T12:00:00Z") });
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, confirm, restartActiveService, hasInstanceData: () => true, now: () => new Date("2026-07-22T12:00:00Z") });
|
||||
expect(confirm).toHaveBeenCalledWith(expect.stringContaining(`commit ${newSha.slice(0, 12)}`));
|
||||
expect(backup).toHaveBeenCalledOnce();
|
||||
expect(restartActiveService).toHaveBeenCalledWith("0.3.1");
|
||||
@@ -136,7 +131,6 @@ describe("update command", () => {
|
||||
});
|
||||
|
||||
it("backs up, installs side-by-side, flips, and rolls back instantly", async () => {
|
||||
markInstanceOnboarded();
|
||||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const oldPayload = payloadPathFor(paths, "npm", "1.0.0"); const executable = createPayload(oldPayload, "1.0.0"); flipCurrentAtomic(oldPayload, paths);
|
||||
writeInstallManifestAtomic({ schemaVersion: 1, ...record(oldPayload, "1.0.0"), previous: [] }, paths);
|
||||
@@ -147,7 +141,7 @@ describe("update command", () => {
|
||||
if (file === "npm" && args[0] === "install") { const prefix = args[args.indexOf("--prefix") + 1]; createPayload(prefix, "2.0.0"); return { stdout: "", stderr: "" }; }
|
||||
return { stdout: "2.0.0\n", stderr: "" };
|
||||
});
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, restartActiveService, now: () => new Date("2026-07-22T12:00:00Z") });
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, restartActiveService, hasInstanceData: () => true, now: () => new Date("2026-07-22T12:00:00Z") });
|
||||
expect(backup).toHaveBeenCalledOnce();
|
||||
expect(restartActiveService).toHaveBeenCalledWith("2.0.0");
|
||||
expect(readInstallManifest(paths)?.version).toBe("2.0.0");
|
||||
@@ -158,7 +152,6 @@ describe("update command", () => {
|
||||
});
|
||||
|
||||
it("explains how to recover when the pre-update database is unreachable", async () => {
|
||||
markInstanceOnboarded();
|
||||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const oldPayload = payloadPathFor(paths, "npm", "1.0.0"); const executable = createPayload(oldPayload, "1.0.0"); flipCurrentAtomic(oldPayload, paths);
|
||||
writeInstallManifestAtomic({ schemaVersion: 1, ...record(oldPayload, "1.0.0"), previous: [] }, paths);
|
||||
@@ -166,7 +159,7 @@ describe("update command", () => {
|
||||
const backup = vi.fn(async () => { throw backupError; });
|
||||
const runCommand = vi.fn(async () => ({ stdout: '"2.0.0"\n', stderr: "" }));
|
||||
|
||||
await expect(updateCommand({}, { paths, executablePath: executable, runCommand, backup })).rejects.toThrow(
|
||||
await expect(updateCommand({}, { paths, executablePath: executable, runCommand, backup, hasInstanceData: () => true })).rejects.toThrow(
|
||||
"Start the service with `paperclipai service start` and retry, or skip the backup with `paperclipai update --no-backup`.",
|
||||
);
|
||||
expect(backup).toHaveBeenCalledOnce();
|
||||
@@ -174,9 +167,6 @@ describe("update command", () => {
|
||||
});
|
||||
|
||||
it("skips the pre-update backup when there is no onboarded instance data", async () => {
|
||||
vi.stubEnv("PAPERCLIP_CONFIG", path.join(root, "missing-config.json"));
|
||||
vi.stubEnv("PAPERCLIP_INSTANCE_ID", "default");
|
||||
vi.stubEnv("DATABASE_URL", "");
|
||||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const oldPayload = payloadPathFor(paths, "npm", "1.0.0"); const executable = createPayload(oldPayload, "1.0.0"); flipCurrentAtomic(oldPayload, paths);
|
||||
writeInstallManifestAtomic({ schemaVersion: 1, ...record(oldPayload, "1.0.0"), previous: [] }, paths);
|
||||
@@ -188,13 +178,7 @@ describe("update command", () => {
|
||||
return { stdout: "2.0.0\n", stderr: "" };
|
||||
});
|
||||
|
||||
const previousCwd = process.cwd();
|
||||
process.chdir(root);
|
||||
try {
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, restartActiveService: async () => false });
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
}
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, restartActiveService: async () => false, hasInstanceData: () => false });
|
||||
|
||||
expect(backup).not.toHaveBeenCalled();
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("has not been onboarded and has no data to back up"));
|
||||
|
||||
@@ -17,7 +17,7 @@ import { packageVersion } from "../version.js";
|
||||
const execFileAsync = promisify(execFile);
|
||||
export type InstallMode = "managed" | "global-npm" | "npx" | "source" | "unknown";
|
||||
export type UpdateOptions = { canary?: boolean; latest?: boolean; version?: string; rollback?: boolean; check?: boolean; dryRun?: boolean; json?: boolean; yes?: boolean; backup?: boolean };
|
||||
type Dependencies = { executablePath: string; runCommand: CommandRunner; backup: () => Promise<void>; confirm: (message: string) => Promise<boolean>; now: () => Date; paths: InstallStorePaths; restartActiveService: (expectedVersion: string) => Promise<boolean> };
|
||||
type Dependencies = { executablePath: string; runCommand: CommandRunner; backup: () => Promise<void>; confirm: (message: string) => Promise<boolean>; now: () => Date; paths: InstallStorePaths; restartActiveService: (expectedVersion: string) => Promise<boolean>; hasInstanceData: () => boolean };
|
||||
|
||||
const DATABASE_UNREACHABLE_CODES = new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT"]);
|
||||
|
||||
@@ -48,8 +48,8 @@ function isDatabaseUnreachableError(error: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function runPreUpdateBackup(options: UpdateOptions, backup: () => Promise<void>): Promise<void> {
|
||||
if (!hasPaperclipInstanceData()) {
|
||||
async function runPreUpdateBackup(options: UpdateOptions, backup: () => Promise<void>, hasInstanceData = hasPaperclipInstanceData): Promise<void> {
|
||||
if (!hasInstanceData()) {
|
||||
const message = "Skipping the pre-update backup because this Paperclip instance has not been onboarded and has no data to back up.";
|
||||
if (options.json) console.error(message); else console.log(pc.yellow(message));
|
||||
return;
|
||||
@@ -191,7 +191,7 @@ export async function updateCommand(options: UpdateOptions, overrides: Partial<D
|
||||
const confirmed = await (overrides.confirm ?? defaultConfirm)(`Update from ${manifest.repo}@${manifest.ref} and execute build scripts from commit ${targetSha.slice(0, 12)}?`);
|
||||
if (!confirmed) throw new Error("Git update cancelled. Re-run with --yes to confirm executing build scripts from the updated commit.");
|
||||
}
|
||||
if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})));
|
||||
if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})), overrides.hasInstanceData);
|
||||
const installed = await withInstallStoreLock(async () => {
|
||||
const payload = await installGitPayload(manifest.repo!, targetSha, runCommand, paths);
|
||||
const record: InstallRecord = { source: "git", version: payload.version, channel: "pinned", repo: manifest.repo, ref: manifest.ref, sha: targetSha, payloadPath: payload.payloadPath, installedAt: (overrides.now?.() ?? new Date()).toISOString() };
|
||||
@@ -245,7 +245,7 @@ export async function updateCommand(options: UpdateOptions, overrides: Partial<D
|
||||
if (comparison === 0) { emit(options, { mode, currentVersion, targetVersion, changed: false }, `paperclipai ${targetVersion} is already active.`); return; }
|
||||
if (comparison < 0 && options.yes !== true) { const confirmed = await (overrides.confirm ?? defaultConfirm)(`Downgrade paperclipai from ${currentVersion} to ${targetVersion}?`); if (!confirmed) throw new Error("Downgrade cancelled. Re-run with --yes to confirm explicitly."); }
|
||||
if (options.dryRun) { emit(options, { mode, currentVersion, targetVersion, action: comparison < 0 ? "downgrade" : "update", backup: options.backup !== false, dryRun: true }, `Would ${comparison < 0 ? "downgrade" : "update"} paperclipai ${currentVersion} → ${targetVersion}${options.backup === false ? " without a backup" : " after a database backup"}.`); return; }
|
||||
if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})));
|
||||
if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})), overrides.hasInstanceData);
|
||||
const installed = await withInstallStoreLock(async () => {
|
||||
const payload = await installNpmPayload(targetVersion, runCommand, paths);
|
||||
const record: InstallRecord = { source: "npm", version: targetVersion, channel: request.channel, payloadPath: payload.payloadPath, installedAt: (overrides.now?.() ?? new Date()).toISOString() };
|
||||
|
||||
Reference in new issue
Block a user