mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
3622 lines
113 KiB
TypeScript
3622 lines
113 KiB
TypeScript
/* @vitest-environment node */
|
|
|
|
import { getFunctionName } from "convex/server";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { tokenize } from "./lib/searchText";
|
|
import {
|
|
__test,
|
|
directPrefixSkillMatches,
|
|
getExternalSkillSearchCandidates,
|
|
getExactSkillSlugMatch,
|
|
getOwnerQualifiedSkillMatch,
|
|
getRollingSkillSearchUsage,
|
|
hydrateResults,
|
|
lexicalFallbackSkills,
|
|
searchSkills as canonicalSearchSkills,
|
|
searchNativeSkills,
|
|
} from "./search";
|
|
|
|
const { generateEmbeddingMock } = vi.hoisted(() => ({
|
|
generateEmbeddingMock: vi.fn(),
|
|
}));
|
|
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
vi.mock("./lib/embeddings", () => ({
|
|
generateEmbedding: generateEmbeddingMock,
|
|
}));
|
|
|
|
vi.mock("./lib/badges", () => ({
|
|
isSkillHighlighted: (skill: { badges?: Record<string, unknown> }) =>
|
|
Boolean(skill.badges?.highlighted),
|
|
isSkillOfficial: (skill: { badges?: Record<string, unknown> }) => Boolean(skill.badges?.official),
|
|
}));
|
|
|
|
type WrappedHandler<Result = { skill: { slug: string; _id: string } }> = {
|
|
_handler: (ctx: unknown, args: unknown) => Promise<Array<Result>>;
|
|
};
|
|
|
|
const searchSkillsHandler = (
|
|
searchNativeSkills as unknown as WrappedHandler<{
|
|
skill: { slug: string; _id: string };
|
|
score: number;
|
|
semanticScore: number;
|
|
}>
|
|
)._handler;
|
|
const canonicalSearchSkillsHandler = (
|
|
canonicalSearchSkills as unknown as {
|
|
_handler: (ctx: unknown, args: unknown) => Promise<Array<Record<string, unknown>>>;
|
|
}
|
|
)._handler;
|
|
const lexicalFallbackSkillsHandler = (lexicalFallbackSkills as unknown as WrappedHandler)._handler;
|
|
const directPrefixSkillMatchesHandler = (directPrefixSkillMatches as unknown as WrappedHandler)
|
|
._handler;
|
|
const getExactSkillSlugMatchHandler = (
|
|
getExactSkillSlugMatch as unknown as {
|
|
_handler: (
|
|
ctx: unknown,
|
|
args: unknown,
|
|
) => Promise<
|
|
Array<{
|
|
skill: { slug: string; _id: string };
|
|
ownerHandle: string | null;
|
|
owner: { official?: boolean } | null;
|
|
}>
|
|
>;
|
|
}
|
|
)._handler;
|
|
const getOwnerQualifiedSkillMatchHandler = (
|
|
getOwnerQualifiedSkillMatch as unknown as {
|
|
_handler: (
|
|
ctx: unknown,
|
|
args: unknown,
|
|
) => Promise<Array<{ skill: { slug: string }; ownerHandle: string | null }>>;
|
|
}
|
|
)._handler;
|
|
const getExternalSkillSearchCandidatesHandler = (
|
|
getExternalSkillSearchCandidates as unknown as {
|
|
_handler: (ctx: unknown, args: unknown) => Promise<Array<{ externalId: string }>>;
|
|
}
|
|
)._handler;
|
|
const getRollingSkillSearchUsageHandler = (
|
|
getRollingSkillSearchUsage as unknown as {
|
|
_handler: (
|
|
ctx: unknown,
|
|
args: unknown,
|
|
) => Promise<Array<{ skillId: string; installs: number; bookmarks: number }>>;
|
|
}
|
|
)._handler;
|
|
const hydrateResultsHandler = (
|
|
hydrateResults as unknown as {
|
|
_handler: (
|
|
ctx: unknown,
|
|
args: unknown,
|
|
) => Promise<Array<{ skill: { slug: string; _id: string }; ownerHandle: string | null }>>;
|
|
}
|
|
)._handler;
|
|
|
|
describe("search helpers", () => {
|
|
it("returns fallback results when vector candidates are empty", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const fallback = [
|
|
{
|
|
skill: makePublicSkill({ id: "skills:orf", slug: "orf", displayName: "ORF" }),
|
|
version: null,
|
|
ownerHandle: "steipete",
|
|
owner: null,
|
|
},
|
|
];
|
|
// Slug-like queries now do an indexed exact-slug lookup before lexical fallback.
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce(fallback); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
runQuery,
|
|
},
|
|
{ query: "orf", limit: 10 },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf");
|
|
expect(runQuery).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ query: "orf", queryTokens: ["orf"], limit: 200 }),
|
|
);
|
|
});
|
|
|
|
it("falls back to lexical skill search when embedding generation fails", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("API unavailable"));
|
|
const fallback = [
|
|
{
|
|
skill: makePublicSkill({ id: "skills:orf", slug: "orf", displayName: "ORF" }),
|
|
version: null,
|
|
ownerHandle: "steipete",
|
|
owner: null,
|
|
},
|
|
];
|
|
const vectorSearch = vi.fn().mockRejectedValue(new Error("should not be called"));
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce(fallback); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch,
|
|
runQuery,
|
|
},
|
|
{ query: "orf", limit: 10 },
|
|
);
|
|
|
|
expect(vectorSearch).not.toHaveBeenCalled();
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf");
|
|
expect(runQuery).toHaveBeenLastCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ query: "orf", queryTokens: ["orf"] }),
|
|
);
|
|
});
|
|
|
|
it("applies normalized author topics before slicing search results", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("API unavailable"));
|
|
const directMatches = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:calendar",
|
|
slug: "calendar-workflow",
|
|
displayName: "Calendar Workflow",
|
|
topics: ["google-calendar"],
|
|
}),
|
|
version: null,
|
|
ownerHandle: "steipete",
|
|
owner: null,
|
|
},
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:legacy",
|
|
slug: "calendar-workflow-legacy",
|
|
displayName: "Calendar Workflow Legacy",
|
|
topics: ["legacy"],
|
|
}),
|
|
version: null,
|
|
ownerHandle: "steipete",
|
|
owner: null,
|
|
},
|
|
];
|
|
const runQuery = vi.fn().mockResolvedValueOnce(directMatches).mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn(),
|
|
runQuery,
|
|
},
|
|
{ query: "calendar workflow", topic: "Google Calendar", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["calendar-workflow"]);
|
|
expect(runQuery).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ topic: "google-calendar" }),
|
|
);
|
|
});
|
|
|
|
it("passes normalized selected categories through every skill recall path", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const development = {
|
|
embeddingId: "skillEmbeddings:development",
|
|
skill: makePublicSkill({
|
|
id: "skills:development",
|
|
slug: "development-helper",
|
|
displayName: "Development Helper",
|
|
categories: ["development"],
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const automation = {
|
|
embeddingId: "skillEmbeddings:automation",
|
|
skill: makePublicSkill({
|
|
id: "skills:automation",
|
|
slug: "automation-helper",
|
|
displayName: "Automation Helper",
|
|
categories: ["automation"],
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([development, automation]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([development, automation]) // hydrateResults
|
|
.mockResolvedValueOnce([development, automation]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([
|
|
{ _id: "skillEmbeddings:development", _score: 0.8 },
|
|
{ _id: "skillEmbeddings:automation", _score: 0.9 },
|
|
]),
|
|
runQuery,
|
|
},
|
|
{ query: "helper", categorySlug: "Development", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["development-helper"]);
|
|
for (const [, args] of runQuery.mock.calls) {
|
|
expect(args).toEqual(expect.objectContaining({ categorySlug: "development" }));
|
|
}
|
|
});
|
|
|
|
it("uses stored categories as skill search evidence", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("API unavailable"));
|
|
const fallback = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:category-match",
|
|
slug: "focused-helper",
|
|
displayName: "Focused Helper",
|
|
summary: "Keeps projects tidy.",
|
|
categories: ["development"],
|
|
}),
|
|
version: null,
|
|
ownerHandle: "steipete",
|
|
owner: null,
|
|
},
|
|
];
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce(fallback); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn(),
|
|
runQuery,
|
|
},
|
|
{ query: "dev", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["focused-helper"]);
|
|
});
|
|
|
|
it("uses normalized prefix matches so lowercase name queries do not depend on vector recall", async () => {
|
|
const scienceClawSkills = [
|
|
"ScienceClaw: Query (Dry Run)",
|
|
"ScienceClaw: Multi-Agent Investigation",
|
|
"ScienceClaw: Agent Status",
|
|
"ScienceClaw: Local File Investigation",
|
|
"ScienceClaw: Post to Infinite",
|
|
"ScienceClaw: Watch (Live Collaboration)",
|
|
].map((displayName, index) =>
|
|
makeSkillDoc({
|
|
id: `skills:scienceclaw-${index}`,
|
|
slug: displayName
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, ""),
|
|
displayName,
|
|
}),
|
|
);
|
|
const ctx = makeDirectPrefixCtx(scienceClawSkills);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "scienceclaw",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(
|
|
scienceClawSkills.map((skill) => skill.slug),
|
|
);
|
|
expect(ctx.usedIndexes).toEqual(
|
|
expect.arrayContaining([
|
|
"by_active_normalized_slug",
|
|
"by_active_normalized_display_name",
|
|
"by_active_normalized_slug_first_token",
|
|
"by_active_normalized_display_name_first_token",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("exact mode bypasses vector and fallback recall", async () => {
|
|
const exactEntry = {
|
|
skill: makePublicSkill({
|
|
id: "skills:exact",
|
|
slug: "exact-skill",
|
|
displayName: "Exact Skill",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi.fn().mockResolvedValueOnce([exactEntry]);
|
|
const vectorSearch = vi.fn();
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch,
|
|
runQuery,
|
|
},
|
|
{ query: "exact-skill", mode: "exact", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["exact-skill"]);
|
|
expect(vectorSearch).not.toHaveBeenCalled();
|
|
expect(runQuery).toHaveBeenCalledOnce();
|
|
expect(runQuery).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ slug: "exact-skill" }),
|
|
);
|
|
});
|
|
|
|
it("recalls non-first-token slug matches via the full-text search index (Bug 1)", async () => {
|
|
// Repro of the original bug: searching "yijian" against a skill whose
|
|
// slug is "baidu-yijian-vision" returned zero results because all four
|
|
// prefix indexes only match the *first* token. The new search index
|
|
// should match any token at any position.
|
|
const skill = makeSkillDoc({
|
|
id: "skills:baidu-yijian-vision",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
});
|
|
const ctx = makeDirectPrefixCtx([skill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "yijian",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["baidu-yijian-vision"]);
|
|
expect(ctx.usedSearchIndexes).toEqual(
|
|
expect.arrayContaining(["search_by_display_name", "search_by_slug"]),
|
|
);
|
|
});
|
|
|
|
it("recalls non-first-token displayName matches via the full-text search index", async () => {
|
|
// Companion case to the slug repro above: a query that only matches
|
|
// the displayName (not the slug) at a non-first position must still
|
|
// surface the skill.
|
|
const skill = makeSkillDoc({
|
|
id: "skills:baidu-yijian-vision",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
});
|
|
const ctx = makeDirectPrefixCtx([skill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "Vision",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["baidu-yijian-vision"]);
|
|
});
|
|
|
|
it("recalls exact author topics through the indexed topic digest", async () => {
|
|
const skill = makeSkillDoc({
|
|
id: "skills:gpu-helper",
|
|
slug: "accelerated-helper",
|
|
displayName: "Accelerated Helper",
|
|
topics: ["GPU development"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([skill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "gpu development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["accelerated-helper"]);
|
|
expect(ctx.usedIndexes).toEqual(
|
|
expect.arrayContaining(["by_active_topic_updated", "by_skill"]),
|
|
);
|
|
});
|
|
|
|
it("recalls author topics by normalized prefix through the indexed topic digest", async () => {
|
|
const skill = makeSkillDoc({
|
|
id: "skills:gpu-helper",
|
|
slug: "accelerated-helper",
|
|
displayName: "Accelerated Helper",
|
|
topics: ["GPU development"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([skill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "gpu",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["accelerated-helper"]);
|
|
expect(ctx.usedIndexes).toEqual(
|
|
expect.arrayContaining(["by_active_topic_updated", "by_skill"]),
|
|
);
|
|
});
|
|
|
|
it("prioritizes exact author-topic recall ahead of prefix expansion", async () => {
|
|
const prefixSkill = makeSkillDoc({
|
|
id: "skills:react-native-helper",
|
|
slug: "mobile-helper",
|
|
displayName: "Mobile Helper",
|
|
topics: ["React Native"],
|
|
});
|
|
const exactSkill = makeSkillDoc({
|
|
id: "skills:react-helper",
|
|
slug: "web-helper",
|
|
displayName: "Web Helper",
|
|
topics: ["React"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([prefixSkill, exactSkill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "react",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["web-helper", "mobile-helper"]);
|
|
});
|
|
|
|
it("recalls text matches from the selected topic digest", async () => {
|
|
const skill = makeSkillDoc({
|
|
id: "skills:calendar-helper",
|
|
slug: "temporal-helper",
|
|
displayName: "Temporal Helper",
|
|
summary: "Coordinates calendar events.",
|
|
topics: ["Scheduling"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([skill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "calendar",
|
|
topic: "scheduling",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["temporal-helper"]);
|
|
expect(ctx.usedIndexes).toEqual(
|
|
expect.arrayContaining(["by_active_topic_updated", "by_skill"]),
|
|
);
|
|
});
|
|
|
|
it("continues topic recall past globally capped rows before category filtering", async () => {
|
|
const distractors = Array.from({ length: 100 }, (_, index) =>
|
|
makeSkillDoc({
|
|
id: `skills:scheduling-${index}`,
|
|
slug: `temporal-${index}`,
|
|
displayName: `Temporal ${index}`,
|
|
summary: "Coordinates events.",
|
|
categories: ["automation"],
|
|
topics: ["scheduling"],
|
|
}),
|
|
);
|
|
const development = makeSkillDoc({
|
|
id: "skills:calendar-helper",
|
|
slug: "temporal-helper",
|
|
displayName: "Temporal Helper",
|
|
summary: "Coordinates calendar events.",
|
|
categories: ["development"],
|
|
topics: ["scheduling"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([...distractors, development]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "calendar",
|
|
categorySlug: "development",
|
|
topic: "scheduling",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["temporal-helper"]);
|
|
});
|
|
|
|
it("filters direct prefix matches by the selected category", async () => {
|
|
const development = makeSkillDoc({
|
|
id: "skills:development-helper",
|
|
slug: "development-helper",
|
|
displayName: "Development Helper",
|
|
categories: ["development"],
|
|
});
|
|
const automation = makeSkillDoc({
|
|
id: "skills:automation-helper",
|
|
slug: "automation-helper",
|
|
displayName: "Automation Helper",
|
|
categories: ["automation"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([development, automation]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "helper",
|
|
categorySlug: "development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["development-helper"]);
|
|
});
|
|
|
|
it("continues direct recall past globally capped matches for the selected category", async () => {
|
|
const distractors = Array.from({ length: 150 }, (_, index) =>
|
|
makeSkillDoc({
|
|
id: `skills:automation-${index}`,
|
|
slug: `helper-automation-${index}`,
|
|
displayName: `Helper Automation ${index}`,
|
|
categories: ["automation"],
|
|
}),
|
|
);
|
|
const development = makeSkillDoc({
|
|
id: "skills:development-helper",
|
|
slug: "helper-development",
|
|
displayName: "Helper Development",
|
|
categories: ["development"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([...distractors, development]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "helper",
|
|
categorySlug: "development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["helper-development"]);
|
|
expect(ctx.paginateCalls).toBe(0);
|
|
expect(Math.max(...ctx.takeLimits)).toBeLessThanOrEqual(250);
|
|
expect(ctx.takeLimits.reduce((total, limit) => total + limit, 0)).toBeLessThanOrEqual(2_500);
|
|
});
|
|
|
|
it("does not let unhighlighted scoped matches consume featured recall", async () => {
|
|
const distractors = Array.from({ length: 150 }, (_, index) =>
|
|
makeSkillDoc({
|
|
id: `skills:development-${index}`,
|
|
slug: `helper-development-${index}`,
|
|
displayName: `Helper Development ${index}`,
|
|
categories: ["development"],
|
|
}),
|
|
);
|
|
const highlighted = {
|
|
...makeSkillDoc({
|
|
id: "skills:highlighted-development",
|
|
slug: "helper-highlighted-development",
|
|
displayName: "Helper Highlighted Development",
|
|
categories: ["development"],
|
|
}),
|
|
badges: { highlighted: { byUserId: "users:mod", at: 1 } },
|
|
};
|
|
const ctx = makeDirectPrefixCtx([...distractors, highlighted]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "helper",
|
|
categorySlug: "development",
|
|
highlightedOnly: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["helper-highlighted-development"]);
|
|
});
|
|
|
|
it("does not return suspicious skills via full-text search when nonSuspiciousOnly is set", async () => {
|
|
// Even though the full-text search would token-match the suspicious
|
|
// skill, the filterField `isSuspicious=false` plus the post-hydration
|
|
// `isSkillSuspicious` guard must keep it out of the results.
|
|
const clean = makeSkillDoc({
|
|
id: "skills:clean",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
});
|
|
const flagged = makeSkillDoc({
|
|
id: "skills:flagged",
|
|
slug: "shady-yijian-trick",
|
|
displayName: "Shady Yijian Trick",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
});
|
|
const ctx = makeDirectPrefixCtx([clean, flagged]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "yijian",
|
|
nonSuspiciousOnly: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["baidu-yijian-vision"]);
|
|
});
|
|
|
|
it("does not return soft-deleted skills via full-text search", async () => {
|
|
const active = makeSkillDoc({
|
|
id: "skills:active",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
});
|
|
const softDeleted = makeSkillDoc({
|
|
id: "skills:deleted",
|
|
slug: "deleted-yijian-tool",
|
|
displayName: "Deleted Yijian Tool",
|
|
softDeletedAt: 123,
|
|
});
|
|
const ctx = makeDirectPrefixCtx([active, softDeleted]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "yijian",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["baidu-yijian-vision"]);
|
|
});
|
|
|
|
it("dedupes skills matched by both legacy prefix indexes and the new full-text index", async () => {
|
|
// First-token queries hit *all six* recall paths (4 prefix + 2 full-text).
|
|
// The skillId-based filter inside `directPrefixSkillMatches` must prevent
|
|
// the same skill from being emitted multiple times in the final list.
|
|
const skill = makeSkillDoc({
|
|
id: "skills:baidu-yijian-vision",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
});
|
|
const ctx = makeDirectPrefixCtx([skill]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "baidu",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("baidu-yijian-vision");
|
|
// Sanity: both legacy prefix indexes and the new full-text indexes were
|
|
// queried, so the dedup is doing real work, not just a no-op pass-through.
|
|
expect(ctx.usedIndexes.length).toBeGreaterThanOrEqual(4);
|
|
expect(ctx.usedSearchIndexes.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("rejects multi-token full-text candidates when only some tokens match (AND semantics)", async () => {
|
|
// Convex `withSearchIndex(...).search(field, q)` is OR-disjunctive over
|
|
// tokens: a query like "yijian vision" can return rows that contain
|
|
// *either* token. Without an application-layer AND gate, a `vision`-only
|
|
// distractor would surface as a "direct prefix match" alongside the
|
|
// genuine all-tokens hit. The handler must filter the full-text path
|
|
// through `matchesExactTokens` so only skills whose text contains every
|
|
// query token survive.
|
|
const distractor = makeSkillDoc({
|
|
id: "skills:cv-expert",
|
|
slug: "computer-vision-expert",
|
|
displayName: "Computer Vision Expert",
|
|
});
|
|
const target = makeSkillDoc({
|
|
id: "skills:baidu-yijian-vision",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
});
|
|
const ctx = makeDirectPrefixCtx([distractor, target]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "yijian vision",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["baidu-yijian-vision"]);
|
|
});
|
|
|
|
it("returns nothing when no single skill contains all query tokens", async () => {
|
|
// Each skill matches exactly one token of the multi-token query. The
|
|
// disjunctive search index would yield both, but the AND gate must drop
|
|
// them — no skill in the corpus contains *both* `yijian` and `vision`.
|
|
const onlyVision = makeSkillDoc({
|
|
id: "skills:cv-expert",
|
|
slug: "computer-vision-expert",
|
|
displayName: "Computer Vision Expert",
|
|
});
|
|
const onlyYijian = makeSkillDoc({
|
|
id: "skills:yijian-misc",
|
|
slug: "yijian-misc-tool",
|
|
displayName: "Yijian Misc Tool",
|
|
});
|
|
const ctx = makeDirectPrefixCtx([onlyVision, onlyYijian]);
|
|
|
|
const result = await directPrefixSkillMatchesHandler(ctx, {
|
|
query: "yijian vision",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("applies highlightedOnly filtering in lexical fallback", async () => {
|
|
const highlighted = {
|
|
...makeSkillDoc({
|
|
id: "skills:hl",
|
|
slug: "orf-highlighted",
|
|
displayName: "ORF Highlighted",
|
|
}),
|
|
badges: { highlighted: { byUserId: "users:mod", at: 1 } },
|
|
};
|
|
const plain = makeSkillDoc({ id: "skills:plain", slug: "orf-plain", displayName: "ORF Plain" });
|
|
|
|
const result = await lexicalFallbackSkillsHandler(
|
|
makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [highlighted, plain],
|
|
}),
|
|
{ query: "orf", queryTokens: ["orf"], highlightedOnly: true, limit: 10 },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf-highlighted");
|
|
});
|
|
|
|
it("applies nonSuspiciousOnly filtering in lexical fallback", async () => {
|
|
const suspicious = makeSkillDoc({
|
|
id: "skills:suspicious",
|
|
slug: "orf-suspicious",
|
|
displayName: "ORF Suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
});
|
|
const clean = makeSkillDoc({ id: "skills:clean", slug: "orf-clean", displayName: "ORF Clean" });
|
|
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [suspicious, clean],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "orf",
|
|
queryTokens: ["orf"],
|
|
nonSuspiciousOnly: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf-clean");
|
|
expect(ctx.usedIndexes).toEqual(
|
|
expect.arrayContaining(["by_nonsuspicious_updated", "by_nonsuspicious_created"]),
|
|
);
|
|
});
|
|
|
|
it("excludes suspicious lexical fallback results from public search", async () => {
|
|
const clean = makeSkillDoc({ id: "skills:clean", slug: "orf-clean", displayName: "ORF Clean" });
|
|
const suspicious = makeSkillDoc({
|
|
id: "skills:suspicious",
|
|
slug: "orf-suspicious",
|
|
displayName: "ORF Suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [clean, suspicious],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "orf",
|
|
queryTokens: ["orf"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["orf-clean"]);
|
|
expect(ctx.usedIndexes).toEqual(
|
|
expect.arrayContaining(["by_active_updated", "by_active_created"]),
|
|
);
|
|
});
|
|
|
|
it("uses the requested fallback limit as the digest scan budget", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [
|
|
makeSkillDoc({ id: "skills:updated", slug: "orf-updated", displayName: "ORF Updated" }),
|
|
],
|
|
recentByCreated: [
|
|
makeSkillDoc({ id: "skills:created", slug: "orf-created", displayName: "ORF Created" }),
|
|
],
|
|
});
|
|
|
|
await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "orf",
|
|
queryTokens: ["orf"],
|
|
limit: 25,
|
|
skipExactSlugLookup: true,
|
|
});
|
|
|
|
expect(ctx.takeLimits).toEqual([25, 25]);
|
|
});
|
|
|
|
it("uses the digest public version without reading version history", async () => {
|
|
const pending = makeSkillDoc({
|
|
id: "skills:pending-update",
|
|
slug: "pending-update",
|
|
displayName: "Pending Update",
|
|
moderationReason: "pending.scan",
|
|
moderationSourceVersionId: "skillVersions:pending",
|
|
latestVersionId: "skillVersions:pending",
|
|
statsVersions: 2,
|
|
publicVersion: {
|
|
status: "available",
|
|
versionId: "skillVersions:approved",
|
|
},
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [pending],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "pending",
|
|
queryTokens: ["pending"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["pending-update"]);
|
|
expect(ctx.db.query).not.toHaveBeenCalledWith("skillVersions");
|
|
});
|
|
|
|
it("fails closed from the digest without reading version history", async () => {
|
|
const pending = makeSkillDoc({
|
|
id: "skills:unavailable-update",
|
|
slug: "unavailable-update",
|
|
displayName: "Unavailable Update",
|
|
moderationReason: "pending.scan",
|
|
moderationSourceVersionId: "skillVersions:pending",
|
|
latestVersionId: "skillVersions:pending",
|
|
statsVersions: 2,
|
|
publicVersion: { status: "unavailable" },
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [pending],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "unavailable",
|
|
queryTokens: ["unavailable"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
expect(ctx.db.query).not.toHaveBeenCalledWith("skillVersions");
|
|
});
|
|
|
|
it("includes exact slug match from by_slug even when recent scan is empty", async () => {
|
|
const exactSlugSkill = makeSkillDoc({ id: "skills:orf", slug: "orf", displayName: "ORF" });
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill,
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "orf",
|
|
queryTokens: ["orf"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf");
|
|
expect(ctx.db.query).toHaveBeenCalledWith("skills");
|
|
expect(ctx.db.query).toHaveBeenCalledWith("skillSearchDigest");
|
|
});
|
|
|
|
it("includes review-only first-publish skills in exact slug search", async () => {
|
|
const exactSlugSkill = makeSkillDoc({
|
|
id: "skills:wechatpay",
|
|
slug: "wechatpay-payment-integration",
|
|
displayName: "WeChat Pay Integration",
|
|
moderationFlags: ["flagged.review"],
|
|
moderationReason: "scanner.llm.review",
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill,
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, {
|
|
slug: "wechatpay-payment-integration",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["wechatpay-payment-integration"]);
|
|
});
|
|
|
|
it("still excludes first-publish pending scan skills from exact slug search", async () => {
|
|
const exactSlugSkill = makeSkillDoc({
|
|
id: "skills:pending",
|
|
slug: "pending-payment-integration",
|
|
displayName: "Pending Payment Integration",
|
|
moderationReason: "pending.scan",
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill,
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, {
|
|
slug: "pending-payment-integration",
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("excludes unfeatured skills from featured-only exact slug search", async () => {
|
|
const exactSlugSkill = makeSkillDoc({
|
|
id: "skills:unfeatured",
|
|
slug: "unfeatured",
|
|
displayName: "Unfeatured",
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill,
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, {
|
|
slug: "unfeatured",
|
|
highlightedOnly: true,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("returns duplicate exact slug matches without requiring global slug uniqueness", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:alice-demo",
|
|
slug: "demo",
|
|
displayName: "Alice Demo",
|
|
ownerPublisherId: "publishers:alice",
|
|
}),
|
|
makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, { slug: "demo" });
|
|
|
|
expect(result.map((entry) => entry.skill._id)).toEqual([
|
|
"skills:alice-demo",
|
|
"skills:org-demo",
|
|
]);
|
|
expect(result.map((entry) => entry.ownerHandle)).toEqual(["alice", "org"]);
|
|
});
|
|
|
|
it("includes official publisher status on skill search owners", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
],
|
|
officialPublisherIds: ["publishers:org"],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, { slug: "demo" });
|
|
|
|
expect(result[0]?.ownerHandle).toBe("org");
|
|
expect(result[0]?.owner?.official).toBe(true);
|
|
});
|
|
|
|
it("resolves an owner-qualified skill through indexed publisher and skill lookups", async () => {
|
|
const skill = makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
});
|
|
const usedIndexes: string[] = [];
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: (index: string) => {
|
|
usedIndexes.push(`${table}.${index}`);
|
|
return {
|
|
unique: vi.fn(async () => {
|
|
if (table === "publishers") {
|
|
return {
|
|
_id: "publishers:org",
|
|
_creationTime: 1,
|
|
kind: "org",
|
|
handle: "org",
|
|
displayName: "Org",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
};
|
|
}
|
|
if (table === "skills") return skill;
|
|
return null;
|
|
}),
|
|
};
|
|
},
|
|
})),
|
|
get: vi.fn(async (id: string) =>
|
|
id === "skillVersions:1"
|
|
? { _id: id, skillId: skill._id, softDeletedAt: undefined }
|
|
: null,
|
|
),
|
|
},
|
|
};
|
|
|
|
const result = await getOwnerQualifiedSkillMatchHandler(ctx, {
|
|
owner: "@ORG",
|
|
slug: "demo",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["demo"]);
|
|
expect(usedIndexes).toEqual([
|
|
"publishers.by_handle",
|
|
"skills.by_owner_publisher_slug",
|
|
"officialPublishers.by_publisher",
|
|
]);
|
|
await expect(
|
|
getOwnerQualifiedSkillMatchHandler(ctx, {
|
|
owner: "@ORG",
|
|
slug: "demo",
|
|
highlightedOnly: true,
|
|
}),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it("resolves an owner-qualified legacy user-owned skill", async () => {
|
|
const user = {
|
|
_id: "users:owner",
|
|
_creationTime: 1,
|
|
handle: "legacy",
|
|
displayName: "Legacy User",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
};
|
|
const skill = makeSkillDoc({
|
|
id: "skills:legacy-demo",
|
|
slug: "demo",
|
|
displayName: "Legacy Demo",
|
|
ownerPublisherId: undefined,
|
|
});
|
|
const usedIndexes: string[] = [];
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: (index: string) => {
|
|
usedIndexes.push(`${table}.${index}`);
|
|
return {
|
|
unique: vi.fn(async () => {
|
|
if (table === "publishers") return null;
|
|
if (table === "users" && index === "handle") return user;
|
|
if (table === "skills" && index === "by_owner_slug") return skill;
|
|
if (table === "officialPublishers") return null;
|
|
return null;
|
|
}),
|
|
};
|
|
},
|
|
})),
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === user._id) return user;
|
|
if (id === "skillVersions:1") {
|
|
return { _id: id, skillId: skill._id, softDeletedAt: undefined };
|
|
}
|
|
return null;
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await getOwnerQualifiedSkillMatchHandler(ctx, {
|
|
owner: "@LEGACY",
|
|
slug: "demo",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["demo"]);
|
|
expect(result[0]?.ownerHandle).toBe("legacy");
|
|
expect(usedIndexes).toEqual([
|
|
"publishers.by_handle",
|
|
"users.handle",
|
|
"skills.by_owner_slug",
|
|
"publishers.by_linked_user",
|
|
"officialPublishers.by_publisher",
|
|
]);
|
|
});
|
|
|
|
it("fails owner-qualified lookup closed when no installable public version resolves", async () => {
|
|
const skill = makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
moderationReason: "pending.scan",
|
|
statsVersions: 2,
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: () =>
|
|
table === "skillVersions"
|
|
? { order: () => ({ take: vi.fn().mockResolvedValue([]) }) }
|
|
: {
|
|
unique: vi.fn(async () => {
|
|
if (table === "publishers") {
|
|
return {
|
|
_id: "publishers:org",
|
|
_creationTime: 1,
|
|
kind: "org",
|
|
handle: "org",
|
|
displayName: "Org",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
};
|
|
}
|
|
if (table === "skills") return skill;
|
|
return null;
|
|
}),
|
|
},
|
|
})),
|
|
get: vi.fn().mockResolvedValue(null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
getOwnerQualifiedSkillMatchHandler(ctx, { owner: "org", slug: "demo" }),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it("filters external candidates by visibility and installability and bounds every recall index", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_SKILLS_SH_ROLLOUT_MODE", "test");
|
|
const visible = makeExternalSearchDigest({ externalId: "acme/skills/calendar" });
|
|
const hidden = makeExternalSearchDigest({
|
|
externalId: "acme/skills/hidden",
|
|
publicVisible: false,
|
|
});
|
|
const blocked = makeExternalSearchDigest({
|
|
externalId: "acme/skills/blocked",
|
|
installable: false,
|
|
});
|
|
const takeLimits: number[] = [];
|
|
const usedIndexes: string[] = [];
|
|
const equalityFields: string[] = [];
|
|
const makeRange = (rows: unknown[]) => ({
|
|
take: vi.fn(async (limit: number) => {
|
|
takeLimits.push(limit);
|
|
return rows;
|
|
}),
|
|
});
|
|
const queryBuilder = {
|
|
eq: (field: string) => {
|
|
equalityFields.push(field);
|
|
return queryBuilder;
|
|
},
|
|
gte: () => queryBuilder,
|
|
lt: () => queryBuilder,
|
|
search: () => queryBuilder,
|
|
};
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: (index: string, build: (q: typeof queryBuilder) => unknown) => {
|
|
usedIndexes.push(index);
|
|
build(queryBuilder);
|
|
if (table === "skillsShCatalogControls") {
|
|
return {
|
|
unique: vi.fn(async () => ({
|
|
mode: "staging-live",
|
|
paused: false,
|
|
discoveryEnabled: true,
|
|
publicVisibilityEnabled: true,
|
|
mirrorPublicVisibilityEnabled: true,
|
|
})),
|
|
};
|
|
}
|
|
if (index === "by_external_id") {
|
|
return { unique: vi.fn(async () => visible) };
|
|
}
|
|
return makeRange([visible, hidden, blocked]);
|
|
},
|
|
withSearchIndex: (index: string, build: (q: typeof queryBuilder) => unknown) => {
|
|
usedIndexes.push(index);
|
|
build(queryBuilder);
|
|
return makeRange([visible, hidden, blocked]);
|
|
},
|
|
})),
|
|
},
|
|
};
|
|
|
|
const result = await getExternalSkillSearchCandidatesHandler(ctx, {
|
|
query: "acme/skills/calendar",
|
|
exactExternalId: "acme/skills/calendar",
|
|
});
|
|
|
|
expect(result.map((row) => row.externalId)).toEqual([visible.externalId]);
|
|
expect(takeLimits).toEqual([50, 50, 50, 50, 50]);
|
|
expect(usedIndexes).toEqual([
|
|
"by_key",
|
|
"by_external_id",
|
|
"by_active_visible_installable_fresh_slug",
|
|
"by_active_visible_installable_fresh_display",
|
|
"by_active_visible_installable_fresh_slug_token",
|
|
"by_active_visible_installable_fresh_display_token",
|
|
"search_by_search_text",
|
|
]);
|
|
expect(equalityFields).toEqual(
|
|
expect.arrayContaining(["active", "publicVisible", "installable", "sourceFreshnessStatus"]),
|
|
);
|
|
});
|
|
|
|
it("returns no external candidates while the atomic public catalog gate is off", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_SKILLS_SH_ROLLOUT_MODE", "test");
|
|
const unique = vi.fn(async () => ({
|
|
mode: "staging-live",
|
|
paused: false,
|
|
discoveryEnabled: true,
|
|
publicVisibilityEnabled: false,
|
|
mirrorPublicVisibilityEnabled: false,
|
|
}));
|
|
const withIndex = vi.fn((_index: string, build: (q: { eq: () => unknown }) => unknown) => {
|
|
build({ eq: () => undefined });
|
|
return { unique };
|
|
});
|
|
|
|
await expect(
|
|
getExternalSkillSearchCandidatesHandler(
|
|
{ db: { query: vi.fn(() => ({ withIndex })) } },
|
|
{ query: "calendar" },
|
|
),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it("aggregates only the bounded 60-day install and bookmark window", async () => {
|
|
const take = vi.fn().mockResolvedValue([
|
|
{ installs: 4, bookmarks: 2 },
|
|
{ installs: 5, bookmarks: 3 },
|
|
]);
|
|
const queryBuilder = {
|
|
eq: () => queryBuilder,
|
|
gte: () => queryBuilder,
|
|
lte: () => queryBuilder,
|
|
};
|
|
const withIndex = vi.fn((_index: string, build: (q: typeof queryBuilder) => unknown) => {
|
|
build(queryBuilder);
|
|
return { take };
|
|
});
|
|
|
|
const result = await getRollingSkillSearchUsageHandler(
|
|
{ db: { query: vi.fn(() => ({ withIndex })) } },
|
|
{ skillIds: ["skills:demo"], startDay: 100, endDay: 159 },
|
|
);
|
|
|
|
expect(result).toEqual([{ skillId: "skills:demo", installs: 9, bookmarks: 5 }]);
|
|
expect(withIndex).toHaveBeenCalledWith("by_skill_day", expect.any(Function));
|
|
expect(take).toHaveBeenCalledWith(60);
|
|
});
|
|
|
|
it("rejects rolling usage batches that could exceed one query transaction budget", async () => {
|
|
await expect(
|
|
getRollingSkillSearchUsageHandler(
|
|
{ db: { query: vi.fn() } },
|
|
{
|
|
skillIds: Array.from({ length: 21 }, (_, index) => `skills:${index}`),
|
|
startDay: 100,
|
|
endDay: 159,
|
|
},
|
|
),
|
|
).rejects.toThrow("skillIds exceeds 20");
|
|
});
|
|
|
|
it("returns one ordered native and external contract with canonical routes and install refs", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const native = {
|
|
skill: makePublicSkill({
|
|
id: "skills:calendar",
|
|
slug: "calendar",
|
|
displayName: "Calendar",
|
|
icon: `/api/v1/skill-icons/${"a".repeat(64)}`,
|
|
downloads: 1_000_000,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "openclaw",
|
|
owner: {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
},
|
|
};
|
|
const external = {
|
|
...makeExternalSearchDigest({ externalId: "acme/skills/calendar" }),
|
|
owner: "acme",
|
|
repo: "skills",
|
|
upstreamInstalls: 10_000_000,
|
|
};
|
|
const runQuery = vi.fn(async (ref: Parameters<typeof getFunctionName>[0]) => {
|
|
switch (getFunctionName(ref)) {
|
|
case "search:getExactSkillSlugMatch":
|
|
case "search:directPrefixSkillMatches":
|
|
return [native];
|
|
case "search:lexicalFallbackSkills":
|
|
return [];
|
|
case "search:getExternalSkillSearchCandidates":
|
|
return [external];
|
|
case "search:getRollingSkillSearchUsage":
|
|
return [{ skillId: native.skill._id, installs: 12, bookmarks: 3 }];
|
|
default:
|
|
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
|
|
}
|
|
});
|
|
|
|
const result = await canonicalSearchSkillsHandler(
|
|
{ runQuery, vectorSearch: vi.fn() },
|
|
{ query: "calendar", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((row) => row.source)).toEqual(["clawhub", "skills-sh"]);
|
|
expect(result[0]).toMatchObject({
|
|
canonicalUrl: "/openclaw/skills/calendar",
|
|
install: { reference: "openclaw/calendar" },
|
|
icon: `/api/v1/skill-icons/${"a".repeat(64)}`,
|
|
metrics: { rolling60DayInstalls: 12, bookmarks: 3 },
|
|
});
|
|
expect((result[0]?.native as { owner?: unknown })?.owner).not.toHaveProperty("bio");
|
|
expect(result[1]).toMatchObject({
|
|
canonicalUrl: "/skills-sh/acme/skills/calendar",
|
|
links: {
|
|
canonical: "/skills-sh/acme/skills/calendar",
|
|
source: "https://skills.sh/acme/skills/calendar",
|
|
},
|
|
install: { reference: "skills-sh:acme/skills/calendar" },
|
|
icon: null,
|
|
sourceIdentity: { lifetimeInstalls: 10_000_000 },
|
|
downloads: 10_000_000,
|
|
});
|
|
});
|
|
|
|
it("preserves rolling adoption ranking when usage reads require smaller transactions", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const native = Array.from({ length: 100 }, (_, index) => ({
|
|
skill: makePublicSkill({
|
|
id: `skills:calendar-${index}`,
|
|
slug: `calendar-${index}`,
|
|
displayName: `Calendar ${index}`,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "openclaw",
|
|
owner: null,
|
|
}));
|
|
const runQuery = vi.fn(
|
|
async (ref: Parameters<typeof getFunctionName>[0], args?: { skillIds?: string[] }) => {
|
|
switch (getFunctionName(ref)) {
|
|
case "search:getExactSkillSlugMatch":
|
|
case "search:lexicalFallbackSkills":
|
|
case "search:getExternalSkillSearchCandidates":
|
|
return [];
|
|
case "search:directPrefixSkillMatches":
|
|
return native;
|
|
case "search:getRollingSkillSearchUsage": {
|
|
const skillIds = args?.skillIds ?? [];
|
|
if (skillIds.length > 20) {
|
|
throw new Error("Function execution timed out (maximum duration: 1s)");
|
|
}
|
|
return skillIds.map((skillId) => ({
|
|
skillId,
|
|
installs: skillId === "skills:calendar-99" ? 10_000 : 1,
|
|
bookmarks: 0,
|
|
}));
|
|
}
|
|
default:
|
|
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
|
|
}
|
|
},
|
|
);
|
|
|
|
const result = await canonicalSearchSkillsHandler(
|
|
{ runQuery, vectorSearch: vi.fn() },
|
|
{ query: "calendar", limit: 100 },
|
|
);
|
|
|
|
expect(result).toHaveLength(100);
|
|
expect(result[0]).toMatchObject({
|
|
id: "clawhub:skills:calendar-99",
|
|
metrics: { rolling60DayInstalls: 10_000 },
|
|
});
|
|
});
|
|
|
|
it("keeps exact mode deterministic across canonical sources", async () => {
|
|
const native = {
|
|
skill: makePublicSkill({
|
|
id: "skills:calendar",
|
|
slug: "calendar",
|
|
displayName: "Calendar",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "openclaw",
|
|
owner: null,
|
|
};
|
|
const exactExternal = makeExternalSearchDigest({ externalId: "acme/skills/calendar" });
|
|
const prefixExternal = makeExternalSearchDigest({
|
|
externalId: "acme/skills/calendar-tools",
|
|
});
|
|
const runQuery = vi.fn(async (ref: Parameters<typeof getFunctionName>[0]) => {
|
|
switch (getFunctionName(ref)) {
|
|
case "search:getExactSkillSlugMatch":
|
|
return [native];
|
|
case "search:getExternalSkillSearchCandidates":
|
|
return [exactExternal, prefixExternal];
|
|
case "search:getRollingSkillSearchUsage":
|
|
return [{ skillId: native.skill._id, installs: 12, bookmarks: 3 }];
|
|
default:
|
|
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
|
|
}
|
|
});
|
|
|
|
const result = await canonicalSearchSkillsHandler(
|
|
{ runQuery, vectorSearch: vi.fn() },
|
|
{ query: "calendar", mode: "exact", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((row) => `${String(row.source)}:${String(row.slug)}`)).toEqual([
|
|
"clawhub:calendar",
|
|
"skills-sh:calendar",
|
|
]);
|
|
});
|
|
|
|
it("excludes pending owner-qualified matches when pending scans are disabled", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const pending = {
|
|
skill: makePublicSkill({
|
|
id: "skills:pending-calendar",
|
|
slug: "calendar",
|
|
displayName: "Calendar",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
githubScanStatus: "pending",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "openclaw",
|
|
owner: {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
},
|
|
};
|
|
const runQuery = vi.fn(async (ref: Parameters<typeof getFunctionName>[0]) => {
|
|
switch (getFunctionName(ref)) {
|
|
case "search:directPrefixSkillMatches":
|
|
case "search:lexicalFallbackSkills":
|
|
case "search:getExternalSkillSearchCandidates":
|
|
return [];
|
|
case "search:getOwnerQualifiedSkillMatch":
|
|
return [pending];
|
|
case "search:getRollingSkillSearchUsage":
|
|
return [{ skillId: pending.skill._id, installs: 0, bookmarks: 0 }];
|
|
default:
|
|
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
|
|
}
|
|
});
|
|
|
|
const result = await canonicalSearchSkillsHandler(
|
|
{ runQuery, vectorSearch: vi.fn() },
|
|
{ query: "openclaw/calendar", limit: 10, excludePendingScan: true },
|
|
);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("excludes unfeatured owner-qualified matches from featured-only search", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const runQuery = vi.fn(async (ref: Parameters<typeof getFunctionName>[0]) => {
|
|
switch (getFunctionName(ref)) {
|
|
case "search:directPrefixSkillMatches":
|
|
case "search:lexicalFallbackSkills":
|
|
case "search:getExternalSkillSearchCandidates":
|
|
return [];
|
|
case "search:getOwnerQualifiedSkillMatch":
|
|
return [];
|
|
case "search:getRollingSkillSearchUsage":
|
|
return [];
|
|
default:
|
|
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
|
|
}
|
|
});
|
|
|
|
const result = await canonicalSearchSkillsHandler(
|
|
{ runQuery, vectorSearch: vi.fn() },
|
|
{ query: "openclaw/calendar", limit: 10, highlightedOnly: true },
|
|
);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(runQuery).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
owner: "openclaw",
|
|
slug: "calendar",
|
|
highlightedOnly: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps exact-token native recall ahead of semantic prefix hits at the candidate bound", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const exact = {
|
|
skill: makePublicSkill({
|
|
id: "skills:exact-calendar-sync",
|
|
slug: "sync-tool-exact",
|
|
displayName: "Calendar",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "openclaw",
|
|
owner: null,
|
|
};
|
|
const prefixes = Array.from({ length: 100 }, (_, index) => ({
|
|
embeddingId: `skillEmbeddings:prefix-${index}`,
|
|
skill: makePublicSkill({
|
|
id: `skills:prefix-${index}`,
|
|
slug: `calendars-synchronizer-${index}`,
|
|
displayName: `Calendars Synchronizer ${index}`,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "community",
|
|
owner: null,
|
|
}));
|
|
const runQuery = vi.fn(async (ref: Parameters<typeof getFunctionName>[0]) => {
|
|
switch (getFunctionName(ref)) {
|
|
case "search:directPrefixSkillMatches":
|
|
return [exact, ...prefixes.map(({ embeddingId: _embeddingId, ...entry }) => entry)];
|
|
case "search:hydrateResults":
|
|
return prefixes;
|
|
case "search:getExternalSkillSearchCandidates":
|
|
return [];
|
|
case "search:getRollingSkillSearchUsage":
|
|
return [];
|
|
default:
|
|
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
|
|
}
|
|
});
|
|
|
|
const result = await canonicalSearchSkillsHandler(
|
|
{
|
|
runQuery,
|
|
vectorSearch: vi.fn().mockResolvedValue(
|
|
prefixes.map((entry) => ({
|
|
_id: entry.embeddingId,
|
|
_score: 0.99,
|
|
})),
|
|
),
|
|
},
|
|
{ query: "calendar sync", limit: 10 },
|
|
);
|
|
|
|
expect(result[0]).toMatchObject({ id: "clawhub:skills:exact-calendar-sync" });
|
|
});
|
|
|
|
it("filters duplicate exact slug matches by topic", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:alice-demo",
|
|
slug: "demo",
|
|
displayName: "Alice Demo",
|
|
ownerPublisherId: "publishers:alice",
|
|
topics: ["scheduling", "Official"],
|
|
}),
|
|
makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
topics: ["monitoring"],
|
|
}),
|
|
],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, {
|
|
slug: "demo",
|
|
topic: "Scheduling",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill._id)).toEqual(["skills:alice-demo"]);
|
|
});
|
|
|
|
it("filters duplicate exact slug matches by category", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:development-demo",
|
|
slug: "demo",
|
|
displayName: "Development Demo",
|
|
ownerPublisherId: "publishers:development",
|
|
categories: ["development"],
|
|
}),
|
|
makeSkillDoc({
|
|
id: "skills:automation-demo",
|
|
slug: "demo",
|
|
displayName: "Automation Demo",
|
|
ownerPublisherId: "publishers:automation",
|
|
categories: ["automation"],
|
|
}),
|
|
],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, {
|
|
slug: "demo",
|
|
categorySlug: "development",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill._id)).toEqual(["skills:development-demo"]);
|
|
});
|
|
|
|
it("preserves resolved inferred categories on exact slug results", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:development-demo",
|
|
slug: "demo",
|
|
displayName: "Development Demo",
|
|
inferredCategories: ["development"],
|
|
inferredFromVersionId: "skillVersions:1",
|
|
}),
|
|
],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await getExactSkillSlugMatchHandler(ctx, {
|
|
slug: "demo",
|
|
categorySlug: "development",
|
|
});
|
|
|
|
const [entry] = result;
|
|
if (!entry) throw new Error("Expected an exact slug result");
|
|
expect((entry.skill as { categories?: string[] }).categories).toEqual(["development"]);
|
|
});
|
|
|
|
it("includes duplicate exact slug matches from by_slug when recent scan is empty", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:alice-demo",
|
|
slug: "demo",
|
|
displayName: "Alice Demo",
|
|
ownerPublisherId: "publishers:alice",
|
|
}),
|
|
makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "demo",
|
|
queryTokens: ["demo"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill._id)).toEqual([
|
|
"skills:alice-demo",
|
|
"skills:org-demo",
|
|
]);
|
|
});
|
|
|
|
it("filters duplicate exact slug fallback matches by topic", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:alice-demo",
|
|
slug: "demo",
|
|
displayName: "Alice Demo",
|
|
ownerPublisherId: "publishers:alice",
|
|
topics: ["scheduling"],
|
|
}),
|
|
makeSkillDoc({
|
|
id: "skills:org-demo",
|
|
slug: "demo",
|
|
displayName: "Org Demo",
|
|
ownerPublisherId: "publishers:org",
|
|
topics: ["monitoring"],
|
|
}),
|
|
],
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "demo",
|
|
queryTokens: ["demo"],
|
|
limit: 10,
|
|
topic: "Scheduling",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill._id)).toEqual(["skills:alice-demo"]);
|
|
});
|
|
|
|
it("filters lexical fallback matches by the selected category", async () => {
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [
|
|
makeSkillDoc({
|
|
id: "skills:development",
|
|
slug: "development-helper",
|
|
displayName: "Development Helper",
|
|
categories: ["development"],
|
|
}),
|
|
makeSkillDoc({
|
|
id: "skills:automation",
|
|
slug: "automation-helper",
|
|
displayName: "Automation Helper",
|
|
categories: ["automation"],
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "helper",
|
|
queryTokens: ["helper"],
|
|
categorySlug: "development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["development-helper"]);
|
|
});
|
|
|
|
it("continues fallback recall past global rows for the selected category", async () => {
|
|
const distractors = Array.from({ length: 25 }, (_, index) =>
|
|
makeSkillDoc({
|
|
id: `skills:automation-${index}`,
|
|
slug: `automation-${index}`,
|
|
displayName: `Automation ${index}`,
|
|
summary: "Helper workflow",
|
|
categories: ["automation"],
|
|
}),
|
|
);
|
|
const development = makeSkillDoc({
|
|
id: "skills:development",
|
|
slug: "development-tool",
|
|
displayName: "Development Tool",
|
|
summary: "Helper workflow",
|
|
categories: ["development"],
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [...distractors, development],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "helper",
|
|
queryTokens: ["helper"],
|
|
categorySlug: "development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["development-tool"]);
|
|
expect(ctx.paginateCalls).toBe(0);
|
|
});
|
|
|
|
it("does not let unrelated scoped rows consume fallback recall", async () => {
|
|
const distractors = Array.from({ length: 25 }, (_, index) =>
|
|
makeSkillDoc({
|
|
id: `skills:development-${index}`,
|
|
slug: `development-${index}`,
|
|
displayName: `Development ${index}`,
|
|
summary: "Unrelated workflow",
|
|
categories: ["development"],
|
|
}),
|
|
);
|
|
const target = makeSkillDoc({
|
|
id: "skills:development-target",
|
|
slug: "development-target",
|
|
displayName: "Development Target",
|
|
summary: "Helper workflow",
|
|
categories: ["development"],
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [...distractors, target],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "helper",
|
|
queryTokens: ["helper"],
|
|
categorySlug: "development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["development-target"]);
|
|
});
|
|
|
|
it("dedupes overlap and enforces rank + limit across vector and fallback", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const vectorEntries = [
|
|
{
|
|
embeddingId: "skillEmbeddings:a",
|
|
skill: makePublicSkill({
|
|
id: "skills:a",
|
|
slug: "foo-a",
|
|
displayName: "Foo Alpha",
|
|
downloads: 10,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "one",
|
|
owner: null,
|
|
},
|
|
{
|
|
embeddingId: "skillEmbeddings:b",
|
|
skill: makePublicSkill({
|
|
id: "skills:b",
|
|
slug: "foo-b",
|
|
displayName: "Foo Beta",
|
|
downloads: 2,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "two",
|
|
owner: null,
|
|
},
|
|
];
|
|
const fallbackEntries = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:a",
|
|
slug: "foo-a",
|
|
displayName: "Foo Alpha",
|
|
downloads: 10,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "one",
|
|
owner: null,
|
|
},
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:c",
|
|
slug: "foo-c",
|
|
displayName: "Foo Classic",
|
|
downloads: 1,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "three",
|
|
owner: null,
|
|
},
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce(vectorEntries) // hydrateResults
|
|
.mockResolvedValueOnce(fallbackEntries); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([
|
|
{ _id: "skillEmbeddings:a", _score: 0.4 },
|
|
{ _id: "skillEmbeddings:b", _score: 0.9 },
|
|
]),
|
|
runQuery,
|
|
},
|
|
{ query: "foo", limit: 2 },
|
|
);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].skill.slug).toBe("foo-a");
|
|
expect(new Set(result.map((entry: { skill: { _id: string } }) => entry.skill._id)).size).toBe(
|
|
2,
|
|
);
|
|
});
|
|
|
|
it("uses a stable recall pool without lifetime popularity changing the first page", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const vectorEntries = Array.from({ length: 25 }, (_, index) => ({
|
|
embeddingId: `skillEmbeddings:${index}`,
|
|
skill: makePublicSkill({
|
|
id: `skills:${index}`,
|
|
slug: `image-vector-${index}`,
|
|
displayName: `Image Vector ${index}`,
|
|
downloads: 10,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
}));
|
|
const fallbackEntries = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:fallback",
|
|
slug: "antigravity-image-generator",
|
|
displayName: "Antigravity Image Generator",
|
|
downloads: 1_000_000_000,
|
|
installs: 1_000,
|
|
stars: 100,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce(vectorEntries) // hydrateResults
|
|
.mockResolvedValueOnce(fallbackEntries); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue(
|
|
vectorEntries.map((entry, index) => ({
|
|
_id: entry.embeddingId,
|
|
_score: 0.05 - index * 0.001,
|
|
})),
|
|
),
|
|
runQuery,
|
|
},
|
|
{ query: "image", limit: 25 },
|
|
);
|
|
|
|
expect(runQuery).toHaveBeenCalledTimes(4);
|
|
expect(runQuery.mock.calls.at(-1)?.[1]).toEqual(
|
|
expect.objectContaining({ query: "image", limit: 200 }),
|
|
);
|
|
expect(result).toHaveLength(25);
|
|
expect(result.some((entry) => entry.skill.slug === "antigravity-image-generator")).toBe(false);
|
|
});
|
|
|
|
it("orders lexical name matches above summary-only matches before popularity", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const exactName = {
|
|
skill: makePublicSkill({
|
|
id: "skills:postgres",
|
|
slug: "postgres",
|
|
displayName: "Postgres",
|
|
downloads: 0,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const summaryOnly = {
|
|
skill: {
|
|
...makePublicSkill({
|
|
id: "skills:database-tools",
|
|
slug: "database-tools",
|
|
displayName: "Database Tools",
|
|
downloads: 1_000_000_000,
|
|
}),
|
|
summary: "Postgres database helper.",
|
|
},
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([summaryOnly, exactName]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
runQuery,
|
|
},
|
|
{ query: "postgres", limit: 2 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["postgres", "database-tools"]);
|
|
expect(result[0]).not.toHaveProperty("rankTier");
|
|
expect(result[0]).not.toHaveProperty("matchReason");
|
|
});
|
|
|
|
it("admits strong semantic recall without promoting it above lexical tiers", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const summaryOnly = {
|
|
embeddingId: "skillEmbeddings:ai",
|
|
skill: {
|
|
...makePublicSkill({
|
|
id: "skills:ai-summary",
|
|
slug: "general-helper",
|
|
displayName: "General Helper",
|
|
downloads: 1_000,
|
|
}),
|
|
summary: "AI helper for teams.",
|
|
},
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([summaryOnly]) // hydrateResults
|
|
.mockResolvedValueOnce([]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([{ _id: "skillEmbeddings:ai", _score: 0.99 }]),
|
|
runQuery,
|
|
},
|
|
{ query: "ai", limit: 10 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["general-helper"]);
|
|
expect(result[0]?.semanticScore).toBe(0.99);
|
|
});
|
|
|
|
it("always includes an exact slug match even when vector exact matches already fill the limit", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const vectorEntries = Array.from({ length: 10 }, (_, index) => ({
|
|
embeddingId: `skillEmbeddings:${index}`,
|
|
skill: makePublicSkill({
|
|
id: `skills:${index}`,
|
|
slug: `downloader-${index}`,
|
|
displayName: `Skill Downloader ${index}`,
|
|
downloads: 100 - index,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
}));
|
|
|
|
const exactSlugEntry = {
|
|
skill: makePublicSkill({
|
|
id: "skills:exact",
|
|
slug: "skill-downloader",
|
|
displayName: "Skill Downloader",
|
|
downloads: 1,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "yyang100",
|
|
owner: null,
|
|
};
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(exactSlugEntry)
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce(vectorEntries)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue(
|
|
vectorEntries.map((entry, index) => ({
|
|
_id: entry.embeddingId,
|
|
_score: 0.9 - index * 0.01,
|
|
})),
|
|
),
|
|
runQuery,
|
|
},
|
|
{ query: "skill-downloader", limit: 10 },
|
|
);
|
|
|
|
expect(result).toHaveLength(10);
|
|
expect(result[0].skill.slug).toBe("skill-downloader");
|
|
expect(runQuery).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
it("omits exact slug injection when nonSuspiciousOnly excludes it", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const vectorEntries = [
|
|
{
|
|
embeddingId: "skillEmbeddings:1",
|
|
skill: makePublicSkill({
|
|
id: "skills:1",
|
|
slug: "downloader-1",
|
|
displayName: "Skill Downloader 1",
|
|
downloads: 50,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce(vectorEntries)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([{ _id: "skillEmbeddings:1", _score: 0.9 }]),
|
|
runQuery,
|
|
},
|
|
{ query: "skill-downloader", limit: 10, nonSuspiciousOnly: true },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("downloader-1");
|
|
});
|
|
|
|
it("omits exact slug injection when highlightedOnly excludes it", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const exactSlugEntry = {
|
|
skill: makePublicSkill({
|
|
id: "skills:exact",
|
|
slug: "skill-downloader",
|
|
displayName: "Skill Downloader",
|
|
downloads: 1,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "yyang100",
|
|
owner: null,
|
|
};
|
|
|
|
const vectorEntries = [
|
|
{
|
|
embeddingId: "skillEmbeddings:1",
|
|
skill: {
|
|
...makePublicSkill({
|
|
id: "skills:1",
|
|
slug: "downloader-1",
|
|
displayName: "Skill Downloader 1",
|
|
downloads: 50,
|
|
}),
|
|
badges: { highlighted: { byUserId: "users:mod", at: 1 } },
|
|
},
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(exactSlugEntry)
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce(vectorEntries)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([{ _id: "skillEmbeddings:1", _score: 0.9 }]),
|
|
runQuery,
|
|
},
|
|
{ query: "skill-downloader", limit: 10, highlightedOnly: true },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("downloader-1");
|
|
});
|
|
|
|
it("deduplicates exact slug injection against vector exact matches", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const sharedSkill = makePublicSkill({
|
|
id: "skills:exact",
|
|
slug: "skill-downloader",
|
|
displayName: "Skill Downloader",
|
|
downloads: 100,
|
|
});
|
|
const exactSlugEntry = {
|
|
skill: sharedSkill,
|
|
version: null,
|
|
ownerHandle: "yyang100",
|
|
owner: null,
|
|
};
|
|
const vectorEntries = [
|
|
{
|
|
embeddingId: "skillEmbeddings:exact",
|
|
skill: sharedSkill,
|
|
version: null,
|
|
ownerHandle: "yyang100",
|
|
owner: null,
|
|
},
|
|
{
|
|
embeddingId: "skillEmbeddings:other",
|
|
skill: makePublicSkill({
|
|
id: "skills:other",
|
|
slug: "downloader-2",
|
|
displayName: "Skill Downloader 2",
|
|
downloads: 50,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(exactSlugEntry)
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce(vectorEntries)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([
|
|
{ _id: "skillEmbeddings:exact", _score: 0.95 },
|
|
{ _id: "skillEmbeddings:other", _score: 0.8 },
|
|
]),
|
|
runQuery,
|
|
},
|
|
{ query: "skill-downloader", limit: 10 },
|
|
);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result.filter((entry) => entry.skill._id === "skills:exact")).toHaveLength(1);
|
|
});
|
|
|
|
it("skips duplicate slug lookup inside lexical fallback when search action already did it", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const fallbackEntries = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:orf",
|
|
slug: "orf",
|
|
displayName: "ORF",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "steipete",
|
|
owner: null,
|
|
},
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce([])
|
|
.mockImplementationOnce(async (_ref: unknown, args: { skipExactSlugLookup?: boolean }) => {
|
|
expect(args.skipExactSlugLookup).toBe(true);
|
|
return fallbackEntries;
|
|
});
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
runQuery,
|
|
},
|
|
{ query: "orf", limit: 10 },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf");
|
|
});
|
|
|
|
it("filters suspicious vector results in hydrateResults when requested", async () => {
|
|
const result = await hydrateResultsHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "skillEmbeddings:1") {
|
|
return {
|
|
_id: "skillEmbeddings:1",
|
|
skillId: "skills:1",
|
|
versionId: "skillVersions:1",
|
|
};
|
|
}
|
|
if (id === "skills:1") {
|
|
return makeSkillDoc({
|
|
id: "skills:1",
|
|
slug: "suspicious",
|
|
displayName: "Suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
});
|
|
}
|
|
if (id === "users:owner") return { _id: "users:owner", handle: "owner" };
|
|
if (id === "skillVersions:1") return { _id: "skillVersions:1", version: "1.0.0" };
|
|
return null;
|
|
}),
|
|
query: vi.fn(() => ({
|
|
withIndex: () => ({ unique: vi.fn().mockResolvedValue(null) }),
|
|
})),
|
|
},
|
|
},
|
|
{ embeddingIds: ["skillEmbeddings:1"], nonSuspiciousOnly: true },
|
|
);
|
|
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it("filters vector results by the selected category", async () => {
|
|
const result = await hydrateResultsHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "skillEmbeddings:1") {
|
|
return {
|
|
_id: "skillEmbeddings:1",
|
|
skillId: "skills:1",
|
|
versionId: "skillVersions:1",
|
|
};
|
|
}
|
|
if (id === "skills:1") {
|
|
return makeSkillDoc({
|
|
id: "skills:1",
|
|
slug: "automation-helper",
|
|
displayName: "Automation Helper",
|
|
categories: ["automation"],
|
|
});
|
|
}
|
|
if (id === "users:owner") return { _id: "users:owner", handle: "owner" };
|
|
return null;
|
|
}),
|
|
query: vi.fn(() => ({
|
|
withIndex: () => ({ unique: vi.fn().mockResolvedValue(null) }),
|
|
})),
|
|
},
|
|
},
|
|
{ embeddingIds: ["skillEmbeddings:1"], categorySlug: "development" },
|
|
);
|
|
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it("excludes soft-deleted skills from vector search results (#29)", async () => {
|
|
const result = await hydrateResultsHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "skillEmbeddings:1") {
|
|
return {
|
|
_id: "skillEmbeddings:1",
|
|
skillId: "skills:1",
|
|
versionId: "skillVersions:1",
|
|
};
|
|
}
|
|
if (id === "skillEmbeddings:2") {
|
|
return {
|
|
_id: "skillEmbeddings:2",
|
|
skillId: "skills:2",
|
|
versionId: "skillVersions:2",
|
|
};
|
|
}
|
|
if (id === "skills:1") {
|
|
return {
|
|
...makeSkillDoc({ id: "skills:1", slug: "active-skill", displayName: "Active" }),
|
|
softDeletedAt: undefined,
|
|
};
|
|
}
|
|
if (id === "skills:2") {
|
|
return {
|
|
...makeSkillDoc({ id: "skills:2", slug: "deleted-skill", displayName: "Deleted" }),
|
|
softDeletedAt: 1700000000000,
|
|
};
|
|
}
|
|
if (id === "users:owner") return { _id: "users:owner", handle: "owner" };
|
|
if (id.startsWith("skillVersions:")) return { _id: id, version: "1.0.0" };
|
|
return null;
|
|
}),
|
|
query: vi.fn(() => ({
|
|
withIndex: () => ({ unique: vi.fn().mockResolvedValue(null) }),
|
|
})),
|
|
},
|
|
},
|
|
{ embeddingIds: ["skillEmbeddings:1", "skillEmbeddings:2"] },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("active-skill");
|
|
});
|
|
|
|
it("excludes skills whose owners are deleted or banned from vector search results", async () => {
|
|
const result = await hydrateResultsHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "skillEmbeddings:1") {
|
|
return {
|
|
_id: "skillEmbeddings:1",
|
|
skillId: "skills:1",
|
|
versionId: "skillVersions:1",
|
|
};
|
|
}
|
|
if (id === "skills:1") {
|
|
return {
|
|
...makeSkillDoc({
|
|
id: "skills:1",
|
|
slug: "ownerless-skill",
|
|
displayName: "Ownerless",
|
|
}),
|
|
softDeletedAt: undefined,
|
|
};
|
|
}
|
|
if (id === "users:owner") {
|
|
return { _id: "users:owner", handle: "owner", deletedAt: 1700000000000 };
|
|
}
|
|
if (id === "skillVersions:1") return { _id: "skillVersions:1", version: "1.0.0" };
|
|
return null;
|
|
}),
|
|
query: vi.fn(() => ({
|
|
withIndex: () => ({ unique: vi.fn().mockResolvedValue(null) }),
|
|
})),
|
|
},
|
|
},
|
|
{ embeddingIds: ["skillEmbeddings:1"] },
|
|
);
|
|
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it("excludes soft-deleted exact slug match from lexical fallback (#29)", async () => {
|
|
const deletedSkill = makeSkillDoc({
|
|
id: "skills:deleted",
|
|
slug: "orf",
|
|
displayName: "ORF",
|
|
softDeletedAt: 1700000000000,
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: deletedSkill,
|
|
recentSkills: [],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "orf",
|
|
queryTokens: ["orf"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it("finds recently created skills missed by the updatedAt fallback scan (#1185)", async () => {
|
|
const newSkill = makeSkillDoc({
|
|
id: "skills:new",
|
|
slug: "ai-clipping",
|
|
displayName: "AI Clipping",
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [],
|
|
recentByCreated: [newSkill],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "clipping",
|
|
queryTokens: ["clipping"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("ai-clipping");
|
|
});
|
|
|
|
it("deduplicates skills found by both fallback scan windows", async () => {
|
|
const skill = makeSkillDoc({
|
|
id: "skills:dup",
|
|
slug: "orf-dup",
|
|
displayName: "ORF Dup",
|
|
});
|
|
const ctx = makeLexicalCtx({
|
|
exactSlugSkill: null,
|
|
recentSkills: [skill],
|
|
recentByCreated: [skill],
|
|
});
|
|
|
|
const result = await lexicalFallbackSkillsHandler(ctx, {
|
|
query: "orf",
|
|
queryTokens: ["orf"],
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("orf-dup");
|
|
});
|
|
|
|
it("advances candidate limit until max", () => {
|
|
expect(__test.getNextCandidateLimit(50, 1000)).toBe(100);
|
|
expect(__test.getNextCandidateLimit(800, 1000)).toBe(1000);
|
|
expect(__test.getNextCandidateLimit(1000, 1000)).toBeNull();
|
|
});
|
|
|
|
it("normalizes native owner-qualified and skills.sh install identities", () => {
|
|
expect(__test.parseQualifiedSearchIdentity("@OpenClaw/Calendar")).toEqual({
|
|
native: { owner: "openclaw", slug: "calendar" },
|
|
external: "openclaw/calendar",
|
|
});
|
|
expect(__test.parseQualifiedSearchIdentity("skills-sh/Vercel-Labs/Skills/Find-Skills")).toEqual(
|
|
{
|
|
native: null,
|
|
external: "vercel-labs/skills/find-skills",
|
|
},
|
|
);
|
|
expect(__test.parseQualifiedSearchIdentity("skills-sh:Vercel-Labs/Skills/Find-Skills")).toEqual(
|
|
{
|
|
native: null,
|
|
external: "vercel-labs/skills/find-skills",
|
|
},
|
|
);
|
|
});
|
|
|
|
it("boosts exact slug/name matches over loose matches", () => {
|
|
const queryTokens = tokenize("notion");
|
|
const exactScore = __test.scoreSkillResult(queryTokens, 0.4, "Notion Sync", "notion-sync");
|
|
const looseScore = __test.scoreSkillResult(queryTokens, 0.6, "Notes Sync", "notes-sync");
|
|
expect(exactScore).toBeGreaterThan(looseScore);
|
|
});
|
|
|
|
it("boosts exact full slug over a longer slug containing all query tokens", () => {
|
|
const queryTokens = tokenize("self-improving-agent");
|
|
const exactScore = __test.scoreSkillResult(
|
|
queryTokens,
|
|
0.5,
|
|
"Self Improving Agent",
|
|
"self-improving-agent",
|
|
);
|
|
const containingScore = __test.scoreSkillResult(
|
|
queryTokens,
|
|
0.6,
|
|
"Self Improving Agent",
|
|
"xiucheng-self-improving-agent",
|
|
);
|
|
expect(exactScore).toBeGreaterThan(containingScore);
|
|
});
|
|
|
|
it("keeps extreme popularity below direct lexical relevance", () => {
|
|
const queryTokens = tokenize("needle");
|
|
const exactScore = __test.scoreSkillResult(queryTokens, 0, "Unrelated Name", "needle");
|
|
const popularLooseScore = __test.scoreSkillResult(
|
|
queryTokens,
|
|
0.9,
|
|
"Different Tool",
|
|
"different-tool",
|
|
);
|
|
expect(exactScore).toBeGreaterThan(popularLooseScore);
|
|
});
|
|
|
|
it("keeps popularity from flipping a strong name match", () => {
|
|
const queryTokens = tokenize("notion");
|
|
const nameMatchScore = __test.scoreSkillResult(queryTokens, 0, "Notion Helper", "helper");
|
|
const popularVectorScore = __test.scoreSkillResult(
|
|
queryTokens,
|
|
1,
|
|
"Different Tool",
|
|
"different-tool",
|
|
);
|
|
expect(nameMatchScore).toBeGreaterThan(popularVectorScore);
|
|
});
|
|
|
|
it("keeps lifetime stars and installs out of native candidate scoring", () => {
|
|
const queryTokens = tokenize("notion");
|
|
const score = __test.scoreSkillResult(queryTokens, 0.5, "Notion Helper", "notion-helper");
|
|
expect(score).toBe(__test.getLexicalBoost(queryTokens, "Notion Helper", "notion-helper") + 0.5);
|
|
});
|
|
|
|
it("does not use lifetime installs or downloads in native candidate scoring", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const installed = {
|
|
embeddingId: "skillEmbeddings:installed",
|
|
skill: makePublicSkill({
|
|
id: "skills:installed",
|
|
slug: "tool-installed",
|
|
displayName: "Tool",
|
|
downloads: 0,
|
|
installs: 1_000,
|
|
stars: 0,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const downloaded = {
|
|
embeddingId: "skillEmbeddings:downloaded",
|
|
skill: makePublicSkill({
|
|
id: "skills:downloaded",
|
|
slug: "tool-downloaded",
|
|
displayName: "Tool",
|
|
downloads: 1_000_000_000,
|
|
installs: 0,
|
|
stars: 0,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([installed, downloaded]) // hydrateResults
|
|
.mockResolvedValueOnce([]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([
|
|
{ _id: "skillEmbeddings:installed", _score: 0.5 },
|
|
{ _id: "skillEmbeddings:downloaded", _score: 0.52 },
|
|
]),
|
|
runQuery,
|
|
},
|
|
{ query: "tool", limit: 2 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["tool-installed", "tool-downloaded"]);
|
|
});
|
|
|
|
it("does not use lifetime stars, installs, or downloads as a native tie-breaker", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const installedOnly = {
|
|
skill: makePublicSkill({
|
|
id: "skills:installed",
|
|
slug: "tool-installed",
|
|
displayName: "Tool",
|
|
downloads: 0,
|
|
installs: 1_000,
|
|
stars: 1_000,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const downloadedOnly = {
|
|
skill: makePublicSkill({
|
|
id: "skills:downloaded",
|
|
slug: "tool-downloaded",
|
|
displayName: "Tool",
|
|
downloads: 1_000_000_000,
|
|
installs: 0,
|
|
stars: 1_000,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([installedOnly, downloadedOnly]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
runQuery,
|
|
},
|
|
{ query: "tool", limit: 2 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["tool-installed", "tool-downloaded"]);
|
|
});
|
|
|
|
it("keeps native and combined lifetime downloads out of native tie-breakers", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
const indexed = {
|
|
skill: makePublicSkill({
|
|
id: "skills:indexed",
|
|
slug: "tool-indexed",
|
|
displayName: "Tool",
|
|
downloads: 10_010,
|
|
installs: 0,
|
|
stars: 0,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const native = {
|
|
skill: makePublicSkill({
|
|
id: "skills:native",
|
|
slug: "tool-native",
|
|
displayName: "Tool",
|
|
downloads: 20,
|
|
installs: 0,
|
|
stars: 0,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([indexed, native]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
runQuery,
|
|
},
|
|
{ query: "tool", limit: 2 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["tool-indexed", "tool-native"]);
|
|
});
|
|
|
|
it("uses digest doc instead of full skill doc in hydrateResults but revalidates the owner", async () => {
|
|
// Derive digest from makeSkillDoc so it stays in sync with schema changes.
|
|
const skillDoc = makeSkillDoc({
|
|
id: "skills:1",
|
|
slug: "digest-skill",
|
|
displayName: "Digest Skill",
|
|
});
|
|
const digestDoc = {
|
|
_id: "skillSearchDigest:d1",
|
|
_creationTime: 1,
|
|
skillId: skillDoc._id,
|
|
slug: skillDoc.slug,
|
|
displayName: skillDoc.displayName,
|
|
summary: skillDoc.summary,
|
|
ownerUserId: skillDoc.ownerUserId,
|
|
ownerHandle: "owner",
|
|
ownerName: "Owner",
|
|
ownerDisplayName: "Owner",
|
|
ownerImage: undefined,
|
|
canonicalSkillId: skillDoc.canonicalSkillId,
|
|
forkOf: skillDoc.forkOf,
|
|
latestVersionId: skillDoc.latestVersionId,
|
|
tags: skillDoc.tags,
|
|
badges: skillDoc.badges,
|
|
stats: skillDoc.stats,
|
|
statsDownloads: skillDoc.stats.downloads,
|
|
statsStars: skillDoc.stats.stars,
|
|
statsInstallsCurrent: skillDoc.stats.installsCurrent ?? 0,
|
|
statsInstallsAllTime: skillDoc.stats.installsAllTime ?? 0,
|
|
softDeletedAt: skillDoc.softDeletedAt,
|
|
moderationStatus: skillDoc.moderationStatus,
|
|
moderationFlags: skillDoc.moderationFlags,
|
|
moderationReason: skillDoc.moderationReason,
|
|
isSuspicious: false,
|
|
createdAt: skillDoc.createdAt,
|
|
updatedAt: skillDoc.updatedAt,
|
|
};
|
|
|
|
const getMock = vi.fn(async (id: string) => {
|
|
// Should NOT be called for skills:1 when digest exists
|
|
if (id === "skills:1") throw new Error("Should not read full skill doc");
|
|
if (id === "users:owner") {
|
|
return {
|
|
_id: "users:owner",
|
|
_creationTime: 1,
|
|
handle: "owner",
|
|
name: "Owner",
|
|
displayName: "Owner",
|
|
image: undefined,
|
|
bio: undefined,
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
const result = await hydrateResultsHandler(
|
|
{
|
|
db: {
|
|
get: getMock,
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: (index: string) => ({
|
|
unique: vi.fn(async () => {
|
|
if (table === "embeddingSkillMap" && index === "by_embedding") {
|
|
return { embeddingId: "skillEmbeddings:1", skillId: "skills:1" };
|
|
}
|
|
if (table === "skillSearchDigest" && index === "by_skill") {
|
|
return digestDoc;
|
|
}
|
|
return null;
|
|
}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
{ embeddingIds: ["skillEmbeddings:1"] },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("digest-skill");
|
|
expect(result[0].skill._id).toBe("skills:1");
|
|
expect(result[0].ownerHandle).toBe("owner");
|
|
// Owner resolved from digest — users table should NOT be read
|
|
expect(getMock).not.toHaveBeenCalledWith("users:owner");
|
|
});
|
|
|
|
it("falls back to full skill doc when digest is missing", async () => {
|
|
const result = await hydrateResultsHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return { _id: "users:owner", handle: "owner" };
|
|
if (id === "skills:1") {
|
|
return makeSkillDoc({
|
|
id: "skills:1",
|
|
slug: "fallback-skill",
|
|
displayName: "Fallback Skill",
|
|
});
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: (index: string) => ({
|
|
unique: vi.fn(async () => {
|
|
if (table === "embeddingSkillMap" && index === "by_embedding") {
|
|
return { embeddingId: "skillEmbeddings:1", skillId: "skills:1" };
|
|
}
|
|
// No digest exists — return null
|
|
return null;
|
|
}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
{ embeddingIds: ["skillEmbeddings:1"] },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("fallback-skill");
|
|
});
|
|
|
|
it("hydrates a bounded vector window for ordinary load-more searches", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const batch = Array.from({ length: 128 }, (_, i) => ({
|
|
_id: `skillEmbeddings:e${i}`,
|
|
_score: 0.5 - i * 0.001,
|
|
}));
|
|
|
|
const vectorSearchMock = vi.fn(
|
|
async (_table: unknown, _index: unknown, opts: { limit: number }) =>
|
|
batch.slice(0, opts.limit),
|
|
);
|
|
|
|
const hydrateCalls: string[][] = [];
|
|
const runQuery = vi.fn(
|
|
async (_ref: unknown, args: { embeddingIds?: string[]; query?: string; slug?: string }) => {
|
|
if (args.slug) {
|
|
return null; // getExactSkillSlugMatch
|
|
}
|
|
if (args.embeddingIds) {
|
|
hydrateCalls.push(args.embeddingIds);
|
|
return args.embeddingIds.map((embeddingId: string) => ({
|
|
embeddingId,
|
|
skill: makePublicSkill({
|
|
id: `skills:${embeddingId.split(":")[1]}`,
|
|
slug: `skill-${embeddingId.split(":")[1]}`,
|
|
displayName: `Skill ${embeddingId.split(":")[1]}`,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
}));
|
|
}
|
|
return []; // lexicalFallbackSkills
|
|
},
|
|
);
|
|
|
|
await searchSkillsHandler(
|
|
{ vectorSearch: vectorSearchMock, runQuery },
|
|
{ query: "test", limit: 50 },
|
|
);
|
|
|
|
expect(vectorSearchMock).toHaveBeenCalledTimes(2);
|
|
expect(hydrateCalls).toHaveLength(2);
|
|
expect(hydrateCalls[0]).toHaveLength(100);
|
|
expect(hydrateCalls[1]).toHaveLength(28);
|
|
});
|
|
|
|
it("merges fallback matches without duplicate skill ids", () => {
|
|
const primary = [
|
|
{
|
|
embeddingId: "skillEmbeddings:1",
|
|
skill: { _id: "skills:1" },
|
|
},
|
|
] as unknown as Parameters<typeof __test.mergeUniqueBySkillId>[0];
|
|
const fallback = [
|
|
{
|
|
skill: { _id: "skills:1" },
|
|
},
|
|
{
|
|
skill: { _id: "skills:2" },
|
|
},
|
|
] as unknown as Parameters<typeof __test.mergeUniqueBySkillId>[1];
|
|
|
|
const merged = __test.mergeUniqueBySkillId(primary, fallback);
|
|
expect(merged).toHaveLength(2);
|
|
expect(merged.map((entry) => entry.skill._id)).toEqual(["skills:1", "skills:2"]);
|
|
});
|
|
|
|
it("preserves vector scores for hydrated candidates", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const skillA = makePublicSkill({
|
|
id: "skills:a",
|
|
slug: "baidu-yijian-vision",
|
|
displayName: "Baidu Yijian Vision",
|
|
downloads: 100,
|
|
});
|
|
const skillB = makePublicSkill({
|
|
id: "skills:b",
|
|
slug: "baidu-yijian-test",
|
|
displayName: "Baidu Yijian Test",
|
|
downloads: 50,
|
|
});
|
|
|
|
const vectorResults = [
|
|
{ _id: "skillEmbeddings:a", _score: 0.95 },
|
|
{ _id: "skillEmbeddings:b", _score: 0.5 },
|
|
];
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([
|
|
{
|
|
embeddingId: "skillEmbeddings:a",
|
|
skill: skillA,
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
{
|
|
embeddingId: "skillEmbeddings:b",
|
|
skill: skillB,
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
])
|
|
// lexicalFallbackSkills (exactMatches < limit after loop exits)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi.fn().mockResolvedValueOnce(vectorResults),
|
|
runQuery,
|
|
},
|
|
{ query: "baidu yijian", limit: 50 },
|
|
);
|
|
|
|
const resultA = result.find(
|
|
(r: { skill: { slug: string } }) => r.skill.slug === "baidu-yijian-vision",
|
|
);
|
|
expect(resultA).toBeDefined();
|
|
expect(resultA!.score).toBeGreaterThan(1.0);
|
|
});
|
|
|
|
it("preserves vector scores when a direct lexical match shadows the hydrated candidate", async () => {
|
|
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
|
|
|
const lexicalEntry = {
|
|
skill: makePublicSkill({
|
|
id: "skills:the-news",
|
|
slug: "the-news",
|
|
displayName: "The News",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const vectorEntry = {
|
|
...lexicalEntry,
|
|
embeddingId: "skillEmbeddings:the-news",
|
|
};
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
|
.mockResolvedValueOnce([lexicalEntry]) // directPrefixSkillMatches
|
|
.mockResolvedValueOnce([vectorEntry]) // hydrateResults
|
|
.mockResolvedValueOnce([]); // lexicalFallbackSkills
|
|
|
|
const result = await searchSkillsHandler(
|
|
{
|
|
vectorSearch: vi
|
|
.fn()
|
|
.mockResolvedValueOnce([{ _id: "skillEmbeddings:the-news", _score: 0.33 }]),
|
|
runQuery,
|
|
},
|
|
{ query: "news", limit: 10 },
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].skill.slug).toBe("the-news");
|
|
expect(result[0].score).toBeGreaterThan(2.8);
|
|
});
|
|
|
|
it("filters pending scans before applying the search result limit", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const pending = {
|
|
skill: makePublicSkill({
|
|
id: "skills:pending",
|
|
slug: "search-term-pending",
|
|
displayName: "Search Term Pending",
|
|
githubScanStatus: "pending",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const clean = {
|
|
skill: makePublicSkill({
|
|
id: "skills:clean",
|
|
slug: "search-term-clean",
|
|
displayName: "Search Term Clean",
|
|
githubScanStatus: "clean",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
};
|
|
const runQuery = vi.fn().mockResolvedValueOnce([pending, clean]).mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{ vectorSearch: vi.fn(), runQuery },
|
|
{ query: "search term", limit: 1, excludePendingScan: true },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["search-term-clean"]);
|
|
});
|
|
|
|
it("keeps a later official hit when higher-ranked hits are ineligible", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const matches = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:exact-community",
|
|
slug: "helper",
|
|
displayName: "Helper",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "community",
|
|
owner: null,
|
|
},
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:community",
|
|
slug: "helper-community",
|
|
displayName: "Helper Community",
|
|
}),
|
|
version: null,
|
|
ownerHandle: "community",
|
|
owner: null,
|
|
},
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:official",
|
|
slug: "helper-official",
|
|
displayName: "Helper Official",
|
|
official: true,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "official",
|
|
owner: null,
|
|
},
|
|
];
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce(matches)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{ vectorSearch: vi.fn(), runQuery },
|
|
{ query: "helper", limit: 1, officialOnly: true },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["helper-official"]);
|
|
});
|
|
|
|
it("keeps a later new hit when higher-ranked hits predate the window", async () => {
|
|
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
|
|
const matches = [
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:exact-old",
|
|
slug: "helper",
|
|
displayName: "Helper",
|
|
createdAt: 10,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:older",
|
|
slug: "helper-older",
|
|
displayName: "Helper Older",
|
|
createdAt: 20,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
{
|
|
skill: makePublicSkill({
|
|
id: "skills:new",
|
|
slug: "helper-new",
|
|
displayName: "Helper New",
|
|
createdAt: 200,
|
|
}),
|
|
version: null,
|
|
ownerHandle: "owner",
|
|
owner: null,
|
|
},
|
|
];
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce(matches)
|
|
.mockResolvedValueOnce([]);
|
|
|
|
const result = await searchSkillsHandler(
|
|
{ vectorSearch: vi.fn(), runQuery },
|
|
{ query: "helper", limit: 1, createdAfter: 100 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.skill.slug)).toEqual(["helper-new"]);
|
|
});
|
|
});
|
|
|
|
function makePublicSkill(params: {
|
|
id: string;
|
|
slug: string;
|
|
displayName: string;
|
|
summary?: string;
|
|
icon?: string;
|
|
downloads?: number;
|
|
ownerPublisherId?: string;
|
|
installs?: number;
|
|
stars?: number;
|
|
categories?: string[];
|
|
topics?: string[];
|
|
official?: boolean;
|
|
createdAt?: number;
|
|
githubScanStatus?: "pending" | "clean" | "suspicious" | "malicious" | "not-run";
|
|
}) {
|
|
const badges: Record<string, { byUserId: string; at: number }> = {};
|
|
if (params.official) badges.official = { byUserId: "users:curator", at: 1 };
|
|
return {
|
|
_id: params.id,
|
|
_creationTime: 1,
|
|
slug: params.slug,
|
|
displayName: params.displayName,
|
|
summary: params.summary ?? `${params.displayName} summary`,
|
|
icon: params.icon,
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: params.ownerPublisherId,
|
|
canonicalSkillId: undefined,
|
|
forkOf: undefined,
|
|
latestVersionId: "skillVersions:1",
|
|
tags: {},
|
|
categories: params.categories,
|
|
topics: params.topics,
|
|
githubScanStatus: params.githubScanStatus,
|
|
badges,
|
|
stats: {
|
|
downloads: params.downloads ?? 0,
|
|
installs: params.installs ?? 0,
|
|
stars: params.stars ?? 0,
|
|
versions: 1,
|
|
comments: 0,
|
|
},
|
|
createdAt: params.createdAt ?? 1,
|
|
updatedAt: 1,
|
|
};
|
|
}
|
|
|
|
function makeSkillDoc(params: {
|
|
id: string;
|
|
slug: string;
|
|
displayName: string;
|
|
summary?: string;
|
|
ownerPublisherId?: string;
|
|
moderationFlags?: string[];
|
|
moderationReason?: string;
|
|
softDeletedAt?: number;
|
|
downloads?: number;
|
|
installs?: number;
|
|
stars?: number;
|
|
categories?: string[];
|
|
topics?: string[];
|
|
inferredCategories?: string[];
|
|
inferredFromVersionId?: string;
|
|
latestVersionId?: string;
|
|
moderationSourceVersionId?: string;
|
|
statsVersions?: number;
|
|
publicVersion?: { status: "available"; versionId: string } | { status: "unavailable" };
|
|
}) {
|
|
return {
|
|
...makePublicSkill(params),
|
|
latestVersionId: params.latestVersionId ?? "skillVersions:1",
|
|
stats: {
|
|
downloads: params.downloads ?? 0,
|
|
installsCurrent: 0,
|
|
installsAllTime: params.installs ?? 0,
|
|
stars: params.stars ?? 0,
|
|
versions: params.statsVersions ?? 1,
|
|
comments: 0,
|
|
},
|
|
_creationTime: 1,
|
|
moderationStatus: "active",
|
|
moderationFlags: params.moderationFlags ?? [],
|
|
moderationReason: params.moderationReason,
|
|
moderationSourceVersionId: params.moderationSourceVersionId,
|
|
softDeletedAt: params.softDeletedAt as number | undefined,
|
|
inferredCategories: params.inferredCategories,
|
|
inferredFromVersionId: params.inferredFromVersionId,
|
|
publicVersion: params.publicVersion,
|
|
};
|
|
}
|
|
|
|
function makeExternalSearchDigest(params: {
|
|
externalId: string;
|
|
publicVisible?: boolean;
|
|
installable?: boolean;
|
|
}) {
|
|
const [, , slug = "skill"] = params.externalId.split("/");
|
|
return {
|
|
_id: `skillsShMirrorDigests:${params.externalId}`,
|
|
_creationTime: 1,
|
|
externalId: params.externalId,
|
|
slug,
|
|
displayName: slug,
|
|
normalizedSlug: slug,
|
|
normalizedSlugFirstToken: slug,
|
|
normalizedDisplayName: slug,
|
|
normalizedDisplayNameFirstToken: slug,
|
|
searchText: slug,
|
|
sourceUrl: `https://skills.sh/${params.externalId}`,
|
|
upstreamInstalls: 10,
|
|
upstreamScanners: {},
|
|
inferredCategories: [],
|
|
inferredTopics: [],
|
|
sourceFreshnessStatus: "observed-only",
|
|
active: true,
|
|
publicVisible: params.publicVisible ?? true,
|
|
installable: params.installable ?? true,
|
|
lastObservedAt: 1,
|
|
};
|
|
}
|
|
|
|
function makePaginatedRows<T>(rows: T[], onPaginate?: () => void) {
|
|
return vi.fn(async ({ cursor, numItems }: { cursor: string | null; numItems: number }) => {
|
|
onPaginate?.();
|
|
const start = cursor ? Number(cursor) : 0;
|
|
const page = rows.slice(start, start + numItems);
|
|
const next = start + page.length;
|
|
return {
|
|
page,
|
|
isDone: next >= rows.length,
|
|
continueCursor: String(next),
|
|
};
|
|
});
|
|
}
|
|
|
|
function makeLexicalCtx(params: {
|
|
exactSlugSkill?: ReturnType<typeof makeSkillDoc> | null;
|
|
exactSlugSkills?: Array<ReturnType<typeof makeSkillDoc>>;
|
|
officialPublisherIds?: string[];
|
|
recentSkills: Array<ReturnType<typeof makeSkillDoc>>;
|
|
recentByCreated?: Array<ReturnType<typeof makeSkillDoc>>;
|
|
}) {
|
|
const exactSlugSkills =
|
|
params.exactSlugSkills ?? (params.exactSlugSkill ? [params.exactSlugSkill] : []);
|
|
const officialPublisherIds = new Set(params.officialPublisherIds ?? []);
|
|
// Convert skill docs to digest-shaped rows (add skillId + owner fields).
|
|
const toDigestRows = (skills: Array<ReturnType<typeof makeSkillDoc>>) =>
|
|
skills.map((skill) => ({
|
|
...skill,
|
|
skillId: skill._id,
|
|
ownerHandle: "owner",
|
|
ownerName: "Owner",
|
|
ownerDisplayName: "Owner",
|
|
ownerImage: undefined,
|
|
}));
|
|
const digestByUpdated = toDigestRows(params.recentSkills);
|
|
const digestByCreated = toDigestRows(params.recentByCreated ?? []);
|
|
const usedIndexes: string[] = [];
|
|
const takeLimits: number[] = [];
|
|
let paginateCalls = 0;
|
|
return {
|
|
usedIndexes,
|
|
takeLimits,
|
|
get paginateCalls() {
|
|
return paginateCalls;
|
|
},
|
|
db: {
|
|
query: vi.fn((table: string) => {
|
|
if (table === "officialPublishers") {
|
|
return {
|
|
withIndex: (
|
|
index: string,
|
|
builder: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
usedIndexes.push(index);
|
|
let publisherId = "";
|
|
const q = {
|
|
eq: (field: string, value: unknown) => {
|
|
if (field === "publisherId") publisherId = String(value);
|
|
return q;
|
|
},
|
|
};
|
|
builder(q);
|
|
return {
|
|
unique: vi.fn(async () =>
|
|
officialPublisherIds.has(publisherId) ? { publisherId } : null,
|
|
),
|
|
};
|
|
},
|
|
};
|
|
}
|
|
if (table === "skills") {
|
|
return {
|
|
withIndex: (index: string) => {
|
|
usedIndexes.push(index);
|
|
if (index === "by_slug") {
|
|
return {
|
|
unique: vi.fn(async () => {
|
|
if (exactSlugSkills.length > 1) {
|
|
throw new Error("unique should not be used for duplicate exact slug matches");
|
|
}
|
|
return exactSlugSkills[0] ?? null;
|
|
}),
|
|
take: vi.fn(async (limit: number) => exactSlugSkills.slice(0, limit)),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected skills index ${index}`);
|
|
},
|
|
};
|
|
}
|
|
if (table === "skillSearchDigest") {
|
|
return {
|
|
withIndex: (index: string) => {
|
|
usedIndexes.push(index);
|
|
if (index === "by_active_updated" || index === "by_nonsuspicious_updated") {
|
|
return {
|
|
order: () => ({
|
|
take: vi.fn((limit: number) => {
|
|
takeLimits.push(limit);
|
|
return Promise.resolve(digestByUpdated);
|
|
}),
|
|
paginate: makePaginatedRows(digestByUpdated, () => {
|
|
paginateCalls += 1;
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
if (index === "by_active_created" || index === "by_nonsuspicious_created") {
|
|
return {
|
|
order: () => ({
|
|
take: vi.fn((limit: number) => {
|
|
takeLimits.push(limit);
|
|
return Promise.resolve(digestByCreated);
|
|
}),
|
|
paginate: makePaginatedRows(digestByCreated, () => {
|
|
paginateCalls += 1;
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected digest index ${index}`);
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
get: vi.fn(async (id: string) => {
|
|
if (id.startsWith("publishers:")) {
|
|
const handle = id.split(":")[1] ?? "owner";
|
|
return {
|
|
_id: id,
|
|
_creationTime: 1,
|
|
kind: "org",
|
|
handle,
|
|
displayName: handle,
|
|
image: undefined,
|
|
bio: undefined,
|
|
linkedUserId: undefined,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
};
|
|
}
|
|
if (id.startsWith("users:")) return { _id: id, handle: "owner" };
|
|
if (id.startsWith("skillVersions:")) return { _id: id, version: "1.0.0" };
|
|
return null;
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeDirectPrefixCtx(skills: Array<ReturnType<typeof makeSkillDoc>>) {
|
|
const firstToken = (value: string) => value.toLowerCase().match(/[a-z0-9]+/)?.[0];
|
|
// Token-level splitter that mirrors Convex full-text inverted index behavior:
|
|
// any alphanumeric run of length >= 1 becomes a token, regardless of position.
|
|
const tokensOf = (value: string): string[] =>
|
|
(value.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter(Boolean);
|
|
const digestRows = skills.map((skill) => ({
|
|
...skill,
|
|
skillId: skill._id,
|
|
normalizedSlug: skill.slug.toLowerCase(),
|
|
normalizedSlugFirstToken: firstToken(skill.slug),
|
|
normalizedDisplayName: skill.displayName.toLowerCase(),
|
|
normalizedDisplayNameFirstToken: firstToken(skill.displayName),
|
|
isSuspicious: (skill.moderationFlags ?? []).includes("flagged.suspicious"),
|
|
ownerHandle: "owner",
|
|
ownerName: "Owner",
|
|
ownerDisplayName: "Owner",
|
|
ownerImage: undefined,
|
|
}));
|
|
const usedIndexes: string[] = [];
|
|
const usedSearchIndexes: string[] = [];
|
|
const takeLimits: number[] = [];
|
|
let paginateCalls = 0;
|
|
return {
|
|
usedIndexes,
|
|
usedSearchIndexes,
|
|
takeLimits,
|
|
get paginateCalls() {
|
|
return paginateCalls;
|
|
},
|
|
db: {
|
|
query: vi.fn((table: string) => {
|
|
if (table === "skillTopicSearchDigest") {
|
|
return {
|
|
withIndex: (
|
|
index: string,
|
|
builder: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
gte: (field: string, value: unknown) => unknown;
|
|
lt: (field: string, value: unknown) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
usedIndexes.push(index);
|
|
let topic = "";
|
|
let topicPrefix = "";
|
|
const q = {
|
|
eq: (field: string, value: unknown) => {
|
|
if (field === "topic") topic = String(value);
|
|
return q;
|
|
},
|
|
gte: (field: string, value: unknown) => {
|
|
if (field === "topic") topicPrefix = String(value);
|
|
return q;
|
|
},
|
|
lt: () => q,
|
|
};
|
|
builder(q);
|
|
const rows = digestRows
|
|
.filter((digest) =>
|
|
digest.topics?.some((value) => {
|
|
const topicSlug = tokenize(value).join("-");
|
|
return topic ? topicSlug === topic : topicSlug.startsWith(topicPrefix);
|
|
}),
|
|
)
|
|
.map((digest) => ({
|
|
skillId: digest.skillId,
|
|
topic: topic || topicPrefix,
|
|
}));
|
|
return {
|
|
order: () => ({
|
|
take: vi.fn(async (limit: number) => {
|
|
takeLimits.push(limit);
|
|
return rows.slice(0, limit);
|
|
}),
|
|
paginate: makePaginatedRows(rows, () => {
|
|
paginateCalls += 1;
|
|
}),
|
|
}),
|
|
};
|
|
},
|
|
};
|
|
}
|
|
if (table === "officialPublishers") {
|
|
return {
|
|
withIndex: () => ({
|
|
unique: vi.fn(async () => null),
|
|
}),
|
|
};
|
|
}
|
|
if (table !== "skillSearchDigest") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: (index: string, builder: (q: unknown) => unknown) => {
|
|
usedIndexes.push(index);
|
|
const range: Record<string, string> = {};
|
|
const equality: Record<string, unknown> = {};
|
|
const q = {
|
|
eq: (field: string, value: unknown) => {
|
|
equality[field] = value;
|
|
return q;
|
|
},
|
|
gte: (field: string, value: string) => {
|
|
range[field] = value;
|
|
return q;
|
|
},
|
|
lt: () => q,
|
|
};
|
|
builder(q);
|
|
if (index === "by_skill") {
|
|
return {
|
|
unique: vi.fn(
|
|
async () =>
|
|
digestRows.find((digest) => digest.skillId === equality.skillId) ?? null,
|
|
),
|
|
};
|
|
}
|
|
const rows = digestRows.filter((digest) => {
|
|
const field = index.includes("first_token")
|
|
? index.includes("slug")
|
|
? "normalizedSlugFirstToken"
|
|
: "normalizedDisplayNameFirstToken"
|
|
: index.includes("slug")
|
|
? "normalizedSlug"
|
|
: "normalizedDisplayName";
|
|
const prefix = range[field] ?? "";
|
|
return (digest[field] ?? "").startsWith(prefix);
|
|
});
|
|
return {
|
|
take: vi.fn(async (limit: number) => {
|
|
takeLimits.push(limit);
|
|
return rows.slice(0, limit);
|
|
}),
|
|
paginate: makePaginatedRows(rows, () => {
|
|
paginateCalls += 1;
|
|
}),
|
|
};
|
|
},
|
|
// Mock for the new `searchIndex`-backed full-text queries added to
|
|
// `directPrefixSkillMatches`. Mirrors Convex's documented semantics:
|
|
// tokenize on alphanumeric runs (case-insensitive) and match a row
|
|
// when *any* token in the search field equals *any* token of the
|
|
// user query — i.e. position-independent, unlike `withIndex` which
|
|
// only does string-prefix matches against a normalized field.
|
|
withSearchIndex: (
|
|
indexName: string,
|
|
builder: (q: {
|
|
search: (field: string, query: string) => unknown;
|
|
eq: (field: string, value: unknown) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
usedSearchIndexes.push(indexName);
|
|
let searchField = "";
|
|
let searchQuery = "";
|
|
const filters: Array<{ field: string; value: unknown }> = [];
|
|
const q = {
|
|
search: (field: string, query: string) => {
|
|
searchField = field;
|
|
searchQuery = query;
|
|
return q;
|
|
},
|
|
eq: (field: string, value: unknown) => {
|
|
filters.push({ field, value });
|
|
return q;
|
|
},
|
|
};
|
|
builder(q);
|
|
const queryTokens = new Set(tokensOf(searchQuery));
|
|
const rows =
|
|
queryTokens.size === 0
|
|
? []
|
|
: digestRows.filter((digest) => {
|
|
for (const filter of filters) {
|
|
if ((digest as Record<string, unknown>)[filter.field] !== filter.value) {
|
|
return false;
|
|
}
|
|
}
|
|
const fieldValue =
|
|
(digest as unknown as Record<string, string | undefined>)[searchField] ?? "";
|
|
const fieldTokens = new Set(tokensOf(fieldValue));
|
|
for (const token of queryTokens) {
|
|
if (fieldTokens.has(token)) return true;
|
|
}
|
|
return false;
|
|
});
|
|
return {
|
|
take: vi.fn(async (limit: number) => {
|
|
takeLimits.push(limit);
|
|
return rows.slice(0, limit);
|
|
}),
|
|
paginate: makePaginatedRows(rows, () => {
|
|
paginateCalls += 1;
|
|
}),
|
|
};
|
|
},
|
|
};
|
|
}),
|
|
get: vi.fn(async (id: string) => {
|
|
if (id.startsWith("users:")) return { _id: id, handle: "owner" };
|
|
if (id.startsWith("skillVersions:")) return { _id: id, version: "1.0.0" };
|
|
return null;
|
|
}),
|
|
},
|
|
};
|
|
}
|