Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions convex/featuredPublicOrder.runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/// <reference types="vite/client" />
/* @vitest-environment edge-runtime */
import { convexTest } from "convex-test";
import { expect, it } from "vitest";
import { api } from "./_generated/api";
import { syncPackageSearchDigestForPackageId } from "./functions";
import schema from "./schema";

const modules = import.meta.glob("./**/*.ts");
it("public Featured reads follow approved identity order and omit unbadged reservations", async () => {
const t = convexTest(schema, modules);
const data = await t.run(async (ctx) => {
const ownerUserId = await ctx.db.insert("users", { handle: "curator", role: "moderator" });
const publisher = await ctx.db.insert("publishers", {
kind: "user",
displayName: "Curator",
handle: "curator",
linkedUserId: ownerUserId,
createdAt: 1,
updatedAt: 1,
});
await ctx.db.patch(ownerUserId, { personalPublisherId: publisher });
const items = [];
for (const name of ["telemetry", "editorial", "manual"]) {
const packageId = await ctx.db.insert("packages", {
name,
normalizedName: name,
displayName: name,
family: "code-plugin",
ownerUserId,
ownerPublisherId: publisher,
channel: "community",
isOfficial: false,
categories: ["developer-tools"],
tags: {},
stats: { downloads: 1, installs: 1, stars: 0, versions: 1 },
createdAt: 1,
updatedAt: 1,
});
const skillId = await ctx.db.insert("skills", {
slug: name,
displayName: name,
ownerUserId,
ownerPublisherId: publisher,
moderationStatus: "active",
tags: {},
badges: {},
stats: { comments: 0, downloads: 1, stars: 0, versions: 1 },
createdAt: 1,
updatedAt: 1,
});
const storageId = await ctx.storage.store(new Blob(["x"]));
const file = { path: "SKILL.md", size: 1, storageId, sha256: "a".repeat(64) };
const releaseId = await ctx.db.insert("packageReleases", {
packageId,
version: "1.0.0",
changelog: "Fixture",
distTags: ["latest"],
files: [{ ...file, path: "index.js" }],
integritySha256: "a".repeat(64),
verification: { tier: "structural", scope: "artifact-only", scanStatus: "clean" },
createdBy: ownerUserId,
createdAt: 1,
});
await ctx.db.patch(packageId, {
latestReleaseId: releaseId,
scanStatus: "clean",
tags: { latest: releaseId },
});
await syncPackageSearchDigestForPackageId(ctx, packageId);
const versionId = await ctx.db.insert("skillVersions", {
skillId,
version: "1.0.0",
changelog: "Fixture",
files: [file],
parsed: { frontmatter: {} },
createdBy: ownerUserId,
createdAt: 1,
llmAnalysis: { status: "clean", checkedAt: 1 },
});
await ctx.db.patch(skillId, { latestVersionId: versionId, tags: { latest: versionId } });
items.push({ name, packageId, skillId });
}
return { ownerUserId, items };
});
const staff = t.withIdentity({ subject: `${data.ownerUserId}|session` });
for (const item of data.items) {
await staff.mutation(api.packages.setBatch, {
packageId: item.packageId,
batch: "highlighted",
});
await staff.mutation(api.skills.setBatch, { skillId: item.skillId, batch: "highlighted" });
}
await t.run(async (ctx) => {
for (const artifactKind of ["plugin", "skill"] as const) {
await ctx.db.insert("featuredSelections", {
artifactKind,
revision: 1,
editorial:
artifactKind === "plugin"
? [
{
id: "plugin:pending",
name: "pending",
displayName: "Pending",
reason: "Awaiting publication",
},
]
: [],
updatedAt: 1,
updatedBy: data.ownerUserId,
published: {
reportId: "retained-report-fixture",
reportHash: "retained-evidence-hash",
at: 1,
byUserId: data.ownerUserId,
periodStart: 0,
periodEnd: 1,
items: [data.items[1], data.items[0]].map((item) => ({
id: artifactKind === "plugin" ? `plugin:${item.name}` : `clawhub:${item.skillId}`,
version: "1.0.0",
selectionBasis: "telemetry" as const,
reason: "Reviewed",
})),
},
});
}
});
const expected = ["editorial", "telemetry", "manual"];
const plugins = await t.query(api.packages.listPublicPage, {
highlightedOnly: true,
paginationOpts: { cursor: null, numItems: 16 },
});
expect(plugins.page.map((item) => item.name)).toEqual(expected);
const skills = await t.query(api.skills.listPublicPageV4, {
highlightedOnly: true,
numItems: 16,
});
expect(skills.page.map((item) => item.skill.slug)).toEqual(expected);
expect(
(await t.query(api.skills.listHighlightedPublic, { limit: 16 })).map((item) => item.skill.slug),
).toEqual(expected);
expect(
(await t.query(api.skills.listWithLatest, { batch: "highlighted", limit: 16 })).map(
(item) => item.skill.slug,
),
).toEqual(expected);
const skillPackages = await t.query(api.skills.listPackageCatalogPage, {
highlightedOnly: true,
paginationOpts: { cursor: null, numItems: 16 },
});
expect(skillPackages.page.map((item) => item.name)).toEqual(expected);
let cursor: string | null = null;
const paginated: string[] = [];
for (let index = 0; index < expected.length; index++) {
const page: typeof skillPackages = await t.query(api.skills.listPackageCatalogPage, {
highlightedOnly: true,
paginationOpts: { cursor, numItems: 1 },
});
paginated.push(...page.page.map((item) => item.name));
expect(page.isDone).toBe(index === expected.length - 1);
cursor = page.continueCursor;
}
expect(paginated).toEqual(expected);
});
14 changes: 14 additions & 0 deletions convex/lib/featuredSelections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,17 @@ export async function readPublishedFeaturedOrder(
.unique();
return selection?.published?.items.map((item) => item.id) ?? [];
}

