diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index de1ccfefcf64f..c3c7c27bc4b23 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -632,8 +632,26 @@ const submoduleCleanupFixture = () => it.live("removes clean submodules recursively, including deinitialized repositories", () => runCleanupTest( Effect.gen(function* () { - const { service, fs, repo, worktree, submodule, command, recordSubmodules } = - yield* submoduleCleanupFixture(); + const fs = yield* FileSystem.FileSystem; + const git = yield* GitVcsDriver.GitVcsDriver; + let editedDuringClean = false; + const { service, repo, worktree, submodule, command, recordSubmodules, setPolicy } = + yield* submoduleCleanupFixture().pipe( + Effect.provideService(GitVcsDriver.GitVcsDriver, { + ...git, + execute: (input) => + git.execute(input).pipe( + Effect.tap(() => { + if (input.args[0] !== "clean") return Effect.void; + editedDuringClean = true; + return fs.writeFileString(`${input.cwd}/tracked.txt`, "late edit\n"); + }), + ), + }), + ); + setPolicy("tracked-changes"); + yield* fs.writeFileString(`${worktree}/notes.txt`, "untracked\n"); + yield* fs.writeFileString(`${worktree}/.env`, "ignored\n"); yield* command(submodule, [ "-c", "protocol.file.allow=always", @@ -646,6 +664,7 @@ it.live("removes clean submodules recursively, including deinitialized repositor yield* command(submodule, ["submodule", "deinit", "stored"]); const admin = (yield* command(worktree, ["rev-parse", "--absolute-git-dir"])).stdout.trim(); const report = yield* service.runNow; + expect(editedDuringClean).toBe(false); expect(report.entries.find((entry) => entry.path === worktree)).toMatchObject({ outcome: "removed", }); @@ -751,15 +770,21 @@ it.live("keeps unpushed submodule history after checkout and deinitialization", it.live("keeps submodules when stored repository metadata is malformed", () => runCleanupTest( Effect.gen(function* () { - const { service, fs, worktree, submodule, child, command } = yield* submoduleCleanupFixture(); + const { service, fs, worktree, submodule, child, command, setPolicy } = + yield* submoduleCleanupFixture(); + setPolicy("tracked-changes"); const childGit = (yield* command(child, ["rev-parse", "--absolute-git-dir"])).stdout.trim(); yield* command(submodule, ["submodule", "deinit", "child"]); yield* fs.writeFileString(`${childGit}/HEAD`, "not a ref\n"); + yield* fs.writeFileString(`${worktree}/notes.txt`, "keep while the worktree stays\n"); expect( (yield* service.runNow).entries.find((entry) => entry.path === worktree), ).toMatchObject({ outcome: "kept", reason: expect.stringContaining("inspected") }); expect(yield* fs.exists(worktree)).toBe(true); expect(yield* fs.readFileString(`${childGit}/HEAD`)).toBe("not a ref\n"); + expect(yield* fs.readFileString(`${worktree}/notes.txt`)).toBe( + "keep while the worktree stays\n", + ); }), ), ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index a5f9cd7eefa12..f062a04e7dfaf 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -1206,15 +1206,15 @@ const make = Effect.gen(function* () { ) ) return keep("settings changed since check"); - // Clean only untracked files. Ordinary worktrees retain Git's final tracked-edit check. - if (settings.worktreeKeepWhen === "tracked-changes") - yield* cleanupGit(worktreePath, ["clean", "-ffdx"], "git clean"); const submodules = yield* submoduleRemoval(worktreePath, settings); if (submodules.reason !== null) return keep(submodules.reason); if (submodules.force) { const changes = yield* localChangesReason(worktreePath, settings); if (changes !== null) return keep(changes); } + // Forced removal already discards local files; do not widen its final-check window. + if (settings.worktreeKeepWhen === "tracked-changes" && !submodules.force) + yield* cleanupGit(worktreePath, ["clean", "-ffdx"], "git clean"); yield* removeLinkedWorktree({ operation: "StorageCleanup.cleanupGit", cwd: project.workspaceRoot,