408 lines · 13.0 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it, afterEach, describe, expect, vi } 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 { ChildProcessSpawner } from "effect/unstable/process";import { VcsProcessExitError, VcsProcessSpawnError } from "@t3tools/contracts"; import * as VcsProcess from "../vcs/VcsProcess.ts";import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({  exitCode: ChildProcessSpawner.ExitCode(0),  stdout,  stderr: "",  stdoutTruncated: false,  stderrTruncated: false,}); const mockRun = vi.fn<VcsProcess.VcsProcess["Service"]["run"]>(); const supportLayer = Layer.mergeAll(  Layer.mock(VcsProcess.VcsProcess)({    run: mockRun,  }),  NodeServices.layer,);const layer = Layer.mergeAll(AzureDevOpsCli.layer.pipe(Layer.provide(supportLayer)), supportLayer); afterEach(() => {  mockRun.mockReset();}); describe("AzureDevOpsCli.layer", () => {  it.effect("parses pull request view output", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(        Effect.succeed(          processOutput(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify({              pullRequestId: 42,              title: "Add Azure provider",              sourceRefName: "refs/heads/feature/source-control",              targetRefName: "refs/heads/main",              status: "active",              creationDate: "2026-01-02T00:00:00.000Z",              closedDate: null,              _links: {                web: {                  href: "https://dev.azure.com/acme/project/_git/repo/pullrequest/42",                },              },            }),          ),        ),      );       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const result = yield* az.getPullRequest({        cwd: "/repo",        reference: "#42",      });       assert.strictEqual(result.number, 42);      assert.strictEqual(result.title, "Add Azure provider");      assert.strictEqual(result.url, "https://dev.azure.com/acme/project/_git/repo/pullrequest/42");      assert.strictEqual(result.baseRefName, "main");      assert.strictEqual(result.headRefName, "feature/source-control");      assert.strictEqual(result.state, "open");      assert.deepStrictEqual(result.updatedAt._tag, Option.some(1)._tag);      assert.deepStrictEqual(mockRun.mock.calls.at(-1)?.[0], {        operation: "AzureDevOpsCli.execute",        command: "az",        args: [          "repos",          "pr",          "show",          "--detect",          "true",          "--id",          "42",          "--only-show-errors",          "--output",          "json",        ],        cwd: "/repo",        timeoutMs: 30_000,      });    }).pipe(Effect.provide(layer)),  );   it.effect("builds a web URL when Azure returns only the pull request REST URL", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(        Effect.succeed(          processOutput(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify({              pullRequestId: 863,              title: "Fix Azure link",              url: "https://dev.azure.com/saplyai/a8fe4088-ad76-4eda-aaaa-e3329713a097/_apis/git/repositories/16108a25-93a3-4eff-b77d-85e894ee0fe4/pullRequests/863",              repository: {                name: "CV-engine",                project: {                  name: "CV-engine",                },              },              sourceRefName: "refs/heads/feature/azure-pr-link",              targetRefName: "refs/heads/main",              status: "active",            }),          ),        ),      );       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const result = yield* az.getPullRequest({        cwd: "/repo",        reference: "863",      });       assert.strictEqual(        result.url,        "https://dev.azure.com/saplyai/CV-engine/_git/CV-engine/pullrequest/863",      );    }).pipe(Effect.provide(layer)),  );   it.effect("lists pull requests with Azure status and source branch arguments", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(        Effect.succeed(          processOutput(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify([              {                pullRequestId: 7,                title: "Merged work",                sourceRefName: "refs/heads/feature/merged",                targetRefName: "refs/heads/main",                status: "completed",                closedDate: "2026-01-03T00:00:00.000Z",                _links: {                  web: {                    href: "https://dev.azure.com/acme/project/_git/repo/pullrequest/7",                  },                },              },            ]),          ),        ),      );       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const result = yield* az.listPullRequests({        cwd: "/repo",        headSelector: "origin:feature/merged",        state: "merged",        limit: 10,      });       assert.strictEqual(result[0]?.state, "merged");      expect(mockRun).toHaveBeenCalledWith({        operation: "AzureDevOpsCli.execute",        command: "az",        args: [          "repos",          "pr",          "list",          "--detect",          "true",          "--source-branch",          "feature/merged",          "--status",          "completed",          "--top",          "10",          "--only-show-errors",          "--output",          "json",        ],        cwd: "/repo",        timeoutMs: 30_000,      });    }).pipe(Effect.provide(layer)),  );   it.effect("reads repository clone URLs", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(        Effect.succeed(          processOutput(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify({              name: "repo",              webUrl: "https://dev.azure.com/acme/project/_git/repo",              remoteUrl: "https://dev.azure.com/acme/project/_git/repo",              sshUrl: "git@ssh.dev.azure.com:v3/acme/project/repo",              project: {                name: "project",              },            }),          ),        ),      );       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const result = yield* az.getRepositoryCloneUrls({        cwd: "/repo",        repository: "repo",      });       assert.deepStrictEqual(result, {        nameWithOwner: "project/repo",        url: "https://dev.azure.com/acme/project/_git/repo",        sshUrl: "git@ssh.dev.azure.com:v3/acme/project/repo",      });    }).pipe(Effect.provide(layer)),  );   it.effect("creates repositories through Azure Repos", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(        Effect.succeed(          processOutput(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify({              name: "repo",              webUrl: "https://dev.azure.com/acme/project/_git/repo",              remoteUrl: "https://dev.azure.com/acme/project/_git/repo",              sshUrl: "git@ssh.dev.azure.com:v3/acme/project/repo",              project: {                name: "project",              },            }),          ),        ),      );       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const result = yield* az.createRepository({        cwd: "/repo",        repository: "project/repo",        visibility: "private",      });       assert.deepStrictEqual(result, {        nameWithOwner: "project/repo",        url: "https://dev.azure.com/acme/project/_git/repo",        sshUrl: "git@ssh.dev.azure.com:v3/acme/project/repo",      });      expect(mockRun).toHaveBeenCalledWith({        operation: "AzureDevOpsCli.execute",        command: "az",        args: [          "repos",          "create",          "--detect",          "true",          "--name",          "repo",          "--project",          "project",          "--only-show-errors",          "--output",          "json",        ],        cwd: "/repo",        timeoutMs: 30_000,      });    }).pipe(Effect.provide(layer)),  );   it.effect("creates pull requests using the body file as the Azure description", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const bodyFile = `/tmp/t3code-azure-devops-cli-.md`;      yield* fileSystem.writeFileString(bodyFile, "Generated body");      mockRun.mockReturnValueOnce(Effect.succeed(processOutput("{}")));       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      yield* az.createPullRequest({        cwd: "/repo",        baseBranch: "main",        headSelector: "feature/provider",        title: "Provider PR",        bodyFile,      });       expect(mockRun).toHaveBeenCalledWith(        expect.objectContaining({          command: "az",          cwd: "/repo",          args: expect.arrayContaining(["--description", `@${bodyFile}`]),        }),      );      expect(mockRun.mock.calls[0]?.[0].args).not.toContain("--output");    }).pipe(Effect.provide(layer)),  );   it.effect("does not force JSON output on checkout side-effect commands", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(Effect.succeed(processOutput("")));       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      yield* az.checkoutPullRequest({        cwd: "/repo",        reference: "42",      });       expect(mockRun).toHaveBeenCalledWith({        operation: "AzureDevOpsCli.execute",        command: "az",        args: [          "repos",          "pr",          "checkout",          "--only-show-errors",          "--detect",          "true",          "--id",          "42",          "--remote-name",          "origin",        ],        cwd: "/repo",        timeoutMs: 30_000,      });    }).pipe(Effect.provide(layer)),  );   it.effect("preserves VCS causes without copying upstream details into messages", () =>    Effect.gen(function* () {      const cause = new VcsProcessExitError({        operation: "AzureDevOpsCli.execute",        command: "az repos list --organization sensitive-upstream-detail",        cwd: "/repo",        exitCode: 1,        detail: "sensitive-upstream-detail",      });      mockRun.mockReturnValueOnce(Effect.fail(cause));       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const error = yield* az.execute({ cwd: "/repo", args: ["repos", "list"] }).pipe(Effect.flip);       assert.instanceOf(error, AzureDevOpsCli.AzureDevOpsCommandFailedError);      assert.strictEqual(error.operation, "execute");      assert.strictEqual(error.command, "az");      assert.strictEqual(error.cwd, "/repo");      assert.strictEqual(error.argumentCount, 2);      assert.strictEqual(error.detail, "Azure DevOps CLI command failed.");      assert.strictEqual(error.cause, cause);      assert.equal(error.message.includes("sensitive-upstream-detail"), false);    }).pipe(Effect.provide(layer)),  );   it.effect("does not report a missing working directory as a missing Azure CLI", () =>    Effect.gen(function* () {      const cwd = "/missing/repo";      const platformCause = PlatformError.systemError({        _tag: "NotFound",        module: "ChildProcess",        method: "spawn",        syscall: "chdir",        pathOrDescriptor: cwd,      });      const cause = new VcsProcessSpawnError({        operation: "AzureDevOpsCli.execute",        command: "az",        cwd,        argumentCount: 2,        cause: platformCause,      });      mockRun.mockReturnValueOnce(Effect.fail(cause));       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const error = yield* az.execute({ cwd, args: ["repos", "list"] }).pipe(Effect.flip);       assert.instanceOf(error, AzureDevOpsCli.AzureDevOpsCommandFailedError);      assert.strictEqual(error.cwd, cwd);      assert.strictEqual(error.cause, cause);    }).pipe(Effect.provide(layer)),  );   it.effect("keeps invalid pull request output diagnostics structured", () =>    Effect.gen(function* () {      mockRun.mockReturnValueOnce(Effect.succeed(processOutput("not-json")));       const az = yield* AzureDevOpsCli.AzureDevOpsCli;      const error = yield* az.getPullRequest({ cwd: "/repo", reference: "42" }).pipe(Effect.flip);       assert.instanceOf(error, AzureDevOpsCli.AzureDevOpsPullRequestDecodeError);      assert.strictEqual(error.operation, "getPullRequest");      assert.strictEqual(error.command, "az");      assert.strictEqual(error.cwd, "/repo");      assert.strictEqual(error.outputLength, 8);      assert.strictEqual(error.detail, "Azure DevOps CLI returned invalid pull request JSON.");      assert.exists(error.cause);      assert.strictEqual(        error.message,        "Azure DevOps CLI failed in getPullRequest: Azure DevOps CLI returned invalid pull request JSON.",      );    }).pipe(Effect.provide(layer)),  );});