refactor: update testing layers and configurations

- Changed the setup file path in vitest configuration for better organization.
- Refactored the EventBus implementation to streamline event handling.
- Updated various test files to utilize new testing layers for improved clarity and maintainability.
- Introduced new utility layers for file system and persistent service mocks to enhance test reliability.
- Enhanced the platform layer to include necessary services for testing environments.
This commit is contained in:
d-kimsuon
2025-10-18 20:07:47 +09:00
parent 6bea519c57
commit a77d7e205b
20 changed files with 967 additions and 1040 deletions

View File

@@ -2,10 +2,9 @@ import type {
SDKResultMessage,
SDKSystemMessage,
} from "@anthropic-ai/claude-code";
import { Effect, Layer } from "effect";
import { Effect } from "effect";
import { describe, expect, it } from "vitest";
import { EventBus } from "../../events/services/EventBus";
import type { InternalEventDeclaration } from "../../events/types/InternalEventDeclaration";
import { testPlatformLayer } from "../../../../testing/layers/testPlatformLayer";
import type * as CCSessionProcess from "../models/CCSessionProcess";
import type * as CCTask from "../models/ClaudeCodeTask";
import type { InitMessageContext } from "../types";
@@ -57,27 +56,6 @@ const createMockResultMessage = (sessionId: string): SDKResultMessage =>
result: {},
}) as SDKResultMessage;
// Mock EventBus for testing
const MockEventBus = Layer.succeed(EventBus, {
emit: <K extends keyof InternalEventDeclaration>(
_eventName: K,
_event: InternalEventDeclaration[K],
) => Effect.void,
on: <K extends keyof InternalEventDeclaration>(
_eventName: K,
_listener: (event: InternalEventDeclaration[K]) => void,
) => Effect.void,
off: <K extends keyof InternalEventDeclaration>(
_eventName: K,
_listener: (event: InternalEventDeclaration[K]) => void,
) => Effect.void,
});
const TestLayer = Layer.provide(
ClaudeCodeSessionProcessService.Live,
MockEventBus,
);
describe("ClaudeCodeSessionProcessService", () => {
describe("startSessionProcess", () => {
it("can start new session process", async () => {
@@ -96,7 +74,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("pending");
@@ -121,7 +102,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const { result, taskDef } = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.tasks).toHaveLength(1);
@@ -149,7 +133,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const process = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(process.def.sessionProcessId).toBe("process-1");
@@ -168,7 +155,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -189,7 +179,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const processes = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(processes).toHaveLength(0);
@@ -220,7 +213,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const processes = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(processes).toHaveLength(2);
@@ -279,7 +275,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("pending");
@@ -316,7 +315,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -346,7 +348,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -378,7 +383,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("not_initialized");
@@ -415,7 +423,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -455,7 +466,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("initialized");
@@ -488,7 +502,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -537,7 +554,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("paused");
@@ -581,7 +601,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const process = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
const completedTask = process.tasks.find(
@@ -616,7 +639,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -653,7 +679,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("completed");
@@ -689,7 +718,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.task?.status).toBe("completed");
@@ -723,7 +755,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.task?.status).toBe("failed");
@@ -752,7 +787,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.task.def.taskId).toBe("task-1");
@@ -769,7 +807,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const error = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(error).toMatchObject({
@@ -820,7 +861,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("paused");
@@ -895,7 +939,10 @@ describe("ClaudeCodeSessionProcessService", () => {
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestLayer)),
program.pipe(
Effect.provide(ClaudeCodeSessionProcessService.Live),
Effect.provide(testPlatformLayer()),
),
);
expect(result.sessionProcess.type).toBe("paused");

View File

@@ -1,83 +1,67 @@
import { Context, Effect, Layer } from "effect";
import type { InferEffect } from "../../../lib/effect/types";
import type { InternalEventDeclaration } from "../types/InternalEventDeclaration";
type Listener<T> = (data: T) => void | Promise<void>;
interface EventBusService {
readonly emit: <EventName extends keyof InternalEventDeclaration>(
const layerImpl = Effect.gen(function* () {
const listenersMap = new Map<
keyof InternalEventDeclaration,
Set<Listener<unknown>>
>();
const getListeners = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
): Set<Listener<InternalEventDeclaration[EventName]>> => {
if (!listenersMap.has(event)) {
listenersMap.set(event, new Set());
}
return listenersMap.get(event) as Set<
Listener<InternalEventDeclaration[EventName]>
>;
};
const emit = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
data: InternalEventDeclaration[EventName],
) => Effect.Effect<void>;
readonly on: <EventName extends keyof InternalEventDeclaration>(
event: EventName,
listener: Listener<InternalEventDeclaration[EventName]>,
) => Effect.Effect<void>;
readonly off: <EventName extends keyof InternalEventDeclaration>(
event: EventName,
listener: Listener<InternalEventDeclaration[EventName]>,
) => Effect.Effect<void>;
}
export class EventBus extends Context.Tag("EventBus")<
EventBus,
EventBusService
>() {
static Live = Layer.effect(
this,
): Effect.Effect<void> =>
Effect.gen(function* () {
const listenersMap = new Map<
keyof InternalEventDeclaration,
Set<Listener<unknown>>
>();
const listeners = getListeners(event);
const getListeners = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
): Set<Listener<InternalEventDeclaration[EventName]>> => {
if (!listenersMap.has(event)) {
listenersMap.set(event, new Set());
}
return listenersMap.get(event) as Set<
Listener<InternalEventDeclaration[EventName]>
>;
};
void Promise.allSettled(
Array.from(listeners).map(async (listener) => {
await listener(data);
}),
);
});
const emit = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
data: InternalEventDeclaration[EventName],
): Effect.Effect<void> =>
Effect.gen(function* () {
const listeners = getListeners(event);
const on = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
listener: Listener<InternalEventDeclaration[EventName]>,
): Effect.Effect<void> =>
Effect.sync(() => {
const listeners = getListeners(event);
listeners.add(listener);
});
void Promise.allSettled(
Array.from(listeners).map(async (listener) => {
await listener(data);
}),
);
});
const off = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
listener: Listener<InternalEventDeclaration[EventName]>,
): Effect.Effect<void> =>
Effect.sync(() => {
const listeners = getListeners(event);
listeners.delete(listener);
});
const on = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
listener: Listener<InternalEventDeclaration[EventName]>,
): Effect.Effect<void> =>
Effect.sync(() => {
const listeners = getListeners(event);
listeners.add(listener);
});
return {
emit,
on,
off,
} as const;
});
const off = <EventName extends keyof InternalEventDeclaration>(
event: EventName,
listener: Listener<InternalEventDeclaration[EventName]>,
): Effect.Effect<void> =>
Effect.sync(() => {
const listeners = getListeners(event);
listeners.delete(listener);
});
export type IEventBus = InferEffect<typeof layerImpl>;
return {
emit,
on,
off,
} satisfies EventBusService;
}),
);
export class EventBus extends Context.Tag("EventBus")<EventBus, IEventBus>() {
static Live = Layer.effect(this, layerImpl);
}

