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

A test pins variant votes: a dislike closes the pull with one comment and one ledger row, and a refused vote leaves nothing behind #6839

Merged opened this

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

Merged

scout/forge:scout/6823-a-test-pins-variant-votes-a-dislike-clos → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6823-a-test-pins-variant-votes-a-dislike-clos was merged into main as e6796a7.

Review

Sign in to review this change.
1 file changed +159 −0
test/variant-votes.test.ts added +159
@@ -0,0 +1,159 @@
1 /**
2 * `voteVariant` on its own: a writer's dislike closes the pull with one
3 * comment and one ledger row; a like leaves it open; a refused vote writes
4 * nothing.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { DEFAULT_REASON, REASON_MAX, voteVariant } from "../src/actions/variants.ts";
11 import { openPull } from "../src/actions/pulls.ts";
12 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
13 import { createApp, type App } from "../src/app.ts";
14 import { listDecisions } from "../src/core/decisions.ts";
15 import { listComments } from "../src/core/issues.ts";
16 import { getPull } from "../src/core/pulls.ts";
17 import type { Repo } from "../src/core/repos.ts";
18 import { createUser, type User } from "../src/core/users.ts";
19 import { quietLogs } from "./helpers/globals.ts";
20
21 quietLogs();
22
23 describe("voteVariant", () => {
24 let dataDir: string;
25 let app: App;
26 let alice: User;
27 let bob: User;
28 let lib: Repo;
29 let n = 0;
30
31 const variant = async (variantGroup: string | null = "g") => {
32 const branch = `b${++n}`;
33 const c = await commitToBranch(app, alice, lib, {
34 branch,
35 message: branch,
36 files: [{ path: `${branch}.md`, content: "x\n" }],
37 });
38 if (!c.ok) throw new Error(JSON.stringify(c.error));
39 const o = await openPull(app, alice, lib, {
40 title: `Try ${branch}`,
41 baseBranch: "main",
42 headBranch: branch,
43 ...(variantGroup === null ? {} : { variantGroup }),
44 });
45 if (!o.ok) throw new Error(String(o.error));
46 return o.value;
47 };
48 const fresh = (num: number) => getPull(app.db, lib.id, num)!;
49 const ledger = (id: string) =>
50 listDecisions(app.db, lib.id, { kind: "lean" }).filter((r) => r.issueId === id);
51 const comments = (id: string) => listComments(app.db, id).length;
52
53 beforeAll(async () => {
54 dataDir = await mkdtemp(join(tmpdir(), "forge-vvotes-"));
55 app = createApp({ dataDir, port: 0 });
56 const make = async (username: string, first: boolean) => {
57 const u = await createUser(app.db, {
58 origin: "seed",
59 trusted: true,
60 username,
61 email: `${username}@forge.local`,
62 password: "a-good-passphrase",
63 adminIfFirst: first,
64 });
65 if (!u.ok) throw new Error(u.error);
66 return u.value;
67 };
68 alice = await make("alice", true);
69 bob = await make("bob", false);
70 const made = await createRepo(app, alice, { name: "lib", visibility: "public" });
71 if (!made.ok) throw new Error(String(made.error));
72 lib = made.value;
73 const seed = await commitToBranch(app, alice, lib, {
74 message: "seed",
75 files: [{ path: "README.md", content: "# lib\n" }],
76 });
77 if (!seed.ok) throw new Error(JSON.stringify(seed.error));
78 }, 60_000);
79
80 afterAll(async () => {
81 app.close();
82 await rm(dataDir, { recursive: true, force: true });
83 });
84
85 test("a writer's dislike closes the pull with one comment and one vote", async () => {
86 const pull = await variant();
87 const before = comments(pull.id);
88 const r = voteVariant(app, alice, lib, pull, "dislike", "too noisy\n");
89 expect(r).toEqual({ ok: true, value: { closed: true } });
90 expect(fresh(pull.number).state).toBe("closed");
91 const added = listComments(app.db, pull.id).slice(before);
92 expect(added).toHaveLength(1);
93 expect(added[0]!.body.startsWith("Closed: not chosen among its variants.")).toBe(true);
94 expect(added[0]!.body).toContain("too noisy");
95 const rows = ledger(pull.id);
96 expect(rows).toHaveLength(1);
97 expect(rows[0]!.answer).toBe("dislike");
98 expect((rows[0]!.state as any).reason).toBe("too noisy");
99 });
100
101 test("an empty reason is the default; a long one is cut to REASON_MAX", async () => {
102 const a = await variant();
103 voteVariant(app, alice, lib, a, "dislike", " ");
104 expect((ledger(a.id)[0]!.state as any).reason).toBe(DEFAULT_REASON);
105 expect(DEFAULT_REASON).toBe("Not this one.");
106
107 const b = await variant();
108 voteVariant(app, alice, lib, b, "dislike", "x".repeat(500));
109 expect((ledger(b.id)[0]!.state as any).reason).toBe("x".repeat(REASON_MAX));
110 });
111
112 test("a like records a vote and leaves the pull open", async () => {
113 const pull = await variant();
114 const before = comments(pull.id);
115 expect(voteVariant(app, alice, lib, pull, "like")).toEqual({
116 ok: true,
117 value: { closed: false },
118 });
119 expect(fresh(pull.number).state).toBe("open");
120 expect(comments(pull.id)).toBe(before);
121 const rows = ledger(pull.id);
122 expect(rows).toHaveLength(1);
123 expect(rows[0]!.answer).toBe("like");
124 });
125
126 test("a refused vote leaves no comment and no ledger row", async () => {
127 const pull = await variant();
128 const plain = await variant(null);
129 const before = comments(pull.id);
130 const plainBefore = comments(plain.id);
131
132 const guest = voteVariant(app, undefined, lib, pull, "dislike", "no");
133 expect(guest).toEqual({ ok: false, error: "unauthorized" });
134 const reader = voteVariant(app, bob, lib, pull, "dislike", "no");
135 expect(reader).toEqual({ ok: false, error: "forbidden" });

Nice: the refusals check the comment count, ledger and pull state for both pulls, not just the error code.

136 const solo = voteVariant(app, alice, lib, plain, "dislike", "no");
137 expect(solo).toEqual({ ok: false, error: "not_variant" });
138
139 expect(comments(pull.id)).toBe(before);
140 expect(comments(plain.id)).toBe(plainBefore);
141 expect(ledger(pull.id)).toHaveLength(0);
142 expect(ledger(plain.id)).toHaveLength(0);
143 expect(fresh(pull.number).state).toBe("open");
144 expect(fresh(plain.number).state).toBe("open");
145 });
146
147 test("a second dislike on the closed pull is not_open and adds nothing", async () => {
148 const pull = await variant();
149 expect(voteVariant(app, alice, lib, pull, "dislike", "first").ok).toBe(true);
150 const closed = fresh(pull.number);
151 const before = comments(pull.id);
152 expect(voteVariant(app, alice, lib, closed, "dislike", "again")).toEqual({
153 ok: false,
154 error: "not_open",
155 });
156 expect(comments(pull.id)).toBe(before);
157 expect(ledger(pull.id)).toHaveLength(1);
158 });
159 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs