import { beforeEach, expect, mock, test } from "bun:test"; import { Database } from "bun:sqlite"; import { applyCombinedSchema } from "./test-schema.test-helper"; const db = new Database(":memory:"); applyCombinedSchema(db); type Statement = { sql: string; params: unknown[] }; const exec = (sql: string, params: unknown[]) => db.query(sql).run(...(params as never[])); mock.module("./db", () => ({ first: async (sql: string, ...params: unknown[]) => db.query(sql).get(...(params as never[])) ?? null, all: async (sql: string, ...params: unknown[]) => db.query(sql).all(...(params as never[])), run: async (sql: string, ...params: unknown[]) => ({ meta: { changes: exec(sql, params).changes }, }), batch: async (statements: Statement[]) => statements.map((statement) => exec(statement.sql, statement.params)), stmt: (sql: string, ...params: unknown[]): Statement => ({ sql, params }), })); // Every repo and account object shares the one test database (it still has every table), so seeds stay in one place. const scoped = { first: async (sql: string, ...params: unknown[]) => db.query(sql).get(...(params as never[])) ?? null, all: async (sql: string, ...params: unknown[]) => db.query(sql).all(...(params as never[])), run: async (sql: string, ...params: unknown[]) => ({ meta: { changes: db.query(sql).run(...(params as never[])).changes, last_row_id: 0, }, }), batch: async (statements: Array<{ sql: string; params: unknown[] }>) => statements.map((statement) => ({ meta: { changes: db.query(statement.sql).run(...(statement.params as never[])).changes, last_row_id: 0, }, })), }; mock.module("./scoped-db", () => ({ repoDb: (scopeId: string) => ({ scopeId, ...scoped }), accountDb: (scopeId: string) => ({ scopeId, ...scoped }), stmt: (sql: string, ...params: unknown[]) => ({ sql, params }), })); mock.module("./admin-audit", () => ({ recordAdminAction: async () => undefined, })); mock.module("./events/emit", () => ({ publicUrl: (path: string) => path })); mock.module("./user-inbox", () => ({ invalidateUnread: async () => undefined, })); mock.module("./access-revocation", () => ({ dropMcpAccess: async () => undefined, })); mock.module("./repo-state", () => ({ issuesChanged: () => undefined })); const { deleteDiscussionComment, deleteIssueComment } = await import("./comment-removal"); const { setModerationHidden } = await import("./moderation"); const issueCount = () => (db.query("SELECT comments_count AS n FROM issues").get() as { n: number }).n; const discussionCount = () => ( db.query("SELECT comments_count AS n FROM discussions").get() as { n: number; } ).n; beforeEach(() => { db.exec(` DELETE FROM discussion_comments; DELETE FROM discussions; DELETE FROM discussion_categories; DELETE FROM scope_locators; DELETE FROM review_comments; DELETE FROM comments; DELETE FROM issues; DELETE FROM repos; DELETE FROM accounts; INSERT INTO accounts (id, name, kind, created_at) VALUES ('u', 'u', 'user', 0); INSERT INTO repos (id, owner_id, name, artifacts_name, artifacts_remote, visibility, created_at, updated_at) VALUES ('r', 'u', 'r', 'a', 'x', 'public', 0, 0); INSERT INTO issues (id, repo_id, number, author_id, title, state, comments_count, created_at, updated_at) VALUES ('i', 'r', 1, 'u', 't', 'open', 2, 0, 0); INSERT INTO comments (id, issue_id, author_id, body, created_at, updated_at) VALUES ('c1', 'i', 'u', 'a', 0, 0), ('c2', 'i', 'u', 'b', 0, 0); INSERT INTO discussion_categories (id, repo_id, name) VALUES ('cat', 'r', 'General'); INSERT INTO discussions (id, repo_id, number, category_id, author_id, title, comments_count, created_at, updated_at) VALUES ('d', 'r', 2, 'cat', 'u', 't', 3, 0, 0); INSERT INTO discussion_comments (id, discussion_id, parent_id, author_id, body, created_at, updated_at) VALUES ('top', 'd', NULL, 'u', 'a', 0, 0), ('r1', 'd', 'top', 'u', 'b', 0, 0), ('r2', 'd', 'top', 'u', 'c', 0, 0); INSERT INTO scope_locators (kind, key, scope_id) VALUES ('issue', 'i', 'r'), ('comment', 'c1', 'r'), ('comment', 'c2', 'r'), ('discussion', 'd', 'r'), ('discussion_comment', 'top', 'r'), ('discussion_comment', 'r1', 'r'), ('discussion_comment', 'r2', 'r'), ('review_comment', 'rc', 'r'); `); }); test("deleting a hidden issue comment does not take it off the count twice", async () => { await setModerationHidden("comment", "c1", true, "admin"); expect(issueCount()).toBe(1); await deleteIssueComment({ id: "c1", issue_id: "i" }); expect(issueCount()).toBe(1); await deleteIssueComment({ id: "c2", issue_id: "i" }); expect(issueCount()).toBe(0); }); test("deleting a discussion thread counts only its visible comments", async () => { await setModerationHidden("discussion_comment", "r1", true, "admin"); expect(discussionCount()).toBe(2); await deleteDiscussionComment({ id: "top", discussion_id: "d" }); expect(discussionCount()).toBe(0); }); test("review comments carry no count, so hiding and deleting them can't skew one", async () => { db.exec(`INSERT INTO review_comments (id, issue_id, author_id, path, line, side, commit_sha, body, created_at, updated_at) VALUES ('rc', 'i', 'u', 'a.ts', 1, 'RIGHT', 's', 'x', 0, 0)`); await setModerationHidden("review_comment", "rc", true, "admin"); expect(issueCount()).toBe(2); });