Skip to content

Commit db9958e

Browse files
committed
fix(cli): a turn marker without its last write time has ended
Idle-limit tests set file times instead of racing the clock, and the restore refusal test picks a ref no entry id starts with.
1 parent 37be132 commit db9958e

3 files changed

Lines changed: 42 additions & 23 deletions

File tree

‎packages/cli/src/commands/history.test.ts‎

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @vitest-environment node
22
import { spawn } from "node:child_process";
3-
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+
import { mkdtempSync, readdirSync, readFileSync, rmSync, utimesSync, writeFileSync } from "node:fs";
44
import type { AddressInfo } from "node:net";
55
import { tmpdir } from "node:os";
66
import { join, resolve } from "node:path";
@@ -265,17 +265,20 @@ describe("hyperframes history, one owner", () => {
265265
});
266266

267267
it("a turn whose writes come closer together than the idle limit stays the agent's past it", async () => {
268-
const { write, json, hf } = project();
268+
const { dir, write, json, hf } = project();
269269
await hf();
270-
historyDeps.turnIdleMs = 300;
271-
await hf("begin", "--who", "claude", "--label", "Retitle");
272-
await pause(200);
273-
write("index.html", "2");
274-
await hf(); // a command mid-turn files the turn so far
275-
for (const path of ["notes.html", "extra.html"]) {
276-
await pause(200);
270+
historyDeps.turnIdleMs = 400;
271+
// Written "later" by their times, so the test never races the clock.
272+
const writeAt = (path: string, at: number) => {
277273
write(path, "2");
278-
}
274+
utimesSync(join(dir, path), at / 1000, at / 1000);
275+
};
276+
await hf("begin", "--who", "claude", "--label", "Retitle");
277+
const at = Date.now() + 300;
278+
writeAt("index.html", at);
279+
await hf(); // a command mid-turn files the turn so far; the rest counts from index.html
280+
writeAt("notes.html", at + 300);
281+
writeAt("extra.html", at + 600);
279282
await hf("end");
280283
const entries = (await json()).entries.reverse();
281284
expect(
@@ -286,6 +289,17 @@ describe("hyperframes history, one owner", () => {
286289
]);
287290
});
288291

292+
it("a turn marker with no last write time has ended, so the next edit is not the agent's", async () => {
293+
const { dir, write, json, hf } = project();
294+
await hf();
295+
const who = { kind: "agent", name: "claude" };
296+
const marker = { via: "direct", id: "turn-1", who, label: "Retitle", startedAt: 1 };
297+
writeFileSync(join(dir, ".hyperframes", "history-turn.json"), JSON.stringify(marker));
298+
write("index.html", "A2");
299+
300+
expect((await json()).entries[0]).toMatchObject({ who: { kind: "outside" } });
301+
});
302+
289303
it("a CLI run waits while another process holds the history, instead of forking its log", async () => {
290304
const { dir, write } = project();
291305
const home = tempDir("hf-history-cli-home-");
@@ -332,13 +346,15 @@ describe("hyperframes history, refusals", () => {
332346
});
333347

334348
it("restore refuses a ref that is neither an entry nor a date, instead of reading it as a time", async () => {
335-
const { read, write, hf, turn } = project();
349+
const { read, write, hf, json, turn } = project();
336350
await hf();
337351
await turn("claude", "Retitle", "index.html", "A2");
338352
write("added.html", "new");
353+
const ids = (await json()).entries.map((entry: { id: string }) => entry.id);
354+
const digit = [..."0123456789"].find((d) => !ids.some((id: string) => id.startsWith(d)))!;
339355

340-
const refused = await hf("restore", "1");
341-
expect([refused.code, refused.err]).toEqual([2, 'No entry "1" in this history']);
356+
const refused = await hf("restore", digit);
357+
expect([refused.code, refused.err]).toEqual([2, `No entry "${digit}" in this history`]);
342358
expect(read("added.html")).toBe("new");
343359
});
344360

‎packages/cli/src/utils/historyOwner.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ export class Refusal extends Error {}
5252

5353
const turnFile = (dir: string) => join(dir, ".hyperframes", "history-turn.json");
5454

55+
/** The open turn, or null; a marker with no last write time cannot say when the turn ended, so it has. */
5556
function readTurn(dir: string): Turn | null {
5657
try {
57-
return JSON.parse(readFileSync(turnFile(dir), "utf-8")) as Turn;
58+
const turn = JSON.parse(readFileSync(turnFile(dir), "utf-8")) as Turn;
59+
return typeof turn.lastWriteAt === "number" ? turn : null;
5860
} catch {
5961
return null;
6062
}

‎packages/studio-server/src/history/projectHistory.test.ts‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
readFileSync,
99
renameSync,
1010
rmSync,
11+
utimesSync,
1112
writeFileSync,
1213
} from "node:fs";
1314
import { spawnSync } from "node:child_process";
@@ -233,15 +234,15 @@ describe("openProjectHistory", () => {
233234
"c.html": "c1",
234235
});
235236
await history.close();
236-
const lastWriteAt = Date.now();
237-
const pause = () => new Promise((settle) => setTimeout(settle, 250));
238-
await pause();
239-
write("a.html", "a2");
240-
await pause();
241-
write("b.html", "b2"); // 250 ms after a.html: still the window's, 500 ms after its last write
242-
await pause();
243-
await pause();
244-
write("c.html", "c2"); // 500 ms without a write: the window had ended
237+
// Written "later" by their times, so the test never races the clock.
238+
const lastWriteAt = Date.now() + 60_000;
239+
const writeAt = (path: string, at: number) => {
240+
write(path, "2");
241+
utimesSync(join(projectDir, path), at / 1000, at / 1000);
242+
};
243+
writeAt("a.html", lastWriteAt + 300);
244+
writeAt("b.html", lastWriteAt + 600); // 300 ms after a.html: still the window's
245+
writeAt("c.html", lastWriteAt + 1100); // 500 ms without a write: the window had ended
245246
const closedWindow = { id: "turn-1", who: agent, label: "Turn", startedAt: 1, lastWriteAt };
246247

247248
const reopened = await open(projectDir, historyRoot, {

0 commit comments

Comments
 (0)