1391 lines · 44.2 KB · TypeScript
RawDownload
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<ServerConfigShape>;  layers?: {    keybindings?: Partial<KeybindingsShape>;    providerRegistry?: Partial<ProviderRegistryShape>;    serverSettings?: Partial<ServerSettingsShape>;    open?: Partial<OpenShape>;    gitCore?: Partial<GitCoreShape>;    gitManager?: Partial<GitManagerShape>;    terminalManager?: Partial<TerminalManagerShape>;    orchestrationEngine?: Partial<OrchestrationEngineShape>;    projectionSnapshotQuery?: Partial<ProjectionSnapshotQueryShape>;    checkpointDiffQuery?: Partial<CheckpointDiffQueryShape>;    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 = {      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<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({        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, "<svg>router-project-favicon</svg>");    }).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(

Lines 1–500 of 1391.