fix(claws): preserve discovery cursor sort

This commit is contained in:
Patrick Erichsen
2026-07-24 17:58:49 -05:00
parent 5d6bf5cbba
commit a9f1bb419f
2 changed files with 70 additions and 7 deletions
+43 -1
View File
@@ -1825,7 +1825,10 @@ function makePluginExportIndexKey(row: Record<string, unknown>) {
return [row.softDeletedAt, row.family, row.updatedAt, row._creationTime, row._id] as unknown[];
}
function makePluginExportCtx(digests: Array<Record<string, unknown>>) {
function makePluginExportCtx(
digests: Array<Record<string, unknown>>,
options?: { recommendationScoresMissing?: () => boolean },
) {
const packagesById = new Map(
digests.map((digest) => [
String(digest.packageId),
@@ -1851,6 +1854,15 @@ function makePluginExportCtx(digests: Array<Record<string, unknown>>) {
db: {
get: vi.fn(async (id: string) => packagesById.get(id) ?? null),
query: vi.fn((table: string) => {
if (table === "packages") {
return {
withIndex: vi.fn(() => ({
first: vi.fn(async () =>
options?.recommendationScoresMissing?.() ? { _id: "packages:missing-score" } : null,
),
})),
};
}
if (table !== "packageSearchDigest") throw new Error(`Unexpected table ${table}`);
return {
withIndex: vi.fn(
@@ -17008,6 +17020,36 @@ describe("restorePackageInternal", () => {
}
});
it("keeps the cursor's effective sort when recommendation backfill completes between pages", async () => {
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
let recommendationScoresMissing = true;
const ctx = makePluginExportCtx(
[
makeDigest("newer-plugin", { updatedAt: 2, _creationTime: 2 }),
makeDigest("older-plugin", { updatedAt: 1, _creationTime: 1 }),
],
{ recommendationScoresMissing: () => recommendationScoresMissing },
);
try {
const first = await listPublicPageHandler(ctx, {
sort: "recommended",
paginationOpts: { cursor: null, numItems: 1 },
});
recommendationScoresMissing = false;
const second = await listPublicPageHandler(ctx, {
sort: "recommended",
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
});
expect(first.page.map((entry) => entry.name)).toEqual(["newer-plugin"]);
expect(second.page.map((entry) => entry.name)).toEqual(["older-plugin"]);
} finally {
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
}
});
it("does not let disabled Claws starve unfiltered public search", async () => {
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
+27 -6
View File
@@ -3476,6 +3476,23 @@ function encodeStablePackageDiscoveryCursor(state: StablePackageDiscoveryCursorS
})}`;
}
function readStablePackageDiscoveryCursorSort(
raw: string | null | undefined,
): "updated" | "recommended" | null {
if (!raw?.startsWith(STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX)) return null;
try {
const parsed = JSON.parse(raw.slice(STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX.length)) as {
v?: unknown;
sort?: unknown;
};
return parsed.v === 1 && (parsed.sort === "updated" || parsed.sort === "recommended")
? parsed.sort
: null;
} catch {
return null;
}
}
function decodeStablePackageDiscoveryCursor(
raw: string | null | undefined,
sort: StablePackageDiscoveryCursorState["sort"],
@@ -3930,17 +3947,21 @@ async function listStablePackageDiscoveryPage(
Math.min(args.paginationOpts.numItems, MAX_PUBLIC_LIST_PAGE_SIZE),
);
const requestedSort = args.sort ?? "updated";
const sort =
requestedSort === "recommended" &&
(
const cursorSort =
requestedSort === "recommended"
? readStablePackageDiscoveryCursorSort(args.paginationOpts.cursor)
: null;
let sort = cursorSort ?? requestedSort;
if (!cursorSort && requestedSort === "recommended") {
const recommendationScoresMissing = (
await Promise.all(
STABLE_PACKAGE_FAMILIES.map(
async (family) => await hasMissingPackageRecommendedScore(ctx, family),
),
)
).some(Boolean)
? "updated"
: requestedSort;
).some(Boolean);
if (recommendationScoresMissing) sort = "updated";
}
const cursor = decodeStablePackageDiscoveryCursor(args.paginationOpts.cursor, sort);
const scanLimit = Math.min(MAX_PUBLIC_LIST_PAGE_SIZE, Math.max(targetCount * 5, 50));
const loadFamily = async (family: StablePackageFamily) => {