fix(studio): synchronize batch completion state
CI / runtime-diagnostics (pull_request) Skipped
CI / check (pull_request) Successful in 54s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-09-01 19:24:15 +02:00
1 parent 9f349c3343
commit f2e67ebace
2 files changed
+43 -5

No files matched your search

@@ -0,0 +1,34 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
describe("StudioClient batch completion contract", () => {
it("marks React state without recursively invoking its completion callback", () => {
const source = readFileSync(
new URL("./studio-client.tsx", import.meta.url),
"utf8",
);
const callbackBody = source.match(
/const markBatchDone = useCallback\(\(\) => \{([\s\S]*?)\n\t\}, \[\]\);/,
)?.[1];
expect(callbackBody).toBeDefined();
expect(callbackBody).toContain("setBatchDone(true);");
expect(callbackBody).not.toContain("markBatchDone();");
});
it("resets the completion ref and React state before every batch", () => {
const source = readFileSync(
new URL("./studio-client.tsx", import.meta.url),
"utf8",
);
const callbackBody = source.match(
/const resetBatchDone = useCallback\(\(\) => \{([\s\S]*?)\n\t\}, \[\]\);/,
)?.[1];
expect(callbackBody).toBeDefined();
expect(callbackBody).toContain("batchDoneRef.current = false;");
expect(callbackBody).toContain("setBatchDone(false);");
expect(source.match(/resetBatchDone\(\);/g)).toHaveLength(4);
expect(source.match(/setBatchDone\(false\);/g)).toHaveLength(1);
});
});
@@ -154,7 +154,11 @@ export function StudioClient({
const batchDoneRef = useRef(false);
const markBatchDone = useCallback(() => {
batchDoneRef.current = true;
markBatchDone();
setBatchDone(true);
}, []);
const resetBatchDone = useCallback(() => {
batchDoneRef.current = false;
setBatchDone(false);
}, []);
const [batchSucceeded, setBatchSucceeded] = useState(0);
const [batchFailed, setBatchFailed] = useState(0);
@@ -430,7 +434,7 @@ export function StudioClient({
initial.set(cn, { classname: cn, status: "pending" });
}
setBatchProgress(initial);
setBatchDone(false);
resetBatchDone();
setBatchSucceeded(0);
setBatchFailed(0);
setFailedClassnames([]);
@@ -693,7 +697,7 @@ export function StudioClient({
return;
}
setBatchDone(false);
resetBatchDone();
setBatchSucceeded(0);
setBatchFailed(0);
setBatchCancelling(false);
@@ -831,7 +835,7 @@ export function StudioClient({
async function retryFailed(failed: string[]) {
if (!activeSource || failed.length === 0) return;
// For clone sources, re-run cloneAllMissing with only the failed classnames
setBatchDone(false);
resetBatchDone();
setBatchSucceeded(0);
setBatchFailed(0);
setFailedClassnames([]);
@@ -1957,7 +1961,7 @@ export function StudioClient({
onCancel={cloneAllAbortRef.current ? cancelCloneAll : cancelBatch}
onDismiss={() => {
setBatchProgress(null);
setBatchDone(false);
resetBatchDone();
setVerification(null);
setFailedClassnames([]);
}}