mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(cli): refuse undo when a target or backup is missing (#5365)
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
} from "@hyperframes/studio-server";
|
||||
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { join, relative } from "node:path";
|
||||
import { describeProject, type ProjectTimeline, type TimelineRow } from "./describeProject.js";
|
||||
import { resolveRef } from "./resolveRef.js";
|
||||
import { parseTimeExpression } from "./timeExpr.js";
|
||||
@@ -309,8 +309,25 @@ function readUndoInput(input: string): UndoInput {
|
||||
return { ok: true, receipts: values, batch: Array.isArray(value) };
|
||||
}
|
||||
|
||||
function refuseUndoError(error: unknown, projectDir: string, json: boolean): void {
|
||||
if (isRecord(error) && error.code === "ENOENT" && typeof error.path === "string")
|
||||
return refuse(
|
||||
"timeline undo",
|
||||
{
|
||||
reason: `undo file is missing: ${relative(projectDir, error.path).split("\\").join("/")}`,
|
||||
fix: "restore the missing target or backup file before retrying undo",
|
||||
},
|
||||
json,
|
||||
);
|
||||
if (isFileChanged(error))
|
||||
return refusal(error.message, "read the latest timeline before retrying undo", json);
|
||||
throw error;
|
||||
}
|
||||
|
||||
export async function runUndo(args: Record<string, unknown>): Promise<void> {
|
||||
const project = resolveProject(typeof args.dir === "string" ? args.dir : undefined);
|
||||
const project = resolveProject(typeof args.dir === "string" ? args.dir : undefined, {
|
||||
requireIndex: false,
|
||||
});
|
||||
const json = args.json === true;
|
||||
const input = typeof args.receipt === "string" ? args.receipt : positional(args)[1];
|
||||
if (!input)
|
||||
@@ -321,23 +338,21 @@ export async function runUndo(args: Record<string, unknown>): Promise<void> {
|
||||
);
|
||||
const undo = readUndoInput(input);
|
||||
if (!undo.ok) return refuse("timeline undo", undo, json);
|
||||
const inputs = undo.receipts.map((receipt) => {
|
||||
const target = join(project.dir, receipt.file);
|
||||
return {
|
||||
sourceFile: receipt.file,
|
||||
absPath: target,
|
||||
before: readFileSync(target, "utf-8"),
|
||||
after: readFileSync(join(project.dir, receipt.backupPath), "utf-8"),
|
||||
expectedVersion: receipt.version,
|
||||
};
|
||||
});
|
||||
let receipts: AppliedFileMutation[];
|
||||
try {
|
||||
const inputs = undo.receipts.map((receipt) => {
|
||||
const target = join(project.dir, receipt.file);
|
||||
return {
|
||||
sourceFile: receipt.file,
|
||||
absPath: target,
|
||||
before: readFileSync(target, "utf-8"),
|
||||
after: readFileSync(join(project.dir, receipt.backupPath), "utf-8"),
|
||||
expectedVersion: receipt.version,
|
||||
};
|
||||
});
|
||||
receipts = applyFileMutations(project.dir, inputs);
|
||||
} catch (error) {
|
||||
if (isFileChanged(error))
|
||||
return refusal(error.message, "read the latest timeline before retrying undo", json);
|
||||
throw error;
|
||||
return refuseUndoError(error, project.dir, json);
|
||||
}
|
||||
const result = {
|
||||
ok: true,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -778,6 +786,55 @@ describe("timeline edit command", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ format: "single", missing: "target", index: 1 },
|
||||
{ format: "single", missing: "backup", index: 1 },
|
||||
{ format: "batch", missing: "target", index: 1 },
|
||||
{ format: "batch", missing: "backup", index: 1 },
|
||||
{ format: "single", missing: "target", index: 0 },
|
||||
{ format: "batch", missing: "target", index: 0 },
|
||||
])(
|
||||
"refuses a $format undo when its $missing at index $index is missing",
|
||||
({ format, missing, index }) => {
|
||||
const { dir, files, planPath } = batchProject();
|
||||
try {
|
||||
const applied = run(dir, "apply", planPath);
|
||||
expect(applied.status, applied.stderr).toBe(0);
|
||||
const receipt: { file: string; backupPath: string } = JSON.parse(applied.stdout).receipt[
|
||||
index
|
||||
];
|
||||
const missingFile = missing === "target" ? receipt.file : receipt.backupPath;
|
||||
const missingPath = join(dir, missingFile);
|
||||
rmSync(missingPath);
|
||||
const remainingFiles = files.filter((file) => join(dir, file) !== missingPath);
|
||||
const sources = () => remainingFiles.map((file) => readFileSync(join(dir, file), "utf8"));
|
||||
const current = sources();
|
||||
const backupDir = join(dir, ".hyperframes", "backup");
|
||||
const backups = () =>
|
||||
readdirSync(backupDir)
|
||||
.sort()
|
||||
.map((file) => [file, readFileSync(join(backupDir, file), "utf8")]);
|
||||
const currentBackups = backups();
|
||||
const input = format === "single" ? JSON.stringify(receipt) : applied.stdout;
|
||||
|
||||
const undone = run(dir, "undo", input);
|
||||
|
||||
expect(undone.status, undone.stderr).toBe(2);
|
||||
expect(undone.stdout).toBe("");
|
||||
expect(JSON.parse(undone.stderr)).toEqual({
|
||||
ok: false,
|
||||
reason: `timeline undo: undo file is missing: ${missingFile}`,
|
||||
fix: "restore the missing target or backup file before retrying undo",
|
||||
});
|
||||
expect(sources()).toEqual(current);
|
||||
expect(backups()).toEqual(currentBackups);
|
||||
expect(existsSync(missingPath)).toBe(false);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("undoes a complete timeline ids response", () => {
|
||||
const dir = project();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user