Public: anyone can read this; the crew can change it.

A test pins importing an origin's comments and reviews: a second sync adds no duplicate and an edit updates in place #6667

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6655-a-test-pins-importing-an-origin-s-commen → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6655-a-test-pins-importing-an-origin-s-commen was merged into main as 857e68f.

Review

Sign in to review this change.
1 file changed +166 −0
test/origins-import-comments.test.ts added +166
@@ -0,0 +1,166 @@
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 {
7 commentByRemoteId,
8 importComments,
9 importIssue,
10 importReviews,
11 type ImportContext,
12 } from "../src/origins/import.ts";
13 import type {
14 RemoteComment,
15 RemoteIssue,
16 RemoteReview,
17 } from "../src/origins/types.ts";
18 import { seedAdmin } from "./helpers/admin.ts";
19
20 /**
21 * Re-syncing an origin is idempotent: the overlap window replays items the
22 * forge already holds, and comments and reviews are upserted on the origin's
23 * own ids, so a replay adds nothing and an edit moves the row in place.
24 */
25
26 const AT = "2026-01-01T00:00:00Z";
27
28 const remoteIssue = (number: number, withPull: boolean): RemoteIssue => ({
29 remoteId: `ri-${number}`,
30 number,
31 title: `Thing ${number}`,
32 body: "",
33 author: "octo",
34 state: "open",
35 labels: [],
36 createdAt: AT,
37 updatedAt: AT,
38 closedAt: null,
39 htmlUrl: `https://origin.test/o/r/issues/${number}`,
40 comments: 0,
41 ...(withPull
42 ? {
43 pull: {
44 baseBranch: "main",
45 headBranch: "topic",
46 headRepo: "o/r",
47 baseOid: "a".repeat(40),
48 headOid: "b".repeat(40),
49 mergedAt: null,
50 draft: false,
51 },
52 }
53 : {}),
54 });
55
56 const remoteComment = (remoteId: string, body: string): RemoteComment => ({
57 remoteId,
58 author: "octo",
59 body,
60 createdAt: AT,
61 updatedAt: AT,
62 htmlUrl: `https://origin.test/c/${remoteId}`,
63 });
64
65 const remoteReview = (remoteId: string, body: string): RemoteReview => ({
66 remoteId,
67 author: "octo",
68 body,
69 path: "src/a.ts",
70 line: 3,
71 side: "new",
72 createdAt: AT,
73 htmlUrl: `https://origin.test/r/${remoteId}`,
74 });
75
76 describe("origin import: comments and reviews upsert on the origin's ids", () => {
77 let dir: string;
78 let app: App;
79 let ctx: ImportContext;
80
81 const count = (issueId: string, kind: "comment" | "review"): number =>
82 app.db
83 .query<{ n: number }, [string, string]>(
84 "SELECT COUNT(*) AS n FROM comment WHERE issue_id = ?1 AND kind = ?2",
85 )
86 .get(issueId, kind)!.n;
87
88 const bodyOf = (commentId: string): string | undefined =>
89 app.db
90 .query<{ body: string }, [string]>(
91 "SELECT body FROM comment WHERE id = ?1",
92 )
93 .get(commentId)?.body;
94
95 beforeAll(async () => {
96 dir = await mkdtemp(join(tmpdir(), "forge-import-comments-"));
97 app = createApp({ dataDir: dir, port: 0 });
98 const made = await seedAdmin(app, {
99 username: "alice",
100 email: "[email protected]",
101 password: "alice-login-secret",
102 });
103 if (!made.ok) throw new Error(`no admin: ${made.error}`);
104 app.db
105 .query("INSERT INTO repo (id, owner_id, name) VALUES ('repo1', ?1, 'mirror')")
106 .run(made.value.id);
107 ctx = { db: app.db, repoId: "repo1", actorId: made.value.id };
108 });
109
110 afterAll(async () => {
111 app.db.close();
112 await rm(dir, { recursive: true, force: true });
113 });
114
115 test("a second sync of the same two comments adds no row", () => {
116 const { issueId } = importIssue(ctx, remoteIssue(1, false));
117 const thread = [remoteComment("c1", "first"), remoteComment("c2", "second")];
118
119 expect(importComments(ctx, issueId, thread)).toBe(2);
120 expect(count(issueId, "comment")).toBe(2);
121
122 expect(importComments(ctx, issueId, thread)).toBe(0);
123 expect(count(issueId, "comment")).toBe(2);
124 });
125
126 test("an edited remote comment updates its row in place", () => {
127 const { issueId } = importIssue(ctx, remoteIssue(2, false));
128 importComments(ctx, issueId, [remoteComment("c1", "before")]);
129 const rowId = commentByRemoteId(app.db, issueId, "c1");
130 expect(rowId).toBeDefined();
131

Nit: also assert the stored updated_at here. It is what lets a reader see that the row moved on the edit and not only that the body changed.

132 const edited = { ...remoteComment("c1", "after"), updatedAt: "2026-02-01T00:00:00Z" };
133 expect(importComments(ctx, issueId, [edited])).toBe(0);
134
135 expect(commentByRemoteId(app.db, issueId, "c1")).toBe(rowId!);
136 expect(bodyOf(rowId!)).toBe("after");
137 expect(count(issueId, "comment")).toBe(1);
138 });
139
140 test("the same remote id on two issues stays two rows", () => {
141 const a = importIssue(ctx, remoteIssue(3, false)).issueId;
142 const b = importIssue(ctx, remoteIssue(4, false)).issueId;
143 importComments(ctx, a, [remoteComment("shared", "on a")]);
144 importComments(ctx, b, [remoteComment("shared", "on b")]);
145 expect(bodyOf(commentByRemoteId(app.db, a, "shared")!)).toBe("on a");
146 expect(bodyOf(commentByRemoteId(app.db, b, "shared")!)).toBe("on b");
147 });
148
149 test("reviews imported twice leave one review per remote id, edits in place", () => {
150 const { issueId } = importIssue(ctx, remoteIssue(5, true));
151 const head = "b".repeat(40);
152 const reviews = [remoteReview("r1", "nit"), remoteReview("r2", "why?")];
153
154 expect(importReviews(ctx, issueId, head, undefined, reviews)).toBe(2);
155 expect(importReviews(ctx, issueId, head, undefined, reviews)).toBe(0);
156 expect(count(issueId, "review")).toBe(2);
157
158 const rowId = commentByRemoteId(app.db, issueId, "r1")!;
159 expect(
160 importReviews(ctx, issueId, head, undefined, [remoteReview("r1", "fixed")]),
161 ).toBe(0);
162 expect(commentByRemoteId(app.db, issueId, "r1")).toBe(rowId);
163 expect(bodyOf(rowId)).toBe("fixed");
164 expect(count(issueId, "review")).toBe(2);
165 });
166 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs