import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as Layer from "effect/Layer"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); interface PullRequestRow { readonly threadId: string; readonly host: string; readonly repository: string; readonly number: number; readonly url: string; readonly source: string; readonly linkedAt: string; readonly snapshotJson: string | null; readonly stackJson: string | null; } layer("050_ProjectionThreadPullRequests", (it) => { it.effect("creates the link table and backfills legacy single links", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* runMigrations({ toMigrationInclusive: 49 }); yield* sql` INSERT INTO projection_projects ( project_id, title, workspace_root, scripts_json, created_at, updated_at, deleted_at ) VALUES ( 'project-1', 'Project 1', '/tmp/project-1', '[]', '2026-03-01T00:00:00.000Z', '2026-03-01T00:00:00.000Z', NULL ) `; yield* sql` INSERT INTO projection_threads ( thread_id, project_id, title, model_selection_json, linked_pull_request_json, created_at, updated_at ) VALUES ( 'thread-github', 'project-1', 'GitHub link', '{"instanceId":"codex","model":"gpt-5.4"}', '{"projectId":"project-1","repository":"PingDotGG/T3Code","number":42,"url":"https://GitHub.com/pingdotgg/t3code/pull/42"}', '2026-03-01T00:00:01.000Z', '2026-03-02T00:00:00.000Z' ), ( 'thread-bad-url', 'project-1', 'Unparseable URL', '{"instanceId":"codex","model":"gpt-5.4"}', '{"projectId":"project-1","repository":"acme/widgets","number":7,"url":"not a url"}', '2026-03-01T00:00:02.000Z', '2026-03-03T00:00:00.000Z' ), ( 'thread-malformed', 'project-1', 'Malformed JSON', '{"instanceId":"codex","model":"gpt-5.4"}', '{"repository":"acme/widgets"}', '2026-03-01T00:00:03.000Z', '2026-03-04T00:00:00.000Z' ), ( 'thread-unlinked', 'project-1', 'No link', '{"instanceId":"codex","model":"gpt-5.4"}', NULL, '2026-03-01T00:00:04.000Z', '2026-03-05T00:00:00.000Z' ) `; yield* runMigrations({ toMigrationInclusive: 50 }); const rows = yield* sql` SELECT thread_id AS "threadId", host, repository, number, url, source, linked_at AS "linkedAt", snapshot_json AS "snapshotJson", stack_json AS "stackJson" FROM projection_thread_pull_requests ORDER BY thread_id ASC `; assert.deepStrictEqual(rows, [ { threadId: "thread-bad-url", host: "unknown", repository: "acme/widgets", number: 7, url: "not a url", source: "manual", linkedAt: "2026-03-03T00:00:00.000Z", snapshotJson: null, stackJson: null, }, { threadId: "thread-github", host: "github.com", repository: "pingdotgg/t3code", number: 42, url: "https://GitHub.com/pingdotgg/t3code/pull/42", source: "manual", linkedAt: "2026-03-02T00:00:00.000Z", snapshotJson: null, stackJson: null, }, ]); // The legacy column stays so a rollback keeps its data. const columns = yield* sql<{ readonly name: string }>` PRAGMA table_info(projection_threads) `; assert.ok(columns.some((column) => column.name === "linked_pull_request_json")); const indexes = yield* sql<{ readonly name: string }>` PRAGMA index_list(projection_thread_pull_requests) `; assert.ok(indexes.some((index) => index.name === "idx_projection_thread_pull_requests_pr")); }), ); }); it.layer(Layer.fresh(NodeSqliteClient.layerMemory()))("050 Azure legacy links", (it) => { it.effect("keeps legacy Azure repositories distinct across organizations", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* runMigrations({ toMigrationInclusive: 49 }); for (const organization of ["org-a", "org-b"]) { yield* sql` INSERT INTO projection_threads (thread_id, project_id, title, model_selection_json, linked_pull_request_json, created_at, updated_at) VALUES (${organization}, ${organization}, 'Azure', '{"instanceId":"codex","model":"gpt-5.4"}', ${encodeJson({ projectId: organization, repository: "web", number: 7, url: `https://dev.azure.com/${organization}/project/_git/web/pullrequest/7` })}, '2026-03-01T00:00:00.000Z', '2026-03-01T00:00:00.000Z') `; } yield* runMigrations({ toMigrationInclusive: 50 }); const rows = yield* sql`SELECT host, repository, number FROM projection_thread_pull_requests ORDER BY repository`; assert.deepStrictEqual(rows, [ { host: "dev.azure.com", repository: "org-a/project/_git/web", number: 7 }, { host: "dev.azure.com", repository: "org-b/project/_git/web", number: 7 }, ]); }), ); });