fix(cli): tolerate removed files during desktop catch-up (#5350)

This commit is contained in:
Abdullah M. Nazif
2026-10-24 04:10:18 +00:00
committed by GitHub
parent e0317a23c0
commit e7f6c12b91
2 changed files with 53 additions and 1 deletions
+43
View File
@@ -23,6 +23,22 @@ import {
} from "./appHistory.js";
import { readRecord, writeRecord } from "./projectRecords.js";
const { afterDirectoryRead } = vi.hoisted(() => ({
afterDirectoryRead: vi.fn<(path: unknown) => void>(),
}));
vi.mock("node:fs", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs")>();
return {
...original,
readdirSync: vi.fn(original.readdirSync).mockImplementation((path, options) => {
const entries = original.readdirSync(path, options);
afterDirectoryRead(path);
return entries;
}),
};
});
const history = (dir: string) => join(dir, ".hyperframes", APP_HISTORY);
function project(lines: string[] = []): string {
@@ -128,6 +144,33 @@ describe("app history", () => {
expect(filesChangedSince(dir, 5000)).toEqual(["compositions/a.html"]);
});
it("keeps listing changed files when one disappears after the directory read", () => {
const dir = project();
const removed = join(dir, "removed.html");
writeFileSync(removed, "removed");
writeFileSync(join(dir, "index.html"), "kept");
mkdirSync(join(dir, "compositions"));
writeFileSync(join(dir, "compositions", "scene.html"), "kept");
afterDirectoryRead.mockImplementationOnce(() => {
rmSync(removed);
});
expect(filesChangedSince(dir, 0)).toEqual(["compositions/scene.html", "index.html"]);
});
it("skips a file replaced by a directory after the directory read", () => {
const dir = project();
const replaced = join(dir, "replaced.html");
writeFileSync(replaced, "replaced");
writeFileSync(join(dir, "index.html"), "kept");
afterDirectoryRead.mockImplementationOnce(() => {
rmSync(replaced);
mkdirSync(replaced);
});
expect(filesChangedSince(dir, 0)).toEqual(["index.html"]);
});
it("names unseen turns at the end of a command run in the project or on it", () => {
const dir = project([turn("2026-01-01T10:00:00Z", "a"), turn("2026-01-01T11:00:00Z", "b")]);
expect(appHistoryNotice(dir, []), "never handed over from here").toBeNull();
+10 -1
View File
@@ -96,6 +96,15 @@ export function unseenTurns(dir: string, since: number, now = Date.now()): AppTu
);
}
function isChangedFile(path: string, since: number): boolean {
try {
const stats = lstatSync(path);
return stats.isFile() && stats.mtimeMs > since;
} catch {
return false;
}
}
/** Project files changed after `since`, by the app or by hand; hidden folders and outputs are not the video. */
export function filesChangedSince(dir: string, since: number): string[] {
const changed: string[] = [];
@@ -112,7 +121,7 @@ export function filesChangedSince(dir: string, since: number): string[] {
if (entry.name.startsWith(".") || SKIPPED_DIRS.has(entry.name)) continue;
const path = join(folder, entry.name);
if (entry.isDirectory()) walk(path);
else if (entry.isFile() && lstatSync(path).mtimeMs > since)
else if (entry.isFile() && isChangedFile(path, since))
changed.push(relative(dir, path).split(sep).join("/"));
}
};