diff --git a/convex/skills.publicListCursor.test.ts b/convex/skills.publicListCursor.test.ts index b51c90aa..af0e5f29 100644 --- a/convex/skills.publicListCursor.test.ts +++ b/convex/skills.publicListCursor.test.ts @@ -265,6 +265,43 @@ describe("public skill list deterministic cursors", () => { expect(getPageMock).not.toHaveBeenCalled(); }); + it("includes official publisher status on browse owners", async () => { + const digest = makeSearchDigest({ + ownerPublisherId: "publishers:openclaw", + ownerHandle: "openclaw", + ownerKind: "org", + ownerName: undefined, + ownerDisplayName: "OpenClaw", + }); + getPageMock.mockResolvedValueOnce({ + page: [digest], + hasMore: false, + indexKeys: [[undefined, digest.updatedAt, digest._id]], + }); + + const result = await listPublicPageV4Handler( + { + db: { + query: vi.fn((table: string) => { + if (table !== "officialPublishers") { + throw new Error(`Unexpected table: ${table}`); + } + return { + withIndex: vi.fn(() => ({ + unique: vi.fn().mockResolvedValue({ publisherId: "publishers:openclaw" }), + })), + }; + }), + }, + } as never, + { sort: "updated", numItems: 10 }, + ); + + expect((result.page as Array<{ owner?: { official?: boolean } }>)[0]?.owner?.official).toBe( + true, + ); + }); + it("uses the topic digest index for topic-filtered recommended browse", async () => { const digest = makeSearchDigest({ skillId: "skills:calendar", @@ -1486,6 +1523,13 @@ describe("skills.listRelatedByCategory", () => { return { order }; }); const query = vi.fn((table: string) => { + if (table === "officialPublishers") { + return { + withIndex: vi.fn(() => ({ + unique: vi.fn().mockResolvedValue(null), + })), + }; + } if (table !== "skillSearchDigest") throw new Error(`Unexpected query table: ${table}`); return { withIndex }; }); @@ -1555,6 +1599,13 @@ describe("skills.listRelatedByCategory", () => { return { order }; }); const query = vi.fn((table: string) => { + if (table === "officialPublishers") { + return { + withIndex: vi.fn(() => ({ + unique: vi.fn().mockResolvedValue(null), + })), + }; + } if (table !== "skillSearchDigest") throw new Error(`Unexpected query table: ${table}`); return { withIndex }; }); @@ -1584,7 +1635,16 @@ describe("skills.listRelatedByCategory", () => { builder({ eq }); return { order }; }); - const query = vi.fn(() => ({ withIndex })); + const query = vi.fn((table: string) => { + if (table === "officialPublishers") { + return { + withIndex: vi.fn(() => ({ + unique: vi.fn().mockResolvedValue(null), + })), + }; + } + return { withIndex }; + }); const result = await listRelatedByCategoryHandler({ db: { query } } as never, { skillId: "skills:current", diff --git a/convex/skills.ts b/convex/skills.ts index 0186239e..2f94e802 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -79,6 +79,7 @@ import { summarizeReasonCodes, verdictFromCodes, } from "./lib/moderationReasonCodes"; +import { hasOfficialPublisherRow, toPublicPublisherWithOfficial } from "./lib/officialPublishers"; import { type HydratableSkill, type PublicPublisher, @@ -2008,14 +2009,15 @@ async function buildPublicSkillEntries( ownerPublisherId, ownerUserId, }).then((ownerDoc) => { - const publicOwner = toPublicPublisher(ownerDoc); - if (!publicOwner) { - return { ownerHandle: null, owner: null }; - } - return { - ownerHandle: publicOwner.handle ?? String(publicOwner._id), - owner: publicOwner, - }; + return toPublicPublisherWithOfficial(ctx, ownerDoc).then((publicOwner) => { + if (!publicOwner) { + return { ownerHandle: null, owner: null }; + } + return { + ownerHandle: publicOwner.handle ?? String(publicOwner._id), + owner: publicOwner, + }; + }); }); ownerInfoCache.set(cacheKey, ownerPromise); return ownerPromise; @@ -5106,7 +5108,11 @@ export const listPublicPageV3 = query({ if (!hydratable) continue; const publicSkill = toPublicSkill(hydratable); if (!publicSkill) continue; - const ownerInfo = digestToOwnerInfo(digest); + const ownerInfo = await addOfficialStatusToOwnerInfo( + ctx, + digestToOwnerInfo(digest), + digest.ownerPublisherId, + ); if (!ownerInfo?.owner) continue; const latestVersion = await resolveDigestLatestVersionForSkill(ctx, digest); items.push({ @@ -5937,7 +5943,11 @@ async function buildPublicSkillEntryFromDigest( const hydratable = digestToHydratableSkill(digest); const publicSkill = toPublicSkill(hydratable); if (!publicSkill) return null; - const ownerInfo = digestToOwnerInfo(digest); + const ownerInfo = await addOfficialStatusToOwnerInfo( + ctx, + digestToOwnerInfo(digest), + digest.ownerPublisherId, + ); if (!ownerInfo?.owner) return null; const latestVersion = await resolveDigestLatestVersionForSkill(ctx, digest); return { @@ -5948,6 +5958,16 @@ async function buildPublicSkillEntryFromDigest( }; } +async function addOfficialStatusToOwnerInfo( + ctx: Pick, + ownerInfo: { ownerHandle: string | null; owner: PublicPublisher | null } | null, + publisherId?: Id<"publishers">, +) { + if (!ctx?.db || !ownerInfo?.owner || !publisherId) return ownerInfo; + const official = await hasOfficialPublisherRow(ctx, publisherId); + return official ? { ...ownerInfo, owner: { ...ownerInfo.owner, official: true } } : ownerInfo; +} + async function loadPublicLatestVersionForDigest( ctx: Pick, digest: Pick, "skillId" | "latestVersionId" | "latestVersionSkillId">, diff --git a/convex/skills.versions.public.test.ts b/convex/skills.versions.public.test.ts index 035227cd..7a518cc9 100644 --- a/convex/skills.versions.public.test.ts +++ b/convex/skills.versions.public.test.ts @@ -738,6 +738,13 @@ describe("public skill version queries", () => { const ctx = { db: { query: vi.fn((table: string) => { + if (table === "officialPublishers") { + return { + withIndex: vi.fn(() => ({ + unique: vi.fn().mockResolvedValue(null), + })), + }; + } if (table !== "skillBadges") throw new Error(`Unexpected table ${table}`); return { withIndex: vi.fn(() => ({