返回 DeepSeek-Reasonix
packages.test.ts
根目录 / workers / crash-report / src / registry / db / packages.test.ts
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
375 lines TYPESCRIPT