mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-04 06:04:21 +01:00
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:
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
13
src/server/lib/effect/layers.ts
Normal file
13
src/server/lib/effect/layers.ts
Normal 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));
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user