import { expect, it } from "@effect/vitest";import * as Layer from "effect/Layer";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as TestClock from "effect/testing/TestClock";import { ChildProcessSpawner } from "effect/unstable/process";import * as GitHubCli from "../sourceControl/GitHubCli.ts";import { runGitHubStackAction as runStackAction } from "./githubStackActions.ts"; const runGitHubStackAction = ( execute: GitHubCli.GitHubCli["Service"]["execute"], input: Parameters<typeof runStackAction>[0],) => runStackAction(input).pipe(Effect.provide(Layer.mock(GitHubCli.GitHubCli)({ execute }))); const stack = [ { number: 50, url: "https://api.github.com/repos/acme/web/stacks/50", base: { ref: "main" }, pull_requests: [ { number: 1, title: "Base", head: { ref: "base", sha: "aaa" }, state: "closed", merged_at: "2026-01-01T00:00:00Z", }, { number: 2, title: "Middle", head: { ref: "middle", sha: "bbb" }, state: "open", draft: false, }, { number: 3, title: "Top", head: { ref: "top", sha: "ccc" }, state: "open", draft: false }, ], },];const input = { cwd: "/repo", repository: "acme/web", host: "github.com", number: 3, stackNumber: 50, expectedStackHeads: [ { number: 2, headSha: "bbb" }, { number: 3, headSha: "ccc" }, ], action: "merge" as const,};const access = { data: { repository: { pr2: { headRepository: { viewerPermission: "WRITE" }, maintainerCanModify: false }, pr3: { headRepository: { viewerPermission: "WRITE" }, maintainerCanModify: false }, }, },}; const branch = (number: number, headRefOid: string, behindBy = 1, processed: string[] = []) => ({ data: { processed: processed.map((headRefOid) => ({ headRefOid })), repository: { pullRequest: { id: `PR_${number}`, headRefOid, baseRef: { compare: { behindBy } } }, }, },});const rebased = { data: { updatePullRequestBranch: { pullRequest: { headRefOid: "rebased-sha" } } },};const rebaseResponses = [branch(2, "bbb"), rebased, branch(3, "ccc", 1, ["rebased-sha"]), rebased]; function fake(responses: readonly unknown[]) { const calls: ReadonlyArray<string>[] = []; const execute: GitHubCli.GitHubCli["Service"]["execute"] = (request) => Effect.sync(() => { calls.push(request.args); const value = responses[calls.length - 1]; if (value === undefined) throw new Error("Unexpected GitHub request"); return { exitCode: ChildProcessSpawner.ExitCode(0), // @effect-diagnostics-next-line preferSchemaOverJson:off stdout: JSON.stringify(value), stderr: "", stdoutTruncated: false, stderrTruncated: false, stdoutInvalidUtf8: false, }; }); return { execute, calls };} it.effect("submits one atomic merge with the reviewed head and respects the merge queue", () => Effect.gen(function* () { const api = fake([stack, { status: "enqueued", details: {} }]); yield* runGitHubStackAction(api.execute, { ...input, mergeMethod: "squash" }); expect(api.calls).toHaveLength(2); expect(api.calls[1]).toContain("repos/acme/web/pulls/3/merge-async"); expect(api.calls[1]).toContain("sha=ccc"); expect(api.calls[1]).toContain("merge_action=default"); expect(api.calls[1]).toContain("merge_method=squash"); }),); it.effect("merges through the selected layer without including later draft layers", () => Effect.gen(function* () { const fiveLayers = [ { ...stack[0], pull_requests: Array.from({ length: 5 }, (_, index) => ({ number: index + 1, head: { ref: `layer-${index + 1}`, sha: `sha-${index + 1}` }, state: "open", draft: index >= 3, })), }, ]; const api = fake([fiveLayers, { status: "merged", details: {} }]); yield* runGitHubStackAction(api.execute, { ...input, number: 3, expectedStackHeads: [1, 2, 3].map((number) => ({ number, headSha: `sha-${number}` })), }); expect(api.calls).toHaveLength(2); expect(api.calls[1]).toContain("repos/acme/web/pulls/3/merge-async"); expect(api.calls[1]).toContain("sha=sha-3"); expect(api.calls[1]).toContain("merge_action=default"); }),); it.effect("rejects stale reviewed heads below a selected middle layer", () => Effect.gen(function* () { const api = fake([stack]); const result = yield* runGitHubStackAction(api.execute, { ...input, number: 2, expectedStackHeads: [{ number: 2, headSha: "old-head" }], }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError" } }); expect(api.calls).toHaveLength(1); }),); it.effect("does not merge from an already merged layer", () => Effect.gen(function* () { const api = fake([stack]); const result = yield* runGitHubStackAction(api.execute, { ...input, number: 1, expectedStackHeads: [], }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackUnsupportedError" }, }); expect(api.calls).toHaveLength(1); }),); it.effect("polls an accepted merge and reports a later rule rejection", () => Effect.gen(function* () { const api = fake([ stack, { status: "pending", details: { uuid: "operation" } }, { status: "failed", details: { message: "Required checks have not passed" } }, ]); const fiber = yield* runGitHubStackAction(api.execute, input).pipe( Effect.result, Effect.forkChild, ); yield* TestClock.adjust("1 second"); const result = yield* Fiber.join(fiber); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackMergeRejectedError" }, }); expect(api.calls[2]).toContain("repos/acme/web/pulls/3/merge-async/operation"); }),); it.effect("retains stack identity and a rejection response without a message", () => Effect.gen(function* () { const rejection = { status: "failed", details: {} }; const api = fake([stack, rejection]); const result = yield* runGitHubStackAction(api.execute, input).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackMergeRejectedError", repository: input.repository, number: input.number, stackNumber: input.stackNumber, cause: rejection, }, }); }),); it.effect("refuses a changed stack before performing any mutation", () => Effect.gen(function* () { const api = fake([stack]); const result = yield* runGitHubStackAction(api.execute, { ...input, expectedStackHeads: [ { number: 2, headSha: "old" }, { number: 3, headSha: "ccc" }, ], }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError" } }); expect(api.calls).toHaveLength(1); }),); it.effect("rebases unmerged layers bottom to top without local git commands", () => Effect.gen(function* () { const api = fake([stack, access, ...rebaseResponses]); yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch" }); const mutations = api.calls.filter((args) => args.some((arg) => arg.startsWith("query=mutation")), ); expect(mutations).toHaveLength(2); expect(mutations[0]).toContain("id=PR_2"); expect(mutations[0]).toContain("sha=bbb"); expect(mutations[1]).toContain("id=PR_3"); expect(mutations[1]).toContain("sha=ccc"); expect(api.calls.every((args) => args[0] === "api")).toBe(true); }),); it.effect("does not update later layers after a rebase failure", () => Effect.gen(function* () { const api = fake([stack, access, branch(2, "bbb")]); const execute: typeof api.execute = (request) => !request.args.some((arg) => arg.startsWith("query=mutation")) ? api.execute(request) : Effect.fail( new GitHubCli.GitHubCliAuthenticationError({ command: "gh", cwd: "/repo", cause: new Error("denied"), }), ); const result = yield* runGitHubStackAction(execute, { ...input, action: "update-branch" }).pipe( Effect.result, ); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackRebaseFailedError", number: 2, completed: 0 }, }); }),); it.effect("refuses the entire rebase before mutation when a later fork denies write access", () => Effect.gen(function* () { const api = fake([ stack, { data: { repository: { ...access.data.repository, pr3: { headRepository: { viewerPermission: "READ" }, maintainerCanModify: false }, }, }, }, ]); const result = yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch", }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackPermissionError" }, }); expect(api.calls).toHaveLength(2); expect(api.calls.every((args) => args[0] === "api")).toBe(true); }),); it.effect("allows a fork that explicitly permits maintainer updates", () => Effect.gen(function* () { const api = fake([ stack, { data: { repository: { ...access.data.repository, pr3: { headRepository: { viewerPermission: "READ" }, maintainerCanModify: true }, }, }, }, ...rebaseResponses, ]); yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch" }); expect(api.calls.at(-1)).toContain("id=PR_3"); }),); it.effect("bounds polling and reports a still-running merge without claiming success", () => Effect.gen(function* () { const api = fake([ stack, ...Array.from({ length: 40 }, () => ({ status: "pending", details: { uuid: "operation" } })), ]); const fiber = yield* runGitHubStackAction(api.execute, input).pipe( Effect.result, Effect.forkChild, ); yield* TestClock.adjust("6 minutes"); expect(yield* Fiber.join(fiber)).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackMergePendingError" }, }); expect(api.calls.length).toBeLessThan(40); }),); it.effect("rejects a push after preflight without rebasing the new revision", () => Effect.gen(function* () { const api = fake([stack, access, branch(2, "new-head")]); const result = yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch", }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError", number: 2, completed: 0 }, }); expect(api.calls).toHaveLength(3); }),); it.effect("skips current layers without submitting a rebase mutation", () => Effect.gen(function* () { const api = fake([stack, access, branch(2, "bbb", 0), branch(3, "ccc", 0, ["bbb"])]); yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch" }); expect(api.calls.some((args) => args.some((arg) => arg.startsWith("query=mutation")))).toBe( false, ); }),); it.effect("keeps earlier progress and stops after a later layer fails", () => Effect.gen(function* () { const api = fake([ stack, access, branch(2, "bbb"), rebased, branch(3, "ccc", 1, ["rebased-sha"]), { data: { updatePullRequestBranch: null } }, ]); const result = yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch", }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackRebaseFailedError", number: 3, completed: 1 }, }); }),); it.effect("reports partial progress when a later head changes during the rebase", () => Effect.gen(function* () { const api = fake([ stack, access, branch(2, "bbb"), rebased, branch(3, "concurrent-head", 1, ["rebased-sha"]), ]); const result = yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch", }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError", number: 3, completed: 1 }, }); if (result._tag === "Failure") { expect(result.failure.message).toContain("Earlier updates remain on GitHub"); } expect( api.calls.filter((args) => args.some((arg) => arg.startsWith("query=mutation"))), ).toHaveLength(1); }),); it.effect.each([false, true])("rejects a push to a processed layer, rebased=%s", (rebasedParent) => Effect.gen(function* () { const api = fake([ stack, access, branch(2, "bbb", rebasedParent ? 1 : 0), ...(rebasedParent ? [rebased] : []), branch(3, "ccc", 1, ["concurrent-parent-head"]), ]); const result = yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch", }).pipe(Effect.result); expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError", number: 2, completed: 1 }, }); expect(api.calls.at(-1)?.some((arg) => arg.includes('processed:nodes(ids:["PR_2"])'))).toBe( true, ); expect( api.calls.filter((args) => args.some((arg) => arg.startsWith("query=mutation"))), ).toHaveLength(rebasedParent ? 1 : 0); }),);
import { expect, it } from "@effect/vitest";
import * as Layer from "effect/Layer";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as TestClock from "effect/testing/TestClock";
import { ChildProcessSpawner } from "effect/unstable/process";
import * as GitHubCli from "../sourceControl/GitHubCli.ts";
import { runGitHubStackAction as runStackAction } from "./githubStackActions.ts";
const runGitHubStackAction = (
execute: GitHubCli.GitHubCli["Service"]["execute"],
input: Parameters<typeof runStackAction>[0],
) => runStackAction(input).pipe(Effect.provide(Layer.mock(GitHubCli.GitHubCli)({ execute })));
const stack = [
{
number: 50,
url: "https://api.github.com/repos/acme/web/stacks/50",
base: { ref: "main" },
pull_requests: [
{
number: 1,
title: "Base",
head: { ref: "base", sha: "aaa" },
state: "closed",
merged_at: "2026-01-01T00:00:00Z",
},
{
number: 2,
title: "Middle",
head: { ref: "middle", sha: "bbb" },
state: "open",
draft: false,
},
{ number: 3, title: "Top", head: { ref: "top", sha: "ccc" }, state: "open", draft: false },
],
},
];
const input = {
cwd: "/repo",
repository: "acme/web",
host: "github.com",
number: 3,
stackNumber: 50,
expectedStackHeads: [
{ number: 2, headSha: "bbb" },
{ number: 3, headSha: "ccc" },
],
action: "merge" as const,
};
const access = {
data: {
repository: {
pr2: { headRepository: { viewerPermission: "WRITE" }, maintainerCanModify: false },
pr3: { headRepository: { viewerPermission: "WRITE" }, maintainerCanModify: false },
},
},
};
const branch = (number: number, headRefOid: string, behindBy = 1, processed: string[] = []) => ({
data: {
processed: processed.map((headRefOid) => ({ headRefOid })),
repository: {
pullRequest: { id: `PR_${number}`, headRefOid, baseRef: { compare: { behindBy } } },
},
},
});
const rebased = {
data: { updatePullRequestBranch: { pullRequest: { headRefOid: "rebased-sha" } } },
};
const rebaseResponses = [branch(2, "bbb"), rebased, branch(3, "ccc", 1, ["rebased-sha"]), rebased];
function fake(responses: readonly unknown[]) {
const calls: ReadonlyArray<string>[] = [];
const execute: GitHubCli.GitHubCli["Service"]["execute"] = (request) =>
Effect.sync(() => {
calls.push(request.args);
const value = responses[calls.length - 1];
if (value === undefined) throw new Error("Unexpected GitHub request");
return {
exitCode: ChildProcessSpawner.ExitCode(0),
// @effect-diagnostics-next-line preferSchemaOverJson:off
stdout: JSON.stringify(value),
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
stdoutInvalidUtf8: false,
};
});
return { execute, calls };
}
it.effect("submits one atomic merge with the reviewed head and respects the merge queue", () =>
Effect.gen(function* () {
const api = fake([stack, { status: "enqueued", details: {} }]);
yield* runGitHubStackAction(api.execute, { ...input, mergeMethod: "squash" });
expect(api.calls).toHaveLength(2);
expect(api.calls[1]).toContain("repos/acme/web/pulls/3/merge-async");
expect(api.calls[1]).toContain("sha=ccc");
expect(api.calls[1]).toContain("merge_action=default");
expect(api.calls[1]).toContain("merge_method=squash");
}),
);
it.effect("merges through the selected layer without including later draft layers", () =>
Effect.gen(function* () {
const fiveLayers = [
{
...stack[0],
pull_requests: Array.from({ length: 5 }, (_, index) => ({
number: index + 1,
head: { ref: `layer-${index + 1}`, sha: `sha-${index + 1}` },
state: "open",
draft: index >= 3,
})),
},
];
const api = fake([fiveLayers, { status: "merged", details: {} }]);
yield* runGitHubStackAction(api.execute, {
...input,
number: 3,
expectedStackHeads: [1, 2, 3].map((number) => ({ number, headSha: `sha-${number}` })),
});
expect(api.calls).toHaveLength(2);
expect(api.calls[1]).toContain("repos/acme/web/pulls/3/merge-async");
expect(api.calls[1]).toContain("sha=sha-3");
expect(api.calls[1]).toContain("merge_action=default");
}),
);
it.effect("rejects stale reviewed heads below a selected middle layer", () =>
Effect.gen(function* () {
const api = fake([stack]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
number: 2,
expectedStackHeads: [{ number: 2, headSha: "old-head" }],
}).pipe(Effect.result);
expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError" } });
expect(api.calls).toHaveLength(1);
}),
);
it.effect("does not merge from an already merged layer", () =>
Effect.gen(function* () {
const api = fake([stack]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
number: 1,
expectedStackHeads: [],
}).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackUnsupportedError" },
});
expect(api.calls).toHaveLength(1);
}),
);
it.effect("polls an accepted merge and reports a later rule rejection", () =>
Effect.gen(function* () {
const api = fake([
stack,
{ status: "pending", details: { uuid: "operation" } },
{ status: "failed", details: { message: "Required checks have not passed" } },
]);
const fiber = yield* runGitHubStackAction(api.execute, input).pipe(
Effect.result,
Effect.forkChild,
);
yield* TestClock.adjust("1 second");
const result = yield* Fiber.join(fiber);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackMergeRejectedError" },
});
expect(api.calls[2]).toContain("repos/acme/web/pulls/3/merge-async/operation");
}),
);
it.effect("retains stack identity and a rejection response without a message", () =>
Effect.gen(function* () {
const rejection = { status: "failed", details: {} };
const api = fake([stack, rejection]);
const result = yield* runGitHubStackAction(api.execute, input).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: {
_tag: "GitHubStackMergeRejectedError",
repository: input.repository,
number: input.number,
stackNumber: input.stackNumber,
cause: rejection,
},
});
}),
);
it.effect("refuses a changed stack before performing any mutation", () =>
Effect.gen(function* () {
const api = fake([stack]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
expectedStackHeads: [
{ number: 2, headSha: "old" },
{ number: 3, headSha: "ccc" },
],
}).pipe(Effect.result);
expect(result).toMatchObject({ _tag: "Failure", failure: { _tag: "GitHubStackChangedError" } });
expect(api.calls).toHaveLength(1);
}),
);
it.effect("rebases unmerged layers bottom to top without local git commands", () =>
Effect.gen(function* () {
const api = fake([stack, access, ...rebaseResponses]);
yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch" });
const mutations = api.calls.filter((args) =>
args.some((arg) => arg.startsWith("query=mutation")),
);
expect(mutations).toHaveLength(2);
expect(mutations[0]).toContain("id=PR_2");
expect(mutations[0]).toContain("sha=bbb");
expect(mutations[1]).toContain("id=PR_3");
expect(mutations[1]).toContain("sha=ccc");
expect(api.calls.every((args) => args[0] === "api")).toBe(true);
}),
);
it.effect("does not update later layers after a rebase failure", () =>
Effect.gen(function* () {
const api = fake([stack, access, branch(2, "bbb")]);
const execute: typeof api.execute = (request) =>
!request.args.some((arg) => arg.startsWith("query=mutation"))
? api.execute(request)
: Effect.fail(
new GitHubCli.GitHubCliAuthenticationError({
command: "gh",
cwd: "/repo",
cause: new Error("denied"),
}),
);
const result = yield* runGitHubStackAction(execute, { ...input, action: "update-branch" }).pipe(
Effect.result,
);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackRebaseFailedError", number: 2, completed: 0 },
});
}),
);
it.effect("refuses the entire rebase before mutation when a later fork denies write access", () =>
Effect.gen(function* () {
const api = fake([
stack,
{
data: {
repository: {
...access.data.repository,
pr3: { headRepository: { viewerPermission: "READ" }, maintainerCanModify: false },
},
},
},
]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
action: "update-branch",
}).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackPermissionError" },
});
expect(api.calls).toHaveLength(2);
expect(api.calls.every((args) => args[0] === "api")).toBe(true);
}),
);
it.effect("allows a fork that explicitly permits maintainer updates", () =>
Effect.gen(function* () {
const api = fake([
stack,
{
data: {
repository: {
...access.data.repository,
pr3: { headRepository: { viewerPermission: "READ" }, maintainerCanModify: true },
},
},
},
...rebaseResponses,
]);
yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch" });
expect(api.calls.at(-1)).toContain("id=PR_3");
}),
);
it.effect("bounds polling and reports a still-running merge without claiming success", () =>
Effect.gen(function* () {
const api = fake([
stack,
...Array.from({ length: 40 }, () => ({ status: "pending", details: { uuid: "operation" } })),
]);
const fiber = yield* runGitHubStackAction(api.execute, input).pipe(
Effect.result,
Effect.forkChild,
);
yield* TestClock.adjust("6 minutes");
expect(yield* Fiber.join(fiber)).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackMergePendingError" },
});
expect(api.calls.length).toBeLessThan(40);
}),
);
it.effect("rejects a push after preflight without rebasing the new revision", () =>
Effect.gen(function* () {
const api = fake([stack, access, branch(2, "new-head")]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
action: "update-branch",
}).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackChangedError", number: 2, completed: 0 },
});
expect(api.calls).toHaveLength(3);
}),
);
it.effect("skips current layers without submitting a rebase mutation", () =>
Effect.gen(function* () {
const api = fake([stack, access, branch(2, "bbb", 0), branch(3, "ccc", 0, ["bbb"])]);
yield* runGitHubStackAction(api.execute, { ...input, action: "update-branch" });
expect(api.calls.some((args) => args.some((arg) => arg.startsWith("query=mutation")))).toBe(
false,
);
}),
);
it.effect("keeps earlier progress and stops after a later layer fails", () =>
Effect.gen(function* () {
const api = fake([
stack,
access,
branch(2, "bbb"),
rebased,
branch(3, "ccc", 1, ["rebased-sha"]),
{ data: { updatePullRequestBranch: null } },
]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
action: "update-branch",
}).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackRebaseFailedError", number: 3, completed: 1 },
});
}),
);
it.effect("reports partial progress when a later head changes during the rebase", () =>
Effect.gen(function* () {
const api = fake([
stack,
access,
branch(2, "bbb"),
rebased,
branch(3, "concurrent-head", 1, ["rebased-sha"]),
]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
action: "update-branch",
}).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackChangedError", number: 3, completed: 1 },
});
if (result._tag === "Failure") {
expect(result.failure.message).toContain("Earlier updates remain on GitHub");
}
expect(
api.calls.filter((args) => args.some((arg) => arg.startsWith("query=mutation"))),
).toHaveLength(1);
}),
);
it.effect.each([false, true])("rejects a push to a processed layer, rebased=%s", (rebasedParent) =>
Effect.gen(function* () {
const api = fake([
stack,
access,
branch(2, "bbb", rebasedParent ? 1 : 0),
...(rebasedParent ? [rebased] : []),
branch(3, "ccc", 1, ["concurrent-parent-head"]),
]);
const result = yield* runGitHubStackAction(api.execute, {
...input,
action: "update-branch",
}).pipe(Effect.result);
expect(result).toMatchObject({
_tag: "Failure",
failure: { _tag: "GitHubStackChangedError", number: 2, completed: 1 },
});
expect(api.calls.at(-1)?.some((arg) => arg.includes('processed:nodes(ids:["PR_2"])'))).toBe(
true,
);
expect(
api.calls.filter((args) => args.some((arg) => arg.startsWith("query=mutation"))),
).toHaveLength(rebasedParent ? 1 : 0);
}),
);