Files
kandev/apps/web/lib/plugins/change-request-creation.test.ts

250 lines
7.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
createChangeRequestWithProvider,
resolveChangeRequestProviderTarget,
} from "./change-request-creation";
import type { RepositoryProviderRegistration } from "./types";
const WORKSPACE_ID = "workspace-a";
const CREATED_PR_URL = "https://bitbucket.test/pr/42";
function provider(
overrides: Partial<RepositoryProviderRegistration> = {},
): RepositoryProviderRegistration & { pluginId: string } {
return {
pluginId: "bitbucket-plugin",
id: "bitbucket",
label: "Bitbucket",
listRepositories: async () => [],
matchesURL: () => false,
listBranches: async () => [],
inspectURL: async () => null,
createChangeRequest: async () => ({ url: "https://bitbucket.test/pr/1" }),
...overrides,
};
}
const repositories = [
{ id: "repo-a", workspace_id: WORKSPACE_ID, name: "api", provider: "bitbucket" },
{ id: "repo-b", workspace_id: WORKSPACE_ID, name: "web", provider: "github" },
];
const task = {
id: "task-a",
workspaceId: WORKSPACE_ID,
repositories: [
{ repository_id: "repo-a", position: 0 },
{ repository_id: "repo-b", position: 1 },
],
};
describe("resolveChangeRequestProviderTarget", () => {
it("selects the persisted provider repository matching the Git repo scope", () => {
const registration = provider();
const target = resolveChangeRequestProviderTarget({
task,
repositories,
repositoryScope: "api",
getProvider: (id) => (id === "bitbucket" ? registration : undefined),
});
expect(target).toMatchObject({
provider: registration,
workspaceId: WORKSPACE_ID,
taskId: "task-a",
repositoryId: "repo-a",
repository: repositories[0],
});
});
it("uses the primary repository only when no multi-repo scope was supplied", () => {
const registration = provider();
const target = resolveChangeRequestProviderTarget({
task,
repositories,
getProvider: () => registration,
});
expect(target?.repositoryId).toBe("repo-a");
});
it("uses the primary repository for the empty scope emitted by single-repo Git UI", () => {
const registration = provider();
const target = resolveChangeRequestProviderTarget({
task,
repositories,
repositoryScope: "",
getProvider: () => registration,
});
expect(target?.repositoryId).toBe("repo-a");
});
it("falls back to the built-in flow when provider create is absent or scope is ambiguous", () => {
expect(
resolveChangeRequestProviderTarget({
task,
repositories,
repositoryScope: "web",
getProvider: () => provider({ createChangeRequest: undefined }),
}),
).toBeNull();
expect(
resolveChangeRequestProviderTarget({
task: {
...task,
repositories: [...task.repositories, { repository_id: "repo-c", position: 2 }],
},
repositories: [...repositories, { ...repositories[0], id: "repo-c" }],
repositoryScope: "api",
getProvider: () => provider(),
}),
).toBeNull();
});
});
describe("createChangeRequestWithProvider", () => {
it("pushes before invoking provider create and returns native PR result", async () => {
const order: string[] = [];
const push = vi.fn(async () => {
order.push("push");
return { success: true, operation: "push", output: "pushed" };
});
const createChangeRequest = vi.fn(async () => {
order.push("create");
return { url: CREATED_PR_URL, provider: "bitbucket" };
});
const signal = new AbortController().signal;
const result = await createChangeRequestWithProvider({
target: {
provider: provider({ createChangeRequest }),
workspaceId: WORKSPACE_ID,
taskId: "task-a",
repositoryId: "repo-a",
repository: repositories[0],
},
push,
repositoryScope: "api",
title: "Create plugin PR",
body: "Body",
baseBranch: "main",
draft: false,
branchAlreadyPushed: false,
sessionId: "session-a",
signal,
});
expect(order).toEqual(["push", "create"]);
expect(push).toHaveBeenCalledWith({ setUpstream: true }, "api");
expect(createChangeRequest).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId: WORKSPACE_ID,
taskId: "task-a",
sessionId: "session-a",
repositoryId: "repo-a",
title: "Create plugin PR",
body: "Body",
baseBranch: "main",
draft: false,
signal,
}),
);
expect(result).toEqual({
success: true,
branch_pushed: true,
pr_url: CREATED_PR_URL,
provider: "bitbucket",
output: "pushed",
});
});
it("preserves successful remote creation when task association fails", async () => {
const createChangeRequest = vi.fn(async () => ({
url: CREATED_PR_URL,
provider: "bitbucket",
linked: false,
associationError: "Task association could not be saved",
}));
const result = await createChangeRequestWithProvider({
target: {
provider: provider({ createChangeRequest }),
workspaceId: WORKSPACE_ID,
taskId: "task-a",
repositoryId: "repo-a",
repository: repositories[0],
},
push: vi.fn(),
title: "Title",
body: "",
draft: false,
branchAlreadyPushed: true,
sessionId: "session-a",
signal: new AbortController().signal,
});
expect(result).toMatchObject({
success: true,
branch_pushed: true,
pr_url: CREATED_PR_URL,
linked: false,
association_error: "Task association could not be saved",
});
});
});
describe("createChangeRequestWithProvider retry behavior", () => {
it("does not invoke provider create when push fails", async () => {
const createChangeRequest = vi.fn();
const result = await createChangeRequestWithProvider({
target: {
provider: provider({ createChangeRequest }),
workspaceId: WORKSPACE_ID,
taskId: "task-a",
repositoryId: "repo-a",
repository: repositories[0],
},
push: async () => ({ success: false, operation: "push", output: "", error: "denied" }),
title: "Title",
body: "",
draft: false,
branchAlreadyPushed: false,
sessionId: "session-a",
signal: new AbortController().signal,
});
expect(createChangeRequest).not.toHaveBeenCalled();
expect(result).toMatchObject({ success: false, branch_pushed: false, error: "denied" });
});
it("retries only provider creation after a successful push", async () => {
const push = vi.fn();
const createChangeRequest = vi.fn().mockRejectedValueOnce(new Error("provider unavailable"));
const target = {
provider: provider({ createChangeRequest }),
workspaceId: WORKSPACE_ID,
taskId: "task-a",
repositoryId: "repo-a",
repository: repositories[0],
};
const failed = await createChangeRequestWithProvider({
target,
push,
title: "Title",
body: "",
draft: false,
branchAlreadyPushed: true,
sessionId: "session-a",
signal: new AbortController().signal,
});
expect(push).not.toHaveBeenCalled();
expect(failed).toMatchObject({
success: false,
branch_pushed: true,
error: "provider unavailable",
});
});
});