fix(cli): check and snapshot keep their record while another app has the history open (#5845)

* fix(cli): check and snapshot keep their record while another app has the history open

* test(cli): a held history no longer drops the snapshot record
This commit is contained in:
Miguel Ángel
2026-10-18 15:14:48 +00:00
committed by GitHub
parent ab2e939558
commit 01adadb918
4 changed files with 58 additions and 20 deletions
@@ -71,7 +71,7 @@ it("only warns when the report cannot be recorded; the exit status is the check'
}
});
it("still reports the check when another process holds history, without waiting for it", async () => {
it("records the check when another process holds history, without waiting for it", async () => {
const project = mkdtempSync(join(tmpdir(), "hf-check-history-held-"));
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
@@ -90,12 +90,10 @@ it("still reports the check when another process holds history, without waiting
try {
await runCommand(check, { rawArgs: [project, "--json"] });
const output = JSON.parse(String(log.mock.calls.at(-1)?.[0])) as CheckReport;
expect(output).not.toHaveProperty("saved");
expect(output).toHaveProperty("saved.id");
expect(output.browserSkipped).toBe(true);
expect(consumeCommandResult().exitCode).toBe(checkExitCode(output));
expect(
warn.mock.calls.some((call) => String(call[0]).includes("Check report was not recorded")),
).toBe(true);
expect(warn.mock.calls.flat().join("\n")).not.toContain("not recorded");
} finally {
historyDeps.ownerWaitMs = previousWait;
await held.close();
@@ -152,7 +152,7 @@ describe("snapshot contact sheet failures", () => {
expect(console.error).not.toHaveBeenCalled();
});
it("keeps the captured frames when another process holds history", async () => {
it("keeps and records the captured frames when another process holds history", async () => {
const args = {
rawArgs: [
project,
@@ -181,12 +181,13 @@ describe("snapshot contact sheet failures", () => {
expect(readFileSync(frame)).toEqual(Buffer.from("replaced PNG"));
}
const after = JSON.parse(readFileSync(statePath, "utf8"));
expect(after.snapshots.unversioned).toEqual([]);
expect(after.snapshots.latest).toBeNull();
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining("Snapshots were not recorded"),
expect(after.snapshots.unversioned.map((record: { image: string }) => record.image)).toEqual(
["0.5", "1"].map((time, index) =>
join(project, "snapshots", `frame-0${index}-at-${time}s.png`),
),
);
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining("not recorded"));
} finally {
historyDeps.ownerWaitMs = previousWait;
await held.close();
+40 -1
View File
@@ -1,7 +1,9 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import type { AddressInfo } from "node:net";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { createAdaptorServer } from "@hono/node-server";
import {
createStudioApi,
@@ -82,6 +84,43 @@ it("does not assign results when inputs change during capture", async () => {
expect(f.state().snapshots.latest?.revision).toBeNull();
});
it("a check run while the app keeps the history open is still recorded, never under an unfiled revision", async () => {
const f = await fixture();
// Held here as the desktop app holds every open project's history; the CLI runs as the app runs it, a child.
const held = await openProjectHistory({
projectDir: f.dir,
historyRoot: historyDeps.historyRoot,
undoScope: "everyone",
});
const cli = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts");
const checkFromChild = () => {
const run = spawnSync("bun", ["run", cli, "check", "--json", f.dir], {
encoding: "utf-8",
env: {
...process.env,
HYPERFRAMES_HISTORY_ROOT: historyDeps.historyRoot,
HYPERFRAMES_SKIP_UPDATE_CHECK: "1",
HYPERFRAMES_NO_TELEMETRY: "1",
},
});
expect(run.stderr).not.toContain("not recorded");
};
try {
writeFileSync(join(f.dir, "index.html"), "<html>filed</html>");
await held.checkpoint();
checkFromChild();
expect(f.state().currentRevision).toBeTruthy();
expect(f.state().checks.latest?.revision).toBe(f.state().currentRevision);
writeFileSync(join(f.dir, "index.html"), "<html>not filed yet</html>");
checkFromChild();
expect(f.state().checks.latest?.revision).toBeNull();
expect(f.state().checks.unversioned).toHaveLength(1);
} finally {
await held.close();
}
});
it("posts a preview checkpoint instead of sending GET", async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-artifact-preview-"));
roots.push(dir);
+8 -8
View File
@@ -1,15 +1,15 @@
import { beginArtifactCapture } from "@hyperframes/studio-server/history";
import { HistoryBusyError, beginArtifactCapture } from "@hyperframes/studio-server/history";
import { withOwner } from "./historyOwner.js";
const SKIP_BUSY_HISTORY = { ownerWaitMs: 0 };
function unlessAnotherProcessKeepsTheHistory(error: unknown): void {
if (!(error instanceof HistoryBusyError)) throw error;
}
export async function captureArtifactRevision(projectDir: string) {
return withOwner(
projectDir,
async (owner) => {
await owner.checkpoint();
return beginArtifactCapture(projectDir);
},
SKIP_BUSY_HISTORY,
await withOwner(projectDir, (owner) => owner.checkpoint(), SKIP_BUSY_HISTORY).catch(
unlessAnotherProcessKeepsTheHistory,
);
return beginArtifactCapture(projectDir);
}