Files
hyperframes/packages/studio/src/utils/domEditSaveQueue.test.ts
T
James Russo 2417293dab fix(studio): enforce optimistic file concurrency (#2156)
* fix(studio): enforce optimistic file concurrency

* fix(studio): harden conditional file writes

* fix(studio): honor explicit file preconditions

* test(producer): allow zero-ms encode timing
2026-07-15 18:07:04 -04:00

137 lines
3.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { createDomEditSaveQueue } from "./domEditSaveQueue";
import { StudioSaveHttpError } from "./studioSaveDiagnostics";
describe("dom edit save queue", () => {
afterEach(() => {
vi.useRealTimers();
});
it("opens the breaker after consecutive failures and rejects new work until reset", async () => {
const onOpen = vi.fn();
const onReset = vi.fn();
const queue = createDomEditSaveQueue({
failureThreshold: 2,
onOpen,
onReset,
});
await expect(
queue.enqueue(async () => {
throw new StudioSaveHttpError("Server down", 503);
}),
).rejects.toThrow("Server down");
await expect(
queue.enqueue(async () => {
throw new StudioSaveHttpError("Still down", 503);
}),
).rejects.toThrow("Still down");
expect(onOpen).toHaveBeenCalledWith({
consecutiveFailures: 2,
errorMessage: "Still down",
statusCode: 503,
});
let thirdRan = false;
await expect(
queue.enqueue(async () => {
thirdRan = true;
}),
).rejects.toThrow("Auto-save is paused");
expect(thirdRan).toBe(false);
queue.reset();
expect(onReset).toHaveBeenCalledOnce();
await queue.enqueue(async () => {
thirdRan = true;
});
expect(thirdRan).toBe(true);
queue.destroy();
});
it("keeps an open breaker paused even when already queued work succeeds", async () => {
const onOpen = vi.fn();
const onReset = vi.fn();
const queue = createDomEditSaveQueue({
failureThreshold: 1,
onOpen,
onReset,
});
let rejectFirst: ((error: Error) => void) | null = null;
let resolveFirstStarted: (() => void) | null = null;
const firstStarted = new Promise<void>((resolve) => {
resolveFirstStarted = resolve;
});
const first = queue.enqueue(
() =>
new Promise<void>((_resolve, reject) => {
rejectFirst = reject;
resolveFirstStarted?.();
}),
);
const second = queue.enqueue(async () => {});
await firstStarted;
expect(rejectFirst).toBeTypeOf("function");
rejectFirst?.(new StudioSaveHttpError("Server down", 503));
await expect(first).rejects.toThrow("Server down");
await expect(second).resolves.toBeUndefined();
expect(onOpen).toHaveBeenCalledOnce();
expect(onReset).not.toHaveBeenCalled();
await expect(queue.enqueue(async () => {})).rejects.toThrow("Auto-save is paused");
queue.reset();
expect(onReset).toHaveBeenCalledOnce();
queue.destroy();
});
it("resets consecutive failures after a successful save", async () => {
const onOpen = vi.fn();
const queue = createDomEditSaveQueue({
failureThreshold: 2,
onOpen,
});
await expect(
queue.enqueue(async () => {
throw new Error("first failure");
}),
).rejects.toThrow("first failure");
await queue.enqueue(async () => {});
await expect(
queue.enqueue(async () => {
throw new Error("second failure after success");
}),
).rejects.toThrow("second failure after success");
expect(onOpen).not.toHaveBeenCalled();
queue.destroy();
});
it("pauses immediately on a file conflict instead of retrying stale work", async () => {
const onOpen = vi.fn();
const queue = createDomEditSaveQueue({ failureThreshold: 5, onOpen });
await expect(
queue.enqueue(async () => {
throw new StudioSaveHttpError("File changed elsewhere", 409);
}),
).rejects.toThrow("File changed elsewhere");
expect(onOpen).toHaveBeenCalledWith({
consecutiveFailures: 1,
errorMessage: "File changed elsewhere",
statusCode: 409,
});
await expect(queue.enqueue(async () => {})).rejects.toThrow("Auto-save is paused");
queue.destroy();
});
});