feat: add featured state to catalog feeds (#3123)

This commit is contained in:
Patrick Erichsen
2026-07-16 17:29:35 -07:00
committed by GitHub
parent 196f57c0b7
commit 43e44a8eb6
7 changed files with 112 additions and 3 deletions
+74 -2
View File
@@ -132,13 +132,36 @@ function makeFeedSkillEntry(index: number) {
};
}
function makeCtx(packages: unknown[], records: Record<string, unknown>) {
function makeCtx(
packages: unknown[],
records: Record<string, unknown>,
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: [
+9
View File
@@ -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",
+6
View File
@@ -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";
+3
View File
@@ -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,
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"catalogFeed.js","sourceRoot":"","sources":["../src/catalogFeed.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"catalogFeed.js","sourceRoot":"","sources":["../src/catalogFeed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,IAAI,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CACxC,6DAA6D,CAC9D,CAAC;AAGF,MAAM,CAAC,MAAM,+BAA+B,GAAG,IAAI,CAAC,wBAAwB,CAAC,CAAC;AAG9E,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;IAChD,GAAG,EAAE,QAAQ;IACb,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,QAAQ;IACd,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,QAAQ;CACtB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,GAAG,EAAE,QAAQ;IACb,SAAS,EAAE,QAAQ;IACnB,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,6BAA6B,CAAC,QAAQ,EAAE;CACjD,CAAC,CAAC;AAGH,MAAM,0BAA0B,GAAG;IACjC,GAAG,EAAE,QAAQ;IACb,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,QAAQ;IACf,OAAO,EAAE,QAAQ;IACjB,KAAK,EAAE,sBAAsB;IAC7B,oFAAoF;IACpF,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE;QACT,GAAG,EAAE,QAAQ;QACb,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,+BAA+B;KACvC;IACD,OAAO,EAAE;QACP,GAAG,EAAE,QAAQ;QACb,UAAU,EAAE,iCAAiC,CAAC,KAAK,EAAE;KACtD;CACO,CAAC;AAEX,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;IAC/C,GAAG,0BAA0B;IAC7B,IAAI,EAAE,UAAU;CACjB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;IAC9C,GAAG,0BAA0B;IAC7B,IAAI,EAAE,SAAS;CAChB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CACxC,4BAA4B,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAC7D,CAAC;AAGF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACpC,GAAG,EAAE,QAAQ;IACb,aAAa,EAAE,QAAQ;IACvB,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,QAAQ;IACnB,WAAW,EAAE,SAAS;IACtB,OAAO,EAAE,sBAAsB,CAAC,KAAK,EAAE;CACxC,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAClD,MAAM,CAAC,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AACxD,MAAM,CAAC,MAAM,8BAA8B,GAAG,eAAe,CAAC;AAC9D,MAAM,CAAC,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAChE,MAAM,CAAC,MAAM,+BAA+B,GAC1C,8DAA8D,CAAC;AAEjE,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,aAAa,KAAK,2BAA2B,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,4CAA4C,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IACD,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC9C,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAC5C,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAiB;IACpD,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC;SAChC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;SACtD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACf,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrE,SAAS,EAAE;YACT,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE;YACtB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK;SAC7B;QACD,OAAO,EAAE;YACP,UAAU,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;iBACtC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACpB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;iBACzD,IAAI,CAAC,QAAQ,CAAC;iBACd,aAAa,CACZ,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAChF,CACJ;iBACA,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBACnB,SAAS,EAAE,SAAS,CAAC,SAAS;gBAC9B,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,SAAS,EAAE,SAAS,CAAC,SAAS;gBAC9B,GAAG,CAAC,SAAS,CAAC,MAAM;oBAClB,CAAC,CAAC;wBACE,MAAM,EAAE;4BACN,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI;4BAC3B,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI;4BAC3B,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM;4BAC/B,WAAW,EAAE,SAAS,CAAC,MAAM,CAAC,WAAW;yBAC1C;qBACF;oBACH,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;SACN;KACF,CAAC,CAAC,CAAC;IACN,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;QAChF,OAAO;KACR,CAAC,CAAC;AACL,CAAC"}
+16
View File
@@ -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,
+3
View File
@@ -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,