Public: anyone can read this; the crew can change it.
A test pins an origin's labels: `importLabels` and `applyLabelNames` make each name once and attach exactly the remote set #6668
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6656-a-test-pins-an-origin-s-labels-importlab → main 1 commit fork 1 thread · 1 unresolved
1 file
changed +121
−0
test/origins-import-labels.test.ts added +121
| @@ -0,0 +1,121 @@ | ||
| 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 { createRepo } from "../src/actions/repos.ts"; | |
| 7 | import { createIssue } from "../src/core/issues.ts"; | |
| 8 | import { applyLabelNames, importLabels } from "../src/origins/import.ts"; | |
| 9 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 10 | ||
| 11 | /** | |
| 12 | * Labels from an origin are matched by name (case-insensitively, as the | |
| 13 | * module's queries do): a re-sync neither duplicates a label nor keeps one | |
| 14 | * the origin took off an issue. | |
| 15 | */ | |
| 16 | ||
| 17 | let dir: string; | |
| 18 | let app: App; | |
| 19 | let repoId: string; | |
| 20 | let authorId: string; | |
| 21 | ||
| 22 | const rows = () => | |
| 23 | app.db | |
| 24 | .query<{ name: string }, [string]>( | |
| 25 | "SELECT name FROM label WHERE repo_id = ?1 ORDER BY name", | |
| 26 | ) | |
| 27 | .all(repoId) | |
| 28 | .map((r) => r.name); | |
| 29 | ||
| 30 | const attached = (issueId: string) => | |
| 31 | app.db | |
| 32 | .query<{ name: string }, [string]>( | |
| 33 | `SELECT l.name FROM issue_label il JOIN label l ON l.id = il.label_id | |
| 34 | WHERE il.issue_id = ?1 ORDER BY l.name`, | |
| 35 | ) | |
| 36 | .all(issueId) | |
| 37 | .map((r) => r.name); | |
| 38 | ||
| 39 | const newIssue = (title: string) => { | |
| 40 | const made = createIssue(app.db, { repoId, authorId, title }); | |
| 41 | if (!made.ok) throw new Error(made.error); | |
| 42 | return made.value.id; | |
| 43 | }; | |
| 44 | ||
| 45 | beforeAll(async () => { | |
| 46 | dir = await mkdtemp(join(tmpdir(), "forge-origin-labels-")); | |
| 47 | app = createApp({ dataDir: dir, port: 0 }); | |
| 48 | const admin = await seedAdmin(app, { | |
| 49 | username: "alice", | |
| 50 | email: "[email protected]", | |
| 51 | password: "alice-login-secret", | |
| 52 | }); | |
| 53 | if (!admin.ok) throw new Error(admin.error); | |
| 54 | authorId = admin.value.id; | |
| 55 | const repo = await createRepo(app, admin.value, { name: "yard", visibility: "private" }); | |
| 56 | if (!repo.ok) throw new Error(String(repo.error)); | |
| 57 | repoId = repo.value.id; | |
| 58 | }); | |
| 59 | ||
| 60 | afterAll(async () => { | |
| 61 | await rm(dir, { recursive: true, force: true }); | |
| 62 | }); | |
| 63 | ||
| 64 | describe("importLabels", () => { | |
| 65 | test("importing the same labels twice leaves one row per name", () => { | |
| 66 | const remote = [ | |
| 67 | { remoteId: "1", name: "bug", color: "d73a4a" }, | |
| 68 | { remoteId: "2", name: "docs", color: "0075ca" }, | |
| 69 | ]; | |
| 70 | importLabels(app.db, repoId, remote); | |
| 71 | importLabels(app.db, repoId, remote); | |
| 72 | expect(rows()).toEqual(["bug", "docs"]); | |
| 73 | }); | |
| 74 | ||
| 75 | test("a re-sync adopts the row by name, case-insensitively, and takes the new colour", () => { | |
| 76 | importLabels(app.db, repoId, [{ remoteId: "9", name: "BUG", color: "000000" }]); | |
| 77 | expect(rows()).toEqual(["bug", "docs"]); | |
| 78 | const bug = app.db | |
| 79 | .query<{ color: string; origin_remote_id: string }, []>( | |
| 80 | "SELECT color, origin_remote_id FROM label WHERE name = 'bug'", | |
| 81 | ) | |
| 82 | .get(); | |
| 83 | expect(bug).toEqual({ color: "000000", origin_remote_id: "9" }); | |
| 84 | }); | |
| 85 | ||
| 86 | test("a name blank after trimming is skipped", () => { | |
| 87 | const before = rows(); | |
| 88 | const out = importLabels(app.db, repoId, [ | |
| 89 | { remoteId: "7", name: "", color: "ffffff" }, | |
| 90 | { remoteId: "8", name: " \t ", color: "ffffff" }, | |
| 91 | ]); | |
| 92 | expect(out.size).toBe(0); | |
| 93 | expect(rows()).toEqual(before); | |
| 94 | }); | |
| 95 | }); | |
| 96 | ||
| 97 | describe("applyLabelNames", () => { | |
| 98 | test("[a, b] then [b] leaves the issue with only b", () => { | |
| 99 | const issueId = newIssue("labelled"); | |
| 100 | applyLabelNames(app.db, repoId, issueId, ["a", "b"]); | |
| 101 | expect(attached(issueId)).toEqual(["a", "b"]); | |
| 102 | applyLabelNames(app.db, repoId, issueId, ["b"]); | |
| 103 | expect(attached(issueId)).toEqual(["b"]); | |
| 104 | }); | |
| 105 | ||
| 106 | test("each name is made once, however often it is applied", () => { | |
| 107 | const issueId = newIssue("again"); | |
| 108 | applyLabelNames(app.db, repoId, issueId, ["fresh", "fresh"]); | |
| 109 | applyLabelNames(app.db, repoId, issueId, ["fresh"]); | |
| 110 | expect(rows().filter((n) => n === "fresh")).toHaveLength(1); | |
| 111 | expect(attached(issueId)).toEqual(["fresh"]); | |
| 112 | }); | |
| 113 | ||
| 114 | test("a name blank after trimming is skipped, and the rest are trimmed", () => { | |
| 115 | const issueId = newIssue("blanks"); | |
| 116 | const before = rows().length; | |
| 117 | applyLabelNames(app.db, repoId, issueId, ["", " ", " padded "]); | |
| 118 | expect(attached(issueId)).toEqual(["padded"]); | |
| 119 | expect(rows().length).toBe(before + 1); | |
| 120 | }); | |
| 121 | }); |
new above
offline
Nit: this case relies on the
bug/docsrows the previous test made. Seeding them here, or in abeforeEach, would let this case run alone, for example withbun test -t.