diff --git a/convex/catalogFeed.test.ts b/convex/catalogFeed.test.ts index 8a5bad2f..baa950ba 100644 --- a/convex/catalogFeed.test.ts +++ b/convex/catalogFeed.test.ts @@ -132,13 +132,36 @@ function makeFeedSkillEntry(index: number) { }; } -function makeCtx(packages: unknown[], records: Record) { +function makeCtx( + packages: unknown[], + records: Record, + options: { packageHighlighted?: boolean } = {}, +) { return { db: { - query: vi.fn(() => { + query: vi.fn((table: string) => { const query = { eq: vi.fn(() => query), }; + if (table === "packageBadges") { + return { + withIndex: vi.fn((_index: string, apply: (value: typeof query) => unknown) => { + apply(query); + return { + unique: vi.fn(async () => + options.packageHighlighted + ? { + packageId: "packages:1", + kind: "highlighted", + byUserId: "users:moderator", + at: 1, + } + : null, + ), + }; + }), + }; + } return { withIndex: vi.fn((_index: string, apply: (value: typeof query) => unknown) => { apply(query); @@ -181,6 +204,7 @@ describe("catalog feed projection", () => { title: "Demo", version: "1.2.3", state: "available", + featured: false, publisher: { id: "openclaw", trust: "official" }, install: { candidates: [ @@ -196,6 +220,34 @@ describe("catalog feed projection", () => { ]); }); + it("projects highlighted official packages as featured install candidates", async () => { + const result = await listOfficialEntriesHandler( + makeCtx( + [makePackage()], + { + "packageReleases:1": makeRelease(), + }, + { packageHighlighted: true }, + ), + { family: "code-plugin" }, + ); + + expect(result).toEqual([ + expect.objectContaining({ + id: "@openclaw/demo", + state: "available", + featured: true, + install: { + candidates: [ + expect.objectContaining({ + package: "@openclaw/demo", + }), + ], + }, + }), + ]); + }); + it("excludes non-official, blocked, deleted, and undigested releases", async () => { const result = await listOfficialEntriesHandler( makeCtx( @@ -259,6 +311,7 @@ describe("catalog feed projection", () => { title: "Demo skill", version: "1.2.3", state: "available", + featured: false, publisher: { id: "openclaw", trust: "official" }, install: { candidates: [ @@ -276,6 +329,24 @@ describe("catalog feed projection", () => { }); }); + it("projects highlighted official skills as featured install candidates", async () => { + const result = (await listOfficialSkillEntriesHandler( + makeCtx([makeSkill({ badges: { highlighted: { byUserId: "users:moderator", at: 1 } } })], { + "publishers:1": { _id: "publishers:1", kind: "org", handle: "openclaw" }, + "skillVersions:1": makeSkillVersion(), + }), + { publisherId: "publishers:1", cursor: null }, + )) as { entries: unknown[]; isDone: boolean }; + + expect(result.entries).toEqual([ + expect.objectContaining({ + id: "@openclaw/demo", + state: "available", + featured: true, + }), + ]); + }); + it("keeps suspicious hosted skills in hosted ClawHub install candidates", async () => { const result = (await listOfficialSkillEntriesHandler( makeCtx([makeSkill()], { @@ -320,6 +391,7 @@ describe("catalog feed projection", () => { title: "AIQ Deploy", version: "1111111111111111111111111111111111111111", state: "available", + featured: false, publisher: { id: "nvidia", trust: "official" }, install: { candidates: [ diff --git a/convex/catalogFeed.ts b/convex/catalogFeed.ts index ca8703c9..9bd884b5 100644 --- a/convex/catalogFeed.ts +++ b/convex/catalogFeed.ts @@ -15,6 +15,7 @@ import { internal } from "./_generated/api"; import type { Doc } from "./_generated/dataModel"; import { internalAction, internalMutation, internalQuery } from "./_generated/server"; import type { QueryCtx } from "./_generated/server"; +import { isSkillHighlighted } from "./lib/badges"; import { sha256Hex } from "./lib/clawpack"; import { isPublicSkillDoc } from "./lib/globalStats"; import { isOfficialPublisher } from "./lib/officialPublishers"; @@ -61,6 +62,7 @@ const catalogFeedEntryFields = { v.literal("blocked"), v.literal("deprecated"), ), + featured: v.optional(v.boolean()), publisher: v.object({ id: v.string(), trust: v.union(v.literal("official"), v.literal("community")), @@ -117,6 +119,10 @@ async function buildEntry( const title = pkg.displayName.trim() || packageName; const version = release.version.trim(); if (!packageName || !id || !title || !version) return null; + const highlighted = await ctx.db + .query("packageBadges") + .withIndex("by_package_kind", (q) => q.eq("packageId", pkg._id).eq("kind", "highlighted")) + .unique(); return { type: "plugin", @@ -124,6 +130,7 @@ async function buildEntry( title, version, state: "available", + featured: Boolean(highlighted), publisher: { id: publisherId, trust: "official", @@ -225,6 +232,7 @@ async function buildSkillEntry( title, version: commit, state: "available", + featured: isSkillHighlighted(skill), publisher: { id: publisherId, trust: "official", @@ -269,6 +277,7 @@ async function buildSkillEntry( title, version: versionName, state: "available", + featured: isSkillHighlighted(skill), publisher: { id: publisherId, trust: "official", diff --git a/packages/schema/dist/catalogFeed.d.ts b/packages/schema/dist/catalogFeed.d.ts index d270b615..3eb01a34 100644 --- a/packages/schema/dist/catalogFeed.d.ts +++ b/packages/schema/dist/catalogFeed.d.ts @@ -28,6 +28,7 @@ export declare const CatalogFeedPluginEntrySchema: import("arktype/internal/vari title: string; version: string; state: "available" | "blocked" | "deprecated" | "disabled" | "recommended"; + featured?: boolean | undefined; publisher: { id: string; trust: "community" | "official"; @@ -54,6 +55,7 @@ export declare const CatalogFeedSkillEntrySchema: import("arktype/internal/varia title: string; version: string; state: "available" | "blocked" | "deprecated" | "disabled" | "recommended"; + featured?: boolean | undefined; publisher: { id: string; trust: "community" | "official"; @@ -80,6 +82,7 @@ export declare const CatalogFeedEntrySchema: import("arktype/internal/variants/o title: string; version: string; state: "available" | "blocked" | "deprecated" | "disabled" | "recommended"; + featured?: boolean | undefined; publisher: { id: string; trust: "community" | "official"; @@ -104,6 +107,7 @@ export declare const CatalogFeedEntrySchema: import("arktype/internal/variants/o title: string; version: string; state: "available" | "blocked" | "deprecated" | "disabled" | "recommended"; + featured?: boolean | undefined; publisher: { id: string; trust: "community" | "official"; @@ -137,6 +141,7 @@ export declare const CatalogFeedSchema: import("arktype/internal/variants/object title: string; version: string; state: "available" | "blocked" | "deprecated" | "disabled" | "recommended"; + featured?: boolean | undefined; publisher: { id: string; trust: "community" | "official"; @@ -161,6 +166,7 @@ export declare const CatalogFeedSchema: import("arktype/internal/variants/object title: string; version: string; state: "available" | "blocked" | "deprecated" | "disabled" | "recommended"; + featured?: boolean | undefined; publisher: { id: string; trust: "community" | "official"; diff --git a/packages/schema/dist/catalogFeed.js b/packages/schema/dist/catalogFeed.js index 68994407..9300c4a8 100644 --- a/packages/schema/dist/catalogFeed.js +++ b/packages/schema/dist/catalogFeed.js @@ -22,6 +22,8 @@ const CatalogFeedEntryBaseSchema = { title: "string", version: "string", state: CatalogFeedStateSchema, + // Additive v1 metadata: existing hosted-feed consumers ignore unknown entry fields. + featured: "boolean?", publisher: { "+": "reject", id: "string", @@ -89,6 +91,7 @@ export function serializeCatalogFeed(feed) { title: entry.title, version: entry.version, state: entry.state, + ...(entry.featured === undefined ? {} : { featured: entry.featured }), publisher: { id: entry.publisher.id, trust: entry.publisher.trust, diff --git a/packages/schema/dist/catalogFeed.js.map b/packages/schema/dist/catalogFeed.js.map index 4a21d789..ff4aa2a4 100644 --- a/packages/schema/dist/catalogFeed.js.map +++ b/packages/schema/dist/catalogFeed.js.map @@ -1 +1 @@ -{"version":3,"file":"catalogFeed.js","sourceRoot":"","sources":["../src/catalogFeed.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"catalogFeed.js","sourceRoot":"","sources":["../src/catalogFeed.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/src/catalogFeed.test.ts b/packages/schema/src/catalogFeed.test.ts index 08b97423..44d60e3c 100644 --- a/packages/schema/src/catalogFeed.test.ts +++ b/packages/schema/src/catalogFeed.test.ts @@ -146,6 +146,22 @@ describe("catalog feed schema", () => { expect(parseCatalogFeed(feed).entries[0]?.type).toBe("skill"); }); + it("round-trips optional featured state without changing schema version 1", () => { + const feed = makeFeed({ + entries: makeFeed().entries.map((entry, index) => ({ + ...entry, + featured: index === 0, + })), + }); + + const parsed = parseCatalogFeed(JSON.parse(serializeCatalogFeed(feed))); + + expect(parsed.schemaVersion).toBe(1); + expect(parsed.entries.find((entry) => entry.id === "zeta")?.featured).toBe(true); + expect(parsed.entries.find((entry) => entry.id === "alpha")?.featured).toBe(false); + expect(parseCatalogFeed(makeFeed()).entries[0]).not.toHaveProperty("featured"); + }); + it("preserves public GitHub source identity on skill candidates", () => { const feed = makeFeed({ id: CATALOG_SKILLS_FEED_ID, diff --git a/packages/schema/src/catalogFeed.ts b/packages/schema/src/catalogFeed.ts index 3cfbcb59..df65046e 100644 --- a/packages/schema/src/catalogFeed.ts +++ b/packages/schema/src/catalogFeed.ts @@ -33,6 +33,8 @@ const CatalogFeedEntryBaseSchema = { title: "string", version: "string", state: CatalogFeedStateSchema, + // Additive v1 metadata: existing hosted-feed consumers ignore unknown entry fields. + featured: "boolean?", publisher: { "+": "reject", id: "string", @@ -116,6 +118,7 @@ export function serializeCatalogFeed(feed: CatalogFeed): string { title: entry.title, version: entry.version, state: entry.state, + ...(entry.featured === undefined ? {} : { featured: entry.featured }), publisher: { id: entry.publisher.id, trust: entry.publisher.trust,