Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export const make = Effect.gen(function* () {
storageCleanup: true,
storageCleanupRun: true,
databaseStorage: true,
settledArtifactCleanup: true,
otlpEndpointCheck: true,
projectWorktreeCleanup: true,
settledWorktreeCleanup: true,
Expand Down
281 changes: 279 additions & 2 deletions apps/server/src/storageCleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ const cleanupFixture = (
let sessionLive = true;
let fullStatusCalls = 0;
let snapshotReads = 0;
let snapshotError: Error | undefined;
let events: Stream.Stream<OrchestrationV2DomainEvent> = Stream.empty;
const activated = yield* Deferred.make<void>();
const context = yield* Layer.build(StorageCleanup.layer).pipe(
Expand Down Expand Up @@ -304,9 +305,11 @@ const cleanupFixture = (
} as unknown as ProjectStore.ProjectStoreV2["Service"]),
Effect.provideService(ProjectionStore.ProjectionStoreV2, {
getShellSnapshot: (input?: { location?: string }) =>
Effect.sync(() => {
Effect.suspend(() => {
snapshotReads++;
return { threads: input?.location === "archive" ? [] : threads };
return snapshotError
? Effect.fail(snapshotError)
: Effect.succeed({ threads: input?.location === "archive" ? [] : threads });
}),
getThreadWorktreePaths: () =>
Effect.sync(() =>
Expand Down Expand Up @@ -407,6 +410,9 @@ const cleanupFixture = (
setProjectRoots: (roots: string[]) => {
projectRoots = roots;
},
setArtifacts: (patch: Partial<StorageCleanupSettings>) => {
settings = { ...settings, storageCleanup: { ...settings.storageCleanup, ...patch } };
},
setSessionLive: (live: boolean) => {
sessionLive = live;
},
Expand All @@ -415,6 +421,9 @@ const cleanupFixture = (
},
fullStatusCalls: () => fullStatusCalls,
snapshotReads: () => snapshotReads,
failSnapshot: () => {
snapshotError = new Error("snapshot unavailable");
},
activate: (domainEvents: Stream.Stream<OrchestrationV2DomainEvent>) => {
events = domainEvents;
return Deferred.succeed(activated, undefined);
Expand Down Expand Up @@ -1202,6 +1211,274 @@ describe("storage cleanup reports and local file policies", () => {
),
);

it.live("cleans old settled dependencies and Next builds without removing the worktree", () =>
runCleanupTest(
Effect.gen(function* () {
const { service, fs, worktree, disable, setThreads } = yield* cleanupFixture();
disable();
setThreads([
shell({
branch: "feature",
worktreePath: worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
}),
]);
yield* fs.writeFileString(`${worktree}/.gitignore`, "node_modules/\n.next/\n");
for (const dir of ["node_modules/pkg", "apps/web/.next/cache"]) {
yield* fs.makeDirectory(`${worktree}/${dir}`, { recursive: true });
yield* fs.writeFileString(`${worktree}/${dir}/data`, "cache");
}
yield* service.runNow;
expect(yield* fs.exists(`${worktree}/node_modules`)).toBe(false);
expect(yield* fs.exists(`${worktree}/apps/web/.next`)).toBe(false);
expect(yield* fs.readFileString(`${worktree}/tracked.txt`)).toBe("original\n");
expect(yield* fs.exists(`${worktree}/.git`)).toBe(true);
}),
),
);
it.live("uses configured directory suffixes and preserves unignored and tracked files", () =>
runCleanupTest(
Effect.gen(function* () {
const { service, fs, worktree, disable, setThreads, setArtifacts, command } =
yield* cleanupFixture();
disable();
setThreads([
shell({
branch: "feature",
worktreePath: worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
}),
]);
setArtifacts({ settledArtifactDirectories: [".turbo/cache", ".next/cache", "output"] });
yield* fs.makeDirectory(`${worktree}/apps/web/.turbo/cache`, { recursive: true });
yield* fs.writeFileString(`${worktree}/apps/web/.turbo/cache/tracked`, "source");
yield* command(worktree, ["add", "apps/web/.turbo/cache/tracked"]);
yield* fs.writeFileString(
`${worktree}/.gitignore`,
"**/.turbo/cache/\nnode_modules/\n.next/\n",
);
yield* fs.makeDirectory(`${worktree}/apps/web/.next/cache`, { recursive: true });
yield* fs.writeFileString(`${worktree}/apps/web/.next/cache/data`, "cache");
yield* fs.writeFileString(`${worktree}/apps/web/.next/build`, "keep build");
yield* fs.writeFileString(`${worktree}/apps/web/.turbo/cache/generated`, "cache");
yield* fs.makeDirectory(`${worktree}/node_modules`);
yield* fs.makeDirectory(`${worktree}/output`);
yield* fs.writeFileString(`${worktree}/output/notes`, "source");
yield* service.runNow;
expect(yield* fs.exists(`${worktree}/apps/web/.turbo/cache/generated`)).toBe(false);
expect(yield* fs.readFileString(`${worktree}/apps/web/.turbo/cache/tracked`)).toBe(
"source",
);
expect(yield* fs.exists(`${worktree}/node_modules`)).toBe(true);
expect(yield* fs.exists(`${worktree}/apps/web/.next/cache`)).toBe(false);
expect(yield* fs.readFileString(`${worktree}/apps/web/.next/build`)).toBe("keep build");
expect(yield* fs.readFileString(`${worktree}/output/notes`)).toBe("source");
}),
),
);
it.live("keeps artifacts for recent, active, shared, disabled and session-held worktrees", () =>
runCleanupTest(
Effect.gen(function* () {
const { service, fs, worktree, disable, setThreads, setArtifacts, setOpenSessionCwd } =
yield* cleanupFixture();
disable();
const old = shell({
branch: "feature",
worktreePath: worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
});
yield* fs.makeDirectory(`${worktree}/node_modules`);
yield* fs.writeFileString(`${worktree}/node_modules/data`, "keep");
for (const threads of [
[
{
...old,
settledAt: DateTime.makeUnsafe(
DateTime.toEpochMillis(yield* DateTime.now) - 13 * DAY_MS,
),
},
],
[{ ...old, settledOverride: "active" as const }],
[{ ...old, activeRunId: RunId.make("running") }],
[old, { ...old, id: ThreadId.make("other") }],
]) {
setThreads(threads);
yield* service.runNow;
expect(yield* fs.exists(`${worktree}/node_modules/data`)).toBe(true);
}
setThreads([old]);
setOpenSessionCwd(worktree);
yield* service.runNow;
expect(yield* fs.exists(`${worktree}/node_modules/data`)).toBe(true);
setOpenSessionCwd(null);
setArtifacts({ settledArtifactsAfterDays: null });
yield* service.runNow;
expect(yield* fs.exists(`${worktree}/node_modules/data`)).toBe(true);
setArtifacts({ settledArtifactsAfterDays: 14, settledArtifactDirectories: [] });
yield* service.runNow;
expect(yield* fs.exists(`${worktree}/node_modules/data`)).toBe(true);
}),
),
);
it.live("cleans artifacts held by a released provider session", () =>
runCleanupTest(
Effect.gen(function* () {
const fixture = yield* cleanupFixture();
fixture.disable();
fixture.setThreads([
shell({
branch: "feature",
worktreePath: fixture.worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
}),
]);
const target = `${fixture.worktree}/node_modules`;
yield* fixture.fs.makeDirectory(target);
yield* fixture.fs.writeFileString(`${target}/data`, "cache");
fixture.setOpenSessionCwd(target);
yield* fixture.service.runNow;
expect(yield* fixture.fs.exists(target)).toBe(true);
fixture.setSessionLive(false);
const report = yield* fixture.service.runNow;
expect(yield* fixture.fs.exists(target)).toBe(false);
expect(report.entries).toContainEqual(
expect.objectContaining({
kind: "worktree-artifacts",
outcome: "removed",
path: target,
}),
);
}),
),
);
it.live("reports and preserves artifacts when discovery exceeds the entry limit", () =>
runCleanupTest(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
let root: string | undefined;
const fixture = yield* cleanupFixture().pipe(
Effect.provideService(FileSystem.FileSystem, {
...fs,
readDirectory: (directory) =>
directory === root
? Effect.succeed(["node_modules", ...Array.from({ length: 100_000 }, () => ".git")])
: fs.readDirectory(directory),
}),
);
fixture.disable();
root = yield* fs.realPath(fixture.worktree);
fixture.setThreads([
shell({
branch: "feature",
worktreePath: fixture.worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
}),
]);
yield* fs.makeDirectory(`${root}/node_modules`);
yield* fs.writeFileString(`${root}/node_modules/data`, "keep");
const report = yield* fixture.service.runNow;
expect(yield* fs.readFileString(`${root}/node_modules/data`)).toBe("keep");
expect(report.entries).toContainEqual(
expect.objectContaining({
kind: "worktree-artifacts",
outcome: "kept",
path: fixture.worktree,
reason: "Too many entries to inspect (limit: 100000)",
}),
);
}),
),
);
it.live("reports artifact cleanup failures before thread discovery", () =>
runCleanupTest(
Effect.gen(function* () {
const fixture = yield* cleanupFixture();
fixture.disable();
fixture.failSnapshot();
const report = yield* fixture.service.runNow;
expect(report.entries).toContainEqual({
kind: "worktree-artifacts",
outcome: "failed",
reason: "snapshot unavailable",
path: null,
threadId: null,
threadTitle: null,
bytes: null,
files: null,
});
}),
),
);
it.live("does not follow symlinks or clean nested repositories", () =>
runCleanupTest(
Effect.gen(function* () {
const { service, fs, worktree, disable, setThreads, command } = yield* cleanupFixture();
disable();
setThreads([
shell({
branch: "feature",
worktreePath: worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
}),
]);
const outside = yield* fs.makeTempDirectoryScoped();
yield* fs.writeFileString(`${outside}/keep`, "source");
yield* fs.symlink(outside, `${worktree}/node_modules`);
yield* fs.makeDirectory(`${worktree}/nested/node_modules`, { recursive: true });
yield* command(`${worktree}/nested`, ["init"]);
yield* fs.writeFileString(`${worktree}/nested/node_modules/keep`, "source");
yield* service.runNow;
expect(yield* fs.readFileString(`${outside}/keep`)).toBe("source");
expect(yield* fs.readFileString(`${worktree}/nested/node_modules/keep`)).toBe("source");
}),
),
);
it.live("rechecks settlement after discovering artifacts", () =>
runCleanupTest(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const discovering = yield* Deferred.make<void>();
const resume = yield* Deferred.make<void>();
let root: string | undefined;
const fixture = yield* cleanupFixture().pipe(
Effect.provideService(FileSystem.FileSystem, {
...fs,
readDirectory: (directory) =>
Effect.gen(function* () {
if (directory === root) {
yield* Deferred.succeed(discovering, undefined);
yield* Deferred.await(resume);
}
return yield* fs.readDirectory(directory);
}),
}),
);
fixture.disable();
root = yield* fs.realPath(fixture.worktree);
const old = shell({
branch: "feature",
worktreePath: fixture.worktree,
settledOverride: "settled",
settledAt: at(-30 * DAY_MS),
});
fixture.setThreads([old]);
yield* fs.makeDirectory(`${root}/node_modules`);
yield* fs.writeFileString(`${root}/node_modules/data`, "keep");
const cleanup = yield* fixture.service.runNow.pipe(Effect.forkChild);
yield* Deferred.await(discovering);
fixture.setThreads([{ ...old, settledOverride: "active" }]);
yield* Deferred.succeed(resume, undefined);
yield* Fiber.join(cleanup);
expect(yield* fs.exists(`${root}/node_modules/data`)).toBe(true);
}),
),
);
it.live("measures nested regular files without following symlinks", () =>
runCleanupTest(
Effect.gen(function* () {
Expand Down
Loading
Loading