import { describe, expect, mock, test } from "bun:test"; import { Database } from "bun:sqlite"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import type { PullRow } from "./pulls"; mock.module("cloudflare:workers", () => ({ env: {}, waitUntil: () => undefined })); const db = new Database(":memory:"); const migrations = join(import.meta.dir, "../../migrations"); for (const file of readdirSync(migrations).sort()) db.exec(readFileSync(join(migrations, file), "utf8")); const bind = (params: unknown[]) => params as never[]; mock.module("./db", () => ({ first: async (sql: string, ...params: unknown[]) => db.query(sql).get(...bind(params)) ?? null, all: async (sql: string, ...params: unknown[]) => db.query(sql).all(...bind(params)), run: async (sql: string, ...params: unknown[]) => db.query(sql).run(...bind(params)), batch: async () => [], stmt: (sql: string, ...params: unknown[]) => ({ sql, params }), placeholders: (count: number) => Array(count).fill("?").join(", "), chunks: (items: T[]) => [items], MAX_IN_LIST: 90, })); const { loadStack, mergePath, stackChildren, stackParent } = await import("./stacks"); const { PULL_SELECT } = await import("./pulls"); db.exec(` INSERT INTO accounts (id, name, kind, created_at) VALUES ('owner', 'owner', 'user', 0); INSERT INTO repos (id, owner_id, name, visibility, status, default_branch, artifacts_name, artifacts_remote, created_at, updated_at) VALUES ('repo', 'owner', 'app', 'public', 'ready', 'main', 'a', 'r', 0, 0), ('fork', 'owner', 'app-fork', 'public', 'ready', 'main', 'b', 'f', 0, 0); `); function addPull(number: number, head: string, base: string, options: { state?: string; headRepo?: string } = {}) { const id = `pr${number}`; db.query( `INSERT INTO issues (id, repo_id, number, title, body, state, author_id, is_pull, created_at, updated_at) VALUES (?, 'repo', ?, ?, '', ?, 'owner', 1, 0, 0)`, ).run(id, number, `Change ${number}`, options.state ?? "open"); db.query( `INSERT INTO pull_requests (issue_id, repo_id, head_repo_id, head_ref, head_sha, base_ref, base_sha) VALUES (?, 'repo', ?, ?, ?, ?, ?)`, ).run(id, options.headRepo ?? "repo", head, `${number}`.padStart(40, "a"), base, "b".repeat(40)); return id; } const pull = (number: number) => db.query(`${PULL_SELECT} WHERE i.number = ?`).get(number) as PullRow; addPull(1, "feat-a", "main"); addPull(2, "feat-b", "feat-a"); addPull(3, "feat-c", "feat-b"); addPull(4, "feat-c2", "feat-b"); addPull(5, "release", "main"); addPull(6, "hotfix", "main"); addPull(7, "main", "release"); addPull(8, "feat-a", "dev", { headRepo: "fork" }); addPull(9, "fork-child", "feat-a"); describe("stack derivation", () => { test("parents are open same-repo pull requests whose head is this base", async () => { expect((await stackParent(pull(2)))?.number).toBe(1); expect((await stackParent(pull(3)))?.number).toBe(2); expect(await stackParent(pull(1))).toBeNull(); }); test("the default branch never forms a stack even when it is some pull request's head", async () => { expect(await stackParent(pull(6))).toBeNull(); expect(await stackChildren(pull(7))).toEqual([]); }); test("children of a fork head are not stacked on it", async () => { expect(await stackChildren(pull(8))).toEqual([]); expect((await stackChildren(pull(1))).map((row) => row.number)).toEqual([2, 9]); }); test("the stack lists ancestors bottom first, then descendants depth-first", async () => { const stack = await loadStack(pull(2)); expect(stack.map((entry) => [entry.number, entry.depth, entry.current])).toEqual([ [1, 0, false], [2, 1, true], [3, 2, false], [4, 2, false], ]); expect(await loadStack(pull(6))).toEqual([]); }); test("merge path runs from the bottom open pull request to the target", async () => { expect((await mergePath(pull(3))).map((row) => row.number)).toEqual([1, 2, 3]); }); test("a merged parent stays in the stack through stacked_on_issue_id", async () => { db.exec(` UPDATE issues SET state = 'closed' WHERE id = 'pr1'; UPDATE pull_requests SET merged_at = 1 WHERE issue_id = 'pr1'; UPDATE pull_requests SET base_ref = 'main', stacked_on_issue_id = 'pr1' WHERE issue_id = 'pr2'; `); const stack = await loadStack(pull(2)); expect(stack.map((entry) => [entry.number, entry.merged])).toEqual([ [1, true], [2, false], [3, false], [4, false], ]); expect(await stackParent(pull(2))).toBeNull(); expect((await mergePath(pull(3))).map((row) => row.number)).toEqual([2, 3]); }); test("cycles terminate", async () => { const [first, second] = [10, 11]; const loop = { parent: async (row: PullRow) => (row.number === first ? pull(second) : pull(first)), children: async () => [], }; db.exec(`DELETE FROM pull_requests; DELETE FROM issues;`); addPull(10, "x", "y"); addPull(11, "y", "x"); expect((await loadStack(pull(10), loop)).map((entry) => entry.number)).toEqual([11, 10]); }); });