From a13ab16660dd4fe53947bd92a3c4edc30a543c00 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Thu, 17 Sep 2026 19:57:09 +0200 Subject: [PATCH] fix: defer active compile directory cleanup --- server/services/sandbox-runner.ts | 4 + tests/server/services/sandbox-runner.test.ts | 82 ++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/server/services/sandbox-runner.ts b/server/services/sandbox-runner.ts index 0043682f..ff063534 100644 --- a/server/services/sandbox-runner.ts +++ b/server/services/sandbox-runner.ts @@ -418,6 +418,9 @@ export class SandboxRunner { pendingCleanup: s.pendingCleanup, cleanupRetries: new Map(), currentRegistryFile: s.currentRegistryFile, }; + const deferredCompileDir = this.filesystemHelper.isCompilationInProgress(fsState) + ? fsState.currentSketchDir + : null; this.filesystemHelper.markRegistryForCleanup(fsState); this.filesystemHelper.markTempDirForCleanup(fsState); s.currentSketchDir = fsState.currentSketchDir; @@ -425,6 +428,7 @@ export class SandboxRunner { s.pendingCleanup = fsState.pendingCleanup; for (const dir of this.fileBuilder.getCreatedSketchDirs()) { + if (dir === deferredCompileDir) continue; if (!existsSync(dir)) { this.fileBuilder.clearCreatedSketchDir(dir); continue; } if (this.filesystemHelper.attemptCleanupDir(dir)) { this.fileBuilder.clearCreatedSketchDir(dir); diff --git a/tests/server/services/sandbox-runner.test.ts b/tests/server/services/sandbox-runner.test.ts index 5127f7ed..8bfd53ca 100644 --- a/tests/server/services/sandbox-runner.test.ts +++ b/tests/server/services/sandbox-runner.test.ts @@ -743,6 +743,88 @@ describe("SandboxRunner", () => { expect(renameSync).not.toThrow(); }); + it("defers cleanup of an active compile directory until compilation settles", async () => { + const runner = _createRunner(); + const localCompiler = (runner as any).localCompiler as LocalCompiler; + const fileBuilder = (runner as any).fileBuilder; + const filesystemHelper = (runner as any).filesystemHelper; + + let signalCompileStarted!: () => void; + const compileStarted = new Promise((resolve) => { + signalCompileStarted = resolve; + }); + let settleCompile!: () => void; + const compileGate = new Promise((resolve) => { + settleCompile = resolve; + }); + + vi.spyOn(localCompiler, "compile").mockImplementation(async () => { + signalCompileStarted(); + await compileGate; + }); + + const attemptCleanupDir = vi.spyOn(filesystemHelper, "attemptCleanupDir"); + const runPromise = runner.runSketch({ + code: "void setup(){} void loop(){}", + onOutput: vi.fn(), + onError: vi.fn(), + onExit: vi.fn(), + }); + + try { + await compileStarted; + const activeDir = runner.getSketchDir(); + expect(activeDir).toBeTruthy(); + expect((runner as any).executionState.isCompiling).toBe(true); + + const stale = await fileBuilder.build("void setup(){} void loop(){}", "stale-sketch"); + const staleDir = stale.sketchDir; + + await runner.stop(); + + expect((runner as any).executionState.pendingCleanup).toBe(true); + expect(attemptCleanupDir).not.toHaveBeenCalledWith(activeDir); + expect(fileBuilder.getCreatedSketchDirs()).toContain(activeDir); + expect(attemptCleanupDir).toHaveBeenCalledWith(staleDir); + expect(fileBuilder.getCreatedSketchDirs()).not.toContain(staleDir); + + settleCompile(); + await expect(runPromise).resolves.toBe(false); + + expect(attemptCleanupDir.mock.calls.filter(([dir]) => dir === activeDir)).toHaveLength(1); + expect(vi.mocked(renameSync).mock.calls.filter(([dir]) => dir === activeDir)).toHaveLength(1); + expect(fileBuilder.getCreatedSketchDirs()).not.toContain(activeDir); + } finally { + settleCompile(); + await runPromise; + } + }); + + it("cleans current and stale tracked directories when no compile is active", async () => { + const runner = _createRunner(); + const fileBuilder = (runner as any).fileBuilder; + const filesystemHelper = (runner as any).filesystemHelper; + const current = await fileBuilder.build("void setup(){} void loop(){}", "current-sketch"); + const stale = await fileBuilder.build("void setup(){} void loop(){}", "stale-sketch"); + const currentDir = current.sketchDir; + const staleDir = stale.sketchDir; + + const state = (runner as any).executionState; + state.currentSketchDir = currentDir; + state.isCompiling = false; + runner["state"] = "running"; + + const attemptCleanupDir = vi.spyOn(filesystemHelper, "attemptCleanupDir"); + + await runner.stop(); + + expect(state.isCompiling).toBe(false); + expect((runner as any).localCompiler.isBusy).toBe(false); + expect(attemptCleanupDir).toHaveBeenCalledWith(currentDir); + expect(attemptCleanupDir).toHaveBeenCalledWith(staleDir); + expect(fileBuilder.getCreatedSketchDirs()).toEqual([]); + }); + it("should handle serial input", async () => { const runner = new SandboxRunner();