diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index 5f945abd1858c..c1293e3306463 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -461,6 +461,22 @@ const cleanupFixture = ( setOpenSessionCwd: (cwd: string | null) => { openSessionCwd = cwd; }, + openTerminal: (paths: { cwd: string; worktreePath: string | null }) => + terminalListener?.({ + type: "upsert", + terminal: { + threadId: "thread-1", + terminalId: "terminal-1", + status: "running", + pid: 1, + exitCode: null, + exitSignal: null, + hasRunningSubprocess: false, + label: "Terminal", + updatedAt: "2026-06-10T12:00:00.000Z", + ...paths, + }, + }) ?? Effect.void, fullStatusCalls: () => fullStatusCalls, snapshotReads: () => snapshotReads, activate: (domainEvents: Stream.Stream) => { @@ -1594,6 +1610,54 @@ describe("orphan worktree reports", () => { }), ), ); + + it.live.each([ + ["win32", "cwd"], + ["win32", "worktreePath"], + ["linux", "cwd"], + ["linux", "worktreePath"], + ] as const)( + "matches a terminal at the worktree root with host casing rules (%s, %s)", + ([platform, field]) => + runCleanupTest( + Effect.gen(function* () { + const fixture = yield* cleanupFixture().pipe( + Effect.provideService(HostProcess.Platform, platform), + ); + const upper = fixture.worktree.toUpperCase(); + yield* fixture.openTerminal( + field === "cwd" + ? { cwd: upper, worktreePath: null } + : { cwd: fixture.repo, worktreePath: upper }, + ); + const report = yield* fixture.service.runNow; + expect(report.entries[0]).toMatchObject( + platform === "win32" + ? { outcome: "kept", reason: "Open terminal" } + : { outcome: "removed" }, + ); + }), + ), + ); + + it.live.each(["win32", "linux"] as const)( + "matches a provider session at the worktree root with host casing rules (%s)", + (platform) => + runCleanupTest( + Effect.gen(function* () { + const fixture = yield* cleanupFixture().pipe( + Effect.provideService(HostProcess.Platform, platform), + ); + fixture.setOpenSessionCwd(fixture.worktree.toUpperCase()); + const report = yield* fixture.service.runNow; + expect(report.entries[0]).toMatchObject( + platform === "win32" + ? { outcome: "kept", reason: "Provider session is still open" } + : { outcome: "removed" }, + ); + }), + ), + ); }); describe("storage cleanup reports and local file policies", () => { diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index 48154835e7bb4..b9f1e1f839528 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -248,8 +248,8 @@ const make = Effect.gen(function* () { const cwd = path.resolve(terminal.cwd); return ( (terminal.worktreePath !== null && - path.resolve(terminal.worktreePath) === worktreePath) || - cwd === worktreePath || + pathKey(terminal.worktreePath) === pathKey(worktreePath)) || + pathKey(cwd) === pathKey(worktreePath) || inside(worktreePath, cwd) ); }); @@ -987,7 +987,7 @@ const make = Effect.gen(function* () { for (const session of sessions) { const cwd = path.resolve(session.cwd); if ( - (cwd === worktreePath || inside(worktreePath, cwd)) && + (pathKey(cwd) === pathKey(worktreePath) || inside(worktreePath, cwd)) && (yield* providerSessions.isLive(session.id)) ) return waitForStop("provider session is still open");