View File

@@ -1,8 +1,7 @@
import { Path } from "@effect/platform";
import { Effect } from "effect";
import { describe, expect, it } from "vitest";
import { ApplicationContext } from "../../platform/services/ApplicationContext";
import { EnvService } from "../../platform/services/EnvService";
import { testPlatformLayer } from "../../../../testing/layers/testPlatformLayer";
import type { InternalEventDeclaration } from "../types/InternalEventDeclaration";
import { EventBus } from "./EventBus";
import { FileWatcherService } from "./fileWatcher";
@@ -23,9 +22,7 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(testPlatformLayer()),
Effect.provide(Path.layer),
),
);
@@ -49,9 +46,7 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(testPlatformLayer()),
Effect.provide(Path.layer),
),
);
@@ -75,9 +70,7 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(testPlatformLayer()),
Effect.provide(Path.layer),
),
);
@@ -107,9 +100,7 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(testPlatformLayer()),
Effect.provide(Path.layer),
),
);
@@ -156,9 +147,7 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(testPlatformLayer()),
Effect.provide(Path.layer),
),
);
@@ -184,9 +173,7 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(testPlatformLayer()),
Effect.provide(Path.layer),
),
);

View File

@@ -29,7 +29,9 @@ const LayerImpl = Effect.gen(function* () {
if (Either.isLeft(branches)) {
return {
response: [],
response: {
success: false,
},
status: 200,
} as const satisfies ControllerResponse;
}
@@ -59,7 +61,9 @@ const LayerImpl = Effect.gen(function* () {
if (Either.isLeft(commits)) {
return {
response: [],
response: {
success: false,
},
status: 200,
} as const satisfies ControllerResponse;
}

View File

@@ -1,74 +1,15 @@
import { FileSystem, Path } from "@effect/platform";
import { SystemError } from "@effect/platform/Error";
import { Effect, Layer, Option } from "effect";
import { PersistentService } from "../../../lib/storage/FileCacheStorage/PersistentService";
import { ApplicationContext } from "../../platform/services/ApplicationContext";
import { EnvService } from "../../platform/services/EnvService";
import { Effect, Option } from "effect";
import {
createFileInfo,
testFileSystemLayer,
} from "../../../../testing/layers/testFileSystemLayer";
import { testPlatformLayer } from "../../../../testing/layers/testPlatformLayer";
import { testProjectMetaServiceLayer } from "../../../../testing/layers/testProjectMetaServiceLayer";
import type { ProjectMeta } from "../../types";
import { ProjectMetaService } from "../services/ProjectMetaService";
import { ProjectRepository } from "./ProjectRepository";
/**
* Helper function to create FileSystem mock layer
*/
const makeFileSystemMock = (
overrides: Partial<FileSystem.FileSystem>,
): Layer.Layer<FileSystem.FileSystem> => {
return FileSystem.layerNoop(overrides);
};
/**
* Helper function to create Path mock layer
*/
const makePathMock = (): Layer.Layer<Path.Path> => {
return Path.layer;
};
/**
* Helper function to create PersistentService mock layer
*/
const makePersistentServiceMock = (): Layer.Layer<PersistentService> => {
return Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
};
/**
* Helper function to create ProjectMetaService mock layer
*/
const makeProjectMetaServiceMock = (
meta: ProjectMeta,
): Layer.Layer<ProjectMetaService> => {
return Layer.succeed(ProjectMetaService, {
getProjectMeta: () => Effect.succeed(meta),
invalidateProject: () => Effect.void,
});
};
/**
* Helper function to create File.Info mock
*/
const makeFileInfoMock = (
type: "File" | "Directory",
mtime: Date,
): FileSystem.File.Info => ({
type,
mtime: Option.some(mtime),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0o755,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
});
describe("ProjectRepository", () => {
describe("getProject", () => {
it("returns project information when project exists", async () => {
@@ -81,15 +22,14 @@ describe("ProjectRepository", () => {
sessionCount: 5,
};
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: (path: string) => Effect.succeed(path === projectPath),
stat: () => Effect.succeed(makeFileInfoMock("Directory", mockDate)),
stat: () =>
Effect.succeed(
createFileInfo({ type: "Directory", mtime: Option.some(mockDate) }),
),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const ProjectMetaServiceMock = makeProjectMetaServiceMock(mockMeta);
const program = Effect.gen(function* () {
const repo = yield* ProjectRepository;
return yield* repo.getProject(projectId);
@@ -98,12 +38,13 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaServiceMock),
Effect.provide(
testProjectMetaServiceLayer({
meta: mockMeta,
}),
),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -124,15 +65,17 @@ describe("ProjectRepository", () => {
sessionCount: 0,
};
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: () => Effect.succeed(false),
stat: () => Effect.succeed(makeFileInfoMock("Directory", new Date())),
stat: () =>
Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(new Date()),
}),
),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const ProjectMetaServiceMock = makeProjectMetaServiceMock(mockMeta);
const program = Effect.gen(function* () {
const repo = yield* ProjectRepository;
return yield* repo.getProject(projectId);
@@ -142,12 +85,13 @@ describe("ProjectRepository", () => {
Effect.runPromise(
program.pipe(
Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaServiceMock),
Effect.provide(
testProjectMetaServiceLayer({
meta: mockMeta,
}),
),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
),
).rejects.toThrow("Project not found");
@@ -156,20 +100,11 @@ describe("ProjectRepository", () => {
describe("getProjects", () => {
it("returns empty array when project directory does not exist", async () => {
const FileSystemMock = makeFileSystemMock({
exists: () => Effect.succeed(false),
readDirectory: () => Effect.succeed([]),
stat: () => Effect.succeed(makeFileInfoMock("Directory", new Date())),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const mockMeta: ProjectMeta = {
projectName: null,
projectPath: null,
sessionCount: 0,
};
const ProjectMetaServiceMock = makeProjectMetaServiceMock(mockMeta);
const program = Effect.gen(function* () {
const repo = yield* ProjectRepository;
@@ -179,12 +114,25 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaServiceMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testProjectMetaServiceLayer({
meta: mockMeta,
}),
),
Effect.provide(
testFileSystemLayer({
exists: () => Effect.succeed(false),
readDirectory: () => Effect.succeed([]),
stat: () =>
Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(new Date()),
}),
),
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -196,31 +144,6 @@ describe("ProjectRepository", () => {
const date2 = new Date("2024-01-02T00:00:00.000Z");
const date3 = new Date("2024-01-03T00:00:00.000Z");
const FileSystemMock = makeFileSystemMock({
exists: () => Effect.succeed(true),
readDirectory: () =>
Effect.succeed(["project1", "project2", "project3"]),
readFileString: () =>
Effect.succeed('{"type":"user","cwd":"/workspace","text":"test"}'),
stat: (path: string) => {
if (path.includes("project1")) {
return Effect.succeed(makeFileInfoMock("Directory", date2));
}
if (path.includes("project2")) {
return Effect.succeed(makeFileInfoMock("Directory", date3));
}
if (path.includes("project3")) {
return Effect.succeed(makeFileInfoMock("Directory", date1));
}
return Effect.succeed(makeFileInfoMock("Directory", new Date()));
},
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const program = Effect.gen(function* () {
const repo = yield* ProjectRepository;
return yield* repo.getProjects();
@@ -229,12 +152,53 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
exists: () => Effect.succeed(true),
readDirectory: () =>
Effect.succeed(["project1", "project2", "project3"]),
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace","text":"test"}',
),
stat: (path: string) => {
if (path.includes("project1")) {
return Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(date2),
}),
);
}
if (path.includes("project2")) {
return Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(date3),
}),
);
}
if (path.includes("project3")) {
return Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(date1),
}),
);
}
return Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(new Date()),
}),
);
},
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -247,25 +211,6 @@ describe("ProjectRepository", () => {
it("filters only directories", async () => {
const date = new Date("2024-01-01T00:00:00.000Z");
const FileSystemMock = makeFileSystemMock({
exists: () => Effect.succeed(true),
readDirectory: () =>
Effect.succeed(["project1", "file.txt", "project2"]),
readFileString: () =>
Effect.succeed('{"type":"user","cwd":"/workspace","text":"test"}'),
stat: (path: string) => {
if (path.includes("file.txt")) {
return Effect.succeed(makeFileInfoMock("File", date));
}
return Effect.succeed(makeFileInfoMock("Directory", date));
},
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const program = Effect.gen(function* () {
const repo = yield* ProjectRepository;
return yield* repo.getProjects();
@@ -274,12 +219,34 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
exists: () => Effect.succeed(true),
readDirectory: () =>
Effect.succeed(["project1", "file.txt", "project2"]),
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace","text":"test"}',
),
stat: (path: string) => {
if (path.includes("file.txt")) {
return Effect.succeed(
createFileInfo({ type: "File", mtime: Option.some(date) }),
);
}
return Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(date),
}),
);
},
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -292,31 +259,6 @@ describe("ProjectRepository", () => {
it("skips entries where stat retrieval fails", async () => {
const date = new Date("2024-01-01T00:00:00.000Z");
const FileSystemMock = makeFileSystemMock({
exists: () => Effect.succeed(true),
readDirectory: () => Effect.succeed(["project1", "broken", "project2"]),
readFileString: () =>
Effect.succeed('{"type":"user","cwd":"/workspace","text":"test"}'),
stat: (path: string) => {
if (path.includes("broken")) {
return Effect.fail(
new SystemError({
method: "stat",
reason: "PermissionDenied",
module: "FileSystem",
cause: undefined,
}),
);
}
return Effect.succeed(makeFileInfoMock("Directory", date));
},
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const program = Effect.gen(function* () {
const repo = yield* ProjectRepository;
return yield* repo.getProjects();
@@ -325,12 +267,39 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
exists: () => Effect.succeed(true),
readDirectory: () =>
Effect.succeed(["project1", "broken", "project2"]),
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace","text":"test"}',
),
stat: (path: string) => {
if (path.includes("broken")) {
return Effect.fail(
new SystemError({
method: "stat",
reason: "PermissionDenied",
module: "FileSystem",
cause: undefined,
}),
);
}
return Effect.succeed(
createFileInfo({
type: "Directory",
mtime: Option.some(date),
}),
);
},
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
}),
),
Effect.provide(testPlatformLayer()),
),
);

View File

@@ -1,76 +1,14 @@
import { FileSystem, Path } from "@effect/platform";
import { Effect, Layer, Option } from "effect";
import { PersistentService } from "../../../lib/storage/FileCacheStorage/PersistentService";
import { FileSystem } from "@effect/platform";
import { Effect, Option } from "effect";
import { testFileSystemLayer } from "../../../../testing/layers/testFileSystemLayer";
import { testPlatformLayer } from "../../../../testing/layers/testPlatformLayer";
import { ProjectMetaService } from "../services/ProjectMetaService";
/**
* Helper function to create a FileSystem mock layer
* @see FileSystem.layerNoop - Can override only necessary methods
*/
const makeFileSystemMock = (
overrides: Partial<FileSystem.FileSystem>,
): Layer.Layer<FileSystem.FileSystem> => {
return FileSystem.layerNoop(overrides);
};
/**
* Helper function to create a Path mock layer
* @see Path.layer - Uses default POSIX Path implementation
*/
const makePathMock = (): Layer.Layer<Path.Path> => {
return Path.layer;
};
/**
* Helper function to create a PersistentService mock layer
*/
const makePersistentServiceMock = (): Layer.Layer<PersistentService> => {
return Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
};
describe("ProjectMetaService", () => {
describe("getProjectMeta", () => {
it("returns cached metadata", async () => {
let readDirectoryCalls = 0;
const FileSystemMock = makeFileSystemMock({
readDirectory: () => {
readDirectoryCalls++;
return Effect.succeed(["session1.jsonl"]);
},
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace/app","text":"test"}',
),
stat: () =>
Effect.succeed({
type: "File",
mtime: Option.some(new Date("2024-01-01")),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
}),
exists: () => Effect.succeed(true),
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const program = Effect.gen(function* () {
const storage = yield* ProjectMetaService;
const projectId = Buffer.from("/test/project").toString("base64url");
@@ -87,9 +25,39 @@ describe("ProjectMetaService", () => {
const { result1, result2 } = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
readDirectory: () => {
readDirectoryCalls++;
return Effect.succeed(["session1.jsonl"]);
},
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace/app","text":"test"}',
),
stat: () =>
Effect.succeed({
type: "File",
mtime: Option.some(new Date("2024-01-01")),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
}),
exists: () => Effect.succeed(true),
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -101,36 +69,6 @@ describe("ProjectMetaService", () => {
});
it("returns null if project path is not found", async () => {
const FileSystemMock = makeFileSystemMock({
readDirectory: () => Effect.succeed(["session1.jsonl"]),
readFileString: () =>
Effect.succeed('{"type":"summary","text":"summary"}'),
stat: () =>
Effect.succeed({
type: "File",
mtime: Option.some(new Date("2024-01-01")),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
}),
exists: () => Effect.succeed(true),
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const program = Effect.gen(function* () {
const storage = yield* ProjectMetaService;
const projectId = Buffer.from("/test/project").toString("base64url");
@@ -140,9 +78,34 @@ describe("ProjectMetaService", () => {
const result = await Effect.runPromise(
program.pipe(
Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
readDirectory: () => Effect.succeed(["session1.jsonl"]),
readFileString: () =>
Effect.succeed('{"type":"summary","text":"summary"}'),
stat: () =>
Effect.succeed({
type: "File",
mtime: Option.some(new Date("2024-01-01")),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
}),
exists: () => Effect.succeed(true),
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -156,41 +119,6 @@ describe("ProjectMetaService", () => {
it("can invalidate project cache", async () => {
let readDirectoryCalls = 0;
const FileSystemMock = makeFileSystemMock({
readDirectory: () => {
readDirectoryCalls++;
return Effect.succeed(["session1.jsonl"]);
},
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace/app","text":"test"}',
),
stat: () =>
Effect.succeed({
type: "File",
mtime: Option.some(new Date("2024-01-01")),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
}),
exists: () => Effect.succeed(true),
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const program = Effect.gen(function* () {
const storage = yield* ProjectMetaService;
const projectId = Buffer.from("/test/project").toString("base64url");
@@ -208,9 +136,39 @@ describe("ProjectMetaService", () => {
await Effect.runPromise(
program.pipe(
Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
readDirectory: () => {
readDirectoryCalls++;
return Effect.succeed(["session1.jsonl"]);
},
readFileString: () =>
Effect.succeed(
'{"type":"user","cwd":"/workspace/app","text":"test"}',
),
stat: () =>
Effect.succeed({
type: "File",
mtime: Option.some(new Date("2024-01-01")),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
}),
exists: () => Effect.succeed(true),
makeDirectory: () => Effect.void,
writeFileString: () => Effect.void,
}),
),
Effect.provide(testPlatformLayer()),
),
);

View File

@@ -1,59 +1,28 @@
import { FileSystem, Path } from "@effect/platform";
import { SystemError } from "@effect/platform/Error";
import { Effect, Layer, Option } from "effect";
import type { Conversation } from "../../../../lib/conversation-schema";
import { PersistentService } from "../../../lib/storage/FileCacheStorage/PersistentService";
import {
createFileInfo,
testFileSystemLayer,
} from "../../../../testing/layers/testFileSystemLayer";
import { testPlatformLayer } from "../../../../testing/layers/testPlatformLayer";
import { decodeProjectId } from "../../project/functions/id";
import type { ErrorJsonl, SessionDetail, SessionMeta } from "../../types";
import { SessionRepository } from "../infrastructure/SessionRepository";
import { VirtualConversationDatabase } from "../infrastructure/VirtualConversationDatabase";
import { SessionMetaService } from "../services/SessionMetaService";
/**
* Helper function to create a FileSystem mock layer
*/
const makeFileSystemMock = (
overrides: Partial<FileSystem.FileSystem>,
): Layer.Layer<FileSystem.FileSystem> => {
return FileSystem.layerNoop(overrides);
};
/**
* Helper function to create a Path mock layer
*/
const makePathMock = (): Layer.Layer<Path.Path> => {
return Path.layer;
};
/**
* Helper function to create a PersistentService mock layer
*/
const makePersistentServiceMock = (): Layer.Layer<PersistentService> => {
return Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
};
/**
* Helper function to create a SessionMetaService mock layer
*/
const makeSessionMetaServiceMock = (
meta: SessionMeta,
): Layer.Layer<SessionMetaService> => {
return Layer.succeed(SessionMetaService, {
const testSessionMetaServiceLayer = (meta: SessionMeta) => {
return Layer.mock(SessionMetaService, {
getSessionMeta: () => Effect.succeed(meta),
invalidateSession: () => Effect.void,
});
};
/**
* Helper function to create a PredictSessionsDatabase mock layer
*/
const makePredictSessionsDatabaseMock = (
const testPredictSessionsDatabaseLayer = (
sessions: Map<string, SessionDetail>,
): Layer.Layer<VirtualConversationDatabase> => {
return Layer.succeed(VirtualConversationDatabase, {
) => {
return Layer.mock(VirtualConversationDatabase, {
getProjectVirtualConversations: (projectId: string) =>
Effect.succeed(
Array.from(sessions.values())
@@ -79,34 +48,9 @@ const makePredictSessionsDatabaseMock = (
: null,
);
},
createVirtualConversation: () => Effect.void,
deleteVirtualConversations: () => Effect.void,
});
};
/**
* Helper function to create a File.Info mock
*/
const makeFileInfoMock = (
type: "File" | "Directory",
mtime: Date,
): FileSystem.File.Info => ({
type,
mtime: Option.some(mtime),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0o755,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(0n),
blksize: Option.none(),
blocks: Option.none(),
});
describe("SessionRepository", () => {
describe("getSession", () => {
it("returns session details when session file exists", async () => {
@@ -121,26 +65,8 @@ describe("SessionRepository", () => {
const mockContent = `{"type":"user","message":{"role":"user","content":"Hello"}}\n{"type":"assistant","message":{"role":"assistant","content":"Hi"}}\n{"type":"user","message":{"role":"user","content":"Test"}}`;
const FileSystemMock = makeFileSystemMock({
exists: (path: string) => Effect.succeed(path === sessionPath),
readFileString: (path: string) =>
path === sessionPath
? Effect.succeed(mockContent)
: Effect.fail(
new SystemError({
method: "readFileString",
reason: "NotFound",
module: "FileSystem",
cause: undefined,
}),
),
stat: () => Effect.succeed(makeFileInfoMock("File", mockDate)),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock(mockMeta);
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const SessionMetaServiceMock = testSessionMetaServiceLayer(mockMeta);
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -154,9 +80,30 @@ describe("SessionRepository", () => {
Effect.provide(SessionRepository.Live),
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(
testFileSystemLayer({
exists: (path: string) => Effect.succeed(path === sessionPath),
readFileString: (path: string) =>
path === sessionPath
? Effect.succeed(mockContent)
: Effect.fail(
new SystemError({
method: "readFileString",
reason: "NotFound",
module: "FileSystem",
cause: undefined,
}),
),
stat: () =>
Effect.succeed(
createFileInfo({
type: "File",
mtime: Option.some(mockDate),
}),
),
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -190,13 +137,11 @@ describe("SessionRepository", () => {
},
];
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: () => Effect.succeed(false),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock({
const SessionMetaServiceMock = testSessionMetaServiceLayer({
messageCount: 0,
firstUserMessage: null,
});
@@ -230,8 +175,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -246,17 +190,15 @@ describe("SessionRepository", () => {
const projectId = Buffer.from("/test/project").toString("base64url");
const sessionId = "nonexistent-session";
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: () => Effect.succeed(false),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock({
const SessionMetaServiceMock = testSessionMetaServiceLayer({
messageCount: 0,
firstUserMessage: null,
});
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -271,8 +213,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -283,17 +224,15 @@ describe("SessionRepository", () => {
const projectId = Buffer.from("/test/project").toString("base64url");
const sessionId = "resume-session-id";
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: () => Effect.succeed(false),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock({
const SessionMetaServiceMock = testSessionMetaServiceLayer({
messageCount: 0,
firstUserMessage: null,
});
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -308,8 +247,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -329,7 +267,7 @@ describe("SessionRepository", () => {
firstUserMessage: null,
};
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: (path: string) => Effect.succeed(path === projectPath),
readDirectory: (path: string) =>
path === projectPath
@@ -337,19 +275,23 @@ describe("SessionRepository", () => {
: Effect.succeed([]),
stat: (path: string) => {
if (path.includes("session1.jsonl")) {
return Effect.succeed(makeFileInfoMock("File", date2));
return Effect.succeed(
createFileInfo({ type: "File", mtime: Option.some(date2) }),
);
}
if (path.includes("session2.jsonl")) {
return Effect.succeed(makeFileInfoMock("File", date1));
return Effect.succeed(
createFileInfo({ type: "File", mtime: Option.some(date1) }),
);
}
return Effect.succeed(makeFileInfoMock("File", new Date()));
return Effect.succeed(
createFileInfo({ type: "File", mtime: Option.some(new Date()) }),
);
},
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock(mockMeta);
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const SessionMetaServiceMock = testSessionMetaServiceLayer(mockMeta);
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -364,8 +306,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -384,7 +325,7 @@ describe("SessionRepository", () => {
firstUserMessage: null,
};
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: (path: string) => Effect.succeed(path === projectPath),
readDirectory: (path: string) =>
path === projectPath
@@ -394,13 +335,12 @@ describe("SessionRepository", () => {
"session3.jsonl",
])
: Effect.succeed([]),
stat: () => Effect.succeed(makeFileInfoMock("File", mockDate)),
stat: () =>
Effect.succeed(createFileInfo({ mtime: Option.some(mockDate) })),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock(mockMeta);
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const SessionMetaServiceMock = testSessionMetaServiceLayer(mockMeta);
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -415,8 +355,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -433,7 +372,7 @@ describe("SessionRepository", () => {
firstUserMessage: null,
};
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: (path: string) => Effect.succeed(path === projectPath),
readDirectory: (path: string) =>
path === projectPath
@@ -443,13 +382,12 @@ describe("SessionRepository", () => {
"session3.jsonl",
])
: Effect.succeed([]),
stat: () => Effect.succeed(makeFileInfoMock("File", mockDate)),
stat: () =>
Effect.succeed(createFileInfo({ mtime: Option.some(mockDate) })),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock(mockMeta);
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const SessionMetaServiceMock = testSessionMetaServiceLayer(mockMeta);
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -466,8 +404,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -478,7 +415,7 @@ describe("SessionRepository", () => {
it("returns empty array when project does not exist", async () => {
const projectId = Buffer.from("/nonexistent").toString("base64url");
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: () => Effect.succeed(false),
readDirectory: () =>
Effect.fail(
@@ -491,13 +428,11 @@ describe("SessionRepository", () => {
),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock({
const SessionMetaServiceMock = testSessionMetaServiceLayer({
messageCount: 0,
firstUserMessage: null,
});
const PredictSessionsDatabaseMock = makePredictSessionsDatabaseMock(
const PredictSessionsDatabaseMock = testPredictSessionsDatabaseLayer(
new Map(),
);
@@ -512,8 +447,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);
@@ -546,18 +480,17 @@ describe("SessionRepository", () => {
},
];
const FileSystemMock = makeFileSystemMock({
const FileSystemMock = testFileSystemLayer({
exists: (path: string) => Effect.succeed(path === projectPath),
readDirectory: (path: string) =>
path === projectPath
? Effect.succeed(["session1.jsonl"])
: Effect.succeed([]),
stat: () => Effect.succeed(makeFileInfoMock("File", mockDate)),
stat: () =>
Effect.succeed(createFileInfo({ mtime: Option.some(mockDate) })),
});
const PathMock = makePathMock();
const PersistentServiceMock = makePersistentServiceMock();
const SessionMetaServiceMock = makeSessionMetaServiceMock(mockMeta);
const SessionMetaServiceMock = testSessionMetaServiceLayer(mockMeta);
const PredictSessionsDatabaseMock = Layer.succeed(
VirtualConversationDatabase,
{
@@ -590,8 +523,7 @@ describe("SessionRepository", () => {
Effect.provide(SessionMetaServiceMock),
Effect.provide(PredictSessionsDatabaseMock),
Effect.provide(FileSystemMock),
Effect.provide(PathMock),
Effect.provide(PersistentServiceMock),
Effect.provide(testPlatformLayer()),
),
);

View File

@@ -1,172 +1,100 @@
import { Path } from "@effect/platform";
import { Effect, Layer, Ref } from "effect";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { testPlatformLayer } from "../../testing/layers/testPlatformLayer";
import { testProjectMetaServiceLayer } from "../../testing/layers/testProjectMetaServiceLayer";
import { testProjectRepositoryLayer } from "../../testing/layers/testProjectRepositoryLayer";
import { testSessionMetaServiceLayer } from "../../testing/layers/testSessionMetaServiceLayer";
import { testSessionRepositoryLayer } from "../../testing/layers/testSessionRepositoryLayer";
import { EventBus } from "../core/events/services/EventBus";
import { FileWatcherService } from "../core/events/services/fileWatcher";
import type { InternalEventDeclaration } from "../core/events/types/InternalEventDeclaration";
import { ApplicationContext } from "../core/platform/services/ApplicationContext";
import { EnvService } from "../core/platform/services/EnvService";
import { ProjectRepository } from "../core/project/infrastructure/ProjectRepository";
import { ProjectMetaService } from "../core/project/services/ProjectMetaService";
import { SessionRepository } from "../core/session/infrastructure/SessionRepository";
import { VirtualConversationDatabase } from "../core/session/infrastructure/VirtualConversationDatabase";
import { SessionMetaService } from "../core/session/services/SessionMetaService";
import { InitializeService } from "./initialize";
const fileWatcherWithEventBus = FileWatcherService.Live.pipe(
Layer.provide(EventBus.Live),
);
const allDependencies = Layer.mergeAll(
fileWatcherWithEventBus,
VirtualConversationDatabase.Live,
testProjectMetaServiceLayer({
meta: {
projectName: "Test Project",
projectPath: "/path/to/project",
sessionCount: 0,
},
}),
testSessionMetaServiceLayer({
meta: {
messageCount: 0,
firstUserMessage: null,
},
}),
testPlatformLayer(),
);
const sharedTestLayer = Layer.provide(
InitializeService.Live,
allDependencies,
).pipe(Layer.merge(allDependencies));
describe("InitializeService", () => {
const createMockProjectRepository = (
projects: Array<{
id: string;
claudeProjectPath: string;
lastModifiedAt: Date;
meta: {
projectName: string | null;
projectPath: string | null;
sessionCount: number;
};
}> = [],
) =>
Layer.succeed(ProjectRepository, {
getProjects: () => Effect.succeed({ projects }),
getProject: () => Effect.fail(new Error("Not implemented in mock")),
});
const createMockSessionRepository = (
sessions: Array<{
id: string;
jsonlFilePath: string;
lastModifiedAt: Date;
meta: {
messageCount: number;
firstUserMessage: {
kind: "command";
commandName: string;
commandArgs?: string;
commandMessage?: string;
} | null;
};
}> = [],
getSessionsCb?: (projectId: string) => void,
) =>
Layer.succeed(SessionRepository, {
getSessions: (projectId: string) => {
if (getSessionsCb) getSessionsCb(projectId);
return Effect.succeed({ sessions });
},
getSession: () => Effect.fail(new Error("Not implemented in mock")),
});
const createMockProjectMetaService = () =>
Layer.succeed(ProjectMetaService, {
getProjectMeta: () =>
Effect.succeed({
projectName: "Test Project",
projectPath: "/path/to/project",
sessionCount: 0,
}),
invalidateProject: () => Effect.void,
});
const createMockSessionMetaService = () =>
Layer.succeed(SessionMetaService, {
getSessionMeta: () =>
Effect.succeed({
messageCount: 0,
firstUserMessage: null,
}),
invalidateSession: () => Effect.void,
});
const createTestLayer = (
mockProjectRepositoryLayer: Layer.Layer<
ProjectRepository,
never,
never
> = createMockProjectRepository(),
mockSessionRepositoryLayer: Layer.Layer<
SessionRepository,
never,
never
> = createMockSessionRepository(),
) => {
// Provide EventBus first since FileWatcherService depends on it
const fileWatcherWithEventBus = FileWatcherService.Live.pipe(
Layer.provide(EventBus.Live),
);
// Merge all dependencies
const allDependencies = Layer.mergeAll(
EventBus.Live,
fileWatcherWithEventBus,
mockProjectRepositoryLayer,
mockSessionRepositoryLayer,
createMockProjectMetaService(),
createMockSessionMetaService(),
VirtualConversationDatabase.Live,
Path.layer,
);
// Provide dependencies to InitializeService.Live and expose all services
return Layer.provide(InitializeService.Live, allDependencies).pipe(
Layer.merge(allDependencies),
);
};
describe("basic initialization process", () => {
it("service initialization succeeds", async () => {
const mockProjectRepositoryLayer = createMockProjectRepository([
{
id: "project-1",
claudeProjectPath: "/path/to/project-1",
lastModifiedAt: new Date(),
meta: {
projectName: "Project 1",
projectPath: "/path/to/project-1",
sessionCount: 2,
},
},
]);
const mockSessionRepositoryLayer = createMockSessionRepository([
{
id: "session-1",
jsonlFilePath: "/path/to/session-1.jsonl",
lastModifiedAt: new Date(),
meta: {
messageCount: 5,
firstUserMessage: {
kind: "command",
commandName: "test",
},
},
},
{
id: "session-2",
jsonlFilePath: "/path/to/session-2.jsonl",
lastModifiedAt: new Date(),
meta: {
messageCount: 3,
firstUserMessage: null,
},
},
]);
const program = Effect.gen(function* () {
const initialize = yield* InitializeService;
return yield* initialize.startInitialization();
});
const testLayer = createTestLayer(
mockProjectRepositoryLayer,
mockSessionRepositoryLayer,
);
const result = await Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
Effect.provide(sharedTestLayer),
Effect.provide(
testProjectRepositoryLayer({
projects: [
{
id: "project-1",
claudeProjectPath: "/path/to/project-1",
lastModifiedAt: new Date(),
meta: {
projectName: "Project 1",
projectPath: "/path/to/project-1",
sessionCount: 2,
},
},
],
}),
),
Effect.provide(
testSessionRepositoryLayer({
sessions: [
{
id: "session-1",
jsonlFilePath: "/path/to/session-1.jsonl",
lastModifiedAt: new Date(),
meta: {
messageCount: 5,
firstUserMessage: {
kind: "command",
commandName: "test",
},
},
},
{
id: "session-2",
jsonlFilePath: "/path/to/session-2.jsonl",
lastModifiedAt: new Date(),
meta: {
messageCount: 3,
firstUserMessage: null,
},
},
],
}),
),
Effect.provide(testPlatformLayer()),
),
);
@@ -184,14 +112,12 @@ describe("InitializeService", () => {
return "file watcher started";
});
const testLayer = createTestLayer();
const result = await Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
Effect.provide(sharedTestLayer),
Effect.provide(testProjectRepositoryLayer()),
Effect.provide(testSessionRepositoryLayer()),
Effect.provide(testPlatformLayer()),
),
);
@@ -228,14 +154,12 @@ describe("InitializeService", () => {
return events;
});
const testLayer = createTestLayer();
const result = await Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
Effect.provide(sharedTestLayer),
Effect.provide(testProjectRepositoryLayer()),
Effect.provide(testSessionRepositoryLayer()),
Effect.provide(testPlatformLayer()),
),
);
@@ -267,14 +191,12 @@ describe("InitializeService", () => {
return count;
});
const testLayer = createTestLayer();
const result = await Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
Effect.provide(sharedTestLayer),
Effect.provide(testProjectRepositoryLayer()),
Effect.provide(testSessionRepositoryLayer()),
Effect.provide(testPlatformLayer()),
),
);
@@ -285,75 +207,8 @@ describe("InitializeService", () => {
});
describe("cache initialization", () => {
it("project and session caches are initialized", async () => {
const getProjectsCalled = vi.fn();
const getSessionsCalled = vi.fn();
const mockProjectRepositoryLayer = Layer.succeed(ProjectRepository, {
getProjects: () => {
getProjectsCalled();
return Effect.succeed({
projects: [
{
id: "project-1",
claudeProjectPath: "/path/to/project-1",
lastModifiedAt: new Date(),
meta: {
projectName: "Project 1",
projectPath: "/path/to/project-1",
sessionCount: 2,
},
},
],
});
},
getProject: () => Effect.fail(new Error("Not implemented in mock")),
});
const mockSessionRepositoryLayer = createMockSessionRepository(
[
{
id: "session-1",
jsonlFilePath: "/path/to/session-1.jsonl",
lastModifiedAt: new Date(),
meta: {
messageCount: 5,
firstUserMessage: {
kind: "command",
commandName: "test",
},
},
},
],
getSessionsCalled,
);
const program = Effect.gen(function* () {
const initialize = yield* InitializeService;
yield* initialize.startInitialization();
});
const testLayer = createTestLayer(
mockProjectRepositoryLayer,
mockSessionRepositoryLayer,
);
await Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
),
);
expect(getProjectsCalled).toHaveBeenCalledTimes(1);
expect(getSessionsCalled).toHaveBeenCalledTimes(1);
expect(getSessionsCalled).toHaveBeenCalledWith("project-1");
});
it("doesn't throw error even if cache initialization fails", async () => {
const mockProjectRepositoryLayer = Layer.succeed(ProjectRepository, {
const mockProjectRepositoryLayer = Layer.mock(ProjectRepository, {
getProjects: () => Effect.fail(new Error("Failed to get projects")),
getProject: () => Effect.fail(new Error("Not implemented in mock")),
});
@@ -363,16 +218,14 @@ describe("InitializeService", () => {
return yield* initialize.startInitialization();
});
const testLayer = createTestLayer(mockProjectRepositoryLayer);
// Completes without throwing error
await expect(
Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
Effect.provide(sharedTestLayer),
Effect.provide(mockProjectRepositoryLayer),
Effect.provide(testSessionRepositoryLayer()),
Effect.provide(testPlatformLayer()),
),
),
).resolves.toBeUndefined();
@@ -388,14 +241,12 @@ describe("InitializeService", () => {
return "cleaned up";
});
const testLayer = createTestLayer();
const result = await Effect.runPromise(
program.pipe(
Effect.provide(testLayer),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer),
Effect.provide(sharedTestLayer),
Effect.provide(testProjectRepositoryLayer()),
Effect.provide(testSessionRepositoryLayer()),
Effect.provide(testPlatformLayer()),
),
);

View File

@@ -0,0 +1,13 @@
import { NodeContext } from "@effect/platform-node";
import { Layer } from "effect";
import { EventBus } from "../../core/events/services/EventBus";
import { ApplicationContext } from "../../core/platform/services/ApplicationContext";
import { EnvService } from "../../core/platform/services/EnvService";
import { UserConfigService } from "../../core/platform/services/UserConfigService";
export const platformLayer = Layer.mergeAll(
ApplicationContext.Live,
UserConfigService.Live,
EventBus.Live,
EnvService.Live,
).pipe(Layer.provide(EnvService.Live), Layer.provide(NodeContext.layer));

View File

@@ -1,8 +1,8 @@
import { FileSystem } from "@effect/platform";
import { Effect, Layer, Ref } from "effect";
import { z } from "zod";
import { testFileSystemLayer } from "../../../../testing/layers/testFileSystemLayer";
import { testPersistentServiceLayer } from "../../../../testing/layers/testPersistentServiceLayer";
import { FileCacheStorage, makeFileCacheStorageLayer } from "./index";
import { PersistentService } from "./PersistentService";
// Schema for testing
const UserSchema = z.object({
@@ -13,17 +13,9 @@ const UserSchema = z.object({
type User = z.infer<typeof UserSchema>;
const FileSystemMock = FileSystem.layerNoop({});
describe("FileCacheStorage", () => {
describe("basic operations", () => {
it("can save and retrieve data with set and get", async () => {
// PersistentService mock (empty data)
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -43,8 +35,8 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(testPersistentServiceLayer()),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -58,11 +50,6 @@ describe("FileCacheStorage", () => {
});
it("returns undefined when retrieving non-existent key", async () => {
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
return yield* cache.get("non-existent");
@@ -72,8 +59,8 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(testPersistentServiceLayer()),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -83,11 +70,6 @@ describe("FileCacheStorage", () => {
});
it("can delete data with invalidate", async () => {
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -109,8 +91,8 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(testPersistentServiceLayer()),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -120,11 +102,6 @@ describe("FileCacheStorage", () => {
});
it("getAll ですべてのデータを取得できる", async () => {
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -148,8 +125,8 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(testPersistentServiceLayer()),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -172,29 +149,6 @@ describe("FileCacheStorage", () => {
describe("永続化データの読み込み", () => {
it("初期化時に永続化データを読み込む", async () => {
// 永続化データを返すモック
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () =>
Effect.succeed([
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
[
"user-2",
{
id: "user-2",
name: "Bob",
email: "bob@example.com",
},
],
] as const),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
return yield* cache.getAll();
@@ -204,8 +158,29 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
savedEntries: [
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
[
"user-2",
{
id: "user-2",
name: "Bob",
email: "bob@example.com",
},
],
],
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -217,38 +192,6 @@ describe("FileCacheStorage", () => {
});
it("スキーマバリデーションに失敗したデータは無視される", async () => {
// 不正なデータを含む永続化データ
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () =>
Effect.succeed([
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
[
"user-invalid",
{
id: "invalid",
name: "Invalid",
// email が無い(バリデーションエラー)
},
],
[
"user-2",
{
id: "user-2",
name: "Bob",
email: "invalid-email", // 不正なメールアドレス
},
],
] as const),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
return yield* cache.getAll();
@@ -258,8 +201,38 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
savedEntries: [
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
[
"user-invalid",
{
id: "invalid",
name: "Invalid",
// email が無い(バリデーションエラー)
},
],
[
"user-2",
{
id: "user-2",
name: "Bob",
email: "invalid-email", // 不正なメールアドレス
},
],
],
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -277,14 +250,6 @@ describe("FileCacheStorage", () => {
it("set でデータを保存すると save が呼ばれる", async () => {
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -302,8 +267,15 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -316,24 +288,6 @@ describe("FileCacheStorage", () => {
it("同じ値を set しても save は呼ばれない(差分検出)", async () => {
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () =>
Effect.succeed([
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
] as const),
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -352,8 +306,25 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
savedEntries: [
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
],
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -367,24 +338,6 @@ describe("FileCacheStorage", () => {
it("invalidate でデータを削除すると save が呼ばれる", async () => {
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () =>
Effect.succeed([
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
] as const),
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -398,8 +351,25 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
savedEntries: [
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
],
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -412,14 +382,6 @@ describe("FileCacheStorage", () => {
it("存在しないキーを invalidate しても save は呼ばれない", async () => {
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () => Effect.succeed([]),
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -434,8 +396,15 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
save: () =>
Effect.gen(function* () {
yield* Ref.update(saveCallsRef, (n) => n + 1);
}),
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),
@@ -449,21 +418,6 @@ describe("FileCacheStorage", () => {
describe("複雑なシナリオ", () => {
it("複数の操作を順次実行できる", async () => {
const PersistentServiceMock = Layer.succeed(PersistentService, {
load: () =>
Effect.succeed([
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
] as const),
save: () => Effect.void,
});
const program = Effect.gen(function* () {
const cache = yield* FileCacheStorage<User>();
@@ -505,8 +459,21 @@ describe("FileCacheStorage", () => {
program.pipe(
Effect.provide(
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
Layer.provide(PersistentServiceMock),
Layer.provide(FileSystemMock),
Layer.provide(
testPersistentServiceLayer({
savedEntries: [
[
"user-1",
{
id: "user-1",
name: "Alice",
email: "alice@example.com",
},
],
],
}),
),
Layer.provide(testFileSystemLayer()),
),
),
),