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* () {...@@ -151,20 +151,29 @@ export const make = Effect.gen(function* () {
151 if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, normalizedCwd))) {151 if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, normalizedCwd))) {
152 return;152 return;
153 }153 }
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 });
154 yield* Effect.gen(function* () {167 yield* Effect.gen(function* () {
155 const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;168 const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;
156 yield* searchIndex.refresh();169 yield* searchIndex.refresh();
157 }).pipe(170 }).pipe(
158 Effect.provide(workspaceSearchIndexes.get(normalizedCwd)),171 Effect.provide(workspaceSearchIndexes.get(normalizedCwd)),
159 Effect.catch((cause) =>172 Effect.catchTags({
160 Effect.gen(function* () {173 WorkspaceSearchIndexCreateFailed: recoverRefreshFailure,
161 yield* Effect.logWarning("Failed to refresh workspace search index", {174 WorkspaceSearchIndexScanTimedOut: recoverRefreshFailure,
162 cwd,175 WorkspaceSearchIndexRefreshFailed: recoverRefreshFailure,
163 cause,176 }),
164 });
165 yield* workspaceSearchIndexes.invalidate(normalizedCwd);
166 }),
167 ),
168 );177 );
169 },178 },
170 );179 );
apps/server/src/workspace/WorkspaceSearchIndex.test.ts+31
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import { FileFinder } from "@ff-labs/fff-node";1import { FileFinder } from "@ff-labs/fff-node";
2import { afterEach, expect, it } from "@effect/vitest";2import { afterEach, expect, it } from "@effect/vitest";
3import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
4import { vi } from "vite-plus/test";6import { vi } from "vite-plus/test";
57
6import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";8import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";
...@@ -49,6 +51,35 @@ it.effect("keeps returned FileFinder creation diagnostics out of the cause chain...@@ -49,6 +51,35 @@ it.effect("keeps returned FileFinder creation diagnostics out of the cause chain
49 }),51 }),
50);52);
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
52it.effect("preserves search and refresh failures with operation context", () =>83it.effect("preserves search and refresh failures with operation context", () =>
53 Effect.scoped(84 Effect.scoped(
54 Effect.gen(function* () {85 Effect.gen(function* () {
apps/server/src/workspace/WorkspaceSearchIndex.ts+16-1
...@@ -71,6 +71,18 @@ export class WorkspaceSearchIndexRefreshFailed extends Schema.TaggedErrorClass<W...@@ -71,6 +71,18 @@ export class WorkspaceSearchIndexRefreshFailed extends Schema.TaggedErrorClass<W
71 }71 }
72}72}
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
74export type WorkspaceSearchIndexError =86export type WorkspaceSearchIndexError =
75 | WorkspaceSearchIndexCreateFailed87 | WorkspaceSearchIndexCreateFailed
76 | WorkspaceSearchIndexScanTimedOut88 | WorkspaceSearchIndexScanTimedOut
...@@ -201,7 +213,10 @@ const waitForScan = <E>(cwd: string, finder: FileFinder, onFailure: (cause: unkn...@@ -201,7 +213,10 @@ const waitForScan = <E>(cwd: string, finder: FileFinder, onFailure: (cause: unkn
201213
202export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: string) {214export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: string) {
203 const finder = yield* Effect.acquireRelease(createFinder(cwd), (finder) =>215 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),
205 );220 );
206 yield* waitForScan(221 yield* waitForScan(
207 cwd,222 cwd,