3878 lines · 138.3 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFS from "node:fs";import * as NodePath from "node:path";import * as NodeChildProcess from "node:child_process"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PlatformError from "effect/PlatformError";import * as Scope from "effect/Scope";import { ChildProcessSpawner } from "effect/unstable/process";import { expect } from "vite-plus/test";import type {  GitActionProgressEvent,  GitPreparePullRequestThreadInput,  ModelSelection,  ThreadId,} from "@t3tools/contracts"; import { GitCommandError, TextGenerationError } from "@t3tools/contracts";import * as GitHubCli from "../sourceControl/GitHubCli.ts";import * as TextGeneration from "../textGeneration/TextGeneration.ts";import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";import * as VcsProcess from "../vcs/VcsProcess.ts";import * as GitHubSourceControlProvider from "../sourceControl/GitHubSourceControlProvider.ts";import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";import * as ServerConfig from "../config.ts";import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";import * as ServerSettings from "../serverSettings.ts";import * as GitManager from "./GitManager.ts"; interface FakeGhScenario {  prListSequence?: string[];  prListByHeadSelector?: Record<string, string>;  prListSequenceByHeadSelector?: 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?: GitHubCli.GitHubCliError;  /** Let this many gh calls succeed before failWith kicks in (default 0 = fail immediately). */  failAfterCalls?: number;} function fakeGhOutput(stdout: string): VcsProcess.VcsProcessOutput {  return {    exitCode: ChildProcessSpawner.ExitCode(0),    stdout,    stderr: "",    stdoutTruncated: false,    stderrTruncated: false,  };} interface FakeGitTextGeneration {  generateCommitMessage: (input: {    cwd: string;    branch: string | null;    stagedSummary: string;    stagedPatch: string;    includeBranch?: boolean;    modelSelection: ModelSelection;  }) => Effect.Effect<    { subject: string; body: string; branch?: string | undefined },    TextGenerationError  >;  generatePrContent: (input: {    cwd: string;    baseBranch: string;    headBranch: string;    commitSummary: string;    diffSummary: string;    diffPatch: string;    modelSelection: ModelSelection;  }) => Effect.Effect<{ title: string; body: string }, TextGenerationError>;  generateBranchName: (input: {    cwd: string;    message: string;    modelSelection: ModelSelection;  }) => Effect.Effect<{ branch: string }, TextGenerationError>;  generateThreadTitle: (input: {    cwd: string;    message: string;    modelSelection: ModelSelection;  }) => Effect.Effect<{ title: string }, TextGenerationError>;} type FakePullRequest = NonNullable<FakeGhScenario["pullRequest"]>; function normalizeFakePullRequestSummary(raw: unknown): GitHubCli.GitHubPullRequestSummary | null {  if (!raw || typeof raw !== "object") {    return null;  }   const record = raw as Record<string, unknown>;  const number = record.number;  const title = record.title;  const url = record.url;  const baseRefName = record.baseRefName;  const headRefName = record.headRefName;  const headRepository =    typeof record.headRepository === "object" && record.headRepository !== null      ? (record.headRepository as Record<string, unknown>)      : null;  const headRepositoryOwner =    typeof record.headRepositoryOwner === "object" && record.headRepositoryOwner !== null      ? (record.headRepositoryOwner as Record<string, unknown>)      : null;   if (    typeof number !== "number" ||    typeof title !== "string" ||    typeof url !== "string" ||    typeof baseRefName !== "string" ||    typeof headRefName !== "string"  ) {    return null;  }   const state =    typeof record.state === "string"      ? record.state === "OPEN" || record.state === "open"        ? "open"        : record.state === "CLOSED" || record.state === "closed"          ? "closed"          : "merged"      : undefined;  const isCrossRepository =    typeof record.isCrossRepository === "boolean" ? record.isCrossRepository : undefined;  const headRepositoryNameWithOwner =    typeof record.headRepositoryNameWithOwner === "string"      ? record.headRepositoryNameWithOwner      : typeof headRepository?.nameWithOwner === "string"        ? headRepository.nameWithOwner        : undefined;  const headRepositoryOwnerLogin =    typeof record.headRepositoryOwnerLogin === "string"      ? record.headRepositoryOwnerLogin      : typeof headRepositoryOwner?.login === "string"        ? headRepositoryOwner.login        : undefined;   return {    number,    title,    url,    baseRefName,    headRefName,    ...(state ? { state } : {}),    ...(isCrossRepository !== undefined ? { isCrossRepository } : {}),    ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}),    ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}),  };} function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void {  const result = NodeChildProcess.spawnSync("git", args, {    cwd,    encoding: "utf8",  });  if (result.status === 0) {    return;  }  throw new Error(    `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`,  );} 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 removePath(  targetPath: string,): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {  return Effect.gen(function* () {    const fileSystem = yield* FileSystem.FileSystem;    yield* fileSystem.remove(targetPath, { recursive: true, force: true });  });} function makeDirectory(  dirPath: string,): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {  return Effect.gen(function* () {    const fileSystem = yield* FileSystem.FileSystem;    yield* fileSystem.makeDirectory(dirPath, { recursive: true });  });} function runGit(  cwd: string,  args: readonly string[],  allowNonZeroExit = false,): Effect.Effect<  {    readonly exitCode: GitVcsDriver.ExecuteGitResult["exitCode"];    readonly stdout: string;    readonly stderr: string;  },  GitCommandError,  GitVcsDriver.GitVcsDriver> {  return Effect.gen(function* () {    const git = yield* GitVcsDriver.GitVcsDriver;    const result = yield* git.execute({      operation: "GitManager.test.runGit",      cwd,      args,      allowNonZeroExit,    });    return {      exitCode: result.exitCode,      stdout: result.stdout,      stderr: result.stderr,    };  });} function initRepo(  cwd: string,): Effect.Effect<  void,  PlatformError.PlatformError | GitCommandError,  FileSystem.FileSystem | Scope.Scope | GitVcsDriver.GitVcsDriver> {  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(NodePath.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 | GitVcsDriver.GitVcsDriver> {  return Effect.gen(function* () {    const remoteDir = yield* makeTempDir("t3code-git-remote-");    yield* runGit(remoteDir, ["init", "--bare"]);    return remoteDir;  });} function configureRemote(  cwd: string,  remoteName: string,  remotePath: string,  fetchNamespace: string,): Effect.Effect<void, GitCommandError, GitVcsDriver.GitVcsDriver> {  return Effect.gen(function* () {    yield* runGit(cwd, ["config", `remote.${remoteName}.url`, remotePath]);    yield* runGit(cwd, [      "config",      "--replace-all",      `remote.${remoteName}.fetch`,      `+refs/heads/*:refs/remotes/${fetchNamespace}/*`,    ]);  });} function configureVisibleRemoteUrlWithLocalRewrite(  cwd: string,  remoteName: string,  visibleUrl: string,  localRemotePath: string,): Effect.Effect<void, GitCommandError, GitVcsDriver.GitVcsDriver> {  return Effect.gen(function* () {    yield* runGit(cwd, ["config", `remote.${remoteName}.url`, visibleUrl]);    yield* runGit(cwd, ["config", `url.${localRemotePath}.insteadOf`, visibleUrl]);  });} function createTextGeneration(  overrides: Partial<FakeGitTextGeneration> = {},): TextGeneration.TextGeneration["Service"] {  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",      }),    generateThreadTitle: () =>      Effect.succeed({        title: "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 } : {}),            }),        ),      ),    generateThreadTitle: (input) =>      implementation.generateThreadTitle(input).pipe(        Effect.mapError(          (cause) =>            new TextGenerationError({              operation: "generateThreadTitle",              detail: "fake text generation failed",              ...(cause !== undefined ? { cause } : {}),            }),        ),      ),  };} function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {  service: GitHubCli.GitHubCli["Service"];  ghCalls: string[];} {  const prListQueue = [...(scenario.prListSequence ?? [])];  const prListQueueByHeadSelector = new Map(    Object.entries(scenario.prListSequenceByHeadSelector ?? {}).map(([headSelector, values]) => [      headSelector,      [...values],    ]),  );  const ghCalls: string[] = [];   const execute: GitHubCli.GitHubCli["Service"]["execute"] = (input) => {    const args = [...input.args];    ghCalls.push(args.join(" "));     if (scenario.failWith && ghCalls.length > (scenario.failAfterCalls ?? 0)) {      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 mappedQueue =        typeof headSelector === "string"          ? prListQueueByHeadSelector.get(headSelector)?.shift()          : undefined;      const mappedStdout =        typeof headSelector === "string"          ? scenario.prListByHeadSelector?.[headSelector]          : undefined;      const stdout = (mappedQueue ?? mappedStdout ?? prListQueue.shift() ?? "[]") + "\n";      return Effect.succeed(fakeGhOutput(stdout));    }     if (args[0] === "pr" && args[1] === "create") {      return Effect.succeed(        fakeGhOutput(          (scenario.createdPrUrl ?? "https://github.com/pingdotgg/codething-mvp/pull/101") + "\n",        ),      );    }     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(        fakeGhOutput(          JSON.stringify({            ...pullRequest,            ...(pullRequest.headRepositoryNameWithOwner              ? {                  headRepository: {                    nameWithOwner: pullRequest.headRepositoryNameWithOwner,                  },                }              : {}),            ...(pullRequest.headRepositoryOwnerLogin              ? {                  headRepositoryOwner: {                    login: pullRequest.headRepositoryOwnerLogin,                  },                }              : {}),          }) + "\n",        ),      );    }     if (args[0] === "pr" && args[1] === "checkout") {      return Effect.try({        try: () => {          const headBranch = scenario.pullRequest?.headRefName;          if (headBranch) {            const existingBranch = NodeChildProcess.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 fakeGhOutput("");        },        catch: (error) =>          GitHubCli.isGitHubCliError(error)            ? error            : new GitHubCli.GitHubCliCommandError({                command: "gh",                cwd: input.cwd,                cause: error,              }),      });    }     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 GitHubCli.GitHubCliCommandError({              command: "gh",              cwd: input.cwd,              cause: new Error(`Unexpected repository lookup: ${repository}`),            }),          );        }        return Effect.succeed(          fakeGhOutput(            JSON.stringify({              nameWithOwner: repository,              url: cloneUrls.url,              sshUrl: cloneUrls.sshUrl,

Lines 1–500 of 3878.