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, MessageId, OpenError, TerminalNotRunningError, type OrchestrationCommand, 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 { Deferred, Duration, Effect, FileSystem, Layer, ManagedRuntime, Path, Stream,} from "effect";import { FetchHttpClient, HttpBody, HttpClient, HttpRouter, HttpServer,} from "effect/unstable/http";import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";import { RpcClient, RpcSerialization } from "effect/unstable/rpc";import { vi } from "vitest"; 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 { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.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 { OrchestrationListenerCallbackError } from "./orchestration/Errors.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 { BrowserTraceCollector, type BrowserTraceCollectorShape,} from "./observability/Services/BrowserTraceCollector.ts";import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";import { ProjectSetupScriptRunner, type ProjectSetupScriptRunnerShape,} from "./project/Services/ProjectSetupScriptRunner.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 browserOtlpTracingLayer = Layer.mergeAll( FetchHttpClient.layer, OtlpSerialization.layerJson, Layer.succeed(HttpClient.TracerDisabledWhen, () => true),); const makeBrowserOtlpPayload = (spanName: string) => Effect.gen(function* () { const collector = yield* Effect.acquireRelease( Effect.promise(async () => { const NodeHttp = await import("node:http"); return await new Promise<{ readonly close: () => Promise<void>; readonly firstRequest: Promise<{ readonly body: string; readonly contentType: string | null; }>; readonly url: string; }>((resolve, reject) => { let resolveFirstRequest: | ((request: { readonly body: string; readonly contentType: string | null }) => void) | undefined; const firstRequest = new Promise<{ readonly body: string; readonly contentType: string | null; }>((resolveRequest) => { resolveFirstRequest = resolveRequest; }); const server = NodeHttp.createServer((request, response) => { const chunks: Buffer[] = []; request.on("data", (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); request.on("end", () => { resolveFirstRequest?.({ body: Buffer.concat(chunks).toString("utf8"), contentType: request.headers["content-type"] ?? null, }); resolveFirstRequest = undefined; response.statusCode = 204; response.end(); }); }); server.on("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); if (!address || typeof address === "string") { reject(new Error("Expected TCP collector address")); return; } resolve({ url: `http://127.0.0.1:${address.port}/v1/traces`, firstRequest, close: () => new Promise<void>((resolveClose, rejectClose) => { server.close((error) => { if (error) { rejectClose(error); return; } resolveClose(); }); }), }); }); }); }), ({ close }) => Effect.promise(close), ); const runtime = ManagedRuntime.make( OtlpTracer.layer({ url: collector.url, exportInterval: "10 millis", resource: { serviceName: "t3-web", attributes: { "service.runtime": "t3-web", "service.mode": "browser", "service.version": "test", }, }, }).pipe(Layer.provide(browserOtlpTracingLayer)), ); try { yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); } finally { yield* Effect.promise(() => runtime.dispose()); } const request = yield* Effect.promise(() => Promise.race([ collector.firstRequest, new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timed out waiting for OTLP trace export")), 1_000); }), ]), ); return JSON.parse(request.body) as OtlpTracer.TraceData; }); const buildAppUnderTest = (options?: { config?: Partial<ServerConfigShape>; layers?: { keybindings?: Partial<KeybindingsShape>; providerRegistry?: Partial<ProviderRegistryShape>; serverSettings?: Partial<ServerSettingsShape>; open?: Partial<OpenShape>; gitCore?: Partial<GitCoreShape>; gitManager?: Partial<GitManagerShape>; projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>; terminalManager?: Partial<TerminalManagerShape>; orchestrationEngine?: Partial<OrchestrationEngineShape>; projectionSnapshotQuery?: Partial<ProjectionSnapshotQueryShape>; checkpointDiffQuery?: Partial<CheckpointDiffQueryShape>; browserTraceCollector?: Partial<BrowserTraceCollectorShape>; serverLifecycleEvents?: Partial<ServerLifecycleEventsShape>; serverRuntimeStartup?: Partial<ServerRuntimeStartupShape>; };}) => 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: ServerConfigShape = { logLevel: "Info", traceMinLevel: "Info", traceTimingEnabled: true, traceBatchWindowMs: 200, traceMaxBytes: 10 * 1024 * 1024, traceMaxFiles: 10, otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpExportIntervalMs: 10_000, otlpServiceName: "t3-server", 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, }; const layerConfig = Layer.succeed(ServerConfig, config); const gitManagerLayer = Layer.mock(GitManager)({ ...options?.layers?.gitManager, }); const gitStatusBroadcasterLayer = GitStatusBroadcasterLive.pipe(Layer.provide(gitManagerLayer)); 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(gitManagerLayer), Layer.provideMerge(gitStatusBroadcasterLayer), Layer.provide( Layer.mock(ProjectSetupScriptRunner)({ runForThread: () => Effect.succeed({ status: "no-script" as const }), ...options?.layers?.projectSetupScriptRunner, }), ), 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(BrowserTraceCollector)({ record: () => Effect.void, ...options?.layers?.browserTraceCollector, }), ), 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.provideMerge(FetchHttpClient.layer), 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<infer Client, any, any> ? Client : never; const withWsRpcClient = <A, E, R>( wsUrl: string, f: (client: WsRpcClient) => Effect.Effect<A, E, R>,) => 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, "<html>router-static-ok</html>"); 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"), "<svg>router-project-favicon</svg>", ); yield* buildAppUnderTest({Lines 1–500 of 2653. Load more
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, MessageId, OpenError, TerminalNotRunningError, type OrchestrationCommand, 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 { Deferred, Duration, Effect, FileSystem, Layer, ManagedRuntime, Path, Stream, } from "effect"; import { FetchHttpClient, HttpBody, HttpClient, HttpRouter, HttpServer, } from "effect/unstable/http"; import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; import { RpcClient, RpcSerialization } from "effect/unstable/rpc"; import { vi } from "vitest"; 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 { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.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 { OrchestrationListenerCallbackError } from "./orchestration/Errors.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 { BrowserTraceCollector, type BrowserTraceCollectorShape, } from "./observability/Services/BrowserTraceCollector.ts"; import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts"; import { ProjectSetupScriptRunner, type ProjectSetupScriptRunnerShape, } from "./project/Services/ProjectSetupScriptRunner.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 browserOtlpTracingLayer = Layer.mergeAll( FetchHttpClient.layer, OtlpSerialization.layerJson, Layer.succeed(HttpClient.TracerDisabledWhen, () => true), ); const makeBrowserOtlpPayload = (spanName: string) => Effect.gen(function* () { const collector = yield* Effect.acquireRelease( Effect.promise(async () => { const NodeHttp = await import("node:http"); return await new Promise<{ readonly close: () => Promise<void>; readonly firstRequest: Promise<{ readonly body: string; readonly contentType: string | null; }>; readonly url: string; }>((resolve, reject) => { let resolveFirstRequest: | ((request: { readonly body: string; readonly contentType: string | null }) => void) | undefined; const firstRequest = new Promise<{ readonly body: string; readonly contentType: string | null; }>((resolveRequest) => { resolveFirstRequest = resolveRequest; }); const server = NodeHttp.createServer((request, response) => { const chunks: Buffer[] = []; request.on("data", (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); request.on("end", () => { resolveFirstRequest?.({ body: Buffer.concat(chunks).toString("utf8"), contentType: request.headers["content-type"] ?? null, }); resolveFirstRequest = undefined; response.statusCode = 204; response.end(); }); }); server.on("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); if (!address || typeof address === "string") { reject(new Error("Expected TCP collector address")); return; } resolve({ url: `http://127.0.0.1:${address.port}/v1/traces`, firstRequest, close: () => new Promise<void>((resolveClose, rejectClose) => { server.close((error) => { if (error) { rejectClose(error); return; } resolveClose(); }); }), }); }); }); }), ({ close }) => Effect.promise(close), ); const runtime = ManagedRuntime.make( OtlpTracer.layer({ url: collector.url, exportInterval: "10 millis", resource: { serviceName: "t3-web", attributes: { "service.runtime": "t3-web", "service.mode": "browser", "service.version": "test", }, }, }).pipe(Layer.provide(browserOtlpTracingLayer)), ); try { yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); } finally { yield* Effect.promise(() => runtime.dispose()); } const request = yield* Effect.promise(() => Promise.race([ collector.firstRequest, new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timed out waiting for OTLP trace export")), 1_000); }), ]), ); return JSON.parse(request.body) as OtlpTracer.TraceData; }); const buildAppUnderTest = (options?: { config?: Partial<ServerConfigShape>; layers?: { keybindings?: Partial<KeybindingsShape>; providerRegistry?: Partial<ProviderRegistryShape>; serverSettings?: Partial<ServerSettingsShape>; open?: Partial<OpenShape>; gitCore?: Partial<GitCoreShape>; gitManager?: Partial<GitManagerShape>; projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>; terminalManager?: Partial<TerminalManagerShape>; orchestrationEngine?: Partial<OrchestrationEngineShape>; projectionSnapshotQuery?: Partial<ProjectionSnapshotQueryShape>; checkpointDiffQuery?: Partial<CheckpointDiffQueryShape>; browserTraceCollector?: Partial<BrowserTraceCollectorShape>; serverLifecycleEvents?: Partial<ServerLifecycleEventsShape>; serverRuntimeStartup?: Partial<ServerRuntimeStartupShape>; }; }) => 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: ServerConfigShape = { logLevel: "Info", traceMinLevel: "Info", traceTimingEnabled: true, traceBatchWindowMs: 200, traceMaxBytes: 10 * 1024 * 1024, traceMaxFiles: 10, otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpExportIntervalMs: 10_000, otlpServiceName: "t3-server", 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, }; const layerConfig = Layer.succeed(ServerConfig, config); const gitManagerLayer = Layer.mock(GitManager)({ ...options?.layers?.gitManager, }); const gitStatusBroadcasterLayer = GitStatusBroadcasterLive.pipe(Layer.provide(gitManagerLayer)); 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(gitManagerLayer), Layer.provideMerge(gitStatusBroadcasterLayer), Layer.provide( Layer.mock(ProjectSetupScriptRunner)({ runForThread: () => Effect.succeed({ status: "no-script" as const }), ...options?.layers?.projectSetupScriptRunner, }), ), 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(BrowserTraceCollector)({ record: () => Effect.void, ...options?.layers?.browserTraceCollector, }), ), 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.provideMerge(FetchHttpClient.layer), 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<infer Client, any, any> ? Client : never; const withWsRpcClient = <A, E, R>( wsUrl: string, f: (client: WsRpcClient) => Effect.Effect<A, E, R>, ) => 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, "<html>router-static-ok</html>"); 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"), "<svg>router-project-favicon</svg>", ); yield* buildAppUnderTest({