From 0cb6387a689b7e221de41e1149cf8593f12adfb9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 18:43:36 -0700 Subject: [PATCH] Structure workspace search cleanup failures Co-authored-by: codex --- apps/server/src/workspace/WorkspaceEntries.ts | 27 ++++++++++------ .../workspace/WorkspaceSearchIndex.test.ts | 31 +++++++++++++++++++ .../src/workspace/WorkspaceSearchIndex.ts | 17 +++++++++- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceEntries.ts b/apps/server/src/workspace/WorkspaceEntries.ts index 81fb735ea2e..7501cbe0eab 100644 --- a/apps/server/src/workspace/WorkspaceEntries.ts +++ b/apps/server/src/workspace/WorkspaceEntries.ts @@ -151,20 +151,29 @@ export const make = Effect.gen(function* () { if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, normalizedCwd))) { return; } + const recoverRefreshFailure = ( + cause: + | WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed + | WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut + | WorkspaceSearchIndex.WorkspaceSearchIndexRefreshFailed, + ) => + Effect.gen(function* () { + yield* Effect.logWarning("Failed to refresh workspace search index", { + cwd, + cause, + }); + yield* workspaceSearchIndexes.invalidate(normalizedCwd); + }); yield* Effect.gen(function* () { const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex; yield* searchIndex.refresh(); }).pipe( Effect.provide(workspaceSearchIndexes.get(normalizedCwd)), - Effect.catch((cause) => - Effect.gen(function* () { - yield* Effect.logWarning("Failed to refresh workspace search index", { - cwd, - cause, - }); - yield* workspaceSearchIndexes.invalidate(normalizedCwd); - }), - ), + Effect.catchTags({ + WorkspaceSearchIndexCreateFailed: recoverRefreshFailure, + WorkspaceSearchIndexScanTimedOut: recoverRefreshFailure, + WorkspaceSearchIndexRefreshFailed: recoverRefreshFailure, + }), ); }, ); diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts index 41ea90b9735..9b7ed4e2453 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts @@ -1,6 +1,8 @@ import { FileFinder } from "@ff-labs/fff-node"; import { afterEach, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import { vi } from "vite-plus/test"; import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; @@ -49,6 +51,35 @@ it.effect("keeps returned FileFinder creation diagnostics out of the cause chain }), ); +it.effect("preserves FileFinder destroy failures as structured defects", () => + Effect.gen(function* () { + const cause = new Error("native destroy failed"); + const finder = { + destroy: vi.fn(() => { + throw cause; + }), + isScanning: vi.fn(() => false), + } as unknown as FileFinder; + vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); + + const exit = yield* Effect.scoped(WorkspaceSearchIndex.make("/workspace/project")).pipe( + Effect.exit, + ); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.hasDies(exit.cause)).toBe(true); + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(WorkspaceSearchIndex.WorkspaceSearchIndexDestroyFailed); + expect(error).toMatchObject({ + _tag: "WorkspaceSearchIndexDestroyFailed", + cwd: "/workspace/project", + cause, + }); + } + }), +); + it.effect("preserves search and refresh failures with operation context", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.ts b/apps/server/src/workspace/WorkspaceSearchIndex.ts index 2b043e05c0e..db4d46851e7 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.ts @@ -71,6 +71,18 @@ export class WorkspaceSearchIndexRefreshFailed extends Schema.TaggedErrorClass()( + "WorkspaceSearchIndexDestroyFailed", + { + cwd: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to destroy the workspace search index for '${this.cwd}'.`; + } +} + export type WorkspaceSearchIndexError = | WorkspaceSearchIndexCreateFailed | WorkspaceSearchIndexScanTimedOut @@ -201,7 +213,10 @@ const waitForScan = (cwd: string, finder: FileFinder, onFailure: (cause: unkn export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: string) { const finder = yield* Effect.acquireRelease(createFinder(cwd), (finder) => - Effect.sync(() => finder.destroy()), + Effect.try({ + try: () => finder.destroy(), + catch: (cause) => new WorkspaceSearchIndexDestroyFailed({ cwd, cause }), + }).pipe(Effect.orDie), ); yield* waitForScan( cwd,