From 76b5a2bfce249736e557a4a016f05dd604781710 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Fri, 26 Jun 2026 16:24:07 -0700 Subject: [PATCH] fix: export latest security dataset skills only --- convex/securityDataset.test.ts | 115 +++++++++++++++++++++++++++++++++ convex/securityDataset.ts | 13 ++-- 2 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 convex/securityDataset.test.ts diff --git a/convex/securityDataset.test.ts b/convex/securityDataset.test.ts new file mode 100644 index 00000000..92777aa2 --- /dev/null +++ b/convex/securityDataset.test.ts @@ -0,0 +1,115 @@ +/* @vitest-environment node */ +import { describe, expect, it, vi } from "vitest"; +import { skillVersionPageToLatestExportRows } from "./securityDataset"; + +describe("security dataset export", () => { + it("exports only each skill's latest active version", async () => { + const get = vi.fn(async (id: string) => { + if (id === "skills:demo") { + return makeSkill({ + id, + latestVersionId: "skillVersions:latest", + moderationSourceVersionId: "skillVersions:old", + }); + } + if (id === "publishers:owner") { + return { + _id: id, + handle: "owner", + deletedAt: undefined, + deactivatedAt: undefined, + }; + } + throw new Error(`unexpected db.get(${id})`); + }); + + const rows = await skillVersionPageToLatestExportRows( + { db: { get } } as never, + [ + makeVersion({ id: "skillVersions:old", version: "1.0.0" }), + makeVersion({ id: "skillVersions:latest", version: "2.0.0" }), + ] as never, + ); + + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + sourceDocId: "skillVersions:latest", + parentDocId: "skills:demo", + publicOwnerHandle: "owner", + publicSlug: "demo", + version: "2.0.0", + moderationConsensus: null, + }); + }); + + it("skips skills without an active latest version", async () => { + const get = vi.fn(async (id: string) => { + if (id === "skills:missing-latest") { + return makeSkill({ id, latestVersionId: undefined }); + } + if (id === "skills:other-latest") { + return makeSkill({ id, latestVersionId: "skillVersions:other" }); + } + if (id === "skills:deleted") { + return makeSkill({ id, latestVersionId: "skillVersions:deleted", softDeletedAt: 123 }); + } + return null; + }); + + const rows = await skillVersionPageToLatestExportRows( + { db: { get } } as never, + [ + makeVersion({ id: "skillVersions:missing-latest", skillId: "skills:missing-latest" }), + makeVersion({ id: "skillVersions:not-latest", skillId: "skills:other-latest" }), + makeVersion({ id: "skillVersions:deleted", skillId: "skills:deleted" }), + makeVersion({ id: "skillVersions:missing-skill", skillId: "skills:missing" }), + ] as never, + ); + + expect(rows).toEqual([]); + }); +}); + +function makeSkill(input: { + id: string; + latestVersionId: string | undefined; + moderationSourceVersionId?: string; + softDeletedAt?: number; +}) { + return { + _id: input.id, + slug: "demo", + displayName: "Demo", + ownerPublisherId: "publishers:owner", + ownerUserId: "users:owner", + latestVersionId: input.latestVersionId, + moderationSourceVersionId: input.moderationSourceVersionId, + moderationVerdict: "clean", + moderationReasonCodes: [], + moderationSummary: "Clean", + moderationEngineVersion: "test", + moderationEvaluatedAt: 1, + softDeletedAt: input.softDeletedAt, + }; +} + +function makeVersion(input: { + id: string; + version?: string; + softDeletedAt?: number; + skillId?: string; +}) { + return { + _id: input.id, + skillId: input.skillId ?? "skills:demo", + version: input.version ?? "1.0.0", + createdAt: 2, + softDeletedAt: input.softDeletedAt, + sha256hash: "a".repeat(64), + files: [], + vtAnalysis: undefined, + skillSpectorAnalysis: undefined, + staticScan: undefined, + llmAnalysis: undefined, + }; +} diff --git a/convex/securityDataset.ts b/convex/securityDataset.ts index fd2bde3e..a593ef82 100644 --- a/convex/securityDataset.ts +++ b/convex/securityDataset.ts @@ -25,7 +25,7 @@ type StoredVtAnalysis = Doc<"skillVersions">["vtAnalysis"]; type StoredSkillSpectorAnalysis = Doc<"skillVersions">["skillSpectorAnalysis"]; type StoredLlmAnalysis = Doc<"skillVersions">["llmAnalysis"]; type ArtifactExportRow = - | Awaited>[number] + | Awaited>[number] | Awaited>[number]; type ArtifactExportPage = { page: ArtifactExportRow[]; @@ -74,7 +74,7 @@ export const listArtifactExportPageInternal = internalQuery({ .order("asc") .paginate(paginationOpts); return { - page: await skillVersionPageToExportRows(ctx, page.page), + page: await skillVersionPageToLatestExportRows(ctx, page.page), isDone: page.isDone, continueCursor: page.continueCursor, exportMode: args.mode ?? "public", @@ -212,11 +212,14 @@ async function getActiveCreatedBounds(ctx: QueryCtx, sourceKind: "skill" | "pack }; } -async function skillVersionPageToExportRows(ctx: QueryCtx, versions: Array>) { +export async function skillVersionPageToLatestExportRows( + ctx: Pick, + versions: Array>, +) { const rows = []; for (const version of versions) { const skill = await ctx.db.get(version.skillId); - if (!skill || skill.softDeletedAt) continue; + if (!skill || skill.softDeletedAt || skill.latestVersionId !== version._id) continue; const publicOwnerHandle = await getPublicOwnerHandle(ctx, skill); rows.push({ sourceKind: "skill" as const, @@ -462,7 +465,7 @@ function normalizeLlmAnalysis(analysis: StoredLlmAnalysis) { } async function getPublicOwnerHandle( - ctx: QueryCtx, + ctx: Pick, source: Pick | Doc<"packages">, "ownerPublisherId" | "ownerUserId">, ) { const owner = await getOwnerPublisher(ctx, {