418 lines · 14.2 KB · TypeScript
RawDownload
import {  EnvironmentId,  ProjectId,  ProviderInstanceId,  ThreadId,  type OrchestrationCommand,  type OrchestrationProjectShell,  type OrchestrationThreadShell,  type ThreadPullRequestLink,} from "@t3tools/contracts";import { describe, expect, it } from "@effect/vitest";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import type { Tool } from "effect/unstable/ai"; import { OrchestrationCommandInvariantError } from "../../../orchestration/Errors.ts";import {  OrchestrationEngineService,  type OrchestrationEngineShape,} from "../../../orchestration/Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "../../../orchestration/Services/ProjectionSnapshotQuery.ts";import * as McpInvocationContext from "../../McpInvocationContext.ts";import { listThreadPullRequests, PullRequestsToolkitHandlersLive } from "./handlers.ts";import { PullRequestLinkFailedError, PullRequestsToolkit } from "./tools.ts"; const PROJECT_ID = ProjectId.make("project-1");const THREAD_ID = ThreadId.make("thread-1"); const testCrypto = Crypto.make({  randomBytes: (size) => new Uint8Array(size).fill(7),  digest: (_algorithm, data) => Effect.succeed(data),}); const invocation = (  capabilities: ReadonlyArray<McpInvocationContext.McpCapability>,): McpInvocationContext.McpInvocationScope => ({  environmentId: EnvironmentId.make("environment-1"),  threadId: THREAD_ID,  providerSessionId: "provider-session-1",  providerInstanceId: ProviderInstanceId.make("codex"),  capabilities: new Set(capabilities),  issuedAt: 1,}); function makeProject(  repositoryIdentity: OrchestrationProjectShell["repositoryIdentity"] = {    canonicalKey: "github.com/t3tools/t3code",    locator: {      source: "git-remote",      remoteName: "origin",      remoteUrl: "git@github.com:T3Tools/T3Code.git",    },    provider: "github",    displayName: "T3Tools/T3Code",    owner: "T3Tools",    name: "T3Code",  },): OrchestrationProjectShell {  return {    id: PROJECT_ID,    title: "Project",    workspaceRoot: "/workspace/project",    defaultModelSelection: null,    scripts: [],    repositoryIdentity,    createdAt: "2026-08-01T00:00:00.000Z",    updatedAt: "2026-08-01T00:00:00.000Z",  };} function makeThread(pullRequests: ReadonlyArray<ThreadPullRequestLink>): OrchestrationThreadShell {  return {    id: THREAD_ID,    projectId: PROJECT_ID,    title: "Thread",    modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" },    runtimeMode: "full-access",    interactionMode: "default",    branch: null,    worktreePath: null,    pullRequests,    latestTurn: null,    createdAt: "2026-08-01T00:00:00.000Z",    updatedAt: "2026-08-20T00:00:00.000Z",    archivedAt: null,    settledOverride: null,    settledAt: null,    session: null,    latestUserMessageAt: "2026-08-20T00:00:00.000Z",    hasPendingApprovals: false,    hasPendingUserInput: false,    hasActionableProposedPlan: false,  };} function makeLink(  number: number,  overrides: Partial<ThreadPullRequestLink> & {    readonly headBranch?: string;    readonly baseBranch?: string;  } = {},): ThreadPullRequestLink {  const { headBranch, baseBranch, ...rest } = overrides;  return {    host: "github.com",    repository: "t3tools/t3code",    number,    url: `https://github.com/t3tools/t3code/pull/${number}`,    source: "manual",    linkedAt: "2026-08-10T00:00:00.000Z",    snapshot:      headBranch === undefined        ? null        : {            state: "open",            title: `PR ${number}`,            headBranch,            baseBranch: baseBranch ?? "main",            isDraft: false,            updatedAt: null,            syncedAt: "2026-08-27T00:00:00.000Z",          },    stack: null,    ...rest,  };} interface HarnessOptions {  readonly thread?: OrchestrationThreadShell | null;  readonly project?: OrchestrationProjectShell | null;  readonly reject?: (command: OrchestrationCommand) => OrchestrationCommandInvariantError | null;} const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (  options: HarnessOptions = {},) {  const commands = yield* Ref.make<ReadonlyArray<OrchestrationCommand>>([]);  const thread = options.thread === undefined ? makeThread([]) : options.thread;  const project = options.project === undefined ? makeProject() : options.project;  const dispatch: OrchestrationEngineShape["dispatch"] = (command) =>    Effect.gen(function* () {      const rejection = options.reject?.(command) ?? null;      if (rejection !== null) return yield* rejection;      yield* Ref.update(commands, (recorded) => [...recorded, command]);      return { sequence: 1 };    });  const dependencies = Layer.mergeAll(    Layer.mock(ProjectionSnapshotQuery)({      getThreadShellById: (threadId) =>        Effect.succeed(threadId === THREAD_ID ? Option.fromNullishOr(thread) : Option.none()),      getProjectShellById: () => Effect.succeed(Option.fromNullishOr(project)),    }),    Layer.mock(OrchestrationEngineService)({      readEvents: () => Stream.empty,      dispatch,      streamDomainEvents: Stream.empty,      latestSequence: Effect.succeed(0),    }),    Layer.succeed(Crypto.Crypto, testCrypto),  );  const toolkit = yield* PullRequestsToolkit.pipe(    Effect.provide(PullRequestsToolkitHandlersLive.pipe(Layer.provide(dependencies))),  );  const call = <Name extends keyof typeof PullRequestsToolkit.tools>(    name: Name,    params: Parameters<typeof toolkit.handle<Name>>[1],    capabilities: ReadonlyArray<McpInvocationContext.McpCapability> = ["pull-requests"],  ) =>    toolkit.handle(name, params).pipe(      Stream.unwrap,      Stream.runCollect,      // Failure mode is "error", so a delivered result is always the success shape.      Effect.map(        (chunk) => chunk.at(-1)!.result as Tool.Success<(typeof PullRequestsToolkit.tools)[Name]>,      ),      Effect.provideService(McpInvocationContext.McpInvocationContext, invocation(capabilities)),      Effect.provide(dependencies),    );  return { commands, call };}); describe("pull request toolkit handlers", () => {  it.effect("refuses a credential without the pull-requests capability", () =>    Effect.gen(function* () {      const harness = yield* makeHarness();      const error = yield* harness        .call("list_thread_pull_requests", {}, ["preview"])        .pipe(Effect.flip);      expect(error).toMatchObject({        _tag: "McpCapabilityUnavailableError",        capability: "pull-requests",        threadId: THREAD_ID,      });      expect(yield* Ref.get(harness.commands)).toEqual([]);    }),  );   it.effect("links by URL with source agent on the token's thread", () =>    Effect.gen(function* () {      const harness = yield* makeHarness();      const result = yield* harness.call("link_pull_request", {        url: "https://github.com/T3Tools/T3Code/pull/123/files",      });      expect(result).toEqual({        host: "github.com",        repository: "t3tools/t3code",        number: 123,        url: "https://github.com/T3Tools/T3Code/pull/123/files",        alreadyLinked: false,      });      expect(yield* Ref.get(harness.commands)).toMatchObject([        {          type: "thread.pull-request.link",          threadId: THREAD_ID,          host: "github.com",          repository: "t3tools/t3code",          number: 123,          source: "agent",        },      ]);    }),  );   it.effect("links by repository and number, defaulting the host to the project's", () =>    Effect.gen(function* () {      const harness = yield* makeHarness();      const result = yield* harness.call("link_pull_request", {        repository: "T3Tools/Other",        number: 7,      });      expect(result).toEqual({        host: "github.com",        repository: "t3tools/other",        number: 7,        url: "https://github.com/t3tools/other/pull/7",        alreadyLinked: false,      });    }),  );   it.effect("builds the URL in the project host's own shape", () =>    Effect.gen(function* () {      const harness = yield* makeHarness({        project: makeProject({          canonicalKey: "gitlab.com/group/sub/project",          locator: {            source: "git-remote",            remoteName: "origin",            remoteUrl: "git@gitlab.com:group/sub/project.git",          },          provider: "gitlab",          displayName: "group/sub/project",        }),      });      const result = yield* harness.call("link_pull_request", {        repository: "group/sub/project",        number: 42,      });      expect(result.url).toBe("https://gitlab.com/group/sub/project/-/merge_requests/42");      expect(result.host).toBe("gitlab.com");    }),  );   it.effect("rejects a target that names neither a URL nor repository and number", () =>    Effect.gen(function* () {      const harness = yield* makeHarness();      const error = yield* harness        .call("link_pull_request", { repository: "x/y" })        .pipe(Effect.flip);      expect(error).toMatchObject({ _tag: "PullRequestTargetIncompleteError" });      const unknown = yield* harness        .call("link_pull_request", {          url: "https://github.com/t3tools/t3code/issues/1?token=private-value",        })        .pipe(Effect.flip);      expect(unknown).toMatchObject({ _tag: "PullRequestUrlInvalidError" });      expect(unknown.message).not.toContain("private-value");      expect(yield* Ref.get(harness.commands)).toEqual([]);    }),  );   it.effect("treats a duplicate link as alreadyLinked rather than an error", () =>    Effect.gen(function* () {      const harness = yield* makeHarness({        thread: makeThread([makeLink(123)]),        reject: (command) =>          command.type === "thread.pull-request.link"            ? new OrchestrationCommandInvariantError({                commandType: command.type,                detail: "already linked",              })            : null,      });      const result = yield* harness.call("link_pull_request", {        url: "https://github.com/t3tools/t3code/pull/123",      });      expect(result.alreadyLinked).toBe(true);    }),  );   it.effect("unlinks a linked pull request and reports a missing one as wasLinked=false", () =>    Effect.gen(function* () {      const harness = yield* makeHarness({        thread: makeThread([makeLink(5)]),        reject: (command) =>          command.type === "thread.pull-request.unlink" && command.number !== 5            ? new OrchestrationCommandInvariantError({                commandType: command.type,                detail: "not linked",              })            : null,      });      const linked = yield* harness.call("unlink_pull_request", {        repository: "t3tools/t3code",        number: 5,      });      expect(linked).toEqual({        host: "github.com",        repository: "t3tools/t3code",        number: 5,        wasLinked: true,      });      const missing = yield* harness.call("unlink_pull_request", {        url: "https://github.com/t3tools/t3code/pull/9",      });      expect(missing.wasLinked).toBe(false);      expect(yield* Ref.get(harness.commands)).toMatchObject([        { type: "thread.pull-request.unlink", number: 5 },      ]);    }),  );   it.effect("fails cleanly when the token's thread no longer exists", () =>    Effect.gen(function* () {      const harness = yield* makeHarness({ thread: null });      const error = yield* harness.call("list_thread_pull_requests", {}).pipe(Effect.flip);      expect(error).toMatchObject({ _tag: "PullRequestThreadNotFoundError", threadId: THREAD_ID });    }),  );   it.effect("lists visible links with host state and derived chain order", () =>    Effect.gen(function* () {      const harness = yield* makeHarness({        thread: makeThread([          makeLink(3, { headBranch: "feat-c", baseBranch: "feat-b", source: "agent" }),          makeLink(1, { headBranch: "feat-a", baseBranch: "main", source: "created" }),          makeLink(2, { headBranch: "feat-b", baseBranch: "feat-a", source: "agent" }),          makeLink(9, { source: "stack-dismissed" }),          makeLink(10),        ]),      });      const result = yield* harness.call("list_thread_pull_requests", {});      expect(result.pullRequests.map((entry) => entry.number)).toEqual([3, 1, 2, 10]);      expect(result.pullRequests[0]).toEqual({        host: "github.com",        repository: "t3tools/t3code",        number: 3,        url: "https://github.com/t3tools/t3code/pull/3",        source: "agent",        state: "open",        title: "PR 3",        headBranch: "feat-c",        baseBranch: "feat-b",        isDraft: false,        stack: { kind: "derived", position: 3, size: 3 },      });      expect(result.pullRequests[3]).toMatchObject({        number: 10,        state: null,        title: null,        headBranch: null,        stack: null,      });      expect(result.chains).toEqual([        { kind: "derived", numbers: [1, 2, 3] },        { kind: "derived", numbers: [10] },      ]);    }),  );}); describe("listThreadPullRequests", () => {  it("reports a native stack position for each member", () => {    const stack = {      kind: "native" as const,      id: "stack-1",      number: 1,      url: "https://github.com/t3tools/t3code/stack/1",      base: "main",      layers: [        { number: 1, headBranch: "a", state: "open" as const },        { number: 2, headBranch: "b", state: "open" as const },      ],    };    const result = listThreadPullRequests({      pullRequests: [        makeLink(2, { stack, source: "stack" }),        makeLink(1, { stack, source: "created" }),      ],    });    expect(result.pullRequests.map((entry) => [entry.number, entry.stack])).toEqual([      [2, { kind: "native", position: 2, size: 2 }],      [1, { kind: "native", position: 1, size: 2 }],    ]);    expect(result.chains).toEqual([{ kind: "native", numbers: [1, 2] }]);  });}); it("keeps failure diagnostics as the cause rather than exposing them in the tool message", () => {  const cause = new Error("database internals");  const failure = new PullRequestLinkFailedError({ cause });  expect(failure.message).toBe("Could not link the pull request.");  expect(failure.cause).toBe(cause);});