Public: anyone can read this; the crew can change it.
A test pins importing an origin's issue: one row per origin id, the local sequence pushed past its number #6664
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6654-a-test-pins-importing-an-origin-s-issue → main 1 commit fork 1 thread · 1 unresolved
1 file
changed +130
−0
test/origins-import-issue.test.ts added +130
| @@ -0,0 +1,130 @@ | ||
| 1 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 2 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 3 | import { tmpdir } from "node:os"; | |
| 4 | import { join } from "node:path"; | |
| 5 | import { createApp, type App } from "../src/app.ts"; | |
| 6 | import { createIssue } from "../src/core/issues.ts"; | |
| 7 | import { createRepo } from "../src/core/repos.ts"; | |
| 8 | import { importIssue, type ImportContext } from "../src/origins/import.ts"; | |
| 9 | import type { RemoteIssue } from "../src/origins/types.ts"; | |
| 10 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 11 | ||
| 12 | /** `importIssue` on its own: one row per origin id, numbers kept, sequence dragged up. */ | |
| 13 | ||
| 14 | const remote = (over: Partial<RemoteIssue> = {}): RemoteIssue => ({ | |
| 15 | remoteId: "r-40", | |
| 16 | number: 40, | |
| 17 | title: "Remote title", | |
| 18 | body: "body", | |
| 19 | author: "octo", | |
| 20 | state: "open", | |
| 21 | labels: [], | |
| 22 | createdAt: "2026-01-01T00:00:00.000Z", | |
| 23 | updatedAt: "2026-01-02T00:00:00.000Z", | |
| 24 | closedAt: null, | |
| 25 | htmlUrl: "https://origin.test/o/r/issues/40", | |
| 26 | comments: 0, | |
| 27 | ...over, | |
| 28 | }); | |
| 29 | ||
| 30 | describe("importIssue", () => { | |
| 31 | let dir: string; | |
| 32 | let app: App; | |
| 33 | let ctx: ImportContext; | |
| 34 | let authorId: string; | |
| 35 | ||
| 36 | beforeAll(async () => { | |
| 37 | dir = await mkdtemp(join(tmpdir(), "forge-import-issue-")); | |
| 38 | app = createApp({ dataDir: dir, port: 0 }); | |
| 39 | const user = await seedAdmin(app, { | |
| 40 | username: "alice", | |
| 41 | email: "[email protected]", | |
| 42 | password: "alice-login-secret", | |
| 43 | }); | |
| 44 | if (!user.ok) throw new Error(`no user: ${user.error}`); | |
| 45 | authorId = user.value.id; | |
| 46 | const repo = await createRepo(app.db, join(dir, "repos"), { | |
| 47 | ownerId: authorId, | |
| 48 | name: "mirror", | |
| 49 | }); | |
| 50 | if (!repo.ok) throw new Error(`no repo: ${repo.error}`); | |
| 51 | ctx = { db: app.db, repoId: repo.value.id, actorId: authorId }; | |
| 52 | }); | |
| 53 | ||
| 54 | afterAll(async () => { | |
| 55 | app.close(); | |
| 56 | await rm(dir, { recursive: true, force: true }); | |
| 57 | }); | |
| 58 | ||
| 59 | const rows = (remoteId: string) => | |
| 60 | app.db | |
| 61 | .query< | |
| 62 | { id: string; title: string; number: number; kind: string; author_id: string }, | |
| 63 | [string, string] | |
| 64 | >("SELECT id, title, number, kind, author_id FROM issue WHERE repo_id = ?1 AND origin_remote_id = ?2") | |
| 65 | .all(ctx.repoId, remoteId); | |
| 66 | ||
| 67 | test("the same remote issue twice is one row: created, then not; a new title updates it", () => { | |
| 68 | const first = importIssue(ctx, remote()); | |
| 69 | expect(first.created).toBe(true); | |
| 70 | expect(first.number).toBe(40); | |
| 71 | expect(first.kind).toBe("issue"); | |
| 72 | ||
| 73 | const again = importIssue(ctx, remote()); | |
| 74 | expect(again.created).toBe(false); | |
| 75 | expect(again.issueId).toBe(first.issueId); | |
| 76 | expect(rows("r-40")).toHaveLength(1); | |
| 77 | ||
| 78 | const renamed = importIssue( | |
| 79 | ctx, | |
| 80 | remote({ title: "Renamed upstream", updatedAt: "2026-01-03T00:00:00.000Z" }), | |
| 81 | ); | |
| 82 | expect(renamed.created).toBe(false); | |
| 83 | expect(renamed.changed).toBe(true); | |
| 84 | const [row] = rows("r-40"); | |
| 85 | expect(row?.title).toBe("Renamed upstream"); | |
| 86 | expect(row?.author_id).toBe(authorId); | |
| 87 | }); | |
| 88 | ||
| 89 | test("after importing #40 the next native issue is numbered above 40", () => { | |
| 90 | const native = createIssue(app.db, { | |
| 91 | repoId: ctx.repoId, | |
| 92 | authorId, | |
| 93 | title: "native", | |
| 94 | }); | |
| 95 | if (!native.ok) throw new Error(native.error); | |
| 96 | expect(native.value.number).toBeGreaterThan(40); | |
| 97 | }); | |
| 98 | ||
| 99 | test("a blank remote title reads #<number>", () => { | |
| 100 | importIssue(ctx, remote({ remoteId: "r-41", number: 41, title: " \n " })); | |
| 101 | expect(rows("r-41")[0]?.title).toBe("#41"); | |
| 102 | }); | |
| 103 | ||
| 104 | test("a remote with pull set imports as a pull", () => { | |
| 105 | const got = importIssue( | |
| 106 | ctx, | |
| 107 | remote({ | |
| 108 | remoteId: "r-60", | |
| 109 | number: 60, | |
| 110 | pull: { | |
| 111 | baseBranch: "main", | |
| 112 | headBranch: "feature", | |
| 113 | headRepo: "o/r", | |
| 114 | baseOid: "a".repeat(40), | |
| 115 | headOid: "b".repeat(40), | |
| 116 | mergedAt: null, | |
| 117 | draft: false, | |
| 118 | }, | |
| 119 | }), | |
| 120 | ); | |
| 121 | expect(got.kind).toBe("pull"); | |
| 122 | expect(rows("r-60")[0]?.kind).toBe("pull"); | |
| 123 | const pull = app.db | |
| 124 | .query<{ head_branch: string }, [string]>( | |
| 125 | "SELECT head_branch FROM pull WHERE issue_id = ?1", | |
| 126 | ) | |
| 127 | .get(got.issueId); | |
| 128 | expect(pull?.head_branch).toBe("feature"); | |
| 129 | }); | |
| 130 | }); |
new above
offline
This test depends on the previous test having imported #40. Import inside the test and assert toBe(41) so it fails if the sequence is only nudged to 40 or is not moved at all.