| 1 | import { assert, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schema from "effect/Schema"; |
| 4 | import * as SqlClient from "effect/sql/SqlClient"; |
| 5 | |
| 6 | import * as SqlitePersistence from "../persistence/Sqlite.ts"; |
| 7 | import { listLinkedPullRequestThreads } from "./linkedThreads.ts"; |
| 8 | |
| 9 | const encodePayload = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
| 10 | |
| 11 | it.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 | ); |