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(); }), ),);
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();
}),
),
);