Skip to content

Commit 7b1b0bb

Browse files
fix(studio-server): history routes say why a project's history cannot open instead of a bare 500 (#5821)
1 parent 49671b7 commit 7b1b0bb

2 files changed

Lines changed: 40 additions & 13 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,30 @@ describe("history routes", () => {
521521
const call = apiFor(tempDir("hf-history-routes-none-"));
522522
expect((await call("")).status).toBe(404);
523523
});
524+
525+
it("say why when the project's history cannot open, on every route", async () => {
526+
const projectDir = tempDir("hf-history-routes-busy-");
527+
writeFileSync(join(projectDir, "index.html"), "A");
528+
const historyRoot = tempDir("hf-history-routes-busy-root-");
529+
const owner = await openProjectHistory({ projectDir, historyRoot });
530+
cleanup.push(() => owner.close());
531+
const api = createStudioApi({
532+
listProjects: () => [],
533+
resolveProject: (id: string) => (id === "demo" ? { id, dir: projectDir } : null),
534+
history: () => openProjectHistory({ projectDir, historyRoot, ownerWaitMs: 0 }),
535+
} as unknown as StudioApiAdapter);
536+
const busy = {
537+
error: `This project's history is open in another process (pid ${process.pid}).`,
538+
};
539+
540+
const claim = await api.request("/projects/demo/history/claim", {
541+
method: "POST",
542+
body: JSON.stringify({ label: "Color", paths: ["index.html"] }),
543+
});
544+
expect([claim.status, await claim.json()]).toEqual([409, busy]);
545+
const blob = await api.request(`/projects/demo/history/blob/${"0".repeat(64)}`);
546+
expect([blob.status, await blob.json()]).toEqual([409, busy]);
547+
});
524548
});
525549

526550
it("checkpoints pending writes without closing an agent window", async () => {

‎packages/studio-server/src/routes/history.ts‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,17 @@ function nextStep(history: ProjectHistory, direction: "back" | "forward") {
9191
return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
9292
}
9393

94-
/** Runs `task` on the project's history; no history is a 404, an engine refusal ("no longer kept") a 409. */
94+
/** Runs `task` on the project's history; no history is a 404, a refusal to open or to act ("no longer kept") a 409. */
9595
async function withHistory(
9696
adapter: StudioApiAdapter,
9797
c: Context,
9898
task: (history: ProjectHistory, body: Record<string, unknown>) => Promise<unknown> | unknown,
9999
) {
100-
const history = await historyOf(adapter, c);
101-
if (!history) return c.json({ error: "This project has no history here." }, 404);
102100
try {
103-
return c.json((await task(history, await bodyOf(c))) ?? null);
101+
const history = await historyOf(adapter, c);
102+
if (!history) return c.json({ error: "This project has no history here." }, 404);
103+
const result = await task(history, await bodyOf(c));
104+
return result instanceof Response ? result : c.json(result ?? null);
104105
} catch (error) {
105106
return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
106107
}
@@ -149,15 +150,17 @@ export function registerHistoryRoutes(api: Hono, adapter: StudioApiAdapter): voi
149150
return { ok: true };
150151
}),
151152
);
152-
api.get(`${base}/blob/:hash`, async (c) => {
153-
const history = await historyOf(adapter, c);
154-
const hash = c.req.param("hash") ?? "";
155-
const missing = () =>
156-
c.json({ error: "That file is not kept in this project's history." }, 404);
157-
if (!history || !/^[0-9a-f]{64}$/.test(hash)) return missing();
158-
const bytes = await history.readBlob(hash).catch(() => null);
159-
return bytes ? c.body(new Uint8Array(bytes)) : missing();
160-
});
153+
api.get(`${base}/blob/:hash`, (c) =>
154+
withHistory(adapter, c, async (history) => {
155+
const hash = c.req.param("hash") ?? "";
156+
const bytes = /^[0-9a-f]{64}$/.test(hash)
157+
? await history.readBlob(hash).catch(() => null)
158+
: null;
159+
return bytes
160+
? c.body(new Uint8Array(bytes))
161+
: c.json({ error: "That file is not kept in this project's history." }, 404);
162+
}),
163+
);
161164
// Studio records after it writes, under the edit's label; a gesture with no idleMs holds until another key.
162165
api.post(`${base}/claim`, (c) =>
163166
withHistory(adapter, c, async (history, body) => {

0 commit comments

Comments
 (0)