1import { ProjectId, ThreadId } from "@t3tools/contracts";
2import { describe, expect, it } from "@effect/vitest";
3import {
4 planThreadPullRequestMutation,
5 threadPullRequestLinkMode,
6} from "./threadPullRequestCompatibility.ts";
7
8const reference = {
9 host: "github.example",
10 repository: "team/repo",
11 number: 7,
12 url: "https://github.example/team/repo/pull/7",
13};
14const input = {
15 threadId: ThreadId.make("thread"),
16 reference,
17 legacyProjectId: ProjectId.make("exact-checkout"),
18 linked: true,
19};
20
21describe("thread pull request capability negotiation", () => {
22 it.each([true, false])(
23 "preserves Forgejo ports in a multi-link mutation: linked=%s",
24 (linked) => {
25 const mutation = planThreadPullRequestMutation({
26 ...input,
27 linked,
28 capabilities: { threadPullRequests: true },
29 reference: {
30 host: "forge.example",
31 repository: "team/repo",
32 number: 7,
33 url: "http://forge.example:3000/team/repo/pulls/7",
34 },
35 });
36 expect(mutation).toMatchObject({ input: { host: "forge.example:3000", number: 7 } });
37 },
38 );
39
40 it.each([undefined, {}, { threadPullRequests: false, threadPullRequestLinking: false }])(
41 "does not dispatch when linking is unadvertised: %j",
42 (capabilities) => {
43 expect(threadPullRequestLinkMode(capabilities)).toBe("unsupported");
44 expect(planThreadPullRequestMutation({ ...input, capabilities })).toBeNull();
45 },
46 );
47 it("uses metadata updates for old single-link servers, including unlink", () => {
48 const capabilities = { threadPullRequestLinking: true };
49 expect(planThreadPullRequestMutation({ ...input, capabilities })).toEqual({
50 type: "thread.meta.update",
51 input: {
52 threadId: input.threadId,
53 linkedPullRequest: {
54 projectId: input.legacyProjectId,
55 repository: reference.repository,
56 number: 7,
57 url: reference.url,
58 },
59 },
60 });
61 expect(
62 planThreadPullRequestMutation({
63 ...input,
64 capabilities,
65 linked: false,
66 legacyProjectId: null,
67 }),
68 ).toEqual({
69 type: "thread.meta.update",
70 input: { threadId: input.threadId, linkedPullRequest: null },
71 });
72 });
73 it("uses the checkout's Azure selector only for legacy metadata commands", () => {
74 const azure = {
75 ...input,
76 reference: {
77 host: "dev.azure.com",
78 repository: "org/project/_git/web",
79 number: 42,
80 url: "https://dev.azure.com/org/project/_git/web/pullrequest/42",
81 },
82 legacyRepository: "web",
83 };
84 expect(
85 planThreadPullRequestMutation({
86 ...azure,
87 capabilities: { threadPullRequestLinking: true },
88 }),
89 ).toMatchObject({
90 type: "thread.meta.update",
91 input: { linkedPullRequest: { repository: "web", number: 42 } },
92 });
93 expect(
94 planThreadPullRequestMutation({
95 ...azure,
96 capabilities: { threadPullRequests: true },
97 }),
98 ).toMatchObject({
99 type: "thread.pull-request.link",
100 input: { repository: "org/project/_git/web", number: 42 },
101 });
102 });
103
104 it("never sends a same-host route as an exact repository to an old server", () => {
105 expect(
106 planThreadPullRequestMutation({
107 ...input,
108 capabilities: { threadPullRequestLinking: true },
109 legacyProjectId: null,
110 }),
111 ).toBeNull();
112 });
113 it.each([
114 { threadPullRequests: true },
115 { threadPullRequests: true, threadPullRequestLinking: true },
116 ])("prefers multi-link commands when available: %j", (capabilities) => {
117 expect(
118 planThreadPullRequestMutation({ ...input, capabilities, legacyProjectId: null }),
119 ).toEqual({
120 type: "thread.pull-request.link",
121 input: { threadId: input.threadId, ...reference, source: "manual" },
122 });
123 expect(planThreadPullRequestMutation({ ...input, capabilities, linked: false })).toEqual({
124 type: "thread.pull-request.unlink",
125 input: {
126 threadId: input.threadId,
127 host: reference.host,
128 repository: reference.repository,
129 number: reference.number,
130 },
131 });
132 });
133});