authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 18:58:08-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 18:58:08-07:00
log2fbb0565d7976cbb338f5593a566ff53c6165419
treeb5d6965f36db07e44e46e17043dca6d164e6eb74
parenta3dadc04bf38b49bc8476a8e6c59bda7dc81d2e4
signature Signed by PGP key B5690EEEBB952194

[codex] Structure workspace search cleanup failures (#3465)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 65 insertions(+), 10 deletions(-)

apps/server/src/workspace/WorkspaceEntries.ts+18-9
......@@ -151,20 +151,29 @@ export const make = Effect.gen(function* () {
151151 if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, normalizedCwd))) {
152152 return;
153153 }
154 const recoverRefreshFailure = (
155 cause:
156 | WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed
157 | WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut
158 | WorkspaceSearchIndex.WorkspaceSearchIndexRefreshFailed,
159 ) =>
160 Effect.gen(function* () {
161 yield* Effect.logWarning("Failed to refresh workspace search index", {
162 cwd,
163 cause,
164 });
165 yield* workspaceSearchIndexes.invalidate(normalizedCwd);
166 });
154167 yield* Effect.gen(function* () {
155168 const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;
156169 yield* searchIndex.refresh();
157170 }).pipe(
158171 Effect.provide(workspaceSearchIndexes.get(normalizedCwd)),
159 Effect.catch((cause) =>
160 Effect.gen(function* () {
161 yield* Effect.logWarning("Failed to refresh workspace search index", {
162 cwd,
163 cause,
164 });
165 yield* workspaceSearchIndexes.invalidate(normalizedCwd);
166 }),
167 ),
172 Effect.catchTags({
173 WorkspaceSearchIndexCreateFailed: recoverRefreshFailure,
174 WorkspaceSearchIndexScanTimedOut: recoverRefreshFailure,
175 WorkspaceSearchIndexRefreshFailed: recoverRefreshFailure,
176 }),
168177 );
169178 },
170179 );
apps/server/src/workspace/WorkspaceSearchIndex.test.ts+31
......@@ -1,6 +1,8 @@
11import { FileFinder } from "@ff-labs/fff-node";
22import { afterEach, expect, it } from "@effect/vitest";
3import * as Cause from "effect/Cause";
34import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
46import { vi } from "vite-plus/test";
57
68import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";
......@@ -49,6 +51,35 @@ it.effect("keeps returned FileFinder creation diagnostics out of the cause chain
4951 }),
5052);
5153
54it.effect("preserves FileFinder destroy failures as structured defects", () =>
55 Effect.gen(function* () {
56 const cause = new Error("native destroy failed");
57 const finder = {
58 destroy: vi.fn(() => {
59 throw cause;
60 }),
61 isScanning: vi.fn(() => false),
62 } as unknown as FileFinder;
63 vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder });
64
65 const exit = yield* Effect.scoped(WorkspaceSearchIndex.make("/workspace/project")).pipe(
66 Effect.exit,
67 );
68
69 expect(Exit.isFailure(exit)).toBe(true);
70 if (Exit.isFailure(exit)) {
71 expect(Cause.hasDies(exit.cause)).toBe(true);
72 const error = Cause.squash(exit.cause);
73 expect(error).toBeInstanceOf(WorkspaceSearchIndex.WorkspaceSearchIndexDestroyFailed);
74 expect(error).toMatchObject({
75 _tag: "WorkspaceSearchIndexDestroyFailed",
76 cwd: "/workspace/project",
77 cause,
78 });
79 }
80 }),
81);
82
5283it.effect("preserves search and refresh failures with operation context", () =>
5384 Effect.scoped(
5485 Effect.gen(function* () {
apps/server/src/workspace/WorkspaceSearchIndex.ts+16-1
......@@ -71,6 +71,18 @@ export class WorkspaceSearchIndexRefreshFailed extends Schema.TaggedErrorClass<W
7171 }
7272}
7373
74export class WorkspaceSearchIndexDestroyFailed extends Schema.TaggedErrorClass<WorkspaceSearchIndexDestroyFailed>()(
75 "WorkspaceSearchIndexDestroyFailed",
76 {
77 cwd: Schema.String,
78 cause: Schema.Defect(),
79 },
80) {
81 override get message(): string {
82 return `Failed to destroy the workspace search index for '${this.cwd}'.`;
83 }
84}
85
7486export type WorkspaceSearchIndexError =
7587 | WorkspaceSearchIndexCreateFailed
7688 | WorkspaceSearchIndexScanTimedOut
......@@ -201,7 +213,10 @@ const waitForScan = <E>(cwd: string, finder: FileFinder, onFailure: (cause: unkn
201213
202214export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: string) {
203215 const finder = yield* Effect.acquireRelease(createFinder(cwd), (finder) =>
204 Effect.sync(() => finder.destroy()),
216 Effect.try({
217 try: () => finder.destroy(),
218 catch: (cause) => new WorkspaceSearchIndexDestroyFailed({ cwd, cause }),
219 }).pipe(Effect.orDie),
205220 );
206221 yield* waitForScan(
207222 cwd,