| 1 | import { describe, expect, it } from "vitest"; |
| 2 | // @ts-expect-error Node types are intentionally not part of the Worker build. |
| 3 | import { DatabaseSync } from "node:sqlite"; |
| 4 | import type { PackageRow, RegistryUser } from "../types"; |
| 5 | import { PublishSchema } from "../lib/validation"; |
| 6 | import { PackageRepo } from "./packages"; |
| 7 | import registrySchema from "../../../registry-schema.sql?raw"; |
| 8 | |
| 9 | const now = "2026-07-22T00:00:00.000Z"; |
| 10 | const user: RegistryUser = { |
| 11 | id: 7, |
| 12 | handle: "publisher", |
| 13 | role: "member", |
| 14 | emailVerified: true, |
| 15 | }; |
| 16 | |
| 17 | const existing: PackageRow = { |
| 18 | id: 42, |
| 19 | kind: "mcp", |
| 20 | scope_handle: "publisher", |
| 21 | name: "devkit", |
| 22 | slug: "publisher/devkit", |
| 23 | summary: "old", |
| 24 | description: "", |
| 25 | source: "https://github.com/o/r", |
| 26 | install_kind: "auto", |
| 27 | homepage: "", |
| 28 | repo_url: "https://github.com/o/r", |
| 29 | tags: "tool", |
| 30 | latest_version: "2.7.0", |
| 31 | status: "pending", |
| 32 | verified: 0, |
| 33 | publisher_id: 7, |
| 34 | install_count: 0, |
| 35 | star_count: 0, |
| 36 | created_at: now, |
| 37 | updated_at: now, |
| 38 | }; |
| 39 | |
| 40 | function fakePackageDB(reads: PackageRow[]) { |
| 41 | const updates: { sql: string; values: unknown[] }[] = []; |
| 42 | let packageReads = 0; |
| 43 | const db = { |
| 44 | prepare(sql: string) { |
| 45 | let values: unknown[] = []; |
| 46 | const statement = { |
| 47 | bind(...bound: unknown[]) { |
| 48 | values = bound; |
| 49 | return statement; |
| 50 | }, |
| 51 | async first<T>() { |
| 52 | if (sql.startsWith("SELECT * FROM packages")) { |
| 53 | const row = reads[Math.min(packageReads, reads.length - 1)]; |
| 54 | packageReads += 1; |
| 55 | return row as T; |
| 56 | } |
| 57 | return null; |
| 58 | }, |
| 59 | async run() { |
| 60 | if (sql.startsWith("UPDATE packages SET")) updates.push({ sql, values }); |
| 61 | return { meta: { changes: 1 } }; |
| 62 | }, |
| 63 | }; |
| 64 | return statement; |
| 65 | }, |
| 66 | }; |
| 67 | return { db: db as unknown as D1Database, updates }; |
| 68 | } |
| 69 | |
| 70 | function pluginInput() { |
| 71 | return PublishSchema.parse({ |
| 72 | kind: "plugin", |
| 73 | installKind: "plugin", |
| 74 | name: "devkit", |
| 75 | source: "https://github.com/o/r", |
| 76 | repoUrl: "https://github.com/o/r", |
| 77 | version: "2.7.1", |
| 78 | }); |
| 79 | } |
| 80 | |
| 81 | describe("PackageRepo.publish", () => { |
| 82 | it("persists a kind change when an owned pending package is republished as a plugin", async () => { |
| 83 | const updated: PackageRow = { ...existing, kind: "plugin", install_kind: "plugin", latest_version: "2.7.1" }; |
| 84 | const { db, updates } = fakePackageDB([existing, updated]); |
| 85 | const result = await new PackageRepo(db).publish(user, pluginInput(), now); |
| 86 | |
| 87 | expect(result.created).toBe(false); |
| 88 | expect(result.row.kind).toBe("plugin"); |
| 89 | expect(updates).toHaveLength(1); |
| 90 | expect(updates[0].sql).toContain("SET kind = ?1"); |
| 91 | expect(updates[0].values[0]).toBe("plugin"); |
| 92 | expect(updates[0].values[4]).toBe("plugin"); |
| 93 | expect(updates[0].values[10]).toBe("pending"); |
| 94 | expect(updates[0].values[11]).toBe(0); |
| 95 | expect(updates[0].values[12]).toBe(existing.id); |
| 96 | }); |
| 97 | |
| 98 | it("returns an active verified package to review when its kind changes", async () => { |
| 99 | const active: PackageRow = { ...existing, status: "active", verified: 1 }; |
| 100 | const requeued: PackageRow = { |
| 101 | ...active, |
| 102 | kind: "plugin", |
| 103 | install_kind: "plugin", |
| 104 | latest_version: "2.7.1", |
| 105 | status: "pending", |
| 106 | verified: 0, |
| 107 | }; |
| 108 | const { db, updates } = fakePackageDB([active, requeued]); |
| 109 | |
| 110 | const result = await new PackageRepo(db).publish(user, pluginInput(), now); |
| 111 | |
| 112 | expect(result.row.status).toBe("pending"); |
| 113 | expect(result.row.verified).toBe(0); |
| 114 | expect(updates[0].values[10]).toBe("pending"); |
| 115 | expect(updates[0].values[11]).toBe(0); |
| 116 | }); |
| 117 | |
| 118 | it("returns a same-kind active package update to review", async () => { |
| 119 | const active: PackageRow = { ...existing, status: "active", verified: 1 }; |
| 120 | const updated: PackageRow = { |
| 121 | ...active, |
| 122 | summary: "new summary", |
| 123 | source: "https://github.com/o/r2", |
| 124 | repo_url: "https://github.com/o/r2", |
| 125 | install_kind: "mcp", |
| 126 | latest_version: "2.7.1", |
| 127 | status: "pending", |
| 128 | verified: 0, |
| 129 | }; |
| 130 | const { db, updates } = fakePackageDB([active, updated]); |
| 131 | const input = PublishSchema.parse({ |
| 132 | kind: "mcp", |
| 133 | name: "devkit", |
| 134 | summary: "new summary", |
| 135 | source: "https://github.com/o/r2", |
| 136 | repoUrl: "https://github.com/o/r2", |
| 137 | version: "2.7.1", |
| 138 | }); |
| 139 | |
| 140 | const result = await new PackageRepo(db).publish(user, input, now); |
| 141 | |
| 142 | expect(result.row.status).toBe("pending"); |
| 143 | expect(result.row.verified).toBe(0); |
| 144 | expect(updates[0].values[4]).toBe("mcp"); |
| 145 | expect(updates[0].values[10]).toBe("pending"); |
| 146 | expect(updates[0].values[11]).toBe(0); |
| 147 | }); |
| 148 | |
| 149 | it("returns a hidden package update to review and clears verification", async () => { |
| 150 | const hidden: PackageRow = { ...existing, status: "hidden", verified: 1 }; |
| 151 | const updated: PackageRow = { |
| 152 | ...hidden, |
| 153 | kind: "plugin", |
| 154 | install_kind: "plugin", |
| 155 | latest_version: "2.7.1", |
| 156 | status: "pending", |
| 157 | verified: 0, |
| 158 | }; |
| 159 | const { db, updates } = fakePackageDB([hidden, updated]); |
| 160 | |
| 161 | const result = await new PackageRepo(db).publish(user, pluginInput(), now); |
| 162 | |
| 163 | expect(result.row.status).toBe("pending"); |
| 164 | expect(result.row.verified).toBe(0); |
| 165 | expect(updates[0].values[10]).toBe("pending"); |
| 166 | expect(updates[0].values[11]).toBe(0); |
| 167 | }); |
| 168 | |
| 169 | it("returns a rejected package update to review", async () => { |
| 170 | const rejected: PackageRow = { ...existing, status: "rejected", verified: 0 }; |
| 171 | const requeued: PackageRow = { ...rejected, latest_version: "2.7.1", status: "pending" }; |
| 172 | const { db, updates } = fakePackageDB([rejected, requeued]); |
| 173 | const input = PublishSchema.parse({ |
| 174 | kind: "mcp", |
| 175 | name: "devkit", |
| 176 | source: "https://github.com/o/r", |
| 177 | repoUrl: "https://github.com/o/r", |
| 178 | version: "2.7.1", |
| 179 | }); |
| 180 | |
| 181 | const result = await new PackageRepo(db).publish(user, input, now); |
| 182 | |
| 183 | expect(result.row.status).toBe("pending"); |
| 184 | expect(updates[0].values[10]).toBe("pending"); |
| 185 | expect(updates[0].values[11]).toBe(0); |
| 186 | }); |
| 187 | |
| 188 | it("preserves status and verification for trusted admin updates", async () => { |
| 189 | const admin: RegistryUser = { ...user, role: "admin" }; |
| 190 | const active: PackageRow = { ...existing, status: "active", verified: 1 }; |
| 191 | const updated: PackageRow = { ...active, install_kind: "mcp", latest_version: "2.7.1" }; |
| 192 | const { db, updates } = fakePackageDB([active, updated]); |
| 193 | const input = PublishSchema.parse({ |
| 194 | kind: "mcp", |
| 195 | name: "devkit", |
| 196 | source: "https://github.com/o/r", |
| 197 | repoUrl: "https://github.com/o/r", |
| 198 | version: "2.7.1", |
| 199 | }); |
| 200 | |
| 201 | const result = await new PackageRepo(db).publish(admin, input, now); |
| 202 | |
| 203 | expect(result.row.status).toBe("active"); |
| 204 | expect(result.row.verified).toBe(1); |
| 205 | expect(updates[0].values[10]).toBe("active"); |
| 206 | expect(updates[0].values[11]).toBe(1); |
| 207 | }); |
| 208 | }); |
| 209 | |
| 210 | describe("PackageRepo.setStatusIfCurrent", () => { |
| 211 | it("approves only the exact package revision the admin reviewed", async () => { |
| 212 | const approvedAt = "2026-07-22T01:00:00.000Z"; |
| 213 | const approved: PackageRow = { ...existing, status: "active", updated_at: approvedAt }; |
| 214 | const statements: { sql: string; values: unknown[] }[] = []; |
| 215 | const db = { |
| 216 | prepare(sql: string) { |
| 217 | let values: unknown[] = []; |
| 218 | const statement = { |
| 219 | bind(...bound: unknown[]) { |
| 220 | values = bound; |
| 221 | return statement; |
| 222 | }, |
| 223 | async first<T>() { |
| 224 | statements.push({ sql, values }); |
| 225 | return approved as T; |
| 226 | }, |
| 227 | }; |
| 228 | return statement; |
| 229 | }, |
| 230 | } as unknown as D1Database; |
| 231 | |
| 232 | const row = await new PackageRepo(db).setStatusIfCurrent( |
| 233 | existing.slug, |
| 234 | "active", |
| 235 | existing.latest_version, |
| 236 | existing.updated_at, |
| 237 | existing.status, |
| 238 | approvedAt, |
| 239 | ); |
| 240 | |
| 241 | expect(row).toEqual(approved); |
| 242 | expect(statements[0].sql).toContain("latest_version = ?4 AND updated_at = ?5 AND status = ?6"); |
| 243 | expect(statements[0].sql).toContain("RETURNING *"); |
| 244 | expect(statements[0].values).toEqual([ |
| 245 | "active", |
| 246 | approvedAt, |
| 247 | existing.slug, |
| 248 | existing.latest_version, |
| 249 | existing.updated_at, |
| 250 | existing.status, |
| 251 | ]); |
| 252 | }); |
| 253 | |
| 254 | it("returns null when a newer package revision no longer matches", async () => { |
| 255 | const statements: { sql: string; values: unknown[] }[] = []; |
| 256 | const db = { |
| 257 | prepare(sql: string) { |
| 258 | let values: unknown[] = []; |
| 259 | const statement = { |
| 260 | bind(...bound: unknown[]) { |
| 261 | values = bound; |
| 262 | return statement; |
| 263 | }, |
| 264 | async first<T>() { |
| 265 | statements.push({ sql, values }); |
| 266 | return null as T | null; |
| 267 | }, |
| 268 | }; |
| 269 | return statement; |
| 270 | }, |
| 271 | } as unknown as D1Database; |
| 272 | |
| 273 | const row = await new PackageRepo(db).setStatusIfCurrent( |
| 274 | existing.slug, |
| 275 | "active", |
| 276 | existing.latest_version, |
| 277 | existing.updated_at, |
| 278 | existing.status, |
| 279 | "2026-07-22T01:00:00.000Z", |
| 280 | ); |
| 281 | |
| 282 | expect(row).toBeNull(); |
| 283 | expect(statements).toHaveLength(1); |
| 284 | }); |
| 285 | }); |
| 286 | |
| 287 | describe("PackageRepo.versions", () => { |
| 288 | it("returns a bounded page and a stable cursor for older versions", async () => { |
| 289 | let sql = ""; |
| 290 | const rows = [ |
| 291 | { id: 3, version: "0.3.0", source: "s3", content_hash: "h3", risk_level: "", created_at: "2026-07-24T00:00:00.000Z" }, |
| 292 | { id: 2, version: "0.2.0", source: "s2", content_hash: "h2", risk_level: "", created_at: "2026-07-23T00:00:00.000Z" }, |
| 293 | { id: 1, version: "0.1.0", source: "s1", content_hash: "h1", risk_level: "", created_at: "2026-07-22T00:00:00.000Z" }, |
| 294 | ]; |
| 295 | const db = { |
| 296 | prepare(query: string) { |
| 297 | sql = query; |
| 298 | const statement = { |
| 299 | bind() { return statement; }, |
| 300 | async all<T>() { return { results: rows as T[] }; }, |
| 301 | }; |
| 302 | return statement; |
| 303 | }, |
| 304 | } as unknown as D1Database; |
| 305 | const result = await new PackageRepo(db).versions(7, { limit: 2, before: "2026-07-25T00:00:00.000Z", beforeId: 4 }); |
| 306 | expect(result.versions).toHaveLength(2); |
| 307 | expect(result.pageInfo).toEqual({ limit: 2, hasMore: true, nextBefore: rows[1].created_at, nextBeforeId: rows[1].id }); |
| 308 | expect(sql).toContain("created_at < ?2"); |
| 309 | expect(sql).toContain("ORDER BY created_at DESC, id DESC LIMIT ?4"); |
| 310 | }); |
| 311 | }); |
| 312 | |
| 313 | describe("PackageRepo.list", () => { |
| 314 | it("uses the daily install rollup for trending", async () => { |
| 315 | let sql = ""; |
| 316 | const db = { |
| 317 | prepare(query: string) { |
| 318 | sql = query; |
| 319 | const statement = { |
| 320 | bind() { return statement; }, |
| 321 | async all() { return { results: [] }; }, |
| 322 | }; |
| 323 | return statement; |
| 324 | }, |
| 325 | } as unknown as D1Database; |
| 326 | await new PackageRepo(db).list({ kind: "all", q: "", sort: "trending", limit: 24, offset: 0, now }); |
| 327 | expect(sql).toContain("FROM package_install_daily"); |
| 328 | expect(sql).not.toContain("FROM events"); |
| 329 | expect(sql).toContain("SUM(count)"); |
| 330 | }); |
| 331 | }); |
| 332 | |
| 333 | describe("PackageRepo.recordInstall", () => { |
| 334 | it("increments the package and a daily rollup without writing a raw install event", async () => { |
| 335 | const sqlite = new DatabaseSync(":memory:"); |
| 336 | sqlite.exec(registrySchema); |
| 337 | sqlite.prepare( |
| 338 | `INSERT INTO packages (kind, scope_handle, name, slug, source, latest_version, status, publisher_id, created_at, updated_at) |
| 339 | VALUES ('skill', 'publisher', 'devkit', 'publisher/devkit', 'https://github.com/o/r', '0.1.0', 'active', 7, ?1, ?1)`, |
| 340 | ).run(now); |
| 341 | const db = { |
| 342 | prepare(sql: string) { |
| 343 | const statement = sqlite.prepare(sql); |
| 344 | const wrapper: any = { |
| 345 | bind(...values: unknown[]) { wrapper.values = values; return wrapper; }, |
| 346 | values: [] as unknown[], |
| 347 | async first() { return statement.get(...wrapper.values); }, |
| 348 | async all() { return { results: statement.all(...wrapper.values) }; }, |
| 349 | async run() { return { meta: { changes: Number(statement.run(...wrapper.values).changes) } }; }, |
| 350 | }; |
| 351 | return wrapper; |
| 352 | }, |
| 353 | async batch(statements: Array<{ values?: unknown[]; first?: () => Promise<unknown>; run?: () => Promise<unknown> }>) { |
| 354 | const results = [] as Array<{ results: unknown[] }>; |
| 355 | for (const statement of statements) { |
| 356 | if (statement.first) results.push({ results: [await statement.first()] }); |
| 357 | else { |
| 358 | await statement.run?.(); |
| 359 | results.push({ results: [] }); |
| 360 | } |
| 361 | } |
| 362 | return results; |
| 363 | }, |
| 364 | } as unknown as D1Database; |
| 365 | try { |
| 366 | const result = await new PackageRepo(db).recordInstall("publisher/devkit", now); |
| 367 | expect(result).toMatchObject({ count: 1, scopeHandle: "publisher" }); |
| 368 | expect(sqlite.prepare("SELECT count FROM package_install_daily").get()).toEqual({ count: 1 }); |
| 369 | expect(sqlite.prepare("SELECT COUNT(*) AS count FROM events WHERE type = 'install'").get()).toEqual({ count: 0 }); |
| 370 | } finally { |
| 371 | sqlite.close(); |
| 372 | } |
| 373 | }); |
| 374 | }); |
| 375 |