1import { assert, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4import * as SqlClient from "effect/sql/SqlClient";
5
6import * as SqlitePersistence from "../persistence/Sqlite.ts";
7import { listLinkedPullRequestThreads } from "./linkedThreads.ts";
8
9const encodePayload = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
10
11it.effect(
12 "finds active and archived threads for exactly one pull request, excluding deleted and dismissed links",
13 () =>
14 Effect.gen(function* () {
15 const sql = yield* SqlClient.SqlClient;
16 const createdAt = "2026-09-01T00:00:00.000Z";
17 const archivedAt = "2026-09-03T00:00:00.000Z";
18 const fixtures = [
19 {
20 id: "forgejo-old",
21 host: "forge.example",
22 repository: "acme/web",
23 number: 7,
24 source: "manual",
25 url: "http://forge.example:3000/acme/web/pulls/7",
26 },
27 {
28 id: "forgejo-other-port",
29 host: "forge.example:4000",
30 repository: "acme/web",
31 number: 7,
32 source: "manual",
33 url: "http://forge.example:4000/acme/web/pulls/7",
34 },
35 {
36 id: "azure",
37 host: "dev.azure.com",
38 repository: "org/project/_git/web",
39 number: 7,
40 source: "manual",
41 },
42 {
43 id: "other-org",
44 host: "dev.azure.com",
45 repository: "other/project/_git/web",
46 number: 7,
47 source: "manual",
48 },
49 { id: "active", host: "github.com", repository: "acme/web", number: 7, source: "manual" },
50 {
51 id: "archived",
52 host: "github.com",
53 repository: "acme/web",
54 number: 7,
55 source: "created",
56 },
57 { id: "deleted", host: "github.com", repository: "acme/web", number: 7, source: "manual" },
58 {
59 id: "dismissed",
60 host: "github.com",
61 repository: "acme/web",
62 number: 7,
63 source: "stack-dismissed",
64 },
65 {
66 id: "other-host",
67 host: "github.example.com",
68 repository: "acme/web",
69 number: 7,
70 source: "manual",
71 },
72 {
73 id: "other-repository",
74 host: "github.com",
75 repository: "acme/api",
76 number: 7,
77 source: "manual",
78 },
79 {
80 id: "other-number",
81 host: "github.com",
82 repository: "acme/web",
83 number: 8,
84 source: "manual",
85 },
86 ];
87 for (const fixture of fixtures) {
88 const payload = yield* encodePayload({
89 linkedPullRequest: null,
90 pullRequests: [{ ...fixture, linkedAt: createdAt, snapshot: null, stack: null }],
91 });
92 yield* sql`
93 INSERT INTO orchestration_v2_projection_threads (
94 thread_id, project_id, title, default_provider, runtime_mode, interaction_mode,
95 created_at, updated_at, archived_at, deleted_at, payload_json
96 ) VALUES (
97 ${fixture.id}, 'project-1', ${fixture.id}, 'codex', 'full-access', 'default',
98 ${createdAt}, ${fixture.id === "archived" ? archivedAt : createdAt},
99 ${fixture.id === "archived" ? archivedAt : null},
100 ${fixture.id === "deleted" ? archivedAt : null}, ${payload}
101 )
102 `;
103 }
104 yield* sql`
105 INSERT INTO orchestration_v2_projection_threads (
106 thread_id, project_id, title, default_provider, runtime_mode, interaction_mode,
107 created_at, updated_at, payload_json
108 ) VALUES (
109 'legacy-single', 'project-1', 'legacy-single', 'codex', 'full-access', 'default',
110 ${createdAt}, ${createdAt},
111 '{"linkedPullRequest":{"projectId":"project-1","repository":"acme/web","number":7,"url":"https://github.com/acme/web/pull/7"}}'
112 )
113 `;
114 yield* sql`
115 INSERT INTO orchestration_v2_projection_threads (
116 thread_id, project_id, title, default_provider, runtime_mode, interaction_mode,
117 created_at, updated_at, payload_json
118 ) VALUES (
119 'explicitly-unlinked', 'project-1', 'explicitly-unlinked', 'codex', 'full-access', 'default',
120 ${createdAt}, ${createdAt},
121 '{"linkedPullRequest":{"projectId":"project-1","repository":"acme/web","number":7,"url":"https://github.com/acme/web/pull/7"},"pullRequests":[]}'
122 )
123 `;
124 yield* sql`
125 INSERT INTO projection_projects (
126 project_id, title, workspace_root, scripts_json, created_at, updated_at
127 ) VALUES ('project-1', 'Project', '/tmp/project', '[]', ${createdAt}, ${createdAt})
128 `;
129 yield* sql`
130 INSERT INTO projection_threads (
131 thread_id, project_id, title, model_selection_json, created_at, updated_at
132 ) VALUES ('v1-only', 'project-1', 'v1-only',
133 '{"instanceId":"codex","model":"gpt-5.4"}', ${createdAt}, ${createdAt})
134 `;
135 yield* sql`
136 INSERT INTO projection_thread_pull_requests (
137 thread_id, host, repository, number, url, source, linked_at
138 ) VALUES ('v1-only', 'github.com', 'acme/web', 7,
139 'https://github.com/acme/web/pull/7', 'manual', ${createdAt})
140 `;
141
142 expect(
143 (yield* listLinkedPullRequestThreads({
144 host: "forge.example:3000",
145 repository: "acme/web",
146 number: 7,
147 })).threads.map((thread) => thread.id),
148 ).toEqual(["forgejo-old"]);
149 expect(
150 (yield* listLinkedPullRequestThreads({
151 host: "forge.example:4000",
152 repository: "acme/web",
153 number: 7,
154 })).threads.map((thread) => thread.id),
155 ).toEqual(["forgejo-other-port"]);
156 expect(
157 (yield* listLinkedPullRequestThreads({
158 host: "org.visualstudio.com",
159 repository: "project/_git/web",
160 number: 7,
161 })).threads.map((thread) => thread.id),
162 ).toEqual(["azure"]);
163 const result = yield* listLinkedPullRequestThreads({
164 host: "GitHub.Com",
165 repository: "ACME/WEB",
166 number: 7,
167 });
168 expect(result).toEqual({
169 threads: [
170 { id: "archived", projectId: "project-1", title: "archived", archivedAt },
171 { id: "active", projectId: "project-1", title: "active", archivedAt: null },
172 { id: "legacy-single", projectId: "project-1", title: "legacy-single", archivedAt: null },
173 ],
174 });
175 assert.deepStrictEqual(
176 yield* listLinkedPullRequestThreads({
177 host: "github.com",
178 repository: "acme/web",
179 number: 99,
180 }),
181 { threads: [] },
182 );
183 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
184);