// The approved snapshot owns order; legacy/manual badges absent from it retain
// their existing stable order after the published selection.
export function orderPublishedFeatured<T>(
items: readonly T[],
publishedIds: readonly string[],
identity: (item: T) => string,
): T[] {
const ranks = new Map(publishedIds.map((id, index) => [id, index]));
return [...items].sort(
(left, right) =>
(ranks.get(identity(left)) ?? Infinity) - (ranks.get(identity(right)) ?? Infinity),
);
}
2 changes: 2 additions & 0 deletions convex/packages.public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,8 @@ function makeDigestCtx(options: {
return null;
}),
query: vi.fn((table: string) => {
if (table === "featuredSelections")
return { withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })) };
if (table === "packageBadges") {
return {
withIndex: vi.fn(() => ({
Expand Down
10 changes: 7 additions & 3 deletions convex/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
} from "./lib/emails";
import { experimentalClawsEnabled, isClawFamilyPubliclyVisible } from "./lib/experimentalClaws";
import { assertFeaturedCapacity } from "./lib/featuredPolicy";
import { orderPublishedFeatured, readPublishedFeaturedOrder } from "./lib/featuredSelections";
import { requireGitHubAccountAge } from "./lib/githubAccount";
import { normalizeGitHubRepository } from "./lib/githubActionsOidc";
import { readGlobalPublicPluginsCount } from "./lib/globalStats";
Expand Down Expand Up @@ -2872,14 +2873,17 @@ async function fetchHighlightedPackagePage(
numItems: number;
},
) {
const entries = await fetchHighlightedPackageEntries(ctx, args);
const entries = orderPublishedFeatured(
await fetchHighlightedPackageEntries(ctx, args),
await readPublishedFeaturedOrder(ctx, "plugin"),
({ digest }) => `plugin:${digest.name}`,
);
const items = await Promise.all(
entries.map(
async ({ digest, featuredAt }) => await toPublicPackageListItem(ctx, digest, featuredAt),
),
);
// fetchHighlightedPackageEntries follows the badge timestamp index newest-first.
// Preserve that editorial order instead of re-ranking Featured by popularity.
// The published selection supplies order independently from badge timestamps.
if (!args.officialFirst) {
return items.slice(0, args.numItems);
}
Expand Down
5 changes: 4 additions & 1 deletion convex/skills.listPublicPageV4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe("skills.listPublicPageV4", () => {
});
});

it("keeps highlighted results in newest-featured order", async () => {
it("keeps legacy highlighted results in newest-featured order without a published snapshot", async () => {
const result = await listPublicPageV4Handler(
makeHighlightedCtx([
makeDigest({
Expand Down Expand Up @@ -220,6 +220,9 @@ function makeHighlightedCtx(digests: Array<ReturnType<typeof makeDigest>>) {
return {
db: {
query: vi.fn((table: string) => {
if (table === "featuredSelections") {
return { withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })) };
}
if (table === "skillBadges") {
return {
withIndex: vi.fn((_indexName: string, build: (q: EqBuilder) => unknown) => {
Expand Down
50 changes: 40 additions & 10 deletions convex/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
} from "./lib/downloadTrend";
import { embeddingVisibilityFor } from "./lib/embeddingVisibility";
import { assertFeaturedCapacity } from "./lib/featuredPolicy";
import { orderPublishedFeatured, readPublishedFeaturedOrder } from "./lib/featuredSelections";
import {
canHealSkillOwnershipByGitHubProviderAccountId,
getGitHubProviderAccountId,
Expand Down Expand Up @@ -2366,8 +2367,13 @@ async function loadHighlightedSkills(ctx: QueryCtx, limit: number) {
.order("desc")
.take(MAX_LIST_TAKE);

const ordered = orderPublishedFeatured(
entries,
await readPublishedFeaturedOrder(ctx, "skill"),
(badge) => `clawhub:${badge.skillId}`,
);
const skills: Doc<"skills">[] = [];
for (const badge of entries) {
for (const badge of ordered) {
const skill = await ctx.db.get(badge.skillId);
if (!skill || skill.softDeletedAt) continue;
skills.push(skill);
Expand Down Expand Up @@ -3938,13 +3944,7 @@ export const listWithLatest = query({
entries.filter((skill) => !skill.softDeletedAt),
);
const withBadges = await attachBadgesToSkills(ctx, filtered);
const ordered =
args.batch === "highlighted"
? [...withBadges].sort(
(a, b) => (b.badges?.highlighted?.at ?? 0) - (a.badges?.highlighted?.at ?? 0),
)
: withBadges;
const limited = ordered.slice(0, limit);
const limited = withBadges.slice(0, limit);
const items = await Promise.all(
limited.map(async (skill) => {
const latestVersion = await loadPublicLatestVersionForSkill(ctx, skill);
Expand Down Expand Up @@ -6714,6 +6714,32 @@ export const listPackageCatalogPage = query({
if (args.topic !== undefined && !topic) {
return { page: [], isDone: true, continueCursor: "" };
}
if (args.highlightedOnly) {
const skills = await loadHighlightedSkills(ctx, MAX_LIST_TAKE);
const page: PublicSkillCatalogItem[] = [];
for (const skill of skills) {
const digest = await ctx.db
.query("skillSearchDigest")
.withIndex("by_skill", (q) => q.eq("skillId", skill._id))
.unique();
if (!digest || !skillCatalogMatchesFilters(digest, { ...args, topic })) continue;
const item = await toPublicSkillCatalogItem(ctx, digest);
if (item) page.push(item);
}
const { offset } = decodeSkillCatalogCursor(args.paginationOpts.cursor);
const end = offset + args.paginationOpts.numItems;
const isDone = end >= page.length;
return {
page: page.slice(offset, end),
isDone,
continueCursor: encodeSkillCatalogCursor({
cursor: null,
offset: isDone ? 0 : end,
pageSize: null,
done: isDone,
}),
};
}
if (topic) {
return await listSkillPackageCatalogTopicPage(ctx, {
...args,
Expand Down Expand Up @@ -7481,7 +7507,7 @@ async function listOfficialFirstSkillCategoryPage(
};
}

/** Fetch highlighted skills newest-first via the skillBadges timestamp index. */
/** Resolve current highlighted membership in its approved publication order. */
async function fetchHighlightedPage(
ctx: QueryCtx,
opts: {
Expand Down Expand Up @@ -7524,7 +7550,11 @@ async function fetchHighlightedPage(
digests.push(digest);
}

const trimmed = digests.slice(0, opts.numItems);
const trimmed = orderPublishedFeatured(
digests,
await readPublishedFeaturedOrder(ctx, "skill"),
(digest) => `clawhub:${digest.skillId}`,
).slice(0, opts.numItems);

const items: PublicSkillEntry[] = [];
for (const digest of trimmed) {
Expand Down
2 changes: 1 addition & 1 deletion convex/skills.versions.public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ describe("public skill version queries", () => {
const ctx = {
db: {
query: vi.fn((table: string) => {
if (table === "officialPublishers") {
if (table === "officialPublishers" || table === "featuredSelections") {
return {
withIndex: vi.fn(() => ({
unique: vi.fn().mockResolvedValue(null),
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/home-listing-section.claw591.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ describe("HomeListingSection", () => {
await waitFor(() => {
expect(convexQueryMock).toHaveBeenCalledWith(
"skills:listPublicPageV4",
expect.objectContaining({ highlightedOnly: true, numItems: 40 }),
expect.objectContaining({ highlightedOnly: true, numItems: 16 }),
);
});

Expand Down
6 changes: 3 additions & 3 deletions src/lib/homeListingData.claw591.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe("homeListingData", () => {
]);
expect(convexQueryMock).toHaveBeenCalledWith(
"skills:listPublicPageV4",
expect.objectContaining({ highlightedOnly: true, numItems: 40 }),
expect.objectContaining({ highlightedOnly: true, numItems: 16 }),
);
});

Expand Down Expand Up @@ -260,11 +260,11 @@ describe("homeListingData", () => {
);
expect(fetchPluginCatalogMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ featured: true, isOfficial: undefined }),
expect.objectContaining({ featured: true, limit: 16 }),
);
expect(fetchPluginCatalogMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ featured: undefined, isOfficial: true }),
expect.objectContaining({ isOfficial: true }),
);
});

Expand Down
Loading
Loading