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"; afterEach(() => { vi.restoreAllMocks(); }); it.effect("preserves unexpected FileFinder creation failures", () => Effect.gen(function* () { const cause = new Error("native initialization failed"); vi.spyOn(FileFinder, "create").mockImplementationOnce(() => { throw cause; }); const error = yield* Effect.flip( Effect.scoped(WorkspaceSearchIndex.make("/workspace/project")), ); expect(error).toMatchObject({ _tag: "WorkspaceSearchIndexCreateFailed", cwd: "/workspace/project", reason: "FileFinder.create threw unexpectedly.", cause, }); }), ); it.effect("keeps returned FileFinder creation diagnostics out of the cause chain", () => Effect.gen(function* () { vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: false, error: "native index rejected the directory", }); const error = yield* Effect.flip( Effect.scoped(WorkspaceSearchIndex.make("/workspace/project")), ); expect(error).toMatchObject({ _tag: "WorkspaceSearchIndexCreateFailed", cwd: "/workspace/project", reason: "native index rejected the directory", }); expect(error.cause).toBeUndefined(); }), ); 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* () { const searchCause = new Error("native search failed"); const refreshCause = new Error("native scan failed"); const finder = { destroy: vi.fn(), isScanning: vi.fn(() => false), mixedSearch: vi.fn(() => { throw searchCause; }), scanFiles: vi.fn(() => { throw refreshCause; }), } as unknown as FileFinder; vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); const searchIndex = yield* WorkspaceSearchIndex.make("/workspace/project"); const query = "authorization: Bearer secret-token"; const searchError = yield* Effect.flip(searchIndex.search(query, 3)); const refreshError = yield* Effect.flip(searchIndex.refresh()); expect(searchError).toMatchObject({ _tag: "WorkspaceSearchIndexSearchFailed", cwd: "/workspace/project", queryLength: query.length, pageSize: 4, reason: "FileFinder.mixedSearch threw unexpectedly.", cause: searchCause, }); expect(searchError).not.toHaveProperty("query"); expect(searchError.message).not.toMatch(/Bearer|secret-token/); expect(refreshError).toMatchObject({ _tag: "WorkspaceSearchIndexRefreshFailed", cwd: "/workspace/project", reason: "FileFinder.scanFiles threw unexpectedly.", cause: refreshCause, }); }), ), ); it.effect("keeps returned search diagnostics out of the cause chain", () => Effect.scoped( Effect.gen(function* () { const finder = { destroy: vi.fn(), isScanning: vi.fn(() => false), mixedSearch: vi.fn(() => ({ ok: false, error: "native query rejected" })), scanFiles: vi.fn(() => ({ ok: false, error: "native refresh rejected" })), } as unknown as FileFinder; vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); const searchIndex = yield* WorkspaceSearchIndex.make("/workspace/project"); const query = "authorization: Bearer secret-token"; const searchError = yield* Effect.flip(searchIndex.search(query, 3)); const refreshError = yield* Effect.flip(searchIndex.refresh()); expect(searchError).toMatchObject({ _tag: "WorkspaceSearchIndexSearchFailed", cwd: "/workspace/project", queryLength: query.length, pageSize: 4, reason: "native query rejected", }); expect(searchError).not.toHaveProperty("query"); expect(searchError.message).not.toMatch(/Bearer|secret-token/); expect(searchError.cause).toBeUndefined(); expect(refreshError).toMatchObject({ _tag: "WorkspaceSearchIndexRefreshFailed", cwd: "/workspace/project", reason: "native refresh rejected", }); expect(refreshError.cause).toBeUndefined(); }), ), );