1939 lines · 66.8 KB · TypeScript
RawDownload
import fs from "node:fs";import path from "node:path";import { spawnSync } from "node:child_process"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";import { expect } from "vitest"; import { GitCommandError, GitHubCliError, TextGenerationError } from "../Errors.ts";import { type GitManagerShape } from "../Services/GitManager.ts";import {  type GitHubCliShape,  type GitHubPullRequestSummary,  GitHubCli,} from "../Services/GitHubCli.ts";import { type TextGenerationShape, TextGeneration } from "../Services/TextGeneration.ts";import { GitServiceLive } from "./GitService.ts";import { GitService } from "../Services/GitService.ts";import { GitCoreLive } from "./GitCore.ts";import { makeGitManager } from "./GitManager.ts"; interface FakeGhScenario {  prListSequence?: string[];  prListByHeadSelector?: Record<string, string>;  createdPrUrl?: string;  defaultBranch?: string;  pullRequest?: {    number: number;    title: string;    url: string;    baseRefName: string;    headRefName: string;    state?: "open" | "closed" | "merged";    isCrossRepository?: boolean;    headRepositoryNameWithOwner?: string | null;    headRepositoryOwnerLogin?: string | null;  };  repositoryCloneUrls?: Record<string, { url: string; sshUrl: string }>;  failWith?: GitHubCliError;} interface FakeGitTextGeneration {  generateCommitMessage: (input: {    cwd: string;    branch: string | null;    stagedSummary: string;    stagedPatch: string;    includeBranch?: boolean;  }) => Effect.Effect<    { subject: string; body: string; branch?: string | undefined },    TextGenerationError  >;  generatePrContent: (input: {    cwd: string;    baseBranch: string;    headBranch: string;    commitSummary: string;    diffSummary: string;    diffPatch: string;  }) => Effect.Effect<{ title: string; body: string }, TextGenerationError>;  generateBranchName: (input: {    cwd: string;    message: string;  }) => Effect.Effect<{ branch: string }, TextGenerationError>;} type FakePullRequest = NonNullable<FakeGhScenario["pullRequest"]>; function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void {  const result = spawnSync("git", args, {    cwd,    encoding: "utf8",  });  if (result.status === 0) {    return;  }  throw new GitHubCliError({    operation: "execute",    detail: `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`,  });} function isGitHubCliError(error: unknown): error is GitHubCliError {  return (    typeof error === "object" &&    error !== null &&    "_tag" in error &&    (error as { _tag?: unknown })._tag === "GitHubCliError"  );} function makeTempDir(  prefix: string,): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> {  return Effect.gen(function* () {    const fileSystem = yield* FileSystem.FileSystem;    return yield* fileSystem.makeTempDirectoryScoped({ prefix });  });} function runGit(  cwd: string,  args: readonly string[],  allowNonZeroExit = false,): Effect.Effect<  { readonly code: number; readonly stdout: string; readonly stderr: string },  GitCommandError,  GitService> {  return Effect.gen(function* () {    const gitService = yield* GitService;    return yield* gitService.execute({      operation: "GitManager.test.runGit",      cwd,      args,      allowNonZeroExit,    });  });} function initRepo(  cwd: string,): Effect.Effect<  void,  PlatformError.PlatformError | GitCommandError,  FileSystem.FileSystem | Scope.Scope | GitService> {  return Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    yield* runGit(cwd, ["init", "--initial-branch=main"]);    yield* runGit(cwd, ["config", "user.email", "test@example.com"]);    yield* runGit(cwd, ["config", "user.name", "Test User"]);    yield* fs.writeFileString(path.join(cwd, "README.md"), "hello\n");    yield* runGit(cwd, ["add", "README.md"]);    yield* runGit(cwd, ["commit", "-m", "Initial commit"]);  });} function createBareRemote(): Effect.Effect<  string,  PlatformError.PlatformError | GitCommandError,  FileSystem.FileSystem | Scope.Scope | GitService> {  return Effect.gen(function* () {    const remoteDir = yield* makeTempDir("t3code-git-remote-");    yield* runGit(remoteDir, ["init", "--bare"]);    return remoteDir;  });} function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): TextGenerationShape {  const implementation: FakeGitTextGeneration = {    generateCommitMessage: (input) =>      Effect.succeed({        subject: "Implement stacked git actions",        body: "",        ...(input.includeBranch ? { branch: "feature/implement-stacked-git-actions" } : {}),      }),    generatePrContent: () =>      Effect.succeed({        title: "Add stacked git actions",        body: "## Summary\n- Add stacked git workflow\n\n## Testing\n- Not run",      }),    generateBranchName: () =>      Effect.succeed({        branch: "update-workflow",      }),    ...overrides,  };   return {    generateCommitMessage: (input) =>      implementation.generateCommitMessage(input).pipe(        Effect.mapError(          (cause) =>            new TextGenerationError({              operation: "generateCommitMessage",              detail: "fake text generation failed",              ...(cause !== undefined ? { cause } : {}),            }),        ),      ),    generatePrContent: (input) =>      implementation.generatePrContent(input).pipe(        Effect.mapError(          (cause) =>            new TextGenerationError({              operation: "generatePrContent",              detail: "fake text generation failed",              ...(cause !== undefined ? { cause } : {}),            }),        ),      ),    generateBranchName: (input) =>      implementation.generateBranchName(input).pipe(        Effect.mapError(          (cause) =>            new TextGenerationError({              operation: "generateBranchName",              detail: "fake text generation failed",              ...(cause !== undefined ? { cause } : {}),            }),        ),      ),  };} function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {  service: GitHubCliShape;  ghCalls: string[];} {  const prListQueue = [...(scenario.prListSequence ?? [])];  const ghCalls: string[] = [];   const execute: GitHubCliShape["execute"] = (input) => {    const args = [...input.args];    ghCalls.push(args.join(" "));     if (scenario.failWith) {      return Effect.fail(scenario.failWith);    }     if (args[0] === "pr" && args[1] === "list") {      const headSelectorIndex = args.findIndex((value) => value === "--head");      const headSelector =        headSelectorIndex >= 0 && headSelectorIndex < args.length - 1          ? args[headSelectorIndex + 1]          : undefined;      const mappedStdout =        typeof headSelector === "string"          ? scenario.prListByHeadSelector?.[headSelector]          : undefined;      const stdout = (mappedStdout ?? prListQueue.shift() ?? "[]") + "\n";      return Effect.succeed({        stdout,        stderr: "",        code: 0,        signal: null,        timedOut: false,      });    }     if (args[0] === "pr" && args[1] === "create") {      return Effect.succeed({        stdout:          (scenario.createdPrUrl ?? "https://github.com/pingdotgg/codething-mvp/pull/101") + "\n",        stderr: "",        code: 0,        signal: null,        timedOut: false,      });    }     if (args[0] === "pr" && args[1] === "view") {      const pullRequest: FakePullRequest = scenario.pullRequest ?? {        number: 101,        title: "Pull request",        url: "https://github.com/pingdotgg/codething-mvp/pull/101",        baseRefName: "main",        headRefName: "feature/pull-request",        state: "open",      };      return Effect.succeed({        stdout:          JSON.stringify({            ...pullRequest,            ...(pullRequest.headRepositoryNameWithOwner              ? {                  headRepository: {                    nameWithOwner: pullRequest.headRepositoryNameWithOwner,                  },                }              : {}),            ...(pullRequest.headRepositoryOwnerLogin              ? {                  headRepositoryOwner: {                    login: pullRequest.headRepositoryOwnerLogin,                  },                }              : {}),          }) + "\n",        stderr: "",        code: 0,        signal: null,        timedOut: false,      });    }     if (args[0] === "pr" && args[1] === "checkout") {      return Effect.try({        try: () => {          const headBranch = scenario.pullRequest?.headRefName;          if (headBranch) {            const existingBranch = spawnSync(              "git",              ["show-ref", "--verify", "--quiet", `refs/heads/${headBranch}`],              {                cwd: input.cwd,                encoding: "utf8",              },            );            if (existingBranch.status === 0) {              runGitSyncForFakeGh(input.cwd, ["checkout", headBranch]);            } else {              runGitSyncForFakeGh(input.cwd, ["checkout", "-b", headBranch]);            }          }          return {            stdout: "",            stderr: "",            code: 0,            signal: null,            timedOut: false,          };        },        catch: (error) =>          isGitHubCliError(error)            ? error            : new GitHubCliError({                operation: "execute",                detail:                  error instanceof Error                    ? `Failed to simulate gh checkout: ${error.message}`                    : "Failed to simulate gh checkout.",              }),      });    }     if (args[0] === "repo" && args[1] === "view") {      const repository = args[2];      if (typeof repository === "string" && args.includes("nameWithOwner,url,sshUrl")) {        const cloneUrls = scenario.repositoryCloneUrls?.[repository];        if (!cloneUrls) {          return Effect.fail(            new GitHubCliError({              operation: "execute",              detail: `Unexpected repository lookup: ${repository}`,            }),          );        }        return Effect.succeed({          stdout:            JSON.stringify({              nameWithOwner: repository,              url: cloneUrls.url,              sshUrl: cloneUrls.sshUrl,            }) + "\n",          stderr: "",          code: 0,          signal: null,          timedOut: false,        });      }      return Effect.succeed({        stdout: `${scenario.defaultBranch ?? "main"}\n`,        stderr: "",        code: 0,        signal: null,        timedOut: false,      });    }     return Effect.fail(      new GitHubCliError({        operation: "execute",        detail: `Unexpected gh command: ${args.join(" ")}`,      }),    );  };   return {    service: {      execute,      listOpenPullRequests: (input) =>        execute({          cwd: input.cwd,          args: [            "pr",            "list",            "--head",            input.headSelector,            "--state",            "open",            "--limit",            String(input.limit ?? 1),            "--json",            "number,title,url,baseRefName,headRefName",          ],        }).pipe(          Effect.map(            (result) => JSON.parse(result.stdout) as ReadonlyArray<GitHubPullRequestSummary>,          ),        ),      createPullRequest: (input) =>        execute({          cwd: input.cwd,          args: [            "pr",            "create",            "--base",            input.baseBranch,            "--head",            input.headSelector,            "--title",            input.title,            "--body-file",            input.bodyFile,          ],        }).pipe(Effect.asVoid),      getDefaultBranch: (input) =>        execute({          cwd: input.cwd,          args: ["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"],        }).pipe(          Effect.map((result) => {            const value = result.stdout.trim();            return value.length > 0 ? value : null;          }),        ),      getPullRequest: (input) =>        execute({          cwd: input.cwd,          args: [            "pr",            "view",            input.reference,            "--json",            "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",          ],        }).pipe(Effect.map((result) => JSON.parse(result.stdout) as GitHubPullRequestSummary)),      getRepositoryCloneUrls: (input) =>        execute({          cwd: input.cwd,          args: ["repo", "view", input.repository, "--json", "nameWithOwner,url,sshUrl"],        }).pipe(Effect.map((result) => JSON.parse(result.stdout))),      checkoutPullRequest: (input) =>        execute({          cwd: input.cwd,          args: ["pr", "checkout", input.reference, ...(input.force ? ["--force"] : [])],        }).pipe(Effect.asVoid),    },    ghCalls,  };} function runStackedAction(  manager: GitManagerShape,  input: {    cwd: string;    action: "commit" | "commit_push" | "commit_push_pr";    commitMessage?: string;    featureBranch?: boolean;    filePaths?: readonly string[];  },) {  return manager.runStackedAction(input);} function resolvePullRequest(manager: GitManagerShape, input: { cwd: string; reference: string }) {  return manager.resolvePullRequest(input);} function preparePullRequestThread(  manager: GitManagerShape,  input: { cwd: string; reference: string; mode: "local" | "worktree" },) {  return manager.preparePullRequestThread(input);} function makeManager(input?: {  ghScenario?: FakeGhScenario;  textGeneration?: Partial<FakeGitTextGeneration>;}) {  const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);  const textGeneration = createTextGeneration(input?.textGeneration);   const gitCoreLayer = GitCoreLive.pipe(    Layer.provideMerge(GitServiceLive),    Layer.provideMerge(NodeServices.layer),  );   const managerLayer = Layer.mergeAll(    Layer.succeed(GitHubCli, gitHubCli),    Layer.succeed(TextGeneration, textGeneration),    gitCoreLayer,    NodeServices.layer,  );   return makeGitManager.pipe(    Effect.provide(managerLayer),    Effect.map((manager) => ({ manager, ghCalls })),  );} const GitManagerTestLayer = Layer.provideMerge(GitServiceLive, NodeServices.layer); it.layer(GitManagerTestLayer)("GitManager", (it) => {  it.effect("status includes PR metadata when branch already has an open PR", () =>    Effect.gen(function* () {

Lines 1–500 of 1939.