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 { ChildProcessSpawner } from "effect/unstable/process"; 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.VcsProcessShape["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.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("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)), );});
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 { ChildProcessSpawner } from "effect/unstable/process";
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.VcsProcessShape["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.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("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)),
);
});