import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as NodeSocket from "@effect/platform-node/NodeSocket"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { CommandId, DEFAULT_SERVER_SETTINGS, GitCommandError, KeybindingRule, OpenError, TerminalNotRunningError, type OrchestrationEvent, ORCHESTRATION_WS_METHODS, ProjectId, ResolvedKeybindingRule, ThreadId, WS_METHODS, WsRpcGroup, EditorId, } from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; import { assertFailure, assertInclude, assertTrue } from "@effect/vitest/utils"; import { Effect, FileSystem, Layer, Path, Stream } from "effect"; import { HttpClient, HttpRouter, HttpServer } from "effect/unstable/http"; import { RpcClient, RpcSerialization } from "effect/unstable/rpc"; import type { ServerConfigShape } from "./config.ts"; import { deriveServerPaths, ServerConfig } from "./config.ts"; import { makeRoutesLayer } from "./server.ts"; import { resolveAttachmentRelativePath } from "./attachmentPaths.ts"; import { CheckpointDiffQuery, type CheckpointDiffQueryShape, } from "./checkpointing/Services/CheckpointDiffQuery.ts"; import { GitCore, type GitCoreShape } from "./git/Services/GitCore.ts"; import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts"; import { Keybindings, type KeybindingsShape } from "./keybindings.ts"; import { Open, type OpenShape } from "./open.ts"; import { OrchestrationEngineService, type OrchestrationEngineShape, } from "./orchestration/Services/OrchestrationEngine.ts"; import { ProjectionSnapshotQuery, type ProjectionSnapshotQueryShape, } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { PersistenceSqlError } from "./persistence/Errors.ts"; import { ProviderRegistry, type ProviderRegistryShape, } from "./provider/Services/ProviderRegistry.ts"; import { ServerLifecycleEvents, type ServerLifecycleEventsShape } from "./serverLifecycleEvents.ts"; import { ServerRuntimeStartup, type ServerRuntimeStartupShape } from "./serverRuntimeStartup.ts"; import { ServerSettingsService, type ServerSettingsShape } from "./serverSettings.ts"; import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager.ts"; import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts"; import { WorkspaceEntriesLive } from "./workspace/Layers/WorkspaceEntries.ts"; import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts"; import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts"; const defaultProjectId = ProjectId.makeUnsafe("project-default"); const defaultThreadId = ThreadId.makeUnsafe("thread-default"); const defaultModelSelection = { provider: "codex", model: "gpt-5-codex", } as const; const makeDefaultOrchestrationReadModel = () => { const now = new Date().toISOString(); return { snapshotSequence: 0, updatedAt: now, projects: [ { id: defaultProjectId, title: "Default Project", workspaceRoot: "/tmp/default-project", defaultModelSelection, scripts: [], createdAt: now, updatedAt: now, deletedAt: null, }, ], threads: [ { id: defaultThreadId, projectId: defaultProjectId, title: "Default Thread", modelSelection: defaultModelSelection, interactionMode: "default" as const, runtimeMode: "full-access" as const, branch: null, worktreePath: null, createdAt: now, updatedAt: now, archivedAt: null, latestTurn: null, messages: [], session: null, activities: [], proposedPlans: [], checkpoints: [], deletedAt: null, }, ], }; }; const workspaceAndProjectServicesLayer = Layer.mergeAll( WorkspacePathsLive, WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive)), WorkspaceFileSystemLive.pipe( Layer.provide(WorkspacePathsLive), Layer.provide(WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive))), ), ProjectFaviconResolverLive, ); const buildAppUnderTest = (options?: { config?: Partial; layers?: { keybindings?: Partial; providerRegistry?: Partial; serverSettings?: Partial; open?: Partial; gitCore?: Partial; gitManager?: Partial; terminalManager?: Partial; orchestrationEngine?: Partial; projectionSnapshotQuery?: Partial; checkpointDiffQuery?: Partial; serverLifecycleEvents?: Partial; serverRuntimeStartup?: Partial; }; }) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" }); const baseDir = options?.config?.baseDir ?? tempBaseDir; const devUrl = options?.config?.devUrl; const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); const config = { logLevel: "Info", mode: "web", port: 0, host: "127.0.0.1", cwd: process.cwd(), baseDir, ...derivedPaths, staticDir: undefined, devUrl, noBrowser: true, authToken: undefined, autoBootstrapProjectFromCwd: false, logWebSocketEvents: false, ...options?.config, } satisfies ServerConfigShape; const layerConfig = Layer.succeed(ServerConfig, config); const appLayer = HttpRouter.serve(makeRoutesLayer, { disableListenLog: true, disableLogger: true, }).pipe( Layer.provide( Layer.mock(Keybindings)({ streamChanges: Stream.empty, ...options?.layers?.keybindings, }), ), Layer.provide( Layer.mock(ProviderRegistry)({ getProviders: Effect.succeed([]), refresh: () => Effect.succeed([]), streamChanges: Stream.empty, ...options?.layers?.providerRegistry, }), ), Layer.provide( Layer.mock(ServerSettingsService)({ start: Effect.void, ready: Effect.void, getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS), updateSettings: () => Effect.succeed(DEFAULT_SERVER_SETTINGS), streamChanges: Stream.empty, ...options?.layers?.serverSettings, }), ), Layer.provide( Layer.mock(Open)({ ...options?.layers?.open, }), ), Layer.provide( Layer.mock(GitCore)({ ...options?.layers?.gitCore, }), ), Layer.provide( Layer.mock(GitManager)({ ...options?.layers?.gitManager, }), ), Layer.provide( Layer.mock(TerminalManager)({ ...options?.layers?.terminalManager, }), ), Layer.provide( Layer.mock(OrchestrationEngineService)({ getReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()), readEvents: () => Stream.empty, dispatch: () => Effect.succeed({ sequence: 0 }), streamDomainEvents: Stream.empty, ...options?.layers?.orchestrationEngine, }), ), Layer.provide( Layer.mock(ProjectionSnapshotQuery)({ getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()), ...options?.layers?.projectionSnapshotQuery, }), ), Layer.provide( Layer.mock(CheckpointDiffQuery)({ getTurnDiff: () => Effect.succeed({ threadId: defaultThreadId, fromTurnCount: 0, toTurnCount: 0, diff: "", }), getFullThreadDiff: () => Effect.succeed({ threadId: defaultThreadId, fromTurnCount: 0, toTurnCount: 0, diff: "", }), ...options?.layers?.checkpointDiffQuery, }), ), Layer.provide( Layer.mock(ServerLifecycleEvents)({ publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }), snapshot: Effect.succeed({ sequence: 0, events: [] }), stream: Stream.empty, ...options?.layers?.serverLifecycleEvents, }), ), Layer.provide( Layer.mock(ServerRuntimeStartup)({ awaitCommandReady: Effect.void, markHttpListening: Effect.void, enqueueCommand: (effect) => effect, ...options?.layers?.serverRuntimeStartup, }), ), Layer.provide(workspaceAndProjectServicesLayer), Layer.provide(layerConfig), ); yield* Layer.build(appLayer); return config; }); const wsRpcProtocolLayer = (wsUrl: string) => RpcClient.layerProtocolSocket().pipe( Layer.provide(NodeSocket.layerWebSocket(wsUrl)), Layer.provide(RpcSerialization.layerJson), ); const makeWsRpcClient = RpcClient.make(WsRpcGroup); type WsRpcClient = typeof makeWsRpcClient extends Effect.Effect ? Client : never; const withWsRpcClient = ( wsUrl: string, f: (client: WsRpcClient) => Effect.Effect, ) => makeWsRpcClient.pipe(Effect.flatMap(f), Effect.provide(wsRpcProtocolLayer(wsUrl))); const getHttpServerUrl = (pathname = "") => Effect.gen(function* () { const server = yield* HttpServer.HttpServer; const address = server.address as HttpServer.TcpAddress; return `http://127.0.0.1:${address.port}${pathname}`; }); const getWsServerUrl = (pathname = "") => Effect.gen(function* () { const server = yield* HttpServer.HttpServer; const address = server.address as HttpServer.TcpAddress; return `ws://127.0.0.1:${address.port}${pathname}`; }); it.layer(NodeServices.layer)("server router seam", (it) => { it.effect("serves static index content for GET / when staticDir is configured", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-static-" }); const indexPath = path.join(staticDir, "index.html"); yield* fileSystem.writeFileString(indexPath, "router-static-ok"); yield* buildAppUnderTest({ config: { staticDir } }); const response = yield* HttpClient.get("/"); assert.equal(response.status, 200); assert.include(yield* response.text, "router-static-ok"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("redirects to dev URL when configured", () => Effect.gen(function* () { yield* buildAppUnderTest({ config: { devUrl: new URL("http://127.0.0.1:5173") }, }); const url = yield* getHttpServerUrl("/foo/bar"); const response = yield* Effect.promise(() => fetch(url, { redirect: "manual" })); assert.equal(response.status, 302); assert.equal(response.headers.get("location"), "http://127.0.0.1:5173/"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("serves project favicon requests before the dev URL redirect", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const projectDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-project-favicon-", }); yield* fileSystem.writeFileString( path.join(projectDir, "favicon.svg"), "router-project-favicon", ); yield* buildAppUnderTest({ config: { devUrl: new URL("http://127.0.0.1:5173") }, }); const response = yield* HttpClient.get( `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`, ); assert.equal(response.status, 200); assert.equal(yield* response.text, "router-project-favicon"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("serves the fallback project favicon when no icon exists", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const projectDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-project-favicon-fallback-", }); yield* buildAppUnderTest({ config: { devUrl: new URL("http://127.0.0.1:5173") }, }); const response = yield* HttpClient.get( `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`, ); assert.equal(response.status, 200); assert.include(yield* response.text, 'data-fallback="project-favicon"'); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("serves attachment files from state dir", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const attachmentId = "thread-11111111-1111-4111-8111-111111111111"; const config = yield* buildAppUnderTest(); const attachmentPath = resolveAttachmentRelativePath({ attachmentsDir: config.attachmentsDir, relativePath: `${attachmentId}.bin`, }); assert.isNotNull(attachmentPath, "Attachment path should be resolvable"); yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }); yield* fileSystem.writeFileString(attachmentPath, "attachment-ok"); const response = yield* HttpClient.get(`/attachments/${attachmentId}`); assert.equal(response.status, 200); assert.equal(yield* response.text, "attachment-ok"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("serves attachment files for URL-encoded paths", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const config = yield* buildAppUnderTest(); const attachmentPath = resolveAttachmentRelativePath({ attachmentsDir: config.attachmentsDir, relativePath: "thread%20folder/message%20folder/file%20name.png", }); assert.isNotNull(attachmentPath, "Attachment path should be resolvable"); yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }); yield* fileSystem.writeFileString(attachmentPath, "attachment-encoded-ok"); const response = yield* HttpClient.get( "/attachments/thread%20folder/message%20folder/file%20name.png", ); assert.equal(response.status, 200); assert.equal(yield* response.text, "attachment-encoded-ok"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("returns 404 for missing attachment id lookups", () => Effect.gen(function* () { yield* buildAppUnderTest(); const response = yield* HttpClient.get( "/attachments/missing-11111111-1111-4111-8111-111111111111", ); assert.equal(response.status, 404); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc server.upsertKeybinding", () => Effect.gen(function* () { const rule: KeybindingRule = { command: "terminal.toggle", key: "ctrl+k", }; const resolved: ResolvedKeybindingRule = { command: "terminal.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: true, shiftKey: false, altKey: false, modKey: true, }, }; yield* buildAppUnderTest({ layers: { keybindings: { upsertKeybindingRule: () => Effect.succeed([resolved]), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const response = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverUpsertKeybinding](rule)), ); assert.deepEqual(response.issues, []); assert.deepEqual(response.keybindings, [resolved]); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("rejects websocket rpc handshake when auth token is missing", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-auth-required-" }); yield* fs.writeFileString( path.join(workspaceDir, "needle-file.ts"), "export const needle = 1;", ); yield* buildAppUnderTest({ config: { authToken: "secret-token", }, }); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.projectsSearchEntries]({ cwd: workspaceDir, query: "needle", limit: 10, }), ).pipe(Effect.result), ); assertTrue(result._tag === "Failure"); assertInclude(String(result.failure), "SocketOpenError"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("accepts websocket rpc handshake when auth token is provided", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-auth-ok-" }); yield* fs.writeFileString( path.join(workspaceDir, "needle-file.ts"), "export const needle = 1;", ); yield* buildAppUnderTest({ config: { authToken: "secret-token", }, }); const wsUrl = yield* getWsServerUrl("/ws?token=secret-token"); const response = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.projectsSearchEntries]({ cwd: workspaceDir, query: "needle", limit: 10, }), ), ); assert.isAtLeast(response.entries.length, 1); assert.equal(response.truncated, false); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc subscribeServerConfig streams snapshot then update", () => Effect.gen(function* () { const providers = [] as const; const changeEvent = { keybindings: [], issues: [], } as const; yield* buildAppUnderTest({ layers: { keybindings: { loadConfigState: Effect.succeed({ keybindings: [], issues: [], }), streamChanges: Stream.succeed(changeEvent), }, providerRegistry: { getProviders: Effect.succeed(providers), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const events = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect), ), ); const [first, second] = Array.from(events); assert.equal(first?.type, "snapshot"); if (first?.type === "snapshot") { assert.equal(first.version, 1); assert.deepEqual(first.config.keybindings, []); assert.deepEqual(first.config.issues, []); assert.deepEqual(first.config.providers, providers); assert.deepEqual(first.config.settings, DEFAULT_SERVER_SETTINGS); } assert.deepEqual(second, { version: 1, type: "keybindingsUpdated", payload: { issues: [] }, }); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc subscribeServerConfig emits provider status updates", () => Effect.gen(function* () { const providers = [] as const; yield* buildAppUnderTest({ layers: { keybindings: { loadConfigState: Effect.succeed({ keybindings: [], issues: [], }), streamChanges: Stream.empty, }, providerRegistry: { getProviders: Effect.succeed([]), streamChanges: Stream.succeed(providers), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const events = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect), ), ); const [first, second] = Array.from(events); assert.equal(first?.type, "snapshot"); assert.deepEqual(second, { version: 1, type: "providerStatuses", payload: { providers }, }); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect( "routes websocket rpc subscribeServerLifecycle replays snapshot and streams updates", () => Effect.gen(function* () { const lifecycleEvents = [ { version: 1 as const, sequence: 1, type: "welcome" as const, payload: { cwd: "/tmp/project", projectName: "project", }, }, ] as const; const liveEvents = Stream.make({ version: 1 as const, sequence: 2, type: "ready" as const, payload: { at: new Date().toISOString() }, }); yield* buildAppUnderTest({ layers: { serverLifecycleEvents: { snapshot: Effect.succeed({ sequence: 1, events: lifecycleEvents, }), stream: liveEvents, }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const events = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.subscribeServerLifecycle]({}).pipe(Stream.take(2), Stream.runCollect), ), ); const [first, second] = Array.from(events); assert.equal(first?.type, "welcome"); assert.equal(first?.sequence, 1); assert.equal(second?.type, "ready"); assert.equal(second?.sequence, 2); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc projects.searchEntries", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-search-" }); yield* fs.writeFileString( path.join(workspaceDir, "needle-file.ts"), "export const needle = 1;", ); yield* buildAppUnderTest(); const wsUrl = yield* getWsServerUrl("/ws"); const response = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.projectsSearchEntries]({ cwd: workspaceDir, query: "needle", limit: 10, }), ), ); assert.isAtLeast(response.entries.length, 1); assert.isTrue(response.entries.some((entry) => entry.path === "needle-file.ts")); assert.equal(response.truncated, false); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc projects.searchEntries errors", () => Effect.gen(function* () { yield* buildAppUnderTest(); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.projectsSearchEntries]({ cwd: "/definitely/not/a/real/workspace/path", query: "needle", limit: 10, }), ).pipe(Effect.result), ); assertTrue(result._tag === "Failure"); assertTrue(result.failure._tag === "ProjectSearchEntriesError"); assertInclude( result.failure.message, "Workspace root does not exist: /definitely/not/a/real/workspace/path", ); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc projects.writeFile", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-write-" }); yield* buildAppUnderTest(); const wsUrl = yield* getWsServerUrl("/ws"); const response = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.projectsWriteFile]({ cwd: workspaceDir, relativePath: "nested/created.txt", contents: "written-by-rpc", }), ), ); assert.equal(response.relativePath, "nested/created.txt"); const persisted = yield* fs.readFileString(path.join(workspaceDir, "nested", "created.txt")); assert.equal(persisted, "written-by-rpc"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc projects.writeFile errors", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-write-" }); yield* buildAppUnderTest(); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.projectsWriteFile]({ cwd: workspaceDir, relativePath: "../escape.txt", contents: "nope", }), ).pipe(Effect.result), ); assertTrue(result._tag === "Failure"); assertTrue(result.failure._tag === "ProjectWriteFileError"); assert.equal( result.failure.message, "Workspace file path must stay within the project root.", ); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc shell.openInEditor", () => Effect.gen(function* () { let openedInput: { cwd: string; editor: EditorId } | null = null; yield* buildAppUnderTest({ layers: { open: { openInEditor: (input) => Effect.sync(() => { openedInput = input; }), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.shellOpenInEditor]({ cwd: "/tmp/project", editor: "cursor", }), ), ); assert.deepEqual(openedInput, { cwd: "/tmp/project", editor: "cursor" }); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc shell.openInEditor errors", () => Effect.gen(function* () { const openError = new OpenError({ message: "Editor command not found: cursor" }); yield* buildAppUnderTest({ layers: { open: { openInEditor: () => Effect.fail(openError), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.shellOpenInEditor]({ cwd: "/tmp/project", editor: "cursor", }), ).pipe(Effect.result), ); assertFailure(result, openError); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc git methods", () => Effect.gen(function* () { yield* buildAppUnderTest({ layers: { gitManager: { status: () => Effect.succeed({ branch: "main", hasWorkingTreeChanges: false, workingTree: { files: [], insertions: 0, deletions: 0 }, hasUpstream: true, aheadCount: 0, behindCount: 0, pr: null, }), runStackedAction: (input, options) => Effect.gen(function* () { const result = { action: "commit" as const, branch: { status: "skipped_not_requested" as const }, commit: { status: "created" as const, commitSha: "abc123", subject: "feat: demo", }, push: { status: "skipped_not_requested" as const }, pr: { status: "skipped_not_requested" as const }, }; yield* ( options?.progressReporter?.publish({ actionId: options.actionId ?? input.actionId, cwd: input.cwd, action: input.action, kind: "phase_started", phase: "commit", label: "Committing...", }) ?? Effect.void ); yield* ( options?.progressReporter?.publish({ actionId: options.actionId ?? input.actionId, cwd: input.cwd, action: input.action, kind: "action_finished", result, }) ?? Effect.void ); return result; }), resolvePullRequest: () => Effect.succeed({ pullRequest: { number: 1, title: "Demo PR", url: "https://example.com/pr/1", baseBranch: "main", headBranch: "feature/demo", state: "open", }, }), preparePullRequestThread: () => Effect.succeed({ pullRequest: { number: 1, title: "Demo PR", url: "https://example.com/pr/1", baseBranch: "main", headBranch: "feature/demo", state: "open", }, branch: "feature/demo", worktreePath: null, }), }, gitCore: { pullCurrentBranch: () => Effect.succeed({ status: "pulled", branch: "main", upstreamBranch: "origin/main", }), listBranches: () => Effect.succeed({ branches: [ { name: "main", current: true, isDefault: true, worktreePath: null, }, ], isRepo: true, hasOriginRemote: true, }), createWorktree: () => Effect.succeed({ worktree: { path: "/tmp/wt", branch: "feature/demo" }, }), removeWorktree: () => Effect.void, createBranch: () => Effect.void, checkoutBranch: () => Effect.void, initRepo: () => Effect.void, }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const status = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitStatus]({ cwd: "/tmp/repo" })), ); assert.equal(status.branch, "main"); const pull = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitPull]({ cwd: "/tmp/repo" })), ); assert.equal(pull.status, "pulled"); const stackedEvents = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitRunStackedAction]({ actionId: "action-1", cwd: "/tmp/repo", action: "commit", }).pipe( Stream.runCollect, Effect.map((events) => Array.from(events)), ), ), ); const lastStackedEvent = stackedEvents.at(-1); assert.equal(lastStackedEvent?.kind, "action_finished"); if (lastStackedEvent?.kind === "action_finished") { assert.equal(lastStackedEvent.result.action, "commit"); } const resolvedPr = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitResolvePullRequest]({ cwd: "/tmp/repo", reference: "1", }), ), ); assert.equal(resolvedPr.pullRequest.number, 1); const prepared = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitPreparePullRequestThread]({ cwd: "/tmp/repo", reference: "1", mode: "local", }), ), ); assert.equal(prepared.branch, "feature/demo"); const branches = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitListBranches]({ cwd: "/tmp/repo" }), ), ); assert.equal(branches.branches[0]?.name, "main"); const worktree = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitCreateWorktree]({ cwd: "/tmp/repo", branch: "main", path: null, }), ), ); assert.equal(worktree.worktree.branch, "feature/demo"); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitRemoveWorktree]({ cwd: "/tmp/repo", path: "/tmp/wt", }), ), ); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitCreateBranch]({ cwd: "/tmp/repo", branch: "feature/new", }), ), ); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitCheckout]({ cwd: "/tmp/repo", branch: "main", }), ), ); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitInit]({ cwd: "/tmp/repo", }), ), ); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc git.pull errors", () => Effect.gen(function* () { const gitError = new GitCommandError({ operation: "pull", command: "git pull --ff-only", cwd: "/tmp/repo", detail: "upstream missing", }); yield* buildAppUnderTest({ layers: { gitCore: { pullCurrentBranch: () => Effect.fail(gitError), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.gitPull]({ cwd: "/tmp/repo" })).pipe( Effect.result, ), ); assertFailure(result, gitError); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc orchestration methods", () => Effect.gen(function* () { const now = new Date().toISOString(); const snapshot = { snapshotSequence: 1, updatedAt: now, projects: [ { id: ProjectId.makeUnsafe("project-a"), title: "Project A", workspaceRoot: "/tmp/project-a", defaultModelSelection, scripts: [], createdAt: now, updatedAt: now, deletedAt: null, }, ], threads: [ { id: ThreadId.makeUnsafe("thread-1"), projectId: ProjectId.makeUnsafe("project-a"), title: "Thread A", modelSelection: defaultModelSelection, interactionMode: "default" as const, runtimeMode: "full-access" as const, branch: null, worktreePath: null, createdAt: now, updatedAt: now, archivedAt: null, latestTurn: null, messages: [], session: null, activities: [], proposedPlans: [], checkpoints: [], deletedAt: null, }, ], }; yield* buildAppUnderTest({ layers: { projectionSnapshotQuery: { getSnapshot: () => Effect.succeed(snapshot), }, orchestrationEngine: { dispatch: () => Effect.succeed({ sequence: 7 }), readEvents: () => Stream.empty, }, checkpointDiffQuery: { getTurnDiff: () => Effect.succeed({ threadId: ThreadId.makeUnsafe("thread-1"), fromTurnCount: 0, toTurnCount: 1, diff: "turn-diff", }), getFullThreadDiff: () => Effect.succeed({ threadId: ThreadId.makeUnsafe("thread-1"), fromTurnCount: 0, toTurnCount: 1, diff: "full-diff", }), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const snapshotResult = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[ORCHESTRATION_WS_METHODS.getSnapshot]({})), ); assert.equal(snapshotResult.snapshotSequence, 1); const dispatchResult = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ type: "thread.session.stop", commandId: CommandId.makeUnsafe("cmd-1"), threadId: ThreadId.makeUnsafe("thread-1"), createdAt: now, }), ), ); assert.equal(dispatchResult.sequence, 7); const turnDiffResult = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[ORCHESTRATION_WS_METHODS.getTurnDiff]({ threadId: ThreadId.makeUnsafe("thread-1"), fromTurnCount: 0, toTurnCount: 1, }), ), ); assert.equal(turnDiffResult.diff, "turn-diff"); const fullDiffResult = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[ORCHESTRATION_WS_METHODS.getFullThreadDiff]({ threadId: ThreadId.makeUnsafe("thread-1"), toTurnCount: 1, }), ), ); assert.equal(fullDiffResult.diff, "full-diff"); const replayResult = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[ORCHESTRATION_WS_METHODS.replayEvents]({ fromSequenceExclusive: 0, }), ), ); assert.deepEqual(replayResult, []); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect( "routes websocket rpc subscribeOrchestrationDomainEvents with replay/live overlap resilience", () => Effect.gen(function* () { const now = new Date().toISOString(); const threadId = ThreadId.makeUnsafe("thread-1"); let replayCursor: number | null = null; const makeEvent = (sequence: number): OrchestrationEvent => ({ sequence, eventId: `event-${sequence}`, aggregateKind: "thread", aggregateId: threadId, occurredAt: now, commandId: null, causationEventId: null, correlationId: null, metadata: {}, type: "thread.reverted", payload: { threadId, turnCount: sequence, }, }) as OrchestrationEvent; yield* buildAppUnderTest({ layers: { orchestrationEngine: { getReadModel: () => Effect.succeed({ ...makeDefaultOrchestrationReadModel(), snapshotSequence: 1, }), readEvents: (fromSequenceExclusive) => { replayCursor = fromSequenceExclusive; return Stream.make(makeEvent(2), makeEvent(3)); }, streamDomainEvents: Stream.make(makeEvent(3), makeEvent(4)), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const events = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.subscribeOrchestrationDomainEvents]({}).pipe( Stream.take(3), Stream.runCollect, ), ), ); assert.equal(replayCursor, 1); assert.deepEqual( Array.from(events).map((event) => event.sequence), [2, 3, 4], ); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc orchestration.getSnapshot errors", () => Effect.gen(function* () { yield* buildAppUnderTest({ layers: { projectionSnapshotQuery: { getSnapshot: () => Effect.fail( new PersistenceSqlError({ operation: "ProjectionSnapshotQuery.getSnapshot", detail: "projection unavailable", }), ), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[ORCHESTRATION_WS_METHODS.getSnapshot]({})).pipe( Effect.result, ), ); assertTrue(result._tag === "Failure"); assertTrue(result.failure._tag === "OrchestrationGetSnapshotError"); assertInclude(result.failure.message, "Failed to load orchestration snapshot"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc terminal methods", () => Effect.gen(function* () { const snapshot = { threadId: "thread-1", terminalId: "default", cwd: "/tmp/project", status: "running" as const, pid: 1234, history: "", exitCode: null, exitSignal: null, updatedAt: new Date().toISOString(), }; yield* buildAppUnderTest({ layers: { terminalManager: { open: () => Effect.succeed(snapshot), write: () => Effect.void, resize: () => Effect.void, clear: () => Effect.void, restart: () => Effect.succeed(snapshot), close: () => Effect.void, }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const opened = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalOpen]({ threadId: "thread-1", terminalId: "default", cwd: "/tmp/project", }), ), ); assert.equal(opened.terminalId, "default"); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalWrite]({ threadId: "thread-1", terminalId: "default", data: "echo hi\n", }), ), ); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalResize]({ threadId: "thread-1", terminalId: "default", cols: 120, rows: 40, }), ), ); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalClear]({ threadId: "thread-1", terminalId: "default", }), ), ); const restarted = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalRestart]({ threadId: "thread-1", terminalId: "default", cwd: "/tmp/project", cols: 120, rows: 40, }), ), ); assert.equal(restarted.terminalId, "default"); yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalClose]({ threadId: "thread-1", terminalId: "default", }), ), ); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); it.effect("routes websocket rpc terminal.write errors", () => Effect.gen(function* () { const terminalError = new TerminalNotRunningError({ threadId: "thread-1", terminalId: "default", }); yield* buildAppUnderTest({ layers: { terminalManager: { write: () => Effect.fail(terminalError), }, }, }); const wsUrl = yield* getWsServerUrl("/ws"); const result = yield* Effect.scoped( withWsRpcClient(wsUrl, (client) => client[WS_METHODS.terminalWrite]({ threadId: "thread-1", terminalId: "default", data: "echo fail\n", }), ).pipe(Effect.result), ); assertFailure(result, terminalError); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); });