| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schema from "effect/Schema"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as SqlClient from "effect/sql/SqlClient"; |
| 6 | |
| 7 | import { runMigrations } from "../Migrations.ts"; |
| 8 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 9 | |
| 10 | const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); |
| 11 | |
| 12 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); |
| 13 | |
| 14 | interface PullRequestRow { |
| 15 | readonly threadId: string; |
| 16 | readonly host: string; |
| 17 | readonly repository: string; |
| 18 | readonly number: number; |
| 19 | readonly url: string; |
| 20 | readonly source: string; |
| 21 | readonly linkedAt: string; |
| 22 | readonly snapshotJson: string | null; |
| 23 | readonly stackJson: string | null; |
| 24 | } |
| 25 | |
| 26 | layer("050_ProjectionThreadPullRequests", (it) => { |
| 27 | it.effect("creates the link table and backfills legacy single links", () => |
| 28 | Effect.gen(function* () { |
| 29 | const sql = yield* SqlClient.SqlClient; |
| 30 | |
| 31 | yield* runMigrations({ toMigrationInclusive: 49 }); |
| 32 | |
| 33 | yield* sql` |
| 34 | INSERT INTO projection_projects ( |
| 35 | project_id, |
| 36 | title, |
| 37 | workspace_root, |
| 38 | scripts_json, |
| 39 | created_at, |
| 40 | updated_at, |
| 41 | deleted_at |
| 42 | ) |
| 43 | VALUES ( |
| 44 | 'project-1', |
| 45 | 'Project 1', |
| 46 | '/tmp/project-1', |
| 47 | '[]', |
| 48 | '2026-03-01T00:00:00.000Z', |
| 49 | '2026-03-01T00:00:00.000Z', |
| 50 | NULL |
| 51 | ) |
| 52 | `; |
| 53 | |
| 54 | yield* sql` |
| 55 | INSERT INTO projection_threads ( |
| 56 | thread_id, |
| 57 | project_id, |
| 58 | title, |
| 59 | model_selection_json, |
| 60 | linked_pull_request_json, |
| 61 | created_at, |
| 62 | updated_at |
| 63 | ) |
| 64 | VALUES |
| 65 | ( |
| 66 | 'thread-github', |
| 67 | 'project-1', |
| 68 | 'GitHub link', |
| 69 | '{"instanceId":"codex","model":"gpt-5.4"}', |
| 70 | '{"projectId":"project-1","repository":"PingDotGG/T3Code","number":42,"url":"https://GitHub.com/pingdotgg/t3code/pull/42"}', |
| 71 | '2026-03-01T00:00:01.000Z', |
| 72 | '2026-03-02T00:00:00.000Z' |
| 73 | ), |
| 74 | ( |
| 75 | 'thread-bad-url', |
| 76 | 'project-1', |
| 77 | 'Unparseable URL', |
| 78 | '{"instanceId":"codex","model":"gpt-5.4"}', |
| 79 | '{"projectId":"project-1","repository":"acme/widgets","number":7,"url":"not a url"}', |
| 80 | '2026-03-01T00:00:02.000Z', |
| 81 | '2026-03-03T00:00:00.000Z' |
| 82 | ), |
| 83 | ( |
| 84 | 'thread-malformed', |
| 85 | 'project-1', |
| 86 | 'Malformed JSON', |
| 87 | '{"instanceId":"codex","model":"gpt-5.4"}', |
| 88 | '{"repository":"acme/widgets"}', |
| 89 | '2026-03-01T00:00:03.000Z', |
| 90 | '2026-03-04T00:00:00.000Z' |
| 91 | ), |
| 92 | ( |
| 93 | 'thread-unlinked', |
| 94 | 'project-1', |
| 95 | 'No link', |
| 96 | '{"instanceId":"codex","model":"gpt-5.4"}', |
| 97 | NULL, |
| 98 | '2026-03-01T00:00:04.000Z', |
| 99 | '2026-03-05T00:00:00.000Z' |
| 100 | ) |
| 101 | `; |
| 102 | |
| 103 | yield* runMigrations({ toMigrationInclusive: 50 }); |
| 104 | |
| 105 | const rows = yield* sql<PullRequestRow>` |
| 106 | SELECT |
| 107 | thread_id AS "threadId", |
| 108 | host, |
| 109 | repository, |
| 110 | number, |
| 111 | url, |
| 112 | source, |
| 113 | linked_at AS "linkedAt", |
| 114 | snapshot_json AS "snapshotJson", |
| 115 | stack_json AS "stackJson" |
| 116 | FROM projection_thread_pull_requests |
| 117 | ORDER BY thread_id ASC |
| 118 | `; |
| 119 | |
| 120 | assert.deepStrictEqual(rows, [ |
| 121 | { |
| 122 | threadId: "thread-bad-url", |
| 123 | host: "unknown", |
| 124 | repository: "acme/widgets", |
| 125 | number: 7, |
| 126 | url: "not a url", |
| 127 | source: "manual", |
| 128 | linkedAt: "2026-03-03T00:00:00.000Z", |
| 129 | snapshotJson: null, |
| 130 | stackJson: null, |
| 131 | }, |
| 132 | { |
| 133 | threadId: "thread-github", |
| 134 | host: "github.com", |
| 135 | repository: "pingdotgg/t3code", |
| 136 | number: 42, |
| 137 | url: "https://GitHub.com/pingdotgg/t3code/pull/42", |
| 138 | source: "manual", |
| 139 | linkedAt: "2026-03-02T00:00:00.000Z", |
| 140 | snapshotJson: null, |
| 141 | stackJson: null, |
| 142 | }, |
| 143 | ]); |
| 144 | |
| 145 | // The legacy column stays so a rollback keeps its data. |
| 146 | const columns = yield* sql<{ readonly name: string }>` |
| 147 | PRAGMA table_info(projection_threads) |
| 148 | `; |
| 149 | assert.ok(columns.some((column) => column.name === "linked_pull_request_json")); |
| 150 | |
| 151 | const indexes = yield* sql<{ readonly name: string }>` |
| 152 | PRAGMA index_list(projection_thread_pull_requests) |
| 153 | `; |
| 154 | assert.ok(indexes.some((index) => index.name === "idx_projection_thread_pull_requests_pr")); |
| 155 | }), |
| 156 | ); |
| 157 | }); |
| 158 | |
| 159 | it.layer(Layer.fresh(NodeSqliteClient.layer({ filename: ":memory:" })))( |
| 160 | "050 Azure legacy links", |
| 161 | (it) => { |
| 162 | it.effect("keeps legacy Azure repositories distinct across organizations", () => |
| 163 | Effect.gen(function* () { |
| 164 | const sql = yield* SqlClient.SqlClient; |
| 165 | yield* runMigrations({ toMigrationInclusive: 49 }); |
| 166 | for (const organization of ["org-a", "org-b"]) { |
| 167 | yield* sql` |
| 168 | INSERT INTO projection_threads (thread_id, project_id, title, model_selection_json, linked_pull_request_json, created_at, updated_at) |
| 169 | VALUES (${organization}, ${organization}, 'Azure', '{"instanceId":"codex","model":"gpt-5.4"}', |
| 170 | ${encodeJson({ projectId: organization, repository: "web", number: 7, url: `https://dev.azure.com/${organization}/project/_git/web/pullrequest/7` })}, |
| 171 | '2026-03-01T00:00:00.000Z', '2026-03-01T00:00:00.000Z') |
| 172 | `; |
| 173 | } |
| 174 | yield* runMigrations({ toMigrationInclusive: 50 }); |
| 175 | const rows = |
| 176 | yield* sql`SELECT host, repository, number FROM projection_thread_pull_requests ORDER BY repository`; |
| 177 | assert.deepStrictEqual(rows, [ |
| 178 | { host: "dev.azure.com", repository: "org-a/project/_git/web", number: 7 }, |
| 179 | { host: "dev.azure.com", repository: "org-b/project/_git/web", number: 7 }, |
| 180 | ]); |
| 181 | }), |
| 182 | ); |
| 183 | }, |
| 184 | ); |