diff --git a/.changeset/dev-runtime-nitro-prune-protection.md b/.changeset/dev-runtime-nitro-prune-protection.md new file mode 100644 index 000000000..9ef72875a --- /dev/null +++ b/.changeset/dev-runtime-nitro-prune-protection.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Protect dev-runtime snapshots referenced by generated Nitro artifacts from stale snapshot pruning, so `eve dev` routes do not lose boot-time compiled-artifact imports after rebuild churn. diff --git a/packages/eve/src/internal/nitro/dev-runtime-artifacts.integration.test.ts b/packages/eve/src/internal/nitro/dev-runtime-artifacts.integration.test.ts index e1e96ce02..9f87bc317 100644 --- a/packages/eve/src/internal/nitro/dev-runtime-artifacts.integration.test.ts +++ b/packages/eve/src/internal/nitro/dev-runtime-artifacts.integration.test.ts @@ -384,6 +384,58 @@ describe("development runtime artifact snapshots", () => { expect(existsSync(staleSnapshotRoot)).toBe(false); }); + it("preserves snapshots referenced by generated Nitro files", async () => { + const appRoot = await createScratchDirectory("eve-dev-runtime-artifacts-prune-nitro-"); + const snapshotsRoot = join(appRoot, ".eve", "dev-runtime", "snapshots"); + const activeSnapshotRoot = join(snapshotsRoot, "active"); + const nitroReferencedSnapshotRoot = join(snapshotsRoot, "nitro-referenced"); + const staleSnapshotRoot = join(snapshotsRoot, "stale"); + const oldSnapshotTime = new Date(1_000); + + for (const snapshotRoot of [ + activeSnapshotRoot, + nitroReferencedSnapshotRoot, + staleSnapshotRoot, + ]) { + await mkdir(snapshotRoot, { recursive: true }); + await writeFile(join(snapshotRoot, "marker.txt"), snapshotRoot); + await utimes(snapshotRoot, oldSnapshotTime, oldSnapshotTime); + } + + await activateDevelopmentRuntimeArtifactsSnapshot({ + appRoot, + snapshot: { + runtimeAppRoot: join(activeSnapshotRoot, "source", "app"), + snapshotRoot: activeSnapshotRoot, + snapshotSourceRoot: join(activeSnapshotRoot, "source"), + }, + }); + await mkdir(join(appRoot, ".eve", "nitro", "workflow"), { recursive: true }); + await writeFile( + join(appRoot, ".eve", "nitro", "workflow", "steps.mjs"), + `import "${join( + nitroReferencedSnapshotRoot, + "source", + "app", + ".eve", + "compile", + "compiled-artifacts-bootstrap.mjs", + ).replaceAll("\\", "/")}";\n`, + ); + + await pruneDevelopmentRuntimeArtifactsSnapshots({ + appRoot, + now: 1_000_000, + recentWindowMs: 0, + retainCount: 0, + }); + + await expect(readdir(snapshotsRoot)).resolves.toEqual( + expect.arrayContaining(["active", "nitro-referenced"]), + ); + expect(existsSync(staleSnapshotRoot)).toBe(false); + }); + it("removes a partially staged snapshot when staging fails", async () => { const appRoot = await createScratchDirectory("eve-dev-runtime-artifacts-failed-stage-"); const agentRoot = join(appRoot, "agent"); diff --git a/packages/eve/src/internal/nitro/dev-runtime-artifacts.ts b/packages/eve/src/internal/nitro/dev-runtime-artifacts.ts index 88c896d65..da5d31ffa 100644 --- a/packages/eve/src/internal/nitro/dev-runtime-artifacts.ts +++ b/packages/eve/src/internal/nitro/dev-runtime-artifacts.ts @@ -11,7 +11,7 @@ const DEV_RUNTIME_ARTIFACTS_DIRECTORY = "dev-runtime"; const DEV_RUNTIME_ARTIFACTS_POINTER_VERSION = 2; const DEV_RUNTIME_SNAPSHOT_RECENT_WINDOW_MS = 15 * 60 * 1000; const DEV_RUNTIME_SNAPSHOT_RETAIN_COUNT = 5; -const DEV_RUNTIME_WORKFLOW_DATA_MAX_SCAN_BYTES = 1024 * 1024; +const DEV_RUNTIME_PROTECTED_REFERENCE_MAX_SCAN_BYTES = 1024 * 1024; const TERMINAL_WORKFLOW_RUN_STATUSES = new Set(["completed", "failed", "cancelled", "canceled"]); interface DevelopmentRuntimeArtifactsPointerV1 { @@ -184,7 +184,10 @@ export async function pruneDevelopmentRuntimeArtifactsSnapshots(input: { ); const protectedPaths = [ ...collectProtectedSnapshotPaths(pointer), - ...(await collectWorkflowDataSnapshotPaths({ appRoot: input.appRoot, snapshotsDirectory })), + ...(await collectProtectedReferenceSnapshotPaths({ + appRoot: input.appRoot, + snapshotsDirectory, + })), ]; const now = input.now ?? Date.now(); const recentWindowMs = input.recentWindowMs ?? DEV_RUNTIME_SNAPSHOT_RECENT_WINDOW_MS; @@ -297,18 +300,25 @@ function collectProtectedSnapshotPaths( return [pointer.runtimeAppRoot, pointer.snapshotRoot]; } -async function collectWorkflowDataSnapshotPaths(input: { +async function collectProtectedReferenceSnapshotPaths(input: { readonly appRoot: string; readonly snapshotsDirectory: string; }): Promise { - const workflowDataDirectory = join(input.appRoot, ".workflow-data"); + const referenceDirectories = [ + join(input.appRoot, ".workflow-data"), + join(input.appRoot, ".eve", "nitro"), + ]; const snapshotPaths = new Set(); - await collectSnapshotPathsFromDirectory({ - directory: workflowDataDirectory, - snapshotPaths, - snapshotsDirectory: input.snapshotsDirectory, - }); + await Promise.all( + referenceDirectories.map((directory) => + collectSnapshotPathsFromDirectory({ + directory, + snapshotPaths, + snapshotsDirectory: input.snapshotsDirectory, + }), + ), + ); return [...snapshotPaths]; } @@ -348,8 +358,8 @@ async function collectSnapshotPathsFromDirectory(input: { } const fileStats = await stat(path); - // Local workflow run payloads are small; keep this best-effort scan cheap. - if (fileStats.size > DEV_RUNTIME_WORKFLOW_DATA_MAX_SCAN_BYTES) { + // Protected-reference files are small; keep this best-effort scan cheap. + if (fileStats.size > DEV_RUNTIME_PROTECTED_REFERENCE_MAX_SCAN_BYTES) { return; }