1062 lines · 41.2 KB · TypeScript
RawDownload
import { describe, expect, it, vi } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices";import {  CommandId,  EnvironmentId,  type OrchestrationV2ThreadProjection,  type Project,  ProjectId,  ProviderInstanceId,  ThreadId,  WorktreeMcpHandoffInput,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema"; import * as GitWorkflowService from "../git/GitWorkflowService.ts";import {  OrchestratorDispatchError,  OrchestratorProjectionError,} from "../orchestration-v2/Orchestrator.ts";import {  ThreadManagementService,  ThreadManagementThreadArchivedError,  type ThreadManagementSendResult,} from "../orchestration-v2/ThreadManagementService.ts";import * as ProjectService from "../project/ProjectService.ts";import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";import * as ServerSettings from "../serverSettings.ts";import { VcsStatusBroadcaster } from "../vcs/VcsStatusBroadcaster.ts";import type * as McpInvocationContext from "./McpInvocationContext.ts";import { layer as worktreeMcpServiceLayer, WorktreeMcpService } from "./WorktreeMcpService.ts"; const environmentId = EnvironmentId.make("environment-worktree-test");const threadId = ThreadId.make("thread-worktree-test");const projectId = ProjectId.make("project-worktree-test");const workspaceRoot = "/repo/project"; const makeScope = (  capabilities: ReadonlySet<McpInvocationContext.McpCapability>,): McpInvocationContext.McpInvocationScope => ({  environmentId,  threadId,  providerSessionId: "provider-session-worktree-test",  providerInstanceId: ProviderInstanceId.make("claudeAgent"),  capabilities,  issuedAt: 1,}); interface ThreadFixture {  readonly branch?: string | null;  readonly worktreePath?: string | null;  readonly archivedAt?: string | null;  readonly deletedAt?: string | null;} const makeProjection = (overrides: ThreadFixture = {}): OrchestrationV2ThreadProjection =>  ({    thread: {      id: threadId,      projectId,      title: "Worktree test thread",      branch: null,      worktreePath: null,      archivedAt: null,      deletedAt: null,      ...overrides,    },  }) as OrchestrationV2ThreadProjection; const project: Project = {  id: projectId,  title: "Worktree test project",  workspaceRoot,  defaultModelSelection: null,  scripts: [],  createdAt: "2026-01-01T00:00:00.000Z",  updatedAt: "2026-01-01T00:00:00.000Z",  deletedAt: null,} as Project; interface HarnessOptions {  readonly thread?: ThreadFixture | null;  readonly threadReadError?: "projection" | "dispatch";  readonly capabilities?: ReadonlySet<McpInvocationContext.McpCapability>;  readonly currentBranch?: string | null;  readonly notARepo?: boolean;  readonly newWorktreesStartFromOrigin?: boolean;  readonly setupScript?: "started" | "no-script" | "fails" | "dies";  readonly dispatchFails?: boolean;  readonly dispatchDies?: boolean;  readonly dispatchInterrupts?: boolean;  readonly dispatchGate?: Effect.Effect<void>;  readonly threadAttachedOnRecheck?: boolean;  readonly threadArchivedOnRecheck?: boolean;  readonly threadReadFailsOnRecheck?: boolean;  readonly continuation?: "queued" | "fails" | "dies";  readonly projectMissing?: boolean;  readonly projectReadFails?: boolean;  readonly existingBranchWorktreePath?: string | null;  readonly pathSemantics?: "win32" | "posix";  readonly createWorktreeFails?: boolean;  readonly fetchRemoteFails?: boolean;  readonly resolveRemoteFails?: boolean;  readonly removeWorktreeFails?: boolean;  readonly deleteLocalBranchFails?: boolean;  readonly createWorktreeGate?: Effect.Effect<void>;} const makeHarness = (options: HarnessOptions = {}) => {  const thread = options.thread === undefined ? {} : options.thread;  const scope = makeScope(options.capabilities ?? new Set(["preview", "worktree"]));  const dispatch = vi.fn((_: unknown) =>    (options.dispatchGate ?? Effect.void).pipe(      Effect.andThen(        options.dispatchInterrupts          ? (Effect.failCause(Cause.interrupt()) as never)          : options.dispatchDies            ? Effect.die(new Error("dispatch defect"))            : options.dispatchFails              ? (Effect.fail("simulated dispatch failure") as never)              : Effect.succeed({ sequence: 1, storedEvents: [] }),      ),    ),  );  const listLocalBranchNames = vi.fn((_: string) =>    Effect.succeed(      options.existingBranchWorktreePath === undefined        ? ["dev"]        : ["dev", "feature/taken", "feature/taken-idle"],    ),  );  const getThreadProjection = vi.fn((id: ThreadId) => {    if (options.threadReadError === "dispatch") {      return Effect.fail(        new OrchestratorDispatchError({          commandId: CommandId.make("command:test:read"),          commandType: "thread.metadata.update",        }),      ) as never;    }    if (options.threadReadFailsOnRecheck === true && getThreadProjection.mock.calls.length > 1) {      return Effect.fail(        new OrchestratorDispatchError({          commandId: CommandId.make("command:test:recheck"),          commandType: "thread.metadata.update",        }),      ) as never;    }    if (      options.threadAttachedOnRecheck === true &&      getThreadProjection.mock.calls.length > 1 &&      thread !== null    ) {      return Effect.succeed(        makeProjection({ ...thread, worktreePath: "/worktrees/project/raced" }),      );    }    if (      options.threadArchivedOnRecheck === true &&      getThreadProjection.mock.calls.length > 1 &&      thread !== null    ) {      return Effect.succeed(makeProjection({ ...thread, archivedAt: "2026-01-02T00:00:00.000Z" }));    }    return id === threadId && thread !== null      ? Effect.succeed(makeProjection(thread))      : Effect.fail(new OrchestratorProjectionError({ threadId: id }));  });  const sendToThread = vi.fn((_: unknown) => {    switch (options.continuation ?? "queued") {      case "fails":        return Effect.fail(          new ThreadManagementThreadArchivedError({            threadId,          }),        );      case "dies":        return Effect.die(new Error("send defect"));      default:        return Effect.succeed({ delivery: "queued" } as ThreadManagementSendResult);    }  });  const getById = vi.fn((id: ProjectId) =>    options.projectReadFails      ? (Effect.fail("simulated project read failure") as never)      : Effect.succeed(          id === projectId && options.projectMissing !== true            ? Option.some(project)            : Option.none(),        ),  );  const removeWorktree = vi.fn((_: unknown) =>    options.removeWorktreeFails      ? (Effect.fail("simulated worktree removal failure") as never)      : Effect.void,  );  const deleteLocalBranch = vi.fn((_: unknown) =>    options.deleteLocalBranchFails      ? (Effect.fail("simulated local branch deletion failure") as never)      : Effect.void,  );  const fetchRemote = vi.fn((_: unknown) =>    options.fetchRemoteFails ? (Effect.fail("simulated fetch failure") as never) : Effect.void,  );  const resolveRemoteTrackingCommit = vi.fn((_: unknown) =>    options.resolveRemoteFails      ? (Effect.fail("simulated remote resolve failure") as never)      : Effect.succeed({ commitSha: "abc123", remoteRefName: "origin/dev" }),  );  const createWorktree = vi.fn(    (input: { readonly newRefName?: string | undefined; readonly path: string | null }) =>      options.createWorktreeFails        ? (Effect.fail("simulated worktree creation failure") as never)        : (options.createWorktreeGate ?? Effect.void).pipe(            Effect.andThen(              Effect.succeed({                worktree: {                  path: input.path ?? `/worktrees/project/${input.newRefName}`,                  refName: input.newRefName ?? "detached",                },              }),            ),          ),  );  const listRefs = vi.fn((input: { readonly query?: string | undefined }) =>    Effect.succeed({      refs:        options.existingBranchWorktreePath === undefined          ? []          : [              {                name: input.query ?? "",                current: false,                isDefault: false,                worktreePath: options.existingBranchWorktreePath,              },            ],      isRepo: true,      hasPrimaryRemote: true,      nextCursor: null,      totalCount: options.existingBranchWorktreePath === undefined ? 0 : 1,    }),  );  const localStatus = vi.fn((_: unknown) =>    Effect.succeed({      isRepo: options.notARepo !== true,      hasPrimaryRemote: true,      isDefaultRef: false,      refName: options.currentBranch === undefined ? "dev" : options.currentBranch,      hasWorkingTreeChanges: false,      workingTree: { files: [], insertions: 0, deletions: 0 },    }),  );  const refreshStatus = vi.fn((_: string) => Effect.die("refreshStatus stub"));  const runForThread = vi.fn((input: { readonly worktreePath: string }) => {    switch (options.setupScript ?? "started") {      case "no-script":        return Effect.succeed({ status: "no-script" } as const);      case "dies":        return Effect.die(new Error("setup runner defect"));      case "fails":        return Effect.fail(          new ProjectSetupScriptRunner.ProjectSetupScriptProjectNotFoundError({            threadId,            worktreePath: input.worktreePath,          }),        );      default:        return Effect.succeed({          status: "started",          scriptId: "setup",          scriptName: "Setup",          terminalId: "setup-terminal",          cwd: input.worktreePath,        } as const);    }  });   // Optional deterministic Path semantics: providing this BEFORE the general  // mocks means the service resolves Path here rather than from NodeServices,  // so absolute-path validation is testable independently of the host OS. The  // service only calls isAbsolute; the minimal per-platform semantics are  // inlined so the test does not depend on the host's path module.  const win32IsAbsolute = (value: string) => /^(?:[a-zA-Z]:[\\/]|[\\/])/.test(value);  const posixIsAbsolute = (value: string) => value.startsWith("/");  const serviceLayer =    options.pathSemantics === undefined      ? worktreeMcpServiceLayer      : worktreeMcpServiceLayer.pipe(          Layer.provide(            Layer.succeed(Path.Path, {              isAbsolute: options.pathSemantics === "win32" ? win32IsAbsolute : posixIsAbsolute,            } as unknown as Path.Path),          ),        );  const layer = serviceLayer.pipe(    Layer.provide(      Layer.mergeAll(        Layer.mock(ThreadManagementService)({          dispatch,          getThreadProjection,          sendToThread,        } satisfies Partial<ThreadManagementService["Service"]>),        Layer.mock(ProjectService.ProjectService)({          getById,        } satisfies Partial<ProjectService.ProjectService["Service"]>),        ServerSettings.layerTest({          newWorktreesStartFromOrigin: options.newWorktreesStartFromOrigin ?? false,        }),        Layer.mock(GitWorkflowService.GitWorkflowService)({          listRefs,          listLocalBranchNames,          localStatus,          fetchRemote,          resolveRemoteTrackingCommit,          createWorktree,          removeWorktree,          deleteLocalBranch,        } satisfies Partial<GitWorkflowService.GitWorkflowService["Service"]>),        Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({          runForThread,        } satisfies Partial<ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]>),        Layer.mock(VcsStatusBroadcaster)({          refreshStatus,        } satisfies Partial<VcsStatusBroadcaster["Service"]>),        NodeServices.layer,      ),    ),  );   return {    layer,    scope,    dispatch,    sendToThread,    fetchRemote,    resolveRemoteTrackingCommit,    createWorktree,    removeWorktree,    deleteLocalBranch,    localStatus,    runForThread,  };}; const expectTypedFailure = (exit: Exit.Exit<unknown, unknown>, expected: object): void => {  if (!Exit.isFailure(exit)) {    expect.fail(`Expected a failure exit, got: ${JSON.stringify(exit)}`);  }  const reason = exit.cause.reasons[0];  if (reason?._tag !== "Fail") {    expect.fail(`Expected a typed Fail cause, got: ${reason?._tag ?? "no reason"}`);  }  expect(reason.error).toMatchObject(expected);}; // Resolves the service once from the harness layer; used by tests that must// make several calls against the SAME instance (the in-flight guard lives in// the layer's closure, so a fresh layer per call would never see it).const resolveService = (harness: ReturnType<typeof makeHarness>) =>  Effect.gen(function* () {    return yield* WorktreeMcpService;  }).pipe(Effect.provide(harness.layer)); const runHandoff = (  harness: ReturnType<typeof makeHarness>,  input: Parameters<WorktreeMcpService["Service"]["handoff"]>[1],) =>  Effect.gen(function* () {    const service = yield* WorktreeMcpService;    return yield* service.handoff(harness.scope, input);  }).pipe(Effect.provide(harness.layer)); const runStatus = (harness: ReturnType<typeof makeHarness>) =>  Effect.gen(function* () {    const service = yield* WorktreeMcpService;    return yield* service.status(harness.scope);  }).pipe(Effect.provide(harness.layer)); describe("t3_worktree_handoff", () => {  it.effect("creates a worktree from the current branch and re-points the thread", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const result = yield* runHandoff(harness, { branch: "feature/handoff" });       expect(result.branch).toBe("feature/handoff");      expect(result.baseRef).toBe("dev");      expect(result.startedFromOrigin).toBe(false);      expect(result.worktreePath).toBe("/worktrees/project/feature/handoff");      expect(result.setupScript).toMatchObject({ status: "started", scriptName: "Setup" });       expect(harness.fetchRemote).not.toHaveBeenCalled();      expect(harness.createWorktree).toHaveBeenCalledWith({        cwd: workspaceRoot,        refName: "dev",        newRefName: "feature/handoff",        baseRefName: "dev",        path: null,      });      expect(harness.dispatch).toHaveBeenCalledWith(        expect.objectContaining({          type: "thread.metadata.update",          threadId,          branch: "feature/handoff",          worktreePath: "/worktrees/project/feature/handoff",          expectedWorktreePath: null,        }),      );      expect(harness.runForThread).toHaveBeenCalledWith({        threadId,        projectId,        projectCwd: workspaceRoot,        worktreePath: "/worktrees/project/feature/handoff",        project: { workspaceRoot, scripts: [] },      });    });  });   it.effect("skips the continuation when no continuationPrompt is given", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const result = yield* runHandoff(harness, { branch: "feature/no-continuation" });      expect(result.continuation).toEqual({ status: "skipped" });      expect(harness.sendToThread).not.toHaveBeenCalled();    });  });   it.effect("queues the continuation prompt as the thread's next message", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const result = yield* runHandoff(harness, {        branch: "feature/continue",        continuationPrompt: "Keep fixing the login bug in the new worktree.",      });       expect(result.continuation).toEqual({ status: "scheduled", delivery: "queued" });      expect(harness.sendToThread).toHaveBeenCalledWith(        expect.objectContaining({          projectId,          threadId,          text: "Keep fixing the login bug in the new worktree.",          mode: "queue",          createdBy: "agent",          creationSource: "mcp",        }),      );      // The continuation must be durably queued before anything slower runs.      expect(harness.sendToThread.mock.invocationCallOrder[0]).toBeLessThan(        harness.runForThread.mock.invocationCallOrder[0]!,      );    });  });   it.effect("reports a continuation failure without failing the handoff", () => {    const harness = makeHarness({ continuation: "fails" });    return Effect.gen(function* () {      const result = yield* runHandoff(harness, {        branch: "feature/continue-fails",        continuationPrompt: "Keep going.",      });      expect(result.continuation).toMatchObject({ status: "failed" });      expect(result.worktreePath).toBe("/worktrees/project/feature/continue-fails");      expect(harness.dispatch).toHaveBeenCalled();    });  });   it.effect("reports a continuation defect without failing the handoff", () => {    const harness = makeHarness({ continuation: "dies" });    return Effect.gen(function* () {      const result = yield* runHandoff(harness, {        branch: "feature/continue-dies",        continuationPrompt: "Keep going.",      });      expect(result.continuation).toEqual({ status: "failed", detail: "send defect" });      expect(result.setupScript).toMatchObject({ status: "started" });    });  });   it.effect("does not queue a continuation when the thread update fails", () => {    const harness = makeHarness({ dispatchFails: true });    return Effect.gen(function* () {      const exit = yield* Effect.exit(        runHandoff(harness, {          branch: "feature/dispatch-fails-continue",          continuationPrompt: "Keep going.",        }),      );      expectTypedFailure(exit, { _tag: "WorktreeMcpFailure", code: "operation_failed" });      expect(harness.sendToThread).not.toHaveBeenCalled();    });  });   it.effect("starts from origin and honors explicit baseRef and path", () => {

Lines 1–500 of 1062.