Files
clawhub/convex/httpApiV1.handlers.test.ts
T
Patrick Erichsen 7accfb71c7 chore: remove registry artifact backup jobs
Stop custom registry artifact backup/backfill/restore behavior now that Convex backups with file storage are the recovery source of truth. Legacy backup schema tables remain inert until a separate verified cleanup removes stored rows.
2026-06-19 13:35:44 -07:00

14534 lines
484 KiB
TypeScript

/* @vitest-environment node */
import { gzipSync, strFromU8, unzipSync } from "fflate";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { api, internal } from "./_generated/api";
import { RATE_LIMITS } from "./lib/httpRateLimit";
import { MAX_PUBLISH_FILE_BYTES } from "./lib/publishLimits";
vi.mock("@convex-dev/auth/server", () => ({
getAuthUserId: vi.fn(),
}));
vi.mock("./lib/apiTokenAuth", () => ({
requireApiTokenUser: vi.fn(),
getOptionalApiTokenUser: vi.fn(),
getOptionalApiTokenUserId: vi.fn(),
requirePackagePublishAuth: vi.fn(),
}));
vi.mock("./lib/githubActionsOidc", () => ({
fetchGitHubRepositoryIdentity: vi.fn(),
verifyGitHubActionsTrustedPublishJwt: vi.fn(),
}));
vi.mock("./skills", () => ({
publishVersionForUser: vi.fn(),
}));
const { getAuthUserId } = await import("@convex-dev/auth/server");
const {
getOptionalApiTokenUser,
getOptionalApiTokenUserId,
requireApiTokenUser,
requirePackagePublishAuth,
} = await import("./lib/apiTokenAuth");
const { fetchGitHubRepositoryIdentity, verifyGitHubActionsTrustedPublishJwt } =
await import("./lib/githubActionsOidc");
const { buildBundleFingerprint } = await import("./lib/skillCards");
const { publishVersionForUser } = await import("./skills");
const { __handlers } = await import("./httpApiV1");
type ActionCtx = import("./_generated/server").ActionCtx;
type RateLimitArgs = { key: string; limit: number; windowMs: number };
function isRateLimitArgs(args: unknown): args is RateLimitArgs {
if (!args || typeof args !== "object") return false;
const value = args as Record<string, unknown>;
return (
typeof value.key === "string" &&
typeof value.limit === "number" &&
typeof value.windowMs === "number"
);
}
function hasSlugArgs(args: unknown): args is { slug: string } {
if (!args || typeof args !== "object") return false;
const value = args as Record<string, unknown>;
return typeof value.slug === "string";
}
function hasPackageNameArgs(args: unknown): args is { name: string } {
if (!args || typeof args !== "object") return false;
const value = args as Record<string, unknown>;
return typeof value.name === "string";
}
function hasPluginRecommendedScoreReadinessArgs(
args: unknown,
): args is { families: Array<"code-plugin" | "bundle-plugin"> } {
if (!args || typeof args !== "object") return false;
const value = args as Record<string, unknown>;
return (
Array.isArray(value.families) &&
value.families.includes("code-plugin") &&
value.families.includes("bundle-plugin")
);
}
function hasPackageDownloadMetricTarget(args: unknown, packageId: string) {
if (!args || typeof args !== "object") return false;
const value = args as Record<string, unknown>;
const target = value.target;
if (!target || typeof target !== "object") return false;
const targetValue = target as Record<string, unknown>;
return targetValue.kind === "package" && targetValue.id === packageId;
}
function findRateLimitCallArgs(mock: ReturnType<typeof vi.fn>) {
return mock.mock.calls.map(([, args]) => args).find(isRateLimitArgs);
}
function makeInstallResolverRunQuery({
skill,
source = null,
publicVisible = true,
}: {
skill: Record<string, unknown> | null;
source?: Record<string, unknown> | null;
publicVisible?: boolean;
}) {
let slugQueryCount = 0;
return vi.fn(async (query: unknown, args: Record<string, unknown>) => {
void query;
if ("sourceId" in args) return source;
if ("slug" in args) {
slugQueryCount += 1;
if (slugQueryCount === 1) {
return skill;
}
if (slugQueryCount === 2) {
return publicVisible && skill
? {
skill: {
_id: skill._id,
slug: skill.slug,
displayName: skill.displayName,
},
}
: null;
}
}
throw new Error(`unexpected query ${JSON.stringify(args)}`);
});
}
function makeCatalogItem(
name: string,
options: {
family: "code-plugin" | "bundle-plugin" | "skill";
updatedAt: number;
score?: number;
stats?: { downloads: number; installs: number; stars: number; versions: number };
},
) {
return {
name,
displayName: name,
family: options.family,
channel: "community",
isOfficial: false,
createdAt: options.updatedAt,
updatedAt: options.updatedAt,
...(typeof options.score === "number" ? { score: options.score } : {}),
...(options.stats ? { stats: options.stats } : {}),
};
}
const TAR_BLOCK_SIZE = 512;
function tarOctal(value: number, width: number) {
return value.toString(8).padStart(width - 1, "0") + "\0";
}
function writeTarString(target: Uint8Array, offset: number, width: number, value: string) {
const encoded = new TextEncoder().encode(value);
target.set(encoded.subarray(0, width), offset);
}
function tarFile(path: string, content: string | Uint8Array) {
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
const header = new Uint8Array(TAR_BLOCK_SIZE);
writeTarString(header, 0, 100, path);
writeTarString(header, 100, 8, tarOctal(0o644, 8));
writeTarString(header, 108, 8, tarOctal(0, 8));
writeTarString(header, 116, 8, tarOctal(0, 8));
writeTarString(header, 124, 12, tarOctal(bytes.byteLength, 12));
writeTarString(header, 136, 12, tarOctal(0, 12));
header.fill(0x20, 148, 156);
header[156] = "0".charCodeAt(0);
writeTarString(header, 257, 6, "ustar");
writeTarString(header, 263, 2, "00");
let checksum = 0;
for (const byte of header) checksum += byte;
writeTarString(header, 148, 8, tarOctal(checksum, 8));
const paddedSize = Math.ceil(bytes.byteLength / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
const body = new Uint8Array(paddedSize);
body.set(bytes);
return [header, body];
}
function npmPackFixture(files: Record<string, string | Uint8Array>) {
const parts: Uint8Array[] = [];
for (const [path, content] of Object.entries(files)) {
parts.push(...tarFile(path, content));
}
parts.push(new Uint8Array(TAR_BLOCK_SIZE), new Uint8Array(TAR_BLOCK_SIZE));
const size = parts.reduce((sum, part) => sum + part.byteLength, 0);
const tar = new Uint8Array(size);
let offset = 0;
for (const part of parts) {
tar.set(part, offset);
offset += part.byteLength;
}
return gzipSync(tar);
}
function bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
const copy = new Uint8Array(bytes.byteLength);
copy.set(bytes);
return copy.buffer;
}
function packagePublishMetadata(overrides: Record<string, unknown> = {}) {
return {
name: "demo-plugin",
family: "bundle-plugin",
version: "1.0.0",
changelog: "init",
...overrides,
};
}
function packagePublishForm(payload: Record<string, unknown>) {
const form = new FormData();
form.set("payload", JSON.stringify(payload));
return form;
}
function makeCtx(partial: Record<string, unknown>) {
const rateLimitStatus =
typeof partial.rateLimitStatus === "function"
? (partial.rateLimitStatus as (args: RateLimitArgs) => unknown)
: null;
const partialRunQuery =
typeof partial.runQuery === "function"
? (partial.runQuery as (query: unknown, args: Record<string, unknown>) => unknown)
: null;
const runQuery = vi.fn(async (query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) {
return rateLimitStatus?.(args) ?? { ...okRate(), limit: args.limit };
}
return partialRunQuery ? await partialRunQuery(query, args) : null;
});
const runMutation =
typeof partial.runMutation === "function"
? partial.runMutation
: vi.fn().mockResolvedValue(okRate());
return { ...partial, runQuery, runMutation } as unknown as ActionCtx;
}
const okRate = () => ({
allowed: true,
remaining: 10,
limit: 100,
resetAt: Date.now() + 60_000,
});
const blockedRate = () => ({
allowed: false,
remaining: 0,
limit: 100,
resetAt: Date.now() + 60_000,
});
beforeEach(() => {
vi.unstubAllEnvs();
vi.mocked(getAuthUserId).mockReset();
vi.mocked(getAuthUserId).mockResolvedValue(null);
vi.mocked(getOptionalApiTokenUser).mockReset();
vi.mocked(getOptionalApiTokenUser).mockResolvedValue(null);
vi.mocked(getOptionalApiTokenUserId).mockReset();
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue(null);
vi.mocked(requireApiTokenUser).mockReset();
vi.mocked(requirePackagePublishAuth).mockReset();
vi.mocked(fetchGitHubRepositoryIdentity).mockReset();
vi.mocked(verifyGitHubActionsTrustedPublishJwt).mockReset();
vi.mocked(publishVersionForUser).mockReset();
});
describe("httpApiV1 handlers", () => {
it("rejects local scan upload submissions with scan-download guidance", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:owner",
user: { _id: "users:owner", role: "user" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error(`unexpected mutation ${JSON.stringify(args)}`);
});
const response = await __handlers.skillScanSubmitV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/-/scan", {
method: "POST",
body: JSON.stringify({ source: { kind: "upload" } }),
}),
);
expect(response.status).toBe(410);
expect(await response.text()).toContain("clawhub scan download <slug> --version <version>");
});
it("downloads stored scan reports for submitted skill versions", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:owner",
user: { _id: "users:owner", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
expect(args).toMatchObject({
actorUserId: "users:owner",
kind: "skill",
name: "demo-skill",
version: "1.2.3",
});
return {
ok: true,
scanId: "skill:demo-skill:1.2.3",
status: "succeeded",
sourceKind: "published",
update: false,
writtenBack: true,
artifact: {
kind: "skill",
slug: "demo-skill",
version: "1.2.3",
},
report: {
clawscan: { status: "malicious", checkedAt: 1 },
skillspector: null,
staticAnalysis: null,
virustotal: null,
},
createdAt: 1,
updatedAt: 1,
completedAt: 1,
};
});
const response = await __handlers.skillScanGetRouterV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/skills/-/scan/download/demo-skill?version=1.2.3"),
);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("application/zip");
expect(response.headers.get("Content-Disposition")).toBe(
'attachment; filename="clawhub-scan-demo-skill-1.2.3.zip"',
);
const bytes = new Uint8Array(await response.arrayBuffer());
expect(bytes.byteLength).toBeGreaterThan(0);
const entries = unzipSync(bytes);
const readme = entries["README.md"] ? strFromU8(entries["README.md"]) : "";
expect(readme).toContain("ClawScan is the primary security verdict");
expect(readme).toContain("`malicious` means ClawHub blocked the submitted version");
expect(readme).toContain("VirusTotal results are supporting reputation telemetry");
expect(readme).toContain("`clawscan.json`: final ClawScan verdict");
});
it("search returns empty results for blank query", async () => {
const runAction = vi.fn();
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "me") return { publisherId: "publishers:me" };
return okRate();
});
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction, runMutation }),
new Request("https://example.com/api/v1/search?q=%20%20"),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(await response.json()).toEqual({ results: [] });
expect(runAction).not.toHaveBeenCalled();
});
it("skills export allows authenticated non-admin users at the key rate limit", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return { ...okRate(), limit: args.limit };
return { ok: true };
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) return { page: [], nextCursor: null, hasMore: false };
return null;
});
const response = await __handlers.exportSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=2", {
headers: { authorization: "Bearer user-token" },
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("X-RateLimit-Limit")).toBe(String(RATE_LIMITS.export.key));
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
key: "user:users:actor:export",
limit: RATE_LIMITS.export.key,
}),
);
});
it("skills export defaults to the proven 250 item page limit", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) return { page: [], nextCursor: null, hasMore: false };
return null;
});
const response = await __handlers.exportSkillsV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=2", {
headers: { authorization: "Bearer user-token" },
}),
);
expect(response.status).toBe(200);
const listCall = runQuery.mock.calls.find(([, args]) => "startDate" in args);
expect(listCall?.[1]).toMatchObject({ numItems: 250 });
});
it("skills export rejects pages above 250 items", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn();
const response = await __handlers.exportSkillsV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=2&limit=251", {
headers: { authorization: "Bearer user-token" },
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("limit must be <= 250");
expect(runQuery).not.toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills.listByDateRange,
expect.anything(),
);
});
it("skills export rejects unauthenticated requests", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValue(new Error("Unauthorized"));
const runQuery = vi.fn();
const response = await __handlers.exportSkillsV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=2"),
);
expect(response.status).toBe(401);
expect(runQuery).not.toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills.listByDateRange,
expect.anything(),
);
});
it("skills export preserves pagination headers for empty filtered pages", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) return { page: [], nextCursor: "next-page", hasMore: true };
return null;
});
const response = await __handlers.exportSkillsV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=2", {
headers: { authorization: "Bearer user-token" },
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("X-Next-Cursor")).toBe("next-page");
expect(response.headers.get("X-Has-More")).toBe("true");
});
it("skills export namespaces files by publisher and slug", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) {
return {
page: [
{
skillId: "skills:alice",
slug: "demo",
displayName: "Alice Demo",
latestVersionId: "skillVersions:alice",
createdAt: 1,
updatedAt: 2,
stats: {},
ownerUserId: "users:alice",
ownerHandle: "alice",
ownerDisplayName: "Alice",
},
{
skillId: "skills:bob",
slug: "demo",
displayName: "Bob Demo",
latestVersionId: "skillVersions:bob",
createdAt: 1,
updatedAt: 3,
stats: {},
ownerUserId: "users:bob",
ownerHandle: "bob",
ownerDisplayName: "Bob",
},
],
nextCursor: null,
hasMore: false,
};
}
if (args.versionId === "skillVersions:alice") {
return {
skillId: "skills:alice",
version: "1.0.0",
files: [{ storageId: "storage:alice", path: "SKILL.md" }],
};
}
if (args.versionId === "skillVersions:bob") {
return {
skillId: "skills:bob",
version: "1.0.0",
files: [{ storageId: "storage:bob", path: "SKILL.md" }],
};
}
return null;
});
const response = await __handlers.exportSkillsV1Handler(
makeCtx({
runQuery,
storage: {
get: vi.fn(
async (storageId: string) =>
new Blob([storageId === "storage:alice" ? "alice" : "bob"]),
),
},
}),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=5", {
headers: { authorization: "Bearer user-token" },
}),
);
if (response.status !== 200) throw new Error(await response.text());
const zipEntries = unzipSync(new Uint8Array(await response.arrayBuffer()));
expect(Object.keys(zipEntries).sort()).toEqual([
"_manifest.json",
"alice/demo/SKILL.md",
"alice/demo/_export_skill_meta.json",
"bob/demo/SKILL.md",
"bob/demo/_export_skill_meta.json",
]);
});
it("skills export skips stale latest versions before reading blobs", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) {
return {
page: [
{
skillId: "skills:demo",
slug: "demo",
displayName: "Demo",
latestVersionId: "skillVersions:other",
createdAt: 1,
updatedAt: 2,
stats: {},
ownerUserId: "users:alice",
ownerHandle: "alice",
ownerDisplayName: "Alice",
},
{
skillId: "skills:deleted",
slug: "deleted",
displayName: "Deleted",
latestVersionId: "skillVersions:deleted",
createdAt: 1,
updatedAt: 3,
stats: {},
ownerUserId: "users:bob",
ownerHandle: "bob",
ownerDisplayName: "Bob",
},
],
nextCursor: null,
hasMore: false,
};
}
if (args.versionId === "skillVersions:other") {
return {
skillId: "skills:other",
version: "9.9.9",
files: [{ storageId: "storage:other", path: "SKILL.md" }],
softDeletedAt: undefined,
};
}
if (args.versionId === "skillVersions:deleted") {
return {
skillId: "skills:deleted",
version: "1.0.0",
files: [{ storageId: "storage:deleted", path: "SKILL.md" }],
softDeletedAt: 123,
};
}
return null;
});
const storageGet = vi.fn();
const response = await __handlers.exportSkillsV1Handler(
makeCtx({ runQuery, storage: { get: storageGet } }),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=5", {
headers: { authorization: "Bearer user-token" },
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(response.headers.get("X-Export-Errors")).toBe("2");
expect(response.headers.get("X-Total-Returned")).toBe("0");
expect(storageGet).not.toHaveBeenCalled();
const zipEntries = unzipSync(new Uint8Array(await response.arrayBuffer()));
const errors = JSON.parse(new TextDecoder().decode(zipEntries["_errors.json"]));
expect(errors).toEqual([
{
slug: "demo",
error: "version not found (latestVersionId: skillVersions:other)",
},
{
slug: "deleted",
error: "version not available (latestVersionId: skillVersions:deleted)",
},
]);
expect(Object.keys(zipEntries).some((path) => path.endsWith("/SKILL.md"))).toBe(false);
});
it("skills export logs generation failure context", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) {
return {
page: [
{
skillId: "skills:demo",
slug: "demo",
displayName: "Demo",
latestVersionId: "skillVersions:demo",
createdAt: 1,
updatedAt: 2,
stats: {},
ownerUserId: "users:alice",
ownerHandle: "alice",
ownerDisplayName: "Alice",
},
],
nextCursor: null,
hasMore: false,
};
}
if (args.versionId === "skillVersions:demo") {
return {
skillId: "skills:demo",
version: "1.0.0",
files: [
{ storageId: "storage:one", path: "SKILL.md" },
{ storageId: "storage:two", path: "SKILL.md" },
],
};
}
return null;
});
try {
await expect(
__handlers.exportSkillsV1Handler(
makeCtx({
runQuery,
storage: { get: vi.fn(async () => new Blob(["content"])) },
}),
new Request("https://example.com/api/v1/skills/export?startDate=1&endDate=5", {
headers: { authorization: "Bearer user-token" },
}),
),
).rejects.toThrow(/Duplicate ZIP path/);
expect(consoleError).toHaveBeenCalledWith(
"skills_export_failed",
expect.objectContaining({
phase: "build_zip",
startDate: 1,
endDate: 5,
limit: 250,
cursorPresent: false,
pageLength: 1,
versionCount: 1,
blobTaskCount: 2,
blobCount: 2,
zipEntryCount: 3,
manifestCount: 1,
exportErrorCount: 0,
totalExportBytes: 14,
errorName: "Error",
}),
);
expect(JSON.stringify(consoleError.mock.calls)).not.toContain("user-token");
} finally {
consoleError.mockRestore();
}
});
it("plugins export defaults to both plugin families with the proven 250 item page limit", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) return { page: [], nextCursor: null, hasMore: false };
return null;
});
const response = await __handlers.exportPluginsV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/plugins/export?startDate=1&endDate=2", {
headers: { authorization: "Bearer user-token" },
}),
);
expect(response.status).toBe(200);
const listCall = runQuery.mock.calls.find(([, args]) => "startDate" in args);
expect(listCall?.[1]).toMatchObject({
family: undefined,
numItems: 250,
startDate: 1,
endDate: 2,
});
});
it("plugins export accepts a single plugin family filter", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) return { page: [], nextCursor: null, hasMore: false };
return null;
});
const response = await __handlers.exportPluginsV1Handler(
makeCtx({ runQuery }),
new Request(
"https://example.com/api/v1/plugins/export?startDate=1&endDate=2&family=code-plugin",
{ headers: { authorization: "Bearer user-token" } },
),
);
expect(response.status).toBe(200);
const listCall = runQuery.mock.calls.find(([, args]) => "startDate" in args);
expect(listCall?.[1]).toMatchObject({ family: "code-plugin" });
});
it("plugins export rejects non-plugin family filters", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn();
const response = await __handlers.exportPluginsV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/plugins/export?startDate=1&endDate=2&family=skill", {
headers: { authorization: "Bearer user-token" },
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("family must be code-plugin or bundle-plugin");
expect(runQuery).not.toHaveBeenCalled();
});
it("plugins export namespaces latest release files by family and package name", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) {
return {
page: [
{
packageId: "packages:code",
name: "@scope/demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
latestReleaseId: "packageReleases:code",
latestVersion: "1.0.0",
createdAt: 1,
updatedAt: 2,
stats: { downloads: 1, installs: 0, stars: 0, versions: 1 },
ownerUserId: "users:alice",
ownerHandle: "alice",
ownerDisplayName: "Alice",
},
{
packageId: "packages:bundle",
name: "demo-bundle",
displayName: "Demo Bundle",
family: "bundle-plugin",
latestReleaseId: "packageReleases:bundle",
latestVersion: "2.0.0",
createdAt: 1,
updatedAt: 3,
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
ownerUserId: "users:bob",
ownerHandle: "bob",
ownerDisplayName: "Bob",
},
],
nextCursor: null,
hasMore: false,
};
}
if (args.releaseId === "packageReleases:code") {
return {
packageId: "packages:code",
version: "1.0.0",
changelog: "Initial code plugin",
createdAt: 2,
files: [
{
storageId: "storage:package-json",
path: "package.json",
size: 2,
sha256: "sha-package-json",
contentType: "application/json",
},
],
artifactKind: "npm-pack",
softDeletedAt: undefined,
};
}
if (args.releaseId === "packageReleases:bundle") {
return {
packageId: "packages:bundle",
version: "2.0.0",
changelog: "Initial bundle plugin",
createdAt: 3,
files: [
{
storageId: "storage:bundle",
path: "openclaw.bundle.json",
size: 2,
sha256: "sha-bundle",
contentType: "application/json",
},
],
artifactKind: "legacy-zip",
softDeletedAt: undefined,
};
}
return null;
});
const response = await __handlers.exportPluginsV1Handler(
makeCtx({
runQuery,
storage: {
get: vi.fn(
async (storageId: string) =>
new Blob([storageId === "storage:package-json" ? "{}" : "[]"]),
),
},
}),
new Request("https://example.com/api/v1/plugins/export?startDate=1&endDate=5", {
headers: { authorization: "Bearer user-token" },
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(response.headers.get("X-Total-Returned")).toBe("2");
expect(response.headers.get("X-Export-Errors")).toBe("0");
const zipEntries = unzipSync(new Uint8Array(await response.arrayBuffer()));
expect(Object.keys(zipEntries).sort()).toEqual([
"__clawhub_export/bundle-plugin/demo-bundle/plugin_meta.json",
"__clawhub_export/code-plugin/@scope/demo-plugin/plugin_meta.json",
"_manifest.json",
"bundle-plugin/demo-bundle/openclaw.bundle.json",
"code-plugin/@scope/demo-plugin/package.json",
]);
});
it("plugins export skips releases blocked from normal downloads", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) {
return {
page: [
{
packageId: "packages:blocked",
name: "blocked-plugin",
displayName: "Blocked Plugin",
family: "code-plugin",
latestReleaseId: "packageReleases:blocked",
latestVersion: "1.0.0",
createdAt: 1,
updatedAt: 2,
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
ownerUserId: "users:alice",
ownerHandle: "alice",
ownerDisplayName: "Alice",
},
],
nextCursor: null,
hasMore: false,
};
}
if (args.releaseId === "packageReleases:blocked") {
return {
packageId: "packages:blocked",
version: "1.0.0",
changelog: "Blocked",
createdAt: 2,
files: [
{
storageId: "storage:blocked",
path: "package.json",
size: 2,
sha256: "sha-blocked",
contentType: "application/json",
},
],
manualModeration: {
state: "quarantined",
reason: "malware",
reviewerUserId: "users:mod",
updatedAt: 2,
},
artifactKind: "npm-pack",
softDeletedAt: undefined,
};
}
return null;
});
const storageGet = vi.fn();
const response = await __handlers.exportPluginsV1Handler(
makeCtx({ runQuery, storage: { get: storageGet } }),
new Request("https://example.com/api/v1/plugins/export?startDate=1&endDate=5", {
headers: { authorization: "Bearer user-token" },
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(response.headers.get("X-Total-Returned")).toBe("0");
expect(response.headers.get("X-Export-Errors")).toBe("1");
expect(storageGet).not.toHaveBeenCalled();
const zipEntries = unzipSync(new Uint8Array(await response.arrayBuffer()));
expect(Object.keys(zipEntries).sort()).toEqual(["_errors.json", "_manifest.json"]);
});
it("plugins export metadata does not collide with plugin files", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("startDate" in args) {
return {
page: [
{
packageId: "packages:collision",
name: "collision-plugin",
displayName: "Collision Plugin",
family: "code-plugin",
latestReleaseId: "packageReleases:collision",
latestVersion: "1.0.0",
createdAt: 1,
updatedAt: 2,
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
ownerUserId: "users:alice",
ownerHandle: "alice",
ownerDisplayName: "Alice",
},
],
nextCursor: null,
hasMore: false,
};
}
if (args.releaseId === "packageReleases:collision") {
return {
packageId: "packages:collision",
version: "1.0.0",
changelog: "Collision",
createdAt: 2,
files: [
{
storageId: "storage:plugin-meta-file",
path: "_export_plugin_meta.json",
size: 2,
sha256: "sha-plugin-meta-file",
contentType: "application/json",
},
],
artifactKind: "npm-pack",
softDeletedAt: undefined,
};
}
return null;
});
const response = await __handlers.exportPluginsV1Handler(
makeCtx({
runQuery,
storage: { get: vi.fn(async () => new Blob(["{}"])) },
}),
new Request("https://example.com/api/v1/plugins/export?startDate=1&endDate=5", {
headers: { authorization: "Bearer user-token" },
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(response.headers.get("X-Total-Returned")).toBe("1");
expect(response.headers.get("X-Export-Errors")).toBe("0");
const zipEntries = unzipSync(new Uint8Array(await response.arrayBuffer()));
expect(Object.keys(zipEntries).sort()).toEqual([
"__clawhub_export/code-plugin/collision-plugin/plugin_meta.json",
"_manifest.json",
"code-plugin/collision-plugin/_export_plugin_meta.json",
]);
});
it("users/reclaim forbids non-admin api tokens", async () => {
const runQuery = vi.fn();
const runAction = vi.fn();
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "me") return { publisherId: "publishers:me" };
return okRate();
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runAction, runMutation }),
new Request("https://example.com/api/v1/users/reclaim", {
method: "POST",
body: JSON.stringify({ handle: "target", slugs: ["a"] }),
}),
);
expect(response.status).toBe(403);
expect(runQuery).not.toHaveBeenCalled();
});
it("users/reclaim calls reclaim mutation for admin", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return { ok: true, action: "ownership_transferred" };
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("handle" in args) return { _id: "users:target" };
return null;
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/reclaim", {
method: "POST",
body: JSON.stringify({ handle: "Target", slugs: [" A ", "b"], reason: "r" }),
}),
);
if (response.status !== 200) throw new Error(await response.text());
const reclaimCalls = runMutation.mock.calls.filter(([, args]) => hasSlugArgs(args));
expect(reclaimCalls).toHaveLength(2);
expect(reclaimCalls[0]?.[1]).toMatchObject({
actorUserId: "users:admin",
slug: "a",
rightfulOwnerUserId: "users:target",
reason: "r",
transferRootSlugOnly: true,
});
expect(reclaimCalls[1]?.[1]).toMatchObject({
actorUserId: "users:admin",
slug: "b",
rightfulOwnerUserId: "users:target",
reason: "r",
transferRootSlugOnly: true,
});
});
it("users/reserve forbids non-admin api tokens", async () => {
const runQuery = vi.fn();
const runAction = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runAction, runMutation }),
new Request("https://example.com/api/v1/users/reserve", {
method: "POST",
body: JSON.stringify({ handle: "target", slugs: ["a"] }),
}),
);
expect(response.status).toBe(403);
expect(runQuery).not.toHaveBeenCalled();
});
it("users/reserve reserves slugs and package names for admin", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return { ok: true, action: "reserved" };
});
let handleLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (args.handle === "target" && handleLookupCount === 0) {
handleLookupCount += 1;
return { _id: "users:target" };
}
if (args.handle === "target") {
return { _id: "publishers:target", handle: "target" };
}
return null;
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/reserve", {
method: "POST",
body: JSON.stringify({
handle: "Target",
slugs: [" A "],
packageNames: [" @openclaw/a "],
reason: "r",
}),
}),
);
if (response.status !== 200) throw new Error(await response.text());
const slugCalls = runMutation.mock.calls.filter(([, args]) => hasSlugArgs(args));
const packageCalls = runMutation.mock.calls.filter(([, args]) => hasPackageNameArgs(args));
expect(slugCalls).toHaveLength(1);
expect(slugCalls[0]?.[1]).toMatchObject({
actorUserId: "users:admin",
slug: "a",
rightfulOwnerUserId: "users:target",
reason: "r",
});
expect(packageCalls).toHaveLength(1);
expect(packageCalls[0]?.[1]).toMatchObject({
actorUserId: "users:admin",
ownerUserId: "users:target",
ownerPublisherId: "publishers:target",
name: "@openclaw/a",
reason: "r",
});
});
it("users/publisher ensures an org publisher handle for admin", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
publisherId: "publishers:openclaw",
handle: "openclaw",
created: true,
migrated: false,
trusted: true,
};
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher", {
method: "POST",
body: JSON.stringify({ handle: "OpenClaw", displayName: "OpenClaw", trusted: true }),
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
handle: "openclaw",
displayName: "OpenClaw",
trusted: true,
}),
);
});
it("users/publisher-member removes an org publisher member for admin", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
publisherId: "publishers:opik",
handle: "opik",
removed: true,
member: {
userId: "users:patrick",
handle: "patrick-erichsen-2",
role: "owner",
},
};
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher-member", {
method: "POST",
body: JSON.stringify({ handle: "Opik", memberHandle: "@patrick-erichsen-2" }),
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(await response.json()).toMatchObject({
ok: true,
handle: "opik",
removed: true,
});
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
handle: "opik",
memberHandle: "@patrick-erichsen-2",
}),
);
});
it("users/publisher-recovery plans personal publisher recovery for admin", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
dryRun: true,
recovered: false,
publisherId: "publishers:gingiris",
handle: "gingiris",
previousUser: {
userId: "users:legacy",
handle: "gingiris",
nextHandle: "gingiris-recovered",
githubProviderAccountId: "111",
authAccountCount: 1,
},
nextUser: {
userId: "users:current",
handle: "gingiris-1031",
nextHandle: "gingiris",
githubProviderAccountId: "222",
authAccountCount: 1,
},
retiredPersonalPublisher: null,
resourceOwnerMigration: {
limitPerTable: 100,
skills: 1,
skillSlugAliases: 1,
packages: 0,
packageInspectorWarnings: 0,
githubSourcesChecked: 1,
handleReservations: 1,
},
identityVerified: false,
reason: "Verified account continuity for issue #2555",
};
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher-recovery", {
method: "POST",
body: JSON.stringify({
handle: "@Gingiris",
nextUserHandle: "@Gingiris-1031",
previousGitHubProviderAccountId: "111",
nextGitHubProviderAccountId: "222",
reason: "Verified account continuity for issue #2555",
}),
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(await response.json()).toMatchObject({
ok: true,
dryRun: true,
handle: "gingiris",
});
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
publisherHandle: "gingiris",
nextUserHandle: "gingiris-1031",
previousGitHubProviderAccountId: "111",
nextGitHubProviderAccountId: "222",
confirmIdentityVerified: false,
dryRun: true,
}),
);
});
it("users/publisher-recovery requires destination handle guard", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("unexpected mutation");
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher-recovery", {
method: "POST",
body: JSON.stringify({
handle: "@Gingiris",
previousGitHubProviderAccountId: "111",
nextGitHubProviderAccountId: "222",
reason: "Verified account continuity for issue #2555",
}),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Missing nextUserHandle");
expect(runMutation).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ publisherHandle: "gingiris" }),
);
});
it("users/publisher-official lists official publishers for admin", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
items: [
{
officialPublisherId: "officialPublishers:openclaw",
publisherId: "publishers:openclaw",
handle: "openclaw",
displayName: "OpenClaw",
kind: "org",
active: true,
reason: "platform-owned publisher",
createdByUserId: "users:admin",
createdByHandle: "patrick-erichsen-2",
createdAt: 1,
updatedAt: 1,
},
],
};
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return null;
});
const response = await __handlers.usersGetRouterV1Handler(
makeCtx({ runQuery, runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher-official", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
items: [{ handle: "openclaw" }],
});
expect(runQuery).toHaveBeenCalledWith(internal.publishers.listOfficialPublishersInternal, {
actorUserId: "users:admin",
});
});
it("users/publisher-official adds official publishers for admin", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
publisherId: "publishers:nvidia",
handle: "nvidia",
added: true,
officialPublisherId: "officialPublishers:nvidia",
};
});
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher-official", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
action: "add",
handle: "NVIDIA",
reason: "NVIDIA source-backed catalog",
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ ok: true, added: true });
expect(runMutation).toHaveBeenCalledWith(internal.publishers.addOfficialPublisherInternal, {
actorUserId: "users:admin",
handle: "nvidia",
reason: "NVIDIA source-backed catalog",
});
});
it("users/publisher-official removes official publishers for admin", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
publisherId: "publishers:nvidia",
handle: "nvidia",
removed: true,
officialPublisherId: "officialPublishers:nvidia",
};
});
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/users/publisher-official", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
action: "remove",
handle: "NVIDIA",
reason: "requested by publisher",
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ ok: true, removed: true });
expect(runMutation).toHaveBeenCalledWith(internal.publishers.removeOfficialPublisherInternal, {
actorUserId: "users:admin",
handle: "nvidia",
reason: "requested by publisher",
});
});
it("publishers creates a self-serve org publisher for the authenticated user", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
publisherId: "publishers:opik",
handle: "opik",
created: true,
trusted: false,
};
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:vincent",
user: { _id: "users:vincent", role: "user" },
} as never);
const response = await __handlers.createPublisherV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/publishers", {
method: "POST",
body: JSON.stringify({ handle: "Opik", displayName: "Opik" }),
}),
);
if (response.status !== 201) throw new Error(await response.text());
expect(await response.json()).toMatchObject({
ok: true,
publisherId: "publishers:opik",
handle: "opik",
created: true,
trusted: false,
});
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:vincent",
handle: "opik",
displayName: "Opik",
}),
);
});
it("publishers returns conflict when the org already exists", async () => {
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error('Publisher "@opik" already exists');
});
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:vincent",
user: { _id: "users:vincent", role: "user" },
} as never);
const response = await __handlers.createPublisherV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/publishers", {
method: "POST",
body: JSON.stringify({ handle: "opik" }),
}),
);
expect(response.status).toBe(409);
expect(await response.text()).toBe('Publisher "@opik" already exists');
});
it("publishers returns a controlled error when JSON is not an object", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.createPublisherV1Handler(
makeCtx({ runQuery: vi.fn(), runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/publishers", {
method: "POST",
body: "null",
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("JSON body must be an object");
expect(requireApiTokenUser).not.toHaveBeenCalled();
});
it("search forwards limit and highlightedOnly", async () => {
const runAction = vi.fn().mockResolvedValue([
{
score: 1,
skill: {
slug: "a",
displayName: "A",
summary: null,
updatedAt: 1,
stats: { downloads: 9 },
},
ownerHandle: "openclaw",
version: { version: "1.0.0" },
},
]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction, runMutation }),
new Request("https://example.com/api/v1/search?q=test&limit=5&highlightedOnly=true"),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
query: "test",
limit: 5,
highlightedOnly: true,
nonSuspiciousOnly: undefined,
});
expect(await response.json()).toMatchObject({
results: [{ slug: "a", ownerHandle: "openclaw" }],
});
});
it("search includes public owner metadata without publisher bio", async () => {
const runAction = vi.fn().mockResolvedValue([
{
score: 1,
skill: {
slug: "demo",
displayName: "Demo",
summary: "Summary",
updatedAt: 1,
statsDownloads: 42,
stats: { downloads: 1 },
},
version: { version: "1.0.0" },
ownerHandle: "openclaw",
owner: {
handle: "openclaw",
displayName: "OpenClaw",
image: "https://example.com/avatar.png",
bio: "private-ish profile text",
},
},
]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction, runMutation }),
new Request("https://example.com/api/v1/search?q=demo"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
results: [
{
score: 1,
slug: "demo",
displayName: "Demo",
summary: "Summary",
version: "1.0.0",
downloads: 42,
updatedAt: 1,
ownerHandle: "openclaw",
owner: {
handle: "openclaw",
displayName: "OpenClaw",
image: "https://example.com/avatar.png",
},
},
],
});
});
it("search forwards nonSuspiciousOnly", async () => {
const runAction = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction, runMutation }),
new Request("https://example.com/api/v1/search?q=test&nonSuspiciousOnly=1"),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
query: "test",
limit: undefined,
highlightedOnly: undefined,
nonSuspiciousOnly: true,
});
});
it("search forwards legacy nonSuspicious alias", async () => {
const runAction = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction, runMutation }),
new Request("https://example.com/api/v1/search?q=test&nonSuspicious=1"),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
query: "test",
limit: undefined,
highlightedOnly: undefined,
nonSuspiciousOnly: true,
});
});
it("search prefers canonical nonSuspiciousOnly over legacy alias", async () => {
const runAction = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction, runMutation }),
new Request(
"https://example.com/api/v1/search?q=test&nonSuspiciousOnly=false&nonSuspicious=1",
),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
query: "test",
limit: undefined,
highlightedOnly: undefined,
nonSuspiciousOnly: undefined,
});
});
it("search rate limits", async () => {
const runMutation = vi.fn().mockResolvedValue(blockedRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/search?q=test"),
);
expect(response.status).toBe(429);
});
it("429 Retry-After is a relative delay, not an absolute epoch", async () => {
const runMutation = vi.fn().mockResolvedValue(blockedRate());
const response = await __handlers.searchSkillsV1Handler(
makeCtx({ runAction: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/search?q=test"),
);
expect(response.status).toBe(429);
const retryAfter = Number(response.headers.get("Retry-After"));
// Retry-After must be a small relative delay (seconds), not a Unix epoch
expect(retryAfter).toBeGreaterThanOrEqual(1);
expect(retryAfter).toBeLessThanOrEqual(120);
});
it("resolve validates hash", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.resolveSkillVersionV1Handler(
makeCtx({ runQuery: vi.fn(), runMutation }),
new Request("https://example.com/api/v1/resolve?slug=demo&hash=bad"),
);
expect(response.status).toBe(400);
});
it("resolve returns 404 when missing", async () => {
const runQuery = vi.fn().mockResolvedValue(null);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.resolveSkillVersionV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/resolve?slug=demo&hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
),
);
expect(response.status).toBe(404);
});
it("resolve returns ownerHandle guidance when the slug is ambiguous", async () => {
const runQuery = vi.fn().mockResolvedValue({
match: null,
latestVersion: null,
ambiguous: true,
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const hash = "a".repeat(64);
const response = await __handlers.resolveSkillVersionV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/resolve?slug=demo&hash=${hash}`),
);
expect(response.status).toBe(409);
const body = await response.text();
expect(body).toContain('Ambiguous skill slug "demo"');
expect(body).toContain(`/api/v1/resolve?slug=demo&ownerHandle=<owner>&hash=${hash}`);
});
it("resolve returns match and latestVersion", async () => {
const runQuery = vi.fn().mockResolvedValue({
match: { version: "1.0.0" },
latestVersion: { version: "2.0.0" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.resolveSkillVersionV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/resolve?slug=demo&hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.match.version).toBe("1.0.0");
});
it("threads ownerHandle through the legacy skills resolve subroute", async () => {
const runQuery = vi.fn().mockResolvedValue({
match: { version: "1.0.0" },
latestVersion: { version: "2.0.0" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const hash = "a".repeat(64);
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/skills/resolve?slug=demo&ownerHandle=acme&hash=${hash}`,
),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
api.skills.resolveVersionByHash,
expect.objectContaining({ slug: "demo", hash, ownerHandle: "acme" }),
);
});
it("lists skills with resolved tags using batch query", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("cursor" in args || "numItems" in args) {
return {
page: [
{
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "versions:1" },
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: { version: "1.0.0", createdAt: 3, changelog: "c" },
},
],
nextCursor: null,
};
}
// Batch query: versionIds (plural)
if ("versionIds" in args) {
return [
{ _id: "versions:1", skillId: "skills:1", version: "1.0.0", softDeletedAt: undefined },
];
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?limit=1"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items[0].tags.latest).toBe("1.0.0");
});
it("lists skills with long description metadata and setup requirements", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("cursor" in args || "numItems" in args) {
return {
page: [
{
skill: {
_id: "skills:1",
slug: "home-assistant",
displayName: "Home Assistant",
summary: "Control Home Assistant.",
tags: {},
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
version: "1.0.0",
createdAt: 3,
changelog: "c",
parsed: {
description: "Long-form manifest description.",
clawdis: {
requires: { env: ["HA_TOKEN"], config: ["HA_URL"] },
envVars: [
{
name: "HA_TOKEN",
required: false,
description: "Long-lived access token.",
},
{
name: "HA_THEME",
required: false,
description: "Optional dashboard theme.",
},
],
os: ["linux"],
nix: { systems: ["x86_64-linux"] },
},
},
},
},
],
nextCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?limit=1"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items[0].description).toBe("Long-form manifest description.");
expect(json.items[0].metadata.setup).toEqual([
{
key: "HA_TOKEN",
required: true,
},
{
key: "HA_URL",
required: true,
},
{
key: "HA_THEME",
required: false,
},
]);
});
it("lists skills keeps the v1 no-sort default on updated ranking", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("cursor" in args || "numItems" in args) {
expect(args.sort).toBe("updated");
return { page: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills"),
);
expect(response.status).toBe(200);
});
it("batches tag resolution across multiple skills into single query", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("cursor" in args || "numItems" in args) {
return {
page: [
{
skill: {
_id: "skills:1",
slug: "skill-a",
displayName: "Skill A",
summary: "s",
tags: { latest: "versions:1", stable: "versions:2" },
stats: { downloads: 0, stars: 0, versions: 2, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: { version: "2.0.0", createdAt: 3, changelog: "c" },
},
{
skill: {
_id: "skills:2",
slug: "skill-b",
displayName: "Skill B",
summary: "s",
tags: { latest: "versions:3" },
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: { version: "1.0.0", createdAt: 3, changelog: "c" },
},
],
nextCursor: null,
};
}
// Batch query should receive all version IDs from all skills
if ("versionIds" in args) {
const ids = args.versionIds as string[];
expect(ids).toHaveLength(3);
expect(ids).toContain("versions:1");
expect(ids).toContain("versions:2");
expect(ids).toContain("versions:3");
return [
{ _id: "versions:1", skillId: "skills:1", version: "2.0.0", softDeletedAt: undefined },
{ _id: "versions:2", skillId: "skills:1", version: "1.0.0", softDeletedAt: undefined },
{ _id: "versions:3", skillId: "skills:2", version: "1.0.0", softDeletedAt: undefined },
];
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills"),
);
expect(response.status).toBe(200);
const json = await response.json();
// Verify tags are correctly resolved for each skill
expect(json.items[0].tags.latest).toBe("2.0.0");
expect(json.items[0].tags.stable).toBe("1.0.0");
expect(json.items[1].tags.latest).toBe("1.0.0");
// Verify batch query was called exactly once (not per-tag)
const batchCalls = runQuery.mock.calls.filter(
([, args]) => args && "versionIds" in (args as Record<string, unknown>),
);
expect(batchCalls).toHaveLength(1);
});
it("lists skills supports sort aliases", async () => {
const checks: Array<[string, string | null]> = [
["default", "recommended"],
["recommended", "recommended"],
["createdAt", "newest"],
["created-at", "newest"],
["newest", "newest"],
["rating", "stars"],
["downloads", "installs"],
["installs", "installs"],
["installs-all-time", "installs"],
["trending", null],
];
for (const [input, expected] of checks) {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("sort" in args || "cursor" in args || "numItems" in args || "limit" in args) {
if (expected === null) {
expect(args).not.toHaveProperty("sort");
} else {
expect(args.sort).toBe(expected);
}
return expected === null
? { items: [], nextCursor: null }
: { page: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/skills?sort=${input}`),
);
expect(response.status).toBe(200);
}
});
it("lists skills rejects invalid sort", async () => {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?sort=unknown"),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Invalid sort query parameter");
expect(runQuery).not.toHaveBeenCalled();
});
it("lists skills rejects empty sort", async () => {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?sort="),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Invalid sort query parameter");
expect(runQuery).not.toHaveBeenCalled();
});
it("lists skills forwards nonSuspiciousOnly", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("sort" in args || "cursor" in args || "numItems" in args) {
expect(args.nonSuspiciousOnly).toBe(true);
return { page: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?nonSuspiciousOnly=true"),
);
expect(response.status).toBe(200);
});
it("lists skills forwards legacy nonSuspicious alias", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("sort" in args || "cursor" in args || "numItems" in args) {
expect(args.nonSuspiciousOnly).toBe(true);
return { page: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?nonSuspicious=1"),
);
expect(response.status).toBe(200);
});
it("lists skills prefers canonical nonSuspiciousOnly over legacy alias", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("sort" in args || "cursor" in args || "numItems" in args) {
expect(args.nonSuspiciousOnly).toBeUndefined();
return { page: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listSkillsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills?nonSuspiciousOnly=false&nonSuspicious=1"),
);
expect(response.status).toBe(200);
});
it("get skill returns 404 when missing", async () => {
const runQuery = vi.fn().mockResolvedValue(null);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/missing"),
);
expect(response.status).toBe(404);
});
it("get skill returns ownerHandle guidance when the slug is ambiguous", async () => {
const runQuery = vi.fn().mockResolvedValue({
skill: null,
ambiguous: true,
ambiguousMatches: [
{ slug: "demo", ownerHandle: "openclaw" },
{ slug: "demo", ownerHandle: "patrick" },
],
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo"),
);
expect(response.status).toBe(409);
const body = await response.json();
expect(body).toEqual({
code: "AMBIGUOUS_SKILL_SLUG",
message: 'Found multiple skills with the slug "demo"; specify which one you want to install:',
slug: "demo",
matches: [
{
ownerHandle: "openclaw",
slug: "demo",
ref: "@openclaw/demo",
url: "https://example.com/openclaw/demo",
},
{
ownerHandle: "patrick",
slug: "demo",
ref: "@patrick/demo",
url: "https://example.com/patrick/demo",
},
],
});
});
it("get skill returns pending-scan message for owner api token", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
_id: "skills:1",
slug: "demo",
ownerUserId: "users:1",
moderationStatus: "hidden",
moderationReason: "pending.scan",
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo"),
);
expect(response.status).toBe(423);
expect(await response.text()).toContain("security scan is pending");
});
it("get skill returns undelete hint for owner soft-deleted skill", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
_id: "skills:1",
slug: "demo",
ownerUserId: "users:1",
softDeletedAt: 1,
moderationStatus: "hidden",
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo"),
);
expect(response.status).toBe(410);
expect(await response.text()).toContain("clawhub undelete demo");
});
it("get skill returns payload", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "versions:1" },
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
version: "1.0.0",
createdAt: 3,
changelog: "c",
files: [],
},
owner: { handle: "p", displayName: "Peter", image: null },
moderationInfo: {
isSuspicious: true,
isMalwareBlocked: false,
verdict: "suspicious",
reasonCodes: ["suspicious.dynamic_code_execution"],
summary: "Detected: suspicious.dynamic_code_execution",
engineVersion: "v2.0.0",
updatedAt: 4,
},
};
}
// Batch query for tag resolution
if ("versionIds" in args) {
return [{ _id: "versions:1", version: "1.0.0", softDeletedAt: undefined }];
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.skill.slug).toBe("demo");
expect(json.latestVersion.version).toBe("1.0.0");
expect(json.moderation).toEqual({
isSuspicious: true,
isMalwareBlocked: false,
verdict: "suspicious",
reasonCodes: ["suspicious.dynamic_code_execution"],
summary: "Detected: suspicious.dynamic_code_execution",
engineVersion: "v2.0.0",
updatedAt: 4,
});
});
it("get skill includes readme markdown description and setup requirements", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "home-assistant",
displayName: "Home Assistant",
summary: "Control Home Assistant.",
latestVersionId: "skillVersions:1",
tags: {},
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 3,
changelog: "c",
files: [],
parsed: {
description: "Frontmatter description.",
clawdis: {
requires: { env: ["HA_TOKEN"], config: ["HA_URL"] },
envVars: [{ name: "HA_TOKEN", description: "Long-lived access token." }],
},
},
},
owner: { handle: "p", displayName: "Peter", image: null },
moderationInfo: null,
};
}
if ("versionIds" in args) return [];
if ("versionId" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
files: [
{
path: "SKILL.md",
size: 21,
storageId: "_storage:skill-readme",
sha256: "abc123",
contentType: "text/markdown",
},
],
softDeletedAt: undefined,
};
}
return null;
});
const storageGet = vi.fn().mockResolvedValue({
text: vi.fn().mockResolvedValue("# Home Assistant\nSetup."),
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: storageGet } }),
new Request("https://example.com/api/v1/skills/home-assistant"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.skill.description).toBe("# Home Assistant\nSetup.");
expect(json.metadata.setup).toEqual([
{
key: "HA_TOKEN",
required: true,
},
{
key: "HA_URL",
required: true,
},
]);
expect(storageGet).toHaveBeenCalledWith("_storage:skill-readme");
});
it("get skill does not read raw markdown descriptions for malware-blocked skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "home-assistant",
displayName: "Home Assistant",
summary: "Control Home Assistant.",
latestVersionId: "skillVersions:1",
tags: {},
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 3,
changelog: "c",
files: [
{
path: "SKILL.md",
size: 21,
storageId: "_storage:skill-readme",
sha256: "abc123",
contentType: "text/markdown",
},
],
parsed: {
description: "Frontmatter description.",
},
},
owner: null,
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: true,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
verdict: "malicious",
reasonCodes: ["blocked.malware"],
summary: "Malware detected.",
sourceVersionId: "skillVersions:1",
},
};
}
if ("versionIds" in args) return [];
if ("versionId" in args) throw new Error("unexpected raw version lookup");
return null;
});
const storageGet = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: storageGet } }),
new Request("https://example.com/api/v1/skills/home-assistant"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.skill.description).toBe("Frontmatter description.");
expect(json.moderation.isMalwareBlocked).toBe(true);
expect(storageGet).not.toHaveBeenCalled();
});
it("get skill uses GitHub-backed cached markdown when no hosted version exists", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:github",
slug: "aiq-deploy",
displayName: "AIQ Deploy",
summary: "Deploy workflows.",
tags: {},
stats: { downloads: 0, stars: 0, versions: 0, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "nvidia", displayName: "NVIDIA", image: null },
moderationInfo: null,
};
}
if ("versionIds" in args) return [];
if (args.skillId === "skills:github" && args.kind === "readme") {
return {
path: "skills/aiq-deploy/SKILL.md",
text: "# AIQ Deploy\n\nLong GitHub-backed README.",
sourceBaseUrl:
"https://github.com/NVIDIA/skills/blob/1111111111111111111111111111111111111111/skills/aiq-deploy",
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/aiq-deploy"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.skill.description).toBe("# AIQ Deploy\n\nLong GitHub-backed README.");
expect(runQuery).toHaveBeenCalledWith(expect.anything(), {
skillId: "skills:github",
kind: "readme",
});
});
it("skill install resolver returns archive descriptor for hosted direct uploads", async () => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
latestVersionSummary: { version: "1.0.0" },
},
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/install"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
ok: true,
slug: "demo",
installKind: "archive",
archive: {
version: "1.0.0",
downloadUrl: "https://example.com/api/v1/download?slug=demo&version=1.0.0",
},
});
});
it("skill install resolver returns a pinned GitHub descriptor for scan-clean source-backed skills", async () => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:aiq-deploy",
slug: "aiq-deploy",
displayName: "AIQ Deploy",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/aiq-deploy",
githubCurrentCommit: "1".repeat(40),
githubCurrentContentHash: "hash-aiq-deploy",
githubCurrentStatus: "present",
githubScanStatus: "clean",
},
source: {
_id: "githubSkillSources:nvidia",
repo: "NVIDIA/skills",
defaultBranch: "main",
},
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/aiq-deploy/install"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
slug: "aiq-deploy",
installKind: "github",
github: {
repo: "NVIDIA/skills",
path: "skills/aiq-deploy",
commit: "1".repeat(40),
contentHash: "hash-aiq-deploy",
},
});
});
it.each([
{
name: "hosted direct uploads",
slug: "hidden-direct",
skill: {
_id: "skills:hidden-direct",
slug: "hidden-direct",
displayName: "Hidden Direct",
moderationStatus: "hidden",
latestVersionSummary: { version: "1.0.0" },
},
},
{
name: "GitHub-backed skills",
slug: "hidden-github",
skill: {
_id: "skills:hidden-github",
slug: "hidden-github",
displayName: "Hidden GitHub",
moderationStatus: "hidden",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/hidden-github",
githubCurrentCommit: "1".repeat(40),
githubCurrentContentHash: "hash-hidden-github",
githubCurrentStatus: "present",
githubScanStatus: "clean",
},
},
])("skill install resolver hides moderated $name", async ({ slug, skill }) => {
const runQuery = makeInstallResolverRunQuery({
skill,
publicVisible: false,
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/skills/${slug}/install`),
);
expect(response.status).toBe(404);
await expect(response.text()).resolves.toBe("Skill not found");
});
it("skill install resolver hides skills absent from the public skill detail path", async () => {
const runQuery = makeInstallResolverRunQuery({
publicVisible: false,
skill: {
_id: "skills:orphaned-github",
slug: "orphaned-github",
displayName: "Orphaned GitHub",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/orphaned-github",
githubCurrentCommit: "1".repeat(40),
githubCurrentContentHash: "hash-orphaned-github",
githubCurrentStatus: "present",
githubScanStatus: "clean",
},
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/orphaned-github/install"),
);
expect(response.status).toBe(404);
await expect(response.text()).resolves.toBe("Skill not found");
});
it("skill install resolver installs the current GitHub hash after it is clean", async () => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:aiq-deploy",
slug: "aiq-deploy",
displayName: "AIQ Deploy",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/aiq-deploy",
githubCurrentCommit: "2".repeat(40),
githubCurrentContentHash: "hash-aiq-deploy-v2",
githubCurrentStatus: "present",
githubScanStatus: "clean",
},
source: { repo: "NVIDIA/skills" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/aiq-deploy/install"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
installKind: "github",
github: {
commit: "2".repeat(40),
contentHash: "hash-aiq-deploy-v2",
},
});
});
it("skill install resolver returns structured GitHub blocks for pending source-backed skills", async () => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:aiq-deploy",
slug: "aiq-deploy",
displayName: "AIQ Deploy",
moderationStatus: "active",
moderationReason: "pending.scan",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/aiq-deploy",
githubCurrentCommit: "2".repeat(40),
githubCurrentContentHash: "hash-aiq-deploy-v2",
githubCurrentStatus: "present",
githubScanStatus: "pending",
},
source: { repo: "NVIDIA/skills" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/aiq-deploy/install"),
);
expect(response.status).toBe(423);
await expect(response.json()).resolves.toMatchObject({
ok: false,
reason: "github_verification_pending",
});
});
it("skill install resolver force-installs pending GitHub-backed skills", async () => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:aiq-deploy",
slug: "aiq-deploy",
displayName: "AIQ Deploy",
moderationStatus: "active",
moderationReason: "pending.scan",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/aiq-deploy",
githubCurrentCommit: "2".repeat(40),
githubCurrentContentHash: "hash-aiq-deploy-v2",
githubCurrentStatus: "present",
githubScanStatus: "pending",
},
source: { repo: "NVIDIA/skills" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/aiq-deploy/install?forceInstall=1"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
installKind: "github",
github: {
commit: "2".repeat(40),
contentHash: "hash-aiq-deploy-v2",
},
});
});
it("skill install resolver blocks GitHub-backed skills with failed scans", async () => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:bad-source",
slug: "bad-source",
displayName: "Bad Source",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/bad-source",
githubCurrentCommit: "1".repeat(40),
githubCurrentContentHash: "hash-bad-source",
githubCurrentStatus: "present",
githubScanStatus: "failed",
},
source: { repo: "NVIDIA/skills" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/bad-source/install"),
);
expect(response.status).toBe(403);
await expect(response.json()).resolves.toMatchObject({
ok: false,
reason: "github_scan_failed",
});
});
it.each([
{
name: "pending scan",
patch: { githubScanStatus: "pending" },
status: 423,
reason: "github_verification_pending",
},
{
name: "missing upstream path",
patch: { githubCurrentStatus: "missing" },
status: 410,
reason: "github_upstream_missing",
},
])(
"skill install resolver blocks GitHub-backed skills with $name",
async ({ patch, status, reason }) => {
const runQuery = makeInstallResolverRunQuery({
skill: {
_id: "skills:blocked-source",
slug: "blocked-source",
displayName: "Blocked Source",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/blocked-source",
githubCurrentCommit: "1".repeat(40),
githubCurrentContentHash: "hash-blocked-source",
githubCurrentStatus: "present",
githubScanStatus: "clean",
...patch,
},
source: { repo: "NVIDIA/skills" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/blocked-source/install"),
);
expect(response.status).toBe(status);
await expect(response.json()).resolves.toMatchObject({
ok: false,
reason,
});
},
);
it("get skill treats reports as a valid slug", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "reports",
displayName: "Reports",
summary: "s",
tags: { latest: "versions:1" },
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
version: "1.0.0",
createdAt: 3,
changelog: "c",
files: [],
},
owner: null,
moderationInfo: {
isSuspicious: false,
isMalwareBlocked: false,
verdict: "clean",
reasonCodes: [],
summary: null,
engineVersion: null,
updatedAt: null,
},
};
}
if ("versionIds" in args) {
return [{ _id: "versions:1", version: "1.0.0", softDeletedAt: undefined }];
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/reports"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.skill.slug).toBe("reports");
});
it("get moderation returns redacted evidence for public flagged skill", async () => {
let slugCalls = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugCalls += 1;
if (slugCalls === 1) {
return {
_id: "skills:1",
slug: "demo",
ownerUserId: "users:owner",
moderationFlags: ["flagged.suspicious"],
moderationVerdict: "suspicious",
moderationReasonCodes: ["suspicious.dynamic_code_execution"],
moderationSummary: "Detected: suspicious.dynamic_code_execution",
moderationEngineVersion: "v2.0.0",
moderationEvaluatedAt: 5,
moderationReason: "scanner.llm.suspicious",
moderationEvidence: [
{
code: "suspicious.dynamic_code_execution",
severity: "critical",
file: "index.ts",
line: 3,
message: "Dynamic code execution detected.",
evidence: "eval(payload)",
},
],
};
}
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
ownerUserId: "users:owner",
tags: { latest: "versions:1" },
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: null,
moderationInfo: {
isSuspicious: true,
isMalwareBlocked: false,
verdict: "suspicious",
reasonCodes: ["suspicious.dynamic_code_execution"],
summary: "Detected: suspicious.dynamic_code_execution",
engineVersion: "v2.0.0",
updatedAt: 5,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/moderation"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.moderation.legacyReason).toBeNull();
expect(json.moderation.evidence[0].evidence).toBe("");
});
it("get moderation returns full evidence for owner hidden skill", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:owner" as never);
let slugCalls = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("userId" in args) {
return { _id: "users:owner", role: "user" };
}
if ("slug" in args) {
slugCalls += 1;
if (slugCalls === 1) {
return {
_id: "skills:1",
slug: "demo",
ownerUserId: "users:owner",
moderationStatus: "hidden",
moderationReason: "quality.low",
moderationFlags: ["flagged.suspicious"],
moderationVerdict: "suspicious",
moderationReasonCodes: ["suspicious.dynamic_code_execution"],
moderationSummary: "Detected: suspicious.dynamic_code_execution",
moderationEngineVersion: "v2.0.0",
moderationEvaluatedAt: 5,
moderationEvidence: [
{
code: "suspicious.dynamic_code_execution",
severity: "critical",
file: "index.ts",
line: 3,
message: "Dynamic code execution detected.",
evidence: "eval(payload)",
},
],
};
}
return null;
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/moderation"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.moderation.legacyReason).toBe("quality.low");
expect(json.moderation.evidence[0].evidence).toBe("eval(payload)");
});
it("get moderation returns 404 for clean public skill", async () => {
let slugCalls = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugCalls += 1;
if (slugCalls === 1) {
return {
_id: "skills:1",
slug: "demo",
ownerUserId: "users:owner",
moderationVerdict: "clean",
moderationReasonCodes: [],
moderationEvidence: [],
};
}
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
ownerUserId: "users:owner",
tags: { latest: "versions:1" },
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: null,
moderationInfo: {
isSuspicious: false,
isMalwareBlocked: false,
verdict: "clean",
reasonCodes: [],
summary: null,
engineVersion: null,
updatedAt: null,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/moderation"),
);
expect(response.status).toBe(404);
});
it("skill reports lists moderator intake", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
reportId: "skillReports:1",
skillId: "skills:1",
skillVersionId: "skillVersions:1",
slug: "demo",
displayName: "Demo",
version: "1.0.0",
reason: "suspicious",
status: "open",
createdAt: 123,
reporter: { userId: "users:reporter", handle: "reporter", displayName: "Reporter" },
triagedAt: null,
triagedBy: null,
triageNote: null,
},
],
nextCursor: null,
done: true,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/reports?status=open&limit=10", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [{ reportId: "skillReports:1", slug: "demo" }],
});
expect(runQuery).toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills.listSkillReportsInternal,
{
actorUserId: "users:moderator",
cursor: null,
limit: 10,
status: "open",
},
);
});
it("skill report posts user reports", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:reporter",
user: { _id: "users:reporter", role: "user" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
reported: true,
alreadyReported: false,
reportId: "skillReports:1",
skillId: "skills:1",
reportCount: 1,
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/report", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.0.0", reason: "suspicious files" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
reported: true,
reportId: "skillReports:1",
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills
.reportSkillForUserInternal,
{
actorUserId: "users:reporter",
slug: "demo",
version: "1.0.0",
reason: "suspicious files",
},
);
});
it("skill report triage posts moderator decisions", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
reportId: "skillReports:1",
skillId: "skills:1",
status: "confirmed",
reportCount: 0,
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/-/reports/skillReports%3A1/triage", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ status: "confirmed", note: "handled", finalAction: "hide" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ status: "confirmed" });
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills
.triageSkillReportForUserInternal,
{
actorUserId: "users:moderator",
reportId: "skillReports:1",
status: "confirmed",
note: "handled",
finalAction: "hide",
},
);
});
it("skill appeal posts owner appeal requests", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:owner",
user: { _id: "users:owner", role: "user" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
submitted: true,
alreadyOpen: false,
appealId: "skillAppeals:1",
skillId: "skills:1",
status: "open",
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/appeal", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.0.0", message: "please review" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
submitted: true,
appealId: "skillAppeals:1",
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills
.submitSkillAppealForUserInternal,
{
actorUserId: "users:owner",
slug: "demo",
version: "1.0.0",
message: "please review",
},
);
});
it("skill appeals lists moderator intake", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
appealId: "skillAppeals:1",
skillId: "skills:1",
skillVersionId: "skillVersions:1",
slug: "demo",
displayName: "Demo",
version: "1.0.0",
message: "please review",
status: "open",
createdAt: 123,
submitter: { userId: "users:owner", handle: "owner", displayName: "Owner" },
resolvedAt: null,
resolvedBy: null,
resolutionNote: null,
},
],
nextCursor: null,
done: true,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/appeals?status=open&limit=10", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [{ appealId: "skillAppeals:1", slug: "demo" }],
});
expect(runQuery).toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills.listSkillAppealsInternal,
{
actorUserId: "users:moderator",
cursor: null,
limit: 10,
status: "open",
},
);
});
it("skill appeal resolve posts moderator decisions", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
appealId: "skillAppeals:1",
skillId: "skills:1",
status: "accepted",
actionTaken: "restore",
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/-/appeals/skillAppeals%3A1/resolve", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
status: "accepted",
note: "scanner finding cleared",
finalAction: "restore",
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
status: "accepted",
actionTaken: "restore",
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { skills: Record<string, unknown> }).skills
.resolveSkillAppealForUserInternal,
{
actorUserId: "users:moderator",
appealId: "skillAppeals:1",
status: "accepted",
note: "scanner finding cleared",
finalAction: "restore",
},
);
});
it("lists versions", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "cursor" in args) {
return {
items: [
{
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "user",
files: [],
},
],
nextCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions?limit=1"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items[0].version).toBe("1.0.0");
});
it("returns a recovered empty page for stale skill version cursors", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "cursor" in args) {
return { items: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions?limit=1&cursor=legacy-cursor"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ items: [], nextCursor: null });
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
skillId: "skills:1",
limit: 1,
cursor: "legacy-cursor",
}),
);
});
it("returns 404 for versions when the owner is banned", async () => {
const runQuery = vi.fn(async () => null);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions?limit=1"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Skill not found");
});
it("returns ownerHandle guidance for ambiguous version list requests", async () => {
const runQuery = vi.fn().mockResolvedValue({ skill: null, ambiguous: true });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions?limit=1"),
);
expect(response.status).toBe(409);
const body = await response.text();
expect(body).toContain('Ambiguous skill slug "demo"');
expect(body).toContain("/api/v1/skills/demo/versions?ownerHandle=<owner>");
});
it("returns version detail", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "version" in args) {
return {
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.files[0].path).toBe("SKILL.md");
});
it("blocks version detail for moderated skills", async () => {
let slugLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugLookupCount += 1;
if (slugLookupCount === 1) return null;
return {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
tags: { latest: "skillVersions:1" },
moderationStatus: "removed",
moderationReason: "policy.violation",
moderationFlags: [],
moderationSourceVersionId: "skillVersions:1",
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(410);
expect(await response.text()).toBe("This skill has been removed by a moderator.");
});
it("returns 404 for version detail when the owner is banned", async () => {
const runQuery = vi.fn(async () => null);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Skill not found");
});
it("returns ownerHandle guidance for ambiguous version detail requests", async () => {
const runQuery = vi.fn().mockResolvedValue({ skill: null, ambiguous: true });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(409);
const body = await response.text();
expect(body).toContain('Ambiguous skill slug "demo"');
expect(body).toContain("/api/v1/skills/demo/versions/1.0.0?ownerHandle=<owner>");
});
it("returns version detail security from vt analysis", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "version" in args) {
return {
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
sha256hash: "a".repeat(64),
vtAnalysis: {
status: "suspicious",
source: "legacy-ai",
checkedAt: 123,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.security.status).toBe("pending");
expect(json.version.security.scanners.vt.normalizedStatus).toBe("suspicious");
expect(json.version.security.virustotalUrl).toContain("virustotal.com/gui/file/");
});
it("keeps static-scan suspicious status out of version security snapshot verdicts", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "version" in args) {
return {
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
sha256hash: "a".repeat(64),
staticScan: {
status: "suspicious",
reasonCodes: ["suspicious.dangerous_exec"],
summary: "Detected: suspicious.dangerous_exec",
engineVersion: "v2.4.0",
checkedAt: 555,
},
vtAnalysis: {
status: "clean",
verdict: "benign",
checkedAt: 111,
},
llmAnalysis: {
status: "completed",
verdict: "benign",
checkedAt: 222,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.security.status).toBe("clean");
expect(json.version.security.hasWarnings).toBe(false);
expect(json.version.security.hasScanResult).toBe(true);
expect(json.version.security.scanners.static).toBeUndefined();
expect(json.version.security.scanners.vt.normalizedStatus).toBe("clean");
expect(json.version.security.scanners.llm.normalizedStatus).toBe("clean");
});
it("keeps static-scan malicious status advisory when ClawScan is benign", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "version" in args) {
return {
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
sha256hash: "a".repeat(64),
staticScan: {
status: "malicious",
reasonCodes: ["malicious.credential_harvest"],
summary: "Detected: malicious.credential_harvest",
engineVersion: "v2.4.0",
checkedAt: 555,
},
vtAnalysis: {
status: "clean",
verdict: "benign",
checkedAt: 111,
},
llmAnalysis: {
status: "completed",
verdict: "benign",
checkedAt: 222,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.security.status).toBe("clean");
expect(json.version.security.hasWarnings).toBe(false);
expect(json.version.security.hasScanResult).toBe(true);
expect(json.version.security.checkedAt).toBe(222);
expect(json.version.security.scanners.static).toBeUndefined();
});
it("omits version security when only static scan evidence exists", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "version" in args) {
return {
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
staticScan: {
status: "clean",
reasonCodes: [],
summary: "No issues found",
engineVersion: "v2.4.0",
checkedAt: 555,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.security).toBeUndefined();
});
it("keeps hasWarnings true when llm dimensions include non-ok ratings", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
latestVersion: null,
owner: { handle: "owner", displayName: "Owner", image: null },
};
}
if ("skillId" in args && "version" in args) {
return {
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
sha256hash: "a".repeat(64),
llmAnalysis: {
status: "completed",
verdict: "benign",
checkedAt: 123,
dimensions: [
{
name: "scope_alignment",
rating: "warn",
rationale: "broad install footprint",
evidence: "",
},
],
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.security.status).toBe("clean");
expect(json.version.security.hasWarnings).toBe(true);
});
it("returns scan payload for latest version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "versions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
sha256hash: "b".repeat(64),
capabilityTags: ["crypto", "requires-wallet", "can-make-purchases"],
vtAnalysis: {
status: "clean",
checkedAt: 111,
},
llmAnalysis: {
status: "completed",
verdict: "suspicious",
confidence: "high",
summary: "s",
checkedAt: 222,
},
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: true,
isHiddenByMod: false,
isRemoved: false,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.security.status).toBe("suspicious");
expect(json.security.hasScanResult).toBe(true);
expect(json.security).not.toHaveProperty("capabilityTags");
expect(json.security.scanners.llm.verdict).toBe("suspicious");
expect(json.moderation.scope).toBe("skill");
expect(json.moderation.sourceVersion).toEqual({
version: "1.0.0",
createdAt: 1,
});
expect(json.moderation.matchesRequestedVersion).toBe(true);
expect(json.moderation.isSuspicious).toBe(true);
});
it("treats completed llm analysis without verdict as error", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "versions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
sha256hash: "c".repeat(64),
llmAnalysis: {
status: "completed",
summary: "missing verdict",
checkedAt: 222,
},
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.security.status).toBe("error");
expect(json.security.hasScanResult).toBe(false);
expect(json.security.scanners.llm.normalizedStatus).toBe("error");
});
it("blocks latest scan status while security review is pending", async () => {
let slugLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugLookupCount += 1;
if (slugLookupCount === 1) return null;
return {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
latestVersionId: "skillVersions:1",
tags: { latest: "skillVersions:1" },
moderationStatus: "hidden",
moderationReason: "pending.scan",
moderationFlags: [],
moderationSourceVersionId: "skillVersions:1",
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
capabilityTags: ["posts-externally", "requires-oauth-token"],
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan"),
);
expect(response.status).toBe(423);
expect(await response.text()).toContain("pending a ClawScan security review");
});
for (const moderationReason of ["pending.scan.stale", "scanner.llm.pending"] as const) {
it(`blocks latest scan status for ${moderationReason} when unavailable publicly`, async () => {
let slugLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugLookupCount += 1;
if (slugLookupCount === 1) return null;
return {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
latestVersionId: "skillVersions:1",
tags: { latest: "skillVersions:1" },
moderationStatus: "hidden",
moderationReason,
moderationFlags: [],
moderationSourceVersionId: "skillVersions:1",
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan"),
);
expect(response.status).toBe(423);
expect(await response.text()).toContain("pending a ClawScan security review");
});
}
it("omits security when no scanner result exists yet", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "skillVersions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
changelogSource: "auto",
capabilityTags: ["posts-externally", "requires-oauth-token"],
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.security).toBeNull();
});
it("blocks exact scan status for moderated skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
latestVersionId: "skillVersions:2",
tags: { latest: "skillVersions:2" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:2",
skillId: "skills:1",
version: "2.0.0",
createdAt: 2,
changelog: "c2",
changelogSource: "auto",
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: true,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
sourceVersionId: "skillVersions:1",
},
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?version=1.0.0"),
);
expect(response.status).toBe(403);
expect(await response.text()).toContain("flagged as malicious");
});
it("blocks exact scan status when the moderated skill is unavailable publicly", async () => {
let slugLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugLookupCount += 1;
if (slugLookupCount === 1) return null;
return {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
latestVersionId: "skillVersions:1",
tags: { latest: "skillVersions:1" },
moderationStatus: "hidden",
moderationReason: "pending.scan",
moderationFlags: [],
moderationSourceVersionId: "skillVersions:1",
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?version=1.0.0"),
);
expect(response.status).toBe(423);
expect(await response.text()).toContain("pending a ClawScan security review");
});
it("blocks tagged scan status when the moderated skill is unavailable publicly", async () => {
let slugLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
slugLookupCount += 1;
if (slugLookupCount === 1) return null;
return {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
latestVersionId: "skillVersions:2",
tags: { latest: "skillVersions:2", old: "skillVersions:1" },
moderationStatus: "hidden",
moderationReason: "pending.scan",
moderationFlags: [],
moderationSourceVersionId: "skillVersions:1",
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?tag=old"),
);
expect(response.status).toBe(423);
expect(await response.text()).toContain("pending a ClawScan security review");
});
it("keeps historical scan status available when latest-version moderation is blocked", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
latestVersionId: "skillVersions:2",
tags: { latest: "skillVersions:2" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:2",
skillId: "skills:1",
version: "2.0.0",
createdAt: 2,
changelog: "c2",
changelogSource: "auto",
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: true,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
sha256hash: "f".repeat(64),
vtAnalysis: {
status: "clean",
checkedAt: 123,
},
llmAnalysis: {
status: "completed",
verdict: "benign",
checkedAt: 124,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?version=1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.version).toBe("1.0.0");
expect(json.security.status).toBe("clean");
expect(json.moderation.matchesRequestedVersion).toBe(false);
});
it("reports the moderation source version for historical scan matches", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
latestVersionId: "skillVersions:2",
tags: { latest: "skillVersions:2" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:2",
skillId: "skills:1",
version: "2.0.0",
createdAt: 2,
changelog: "c2",
changelogSource: "auto",
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: true,
isHiddenByMod: false,
isRemoved: false,
sourceVersionId: "skillVersions:1",
},
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
sha256hash: "f".repeat(64),
llmAnalysis: {
status: "completed",
verdict: "suspicious",
checkedAt: 123,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?version=1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.moderation.sourceVersion).toEqual({ version: "1.0.0", createdAt: 1 });
expect(json.moderation.matchesRequestedVersion).toBe(true);
});
it("keeps hasScanResult true when one scanner returns a definitive verdict", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "versions:2" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:2",
skillId: "skills:1",
version: "2.0.0",
createdAt: 2,
changelog: "c",
changelogSource: "auto",
sha256hash: "d".repeat(64),
vtAnalysis: {
status: "clean",
checkedAt: 111,
},
llmAnalysis: {
status: "error",
summary: "scanner failed",
checkedAt: 222,
},
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.security.status).toBe("error");
expect(json.security.hasScanResult).toBe(false);
expect(json.security.scanners.vt.normalizedStatus).toBe("clean");
expect(json.security.scanners.llm.normalizedStatus).toBe("error");
});
it("marks moderation as a latest-version snapshot when querying a historical version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "skillVersions:2", old: "skillVersions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:2",
skillId: "skills:1",
version: "2.0.0",
createdAt: 2,
changelog: "c2",
changelogSource: "auto",
sha256hash: "e".repeat(64),
vtAnalysis: {
status: "clean",
checkedAt: 222,
},
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
sha256hash: "f".repeat(64),
llmAnalysis: {
status: "completed",
verdict: "suspicious",
checkedAt: 123,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?version=1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.version).toBe("1.0.0");
expect(json.security.status).toBe("suspicious");
expect(json.moderation.scope).toBe("skill");
expect(json.moderation.sourceVersion).toEqual({
version: "2.0.0",
createdAt: 2,
});
expect(json.moderation.matchesRequestedVersion).toBe(false);
expect(json.moderation.isSuspicious).toBe(false);
});
it("resolves scan by tag and reports moderation context against latest version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { latest: "skillVersions:2", old: "skillVersions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:2",
skillId: "skills:1",
version: "2.0.0",
createdAt: 2,
changelog: "c2",
changelogSource: "auto",
sha256hash: "1".repeat(64),
vtAnalysis: {
status: "clean",
checkedAt: 222,
},
files: [],
},
owner: { _id: "users:1", handle: "owner", displayName: "Owner" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c1",
changelogSource: "auto",
sha256hash: "2".repeat(64),
vtAnalysis: {
status: "malicious",
checkedAt: 123,
},
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?tag=old"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.version.version).toBe("1.0.0");
expect(json.security.status).toBe("pending");
expect(json.moderation.sourceVersion).toEqual({
version: "2.0.0",
createdAt: 2,
});
expect(json.moderation.matchesRequestedVersion).toBe(false);
});
it("does not resolve scan tags to another skill's version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { old: "skillVersions:other" },
stats: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: null,
moderationInfo: null,
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:other",
skillId: "skills:other",
version: "9.9.9",
createdAt: 9,
changelog: "other",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/scan?tag=old"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Version not found");
});
it("returns raw file content", async () => {
const internalVersion = {
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [
{
path: "SKILL.md",
size: 5,
storageId: "storage:1",
sha256: "abcd",
contentType: "text/plain",
},
],
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("versionId" in args) {
return internalVersion;
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = {
get: vi.fn().mockResolvedValue(new Blob(["hello"], { type: "text/plain" })),
};
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/file?path=SKILL.md"),
);
expect(response.status).toBe(200);
expect(await response.text()).toBe("hello");
expect(response.headers.get("X-Content-SHA256")).toBe("abcd");
});
it("looks up raw files in the requested owner namespace", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
tags: {},
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: { handle: "clawkit" },
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
files: [
{
path: "SKILL.md",
size: 5,
storageId: "storage:1",
sha256: "abcd",
contentType: "text/plain",
},
],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = {
get: vi.fn().mockResolvedValue(new Blob(["hello"], { type: "text/plain" })),
};
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/file?path=SKILL.md&ownerHandle=clawkit"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
api.skills.getBySlug,
expect.objectContaining({ slug: "demo", ownerHandle: "clawkit" }),
);
});
it("blocks raw file reads for malware-blocked skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: null,
owner: null,
moderationInfo: {
isMalwareBlocked: true,
isPendingScan: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if (args.versionId === "skillVersions:1") {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
files: [{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "abcd" }],
softDeletedAt: undefined,
};
}
throw new Error("unexpected version lookup");
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = { get: vi.fn() };
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/file?path=SKILL.md"),
);
expect(response.status).toBe(403);
expect(await response.text()).toContain("flagged as malicious");
expect(storage.get).not.toHaveBeenCalled();
});
it("does not serve raw files from another skill's tagged version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { old: "skillVersions:other" },
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: null,
owner: null,
moderationInfo: null,
};
}
if (args.versionId === "skillVersions:1") {
return { _id: "skillVersions:1", skillId: "skills:1", version: "1.0.0", files: [] };
}
if (args.versionId === "skillVersions:other") {
return {
_id: "skillVersions:other",
skillId: "skills:other",
version: "9.9.9",
files: [{ path: "SKILL.md", size: 5, storageId: "storage:other", sha256: "other" }],
softDeletedAt: undefined,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = { get: vi.fn() };
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/file?path=SKILL.md&tag=old"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Version not found");
expect(storage.get).not.toHaveBeenCalled();
});
it("returns stored Skill Card markdown", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [
{
path: "skill-card.md",
size: 12,
storageId: "storage:card",
sha256: "card-sha",
contentType: "text/markdown",
},
],
softDeletedAt: undefined,
};
const generatedBundleFingerprint = await buildBundleFingerprint(internalVersion.files);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { stable: "skillVersions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("skillVersionId" in args) {
return [
{ fingerprint: generatedBundleFingerprint, kind: "generated-bundle", createdAt: 2 },
];
}
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = {
get: vi.fn().mockResolvedValue(new Blob(["# Skill Card"], { type: "text/markdown" })),
};
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/card?ownerHandle=acme&tag=stable"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
api.skills.getBySlug,
expect.objectContaining({ slug: "demo", ownerHandle: "acme" }),
);
expect(await response.text()).toBe("# Skill Card");
expect(response.headers.get("X-Content-SHA256")).toBe("card-sha");
});
it("blocks Skill Card reads for malware-blocked skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: null,
owner: null,
moderationInfo: {
isMalwareBlocked: true,
isPendingScan: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if (args.versionId === "skillVersions:1") {
return {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
files: [{ path: "skill-card.md", size: 5, storageId: "storage:card", sha256: "card" }],
softDeletedAt: undefined,
};
}
throw new Error("unexpected version lookup");
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = { get: vi.fn() };
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/card"),
);
expect(response.status).toBe(403);
expect(await response.text()).toContain("flagged as malicious");
expect(storage.get).not.toHaveBeenCalled();
});
it("does not serve Skill Cards from another skill's tagged version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { old: "skillVersions:other" },
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: null,
owner: null,
moderationInfo: null,
};
}
if (args.versionId === "skillVersions:other") {
return {
_id: "skillVersions:other",
skillId: "skills:other",
version: "9.9.9",
files: [
{
path: "skill-card.md",
size: 12,
storageId: "storage:other",
sha256: "other",
},
],
softDeletedAt: undefined,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = { get: vi.fn() };
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/skills/demo/card?tag=old"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Version not found");
expect(storage.get).not.toHaveBeenCalled();
});
it("does not verify another skill's tagged version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { old: "skillVersions:other" },
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: null,
owner: null,
moderationInfo: null,
};
}
if (args.versionId === "skillVersions:other") {
return {
_id: "skillVersions:other",
skillId: "skills:other",
version: "9.9.9",
files: [],
softDeletedAt: undefined,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/verify?tag=old"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Version not found");
});
it("returns 404 when a Skill Card is missing", async () => {
const internalVersion = {
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "abcd" }],
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("skillVersionId" in args) return [];
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/card"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Skill Card not found");
});
it("does not return publisher-supplied skill-card.md from the Skill Card endpoint", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [
{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "source-sha" },
{ path: "skill-card.md", size: 12, storageId: "storage:card", sha256: "card-sha" },
],
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("skillVersionId" in args) {
return [{ fingerprint: "source-fingerprint", kind: "source", createdAt: 4 }];
}
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/card"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Skill Card not found");
});
it("returns bulk skill security verdicts without card data", async () => {
const version = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "source-sha" }],
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "Static scan clean.",
engineVersion: "static-v1",
checkedAt: 2,
},
depRegistryAnalysis: {
status: "suspicious",
results: [],
notFoundPackages: ["left-pad (npm)"],
unresolvedPackages: [],
summary: "Legacy dependency registry warning.",
checkedAt: 9,
},
depRegistryScanStatus: "suspicious",
llmAnalysis: {
status: "clean",
verdict: "clean",
confidence: "high",
summary: "ClawScan clean.",
checkedAt: 3,
model: "gpt-test",
},
capabilityTags: ["dev-tools"],
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
},
owner: { _id: "users:1", handle: "acme", displayName: "Acme" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
version,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: [{ slug: "demo", version: "1.0.0" }] }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toMatchObject({
schema: "clawhub.skill.security-verdicts.v1",
items: [
{
ok: true,
decision: "pass",
reasons: [],
slug: "demo",
requestedSlug: "demo",
displayName: "Demo",
publisherHandle: "acme",
publisherDisplayName: "Acme",
requestedVersion: "1.0.0",
version: "1.0.0",
createdAt: 1,
checkedAt: 3,
skillUrl: "https://example.com/acme/demo",
securityAuditUrl: "https://example.com/acme/demo/security-audit?version=1.0.0",
security: {
status: "clean",
passed: true,
rawStatus: "clean",
verdict: "clean",
signals: {
staticScan: { status: "clean", rawStatus: "clean" },
dependencyRegistry: null,
},
},
},
],
});
expect(json.items[0].card).toBeUndefined();
expect(json.items[0].artifact).toBeUndefined();
expect(json.items[0].security.signals.staticScan.findings).toBeUndefined();
expect(Object.keys(json.items[0].security.signals)).toEqual([
"staticScan",
"virusTotal",
"skillSpector",
"dependencyRegistry",
]);
expect(json.items[0].security.signals.dependencyRegistry).toBeNull();
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual({
slug: "demo",
version: "1.0.0",
});
expect(runQuery.mock.calls.some(([, args]) => "skillId" in args)).toBe(false);
});
it("uses the public site origin for production bulk verdict links", async () => {
vi.stubEnv("CONVEX_DEPLOYMENT", "prod:wry-manatee-359");
const runQuery = vi.fn(async () => ({
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
owner: { _id: "users:1", handle: "acme", displayName: "Acme" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
version: {
_id: "skillVersions:1",
version: "1.0.0",
createdAt: 1,
llmAnalysis: { status: "clean", verdict: "clean", checkedAt: 2 },
},
}));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://wry-manatee-359.convex.site/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: [{ slug: "demo", version: "1.0.0" }] }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items[0]).toMatchObject({
skillUrl: "https://clawhub.ai/acme/demo",
securityAuditUrl: "https://clawhub.ai/acme/demo/security-audit?version=1.0.0",
});
});
it("honors staff-cleared bulk security verdicts", async () => {
const runQuery = vi.fn(async () => ({
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
owner: { _id: "users:1", handle: "acme", displayName: "Acme" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: false,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
overrideActive: true,
verdict: "clean",
summary: "Security findings were reviewed by moderators and cleared for public use.",
updatedAt: 20,
},
version: {
_id: "skillVersions:1",
version: "1.0.0",
createdAt: 1,
llmAnalysis: {
status: "completed",
verdict: "suspicious",
summary: "Scanner found review-worthy behavior.",
checkedAt: 10,
},
},
}));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: [{ slug: "demo", version: "1.0.0" }] }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items[0]).toMatchObject({
ok: true,
decision: "pass",
reasons: [],
checkedAt: 20,
security: {
status: "clean",
passed: true,
verdict: "clean",
summary: "Security findings were reviewed by moderators and cleared for public use.",
checkedAt: 20,
},
});
});
it("keeps bulk verdict item failures local to each requested skill", async () => {
const softDeletedVersion = {
_id: "skillVersions:deleted",
skillId: "skills:soft",
version: "2.0.0",
createdAt: 2,
changelog: "c",
files: [],
llmAnalysis: { status: "clean", verdict: "clean", checkedAt: 3 },
softDeletedAt: 5,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (args.slug === "missing") return null;
if (args.slug === "no-version") {
return {
skill: {
_id: "skills:no-version",
slug: "no-version",
displayName: "No Version",
},
owner: null,
moderationInfo: null,
version: null,
};
}
if (args.slug === "soft") {
return {
skill: {
_id: "skills:soft",
slug: "soft",
displayName: "Soft",
},
owner: null,
moderationInfo: null,
version: softDeletedVersion,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
items: [
{ slug: "missing", version: "1.0.0" },
{ slug: "no-version", version: "1.0.0" },
{ slug: "soft", version: "2.0.0" },
],
}),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items).toHaveLength(3);
expect(json.items.map((item: { ok: boolean }) => item.ok)).toEqual([false, false, false]);
expect(json.items[0]).toMatchObject({
requestedSlug: "missing",
requestedVersion: "1.0.0",
decision: "fail",
reasons: ["skill.not_found"],
error: { code: "skill_not_found", message: "Skill not found" },
});
expect(json.items[1]).toMatchObject({
requestedSlug: "no-version",
requestedVersion: "1.0.0",
decision: "fail",
reasons: ["version.not_found"],
error: { code: "version_not_found", message: "Version not found" },
});
expect(json.items[2]).toMatchObject({
requestedSlug: "soft",
requestedVersion: "2.0.0",
decision: "fail",
reasons: ["version.unavailable"],
error: { code: "version_unavailable", message: "Version not available" },
});
});
it.each([
{
label: "suspicious",
analysis: { status: "completed", verdict: "suspicious", checkedAt: 10 },
reasons: ["security.status_not_clean"],
security: { status: "suspicious", passed: false },
},
{
label: "malicious",
analysis: { status: "completed", verdict: "malicious", checkedAt: 10 },
reasons: ["security.status_not_clean"],
security: { status: "malicious", passed: false },
},
{
label: "pending",
analysis: { status: "completed", checkedAt: 11 },
reasons: ["security.status_not_clean", "security.pending"],
security: { status: "pending", passed: false },
},
{
label: "error",
analysis: { status: "failed", checkedAt: 12 },
reasons: ["security.status_not_clean", "security.error"],
security: { status: "error", passed: false },
},
])("reports $label bulk security verdicts", async ({ analysis, reasons, security }) => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
},
owner: null,
moderationInfo: null,
version: {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [],
llmAnalysis: analysis,
softDeletedAt: undefined,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: [{ slug: "demo", version: "1.0.0" }] }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items[0]).toMatchObject({
ok: false,
decision: "fail",
reasons,
security,
});
});
it("rejects malformed bulk security verdict request bodies", async () => {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const nullBody = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: "null",
}),
);
expect(nullBody.status).toBe(400);
expect(await nullBody.text()).toBe("JSON body must be an object");
const scalarBody = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify("nope"),
}),
);
expect(scalarBody.status).toBe(400);
expect(await scalarBody.text()).toBe("JSON body must be an object");
const missingItems = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: [] }),
}),
);
expect(missingItems.status).toBe(400);
expect(await missingItems.text()).toBe("items must contain 1 to 100 entries");
const duplicate = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
items: [
{ slug: "demo", version: "1.0.0" },
{ slug: "demo", version: "1.0.0" },
],
}),
}),
);
expect(duplicate.status).toBe(400);
expect(await duplicate.text()).toBe("Duplicate item: demo@1.0.0");
const ambiguous = await __handlers.skillSecurityVerdictsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/-/security-verdicts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ items: [{ slug: "demo", version: "1.0.0", tag: "latest" }] }),
}),
);
expect(ambiguous.status).toBe(400);
expect(await ambiguous.text()).toBe("items[0] uses version only; tag is not supported");
expect(runQuery).not.toHaveBeenCalled();
});
it("returns a skill verification envelope with card and security metadata", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
fingerprint: "source-fingerprint",
files: [
{
path: "SKILL.md",
size: 5,
storageId: "storage:1",
sha256: "source-sha",
contentType: "text/markdown",
},
{
path: "skill-card.md",
size: 12,
storageId: "storage:card",
sha256: "card-sha",
contentType: "text/markdown",
},
],
parsed: { license: "MIT-0", clawdis: { requires: { env: ["TOKEN"] } } },
sourceProvenance: {
kind: "github",
url: "https://github.com/acme/demo/tree/main/skills/demo",
repo: "acme/demo",
ref: "main",
commit: "abc123",
path: "skills/demo",
importedAt: 10,
},
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "Static scan clean.",
engineVersion: "static-v1",
checkedAt: 2,
},
llmAnalysis: {
status: "clean",
verdict: "clean",
confidence: "high",
summary: "ClawScan clean.",
checkedAt: 3,
model: "gpt-test",
},
vtAnalysis: {
status: "clean",
verdict: "clean",
analysis: "VirusTotal clean.",
source: "engines",
checkedAt: 4,
},
depRegistryAnalysis: {
status: "suspicious",
results: [],
notFoundPackages: ["left-pad (npm)"],
unresolvedPackages: [],
summary: "Legacy dependency registry warning.",
checkedAt: 9,
},
depRegistryScanStatus: "suspicious",
skillSpectorAnalysis: {
status: "clean",
score: 0,
severity: "LOW",
recommendation: "INSTALL",
issueCount: 0,
issues: [],
scannerVersion: "skillspector-test",
summary: "SkillSpector clean.",
checkedAt: 5,
},
capabilityTags: ["dev-tools"],
softDeletedAt: undefined,
};
const generatedBundleFingerprint = await buildBundleFingerprint(internalVersion.files);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: { stable: "skillVersions:1" },
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: { _id: "users:1", handle: "acme", displayName: "Acme" },
};
}
if ("skillVersionId" in args) {
return [
{ fingerprint: generatedBundleFingerprint, kind: "generated-bundle", createdAt: 5 },
];
}
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/verify?ownerHandle=acme&tag=stable"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
api.skills.getBySlug,
expect.objectContaining({ slug: "demo", ownerHandle: "acme" }),
);
const json = await response.json();
expect(json).toMatchObject({
schema: "clawhub.skill.verify.v1",
ok: true,
decision: "pass",
reasons: [],
slug: "demo",
displayName: "Demo",
pageUrl: "https://clawhub.ai/acme/demo",
publisherHandle: "acme",
publisherDisplayName: "Acme",
publisherProfileUrl: "https://clawhub.ai/user/acme",
version: "1.0.0",
resolvedFrom: "tag",
tag: "stable",
createdAt: 1,
card: {
available: true,
path: "skill-card.md",
url: "https://example.com/api/v1/skills/demo/card?ownerHandle=acme&version=1.0.0",
sha256: "card-sha",
size: 12,
},
artifact: {
sourceFingerprint: "source-fingerprint",
bundleFingerprints: [generatedBundleFingerprint],
files: [{ path: "SKILL.md", sha256: "source-sha", size: 5 }],
},
provenance: {
source: "server-resolved-github-import",
repo: "acme/demo",
commit: "abc123",
path: "skills/demo",
},
security: {
status: "clean",
passed: true,
rawStatus: "clean",
verdict: "clean",
confidence: "high",
summary: "ClawScan clean.",
model: "gpt-test",
checkedAt: 3,
signals: {
staticScan: { status: "clean", rawStatus: "clean", reasonCodes: [] },
virusTotal: {
status: "clean",
rawStatus: "clean",
verdict: "clean",
source: "engines",
},
skillSpector: {
status: "clean",
rawStatus: "clean",
score: 0,
recommendation: "INSTALL",
issueCount: 0,
},
dependencyRegistry: null,
},
},
signature: { status: "unsigned" },
});
expect(json.skill).toBeUndefined();
expect(json.publisher).toBeUndefined();
});
it("does not let publisher-supplied skill-card.md satisfy verification", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
fingerprint: "source-fingerprint",
files: [
{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "source-sha" },
{ path: "skill-card.md", size: 12, storageId: "storage:card", sha256: "card-sha" },
],
parsed: {},
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "Static scan clean.",
checkedAt: 2,
},
llmAnalysis: {
status: "clean",
verdict: "clean",
summary: "ClawScan clean.",
checkedAt: 3,
},
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("skillVersionId" in args) {
return [{ fingerprint: "source-fingerprint", kind: "source", createdAt: 4 }];
}
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/verify"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(false);
expect(json.decision).toBe("fail");
expect(json.reasons).toEqual(["card.missing"]);
expect(json.card).toMatchObject({
available: false,
path: "skill-card.md",
sha256: null,
size: null,
});
expect(json.artifact.bundleFingerprints).toEqual([]);
});
it("fails verification when the skill is malware-blocked by moderation", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
fingerprint: "source-fingerprint",
files: [
{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "source-sha" },
{ path: "skill-card.md", size: 12, storageId: "storage:card", sha256: "card-sha" },
],
parsed: {},
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "Static scan clean.",
checkedAt: 2,
},
llmAnalysis: {
status: "clean",
verdict: "clean",
summary: "ClawScan clean.",
checkedAt: 3,
},
softDeletedAt: undefined,
};
const generatedBundleFingerprint = await buildBundleFingerprint(internalVersion.files);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: true,
isSuspicious: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if ("skillVersionId" in args) {
return [
{ fingerprint: generatedBundleFingerprint, kind: "generated-bundle", createdAt: 4 },
];
}
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/verify"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(false);
expect(json.decision).toBe("fail");
expect(json.reasons).toEqual(["moderation.malware_blocked"]);
expect(json.card).toMatchObject({ available: false, url: null });
expect(json.security).toMatchObject({ status: "clean", passed: true });
});
it("passes verification when static findings are advisory but ClawScan is clean", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
fingerprint: "source-fingerprint",
files: [
{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "source-sha" },
{ path: "skill-card.md", size: 12, storageId: "storage:card", sha256: "card-sha" },
],
parsed: {},
staticScan: {
status: "malicious",
reasonCodes: ["suspicious.external_api"],
findings: [],
summary: "Static advisory warning.",
engineVersion: "static-v1",
checkedAt: 2,
},
llmAnalysis: {
status: "clean",
verdict: "benign",
summary: "ClawScan clean.",
checkedAt: 3,
},
softDeletedAt: undefined,
};
const generatedBundleFingerprint = await buildBundleFingerprint(internalVersion.files);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("skillVersionId" in args) {
return [
{ fingerprint: generatedBundleFingerprint, kind: "generated-bundle", createdAt: 4 },
];
}
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/verify"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(true);
expect(json.decision).toBe("pass");
expect(json.reasons).toEqual([]);
expect(json.security).toMatchObject({
status: "clean",
passed: true,
rawStatus: "clean",
verdict: "benign",
signals: {
staticScan: { status: "malicious", rawStatus: "malicious" },
dependencyRegistry: null,
},
});
});
it("returns ok false when verification has no card or clean scan result", async () => {
const internalVersion = {
_id: "skillVersions:1",
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
fingerprint: "source-fingerprint",
files: [{ path: "SKILL.md", size: 5, storageId: "storage:1", sha256: "source-sha" }],
parsed: {},
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "Static scan clean.",
engineVersion: "static-v1",
checkedAt: 2,
},
llmAnalysis: {
status: "completed",
verdict: "suspicious",
summary: "Review risky behavior.",
checkedAt: 3,
},
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("skillVersionId" in args) return [];
if ("versionId" in args) return internalVersion;
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/verify"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(false);
expect(json.decision).toBe("fail");
expect(json.reasons).toEqual(["card.missing", "security.status_not_clean"]);
expect(json.card.available).toBe(false);
expect(json.security).toMatchObject({ status: "suspicious", passed: false });
});
it("returns 410 for soft-deleted Skill Card versions", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("versionId" in args) return { skillId: "skills:1", softDeletedAt: 123, files: [] };
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/card"),
);
expect(response.status).toBe(410);
});
it("returns ownerHandle guidance for ambiguous raw file requests", async () => {
const runQuery = vi.fn().mockResolvedValue({ skill: null, ambiguous: true });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/file?path=SKILL.md"),
);
expect(response.status).toBe(409);
const body = await response.text();
expect(body).toContain('Ambiguous skill slug "demo"');
expect(body).toContain("/api/v1/skills/demo/file?ownerHandle=<owner>&path=SKILL.md");
});
it("returns 413 when raw file too large", async () => {
const internalVersion = {
skillId: "skills:1",
version: "1.0.0",
createdAt: 1,
changelog: "c",
files: [
{
path: "SKILL.md",
size: 210 * 1024,
storageId: "storage:1",
sha256: "abcd",
contentType: "text/plain",
},
],
softDeletedAt: undefined,
};
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) {
return {
skill: {
_id: "skills:1",
slug: "demo",
displayName: "Demo",
summary: "s",
tags: {},
stats: {},
createdAt: 1,
updatedAt: 2,
latestVersionId: "skillVersions:1",
},
latestVersion: { _id: "skillVersions:1", version: "1.0.0" },
owner: null,
};
}
if ("versionId" in args) {
return internalVersion;
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/skills/demo/file?path=SKILL.md"),
);
expect(response.status).toBe(413);
});
it("publish json succeeds", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "me",
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: true,
forkOf: { slug: "upstream", ownerHandle: "@openclaw", version: "1.0.0" },
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
runMutation.mockImplementation(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "me") return { publisherId: "publishers:me" };
return okRate();
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(true);
expect(publishVersionForUser).toHaveBeenCalledWith(
expect.anything(),
"users:1",
expect.objectContaining({
forkOf: { slug: "upstream", ownerHandle: "openclaw", version: "1.0.0" },
}),
expect.anything(),
);
});
it("publish json defaults omitted ownerHandle to personal publish scope", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("owner resolution should not run for omitted ownerHandle");
});
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: true,
migrateOwner: true,
sourceOwnerHandle: "org",
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(response.status).toBe(200);
expect(publishVersionForUser).toHaveBeenCalledWith(
expect.anything(),
"users:1",
expect.not.objectContaining({ ownerHandle: expect.anything() }),
expect.not.objectContaining({ ownerPublisherId: expect.anything() }),
);
expect(vi.mocked(publishVersionForUser).mock.calls[0]?.[3]).not.toHaveProperty(
"ownerPublisherId",
);
expect(vi.mocked(publishVersionForUser).mock.calls[0]?.[3]).not.toHaveProperty(
"sourceOwnerPublisherId",
);
expect(vi.mocked(publishVersionForUser).mock.calls[0]?.[3]).not.toHaveProperty("migrateOwner");
});
it("keeps client-declared publish source out of trusted provenance options", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const source = {
kind: "github",
url: "https://github.com/spoofed/repo",
repo: "spoofed/repo",
ref: "main",
commit: "f".repeat(40),
path: "skills/demo",
importedAt: 123,
};
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: true,
source,
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(200);
expect(publishVersionForUser).toHaveBeenCalledWith(
expect.anything(),
"users:1",
expect.objectContaining({ source }),
{},
);
expect(vi.mocked(publishVersionForUser).mock.calls[0]?.[3]).not.toHaveProperty(
"sourceProvenance",
);
});
it("publish json resolves requested owner publisher", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "openclaw") return { publisherId: "publishers:openclaw" };
return okRate();
});
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "@openclaw",
migrateOwner: true,
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: true,
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
internal.publishers.resolvePublishTargetForUserInternal,
{
actorUserId: "users:1",
ownerHandle: "openclaw",
minimumRole: "publisher",
},
);
expect(publishVersionForUser).toHaveBeenCalledWith(
expect.anything(),
"users:1",
expect.not.objectContaining({ ownerHandle: expect.anything() }),
{ ownerPublisherId: "publishers:openclaw" },
);
});
it("publish json treats same source and target owner as a normal owner-scoped publish", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "openclaw") return { publisherId: "publishers:openclaw" };
return okRate();
});
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "@openclaw",
sourceOwnerHandle: "openclaw",
migrateOwner: true,
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: true,
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(200);
expect(publishVersionForUser).toHaveBeenCalledWith(
expect.anything(),
"users:1",
expect.not.objectContaining({ ownerHandle: expect.anything() }),
{ ownerPublisherId: "publishers:openclaw" },
);
});
it("publish json returns owner resolution errors", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("Publisher not found");
});
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "@missing",
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: true,
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toMatch(/publisher not found/i);
expect(publishVersionForUser).not.toHaveBeenCalled();
});
it("publish json rejects omitted license terms", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "me",
version: "1.0.0",
changelog: "c",
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toMatch(/license terms must be accepted/i);
expect(publishVersionForUser).not.toHaveBeenCalled();
});
it("publish multipart succeeds", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "me") return { publisherId: "publishers:me" };
return okRate();
});
const form = new FormData();
form.set(
"payload",
JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "me",
version: "1.0.0",
changelog: "",
acceptLicenseTerms: true,
tags: ["latest"],
}),
);
form.append("files", new Blob(["hello"], { type: "text/plain" }), "SKILL.md");
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation, storage: { store: vi.fn().mockResolvedValue("storage:1") } }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
});
it("publish multipart resolves requested owner publisher", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "openclaw") return { publisherId: "publishers:openclaw" };
return okRate();
});
const form = new FormData();
form.set(
"payload",
JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "@openclaw",
migrateOwner: true,
version: "1.0.0",
changelog: "",
acceptLicenseTerms: true,
tags: ["latest"],
}),
);
form.append("files", new Blob(["hello"], { type: "text/plain" }), "SKILL.md");
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation, storage: { store: vi.fn().mockResolvedValue("storage:1") } }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(200);
expect(publishVersionForUser).toHaveBeenCalledWith(
expect.anything(),
"users:1",
expect.not.objectContaining({ ownerHandle: expect.anything() }),
{ ownerPublisherId: "publishers:openclaw" },
);
});
it("publish multipart rejects omitted license terms", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "me") return { publisherId: "publishers:me" };
return okRate();
});
const form = new FormData();
form.set(
"payload",
JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "me",
version: "1.0.0",
changelog: "",
tags: ["latest"],
}),
);
form.append("files", new Blob(["hello"], { type: "text/plain" }), "SKILL.md");
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation, storage: { store: vi.fn().mockResolvedValue("storage:1") } }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toMatch(/license terms must be accepted/i);
expect(publishVersionForUser).not.toHaveBeenCalled();
});
it("publish rejects explicit license refusal", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if (args.ownerHandle === "me") return { publisherId: "publishers:me" };
return okRate();
});
const body = JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "me",
version: "1.0.0",
changelog: "c",
acceptLicenseTerms: false,
files: [
{
path: "SKILL.md",
size: 1,
storageId: "storage:1",
sha256: "abc",
contentType: "text/plain",
},
],
});
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer clh_test" },
body,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toMatch(/license terms must be accepted/i);
});
it("publish multipart ignores mac junk files", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p" },
} as never);
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
skillId: "s",
versionId: "v",
embeddingId: "e",
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const store = vi.fn().mockResolvedValue("storage:1");
const form = new FormData();
form.set(
"payload",
JSON.stringify({
slug: "demo",
displayName: "Demo",
ownerHandle: "me",
version: "1.0.0",
changelog: "",
acceptLicenseTerms: true,
tags: ["latest"],
}),
);
form.append("files", new Blob(["hello"], { type: "text/plain" }), "SKILL.md");
form.append("files", new Blob(["junk"], { type: "application/octet-stream" }), ".DS_Store");
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation, storage: { store } }),
new Request("https://example.com/api/v1/skills", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
if (response.status !== 200) {
throw new Error(await response.text());
}
expect(store).toHaveBeenCalledTimes(1);
const publishArgs = vi.mocked(publishVersionForUser).mock.calls[0]?.[2] as
| { files?: Array<{ path: string }> }
| undefined;
expect(publishArgs?.files?.map((file) => file.path)).toEqual(["SKILL.md"]);
});
it("publish rejects missing token", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.publishSkillV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills", { method: "POST" }),
);
expect(response.status).toBe(401);
});
it("whoami returns user payload", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
userId: "users:1",
user: { handle: "p", displayName: "Peter", image: null },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.whoamiV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/whoami", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.user.handle).toBe("p");
expect(json.user.role).toBeNull();
});
it("delete and undelete require auth", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(
new Error("Unauthorized: API token is invalid or revoked. Run `clawhub login` again."),
);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo", { method: "DELETE" }),
);
expect(response.status).toBe(401);
expect(await response.text()).toBe(
"Unauthorized: API token is invalid or revoked. Run `clawhub login` again.",
);
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
const response2 = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/undelete", { method: "POST" }),
);
expect(response2.status).toBe(401);
});
it("delete and undelete succeed", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return args.deleted ? { ok: true, slugReservedUntil: 123 } : { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ reason: "legal hold" }),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true, slugReservedUntil: 123 });
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
userId: "users:1",
slug: "demo",
deleted: true,
reason: "legal hold",
}),
);
const responseWithOwner = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo?ownerHandle=alice", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(responseWithOwner.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
userId: "users:1",
slug: "demo",
deleted: true,
ownerHandle: "alice",
}),
);
const response2 = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/undelete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ reason: "reviewed", ownerHandle: "alice" }),
}),
);
expect(response2.status).toBe(200);
expect(await response2.json()).toEqual({ ok: true });
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
userId: "users:1",
slug: "demo",
deleted: false,
reason: "reviewed",
ownerHandle: "alice",
}),
);
});
it("deletes one skill version through the authenticated skill delete route", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: " 1.2.3 " }),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true });
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
slug: "demo",
version: "1.2.3",
}),
);
const versionDeleteArgs = runMutation.mock.calls.find(
([, args]) => typeof args === "object" && args !== null && "version" in args,
)?.[1];
expect(versionDeleteArgs).not.toHaveProperty("deleted");
expect(versionDeleteArgs).not.toHaveProperty("userId");
});
it("uses the skill version route when a redirect drops the request body", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
const mutationArgs = runMutation.mock.calls.find(
([, args]) => typeof args === "object" && args !== null && !("key" in args),
)?.[1];
expect(mutationArgs).toMatchObject({
actorUserId: "users:1",
slug: "demo",
version: "1.2.3",
});
expect(mutationArgs).not.toHaveProperty("deleted");
});
it("rejects conflicting skill version selectors across query, body, and path", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
for (const { bodyVersion, queryVersion } of [
{ bodyVersion: "1.2.3", queryVersion: "9.9.9" },
{ bodyVersion: "9.9.9", queryVersion: "1.2.3" },
]) {
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request(
`https://example.com/api/v1/skills/demo/versions/1.2.3?version=${queryVersion}`,
{
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: bodyVersion }),
},
),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Version does not match request target");
}
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects a body-only skill version selector on the whole-skill route", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.2.3" }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toContain("/versions/1.2.3");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects an empty skill version without deleting the whole skill", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: " " }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Version cannot be empty");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects a non-string skill version without deleting the whole skill", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: 123 }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Version must be a non-empty string");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects malformed skill version delete JSON", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: "{",
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Invalid JSON");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("preserves latest-version replacement guidance from skill version deletion", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const message = "Publish a replacement version before deleting the current latest version.";
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error(`ConvexError: ${message}`);
});
const response = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.2.3" }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe(message);
});
it("skill rescan enqueues owner-authorized ClawScan jobs", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
slug: "demo",
version: "1.0.0",
skillId: "skills:1",
skillVersionId: "skillVersions:1",
jobId: "securityScanJobs:1",
alreadyQueued: false,
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/rescan?ownerHandle=openclaw", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.0.0" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
slug: "demo",
version: "1.0.0",
jobId: "securityScanJobs:1",
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { securityScan: Record<string, unknown> }).securityScan
.requestSkillRescanForUserInternal,
{
actorUserId: "users:moderator",
slug: "demo",
ownerHandle: "openclaw",
version: "1.0.0",
},
);
});
it("skill rescan rejects malformed JSON", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("should not enqueue");
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/rescan", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: "{",
}),
);
expect(response.status).toBe(400);
await expect(response.text()).resolves.toBe("Invalid JSON");
expect(runMutation).toHaveBeenCalledTimes(1);
});
it("bulk skill rescan batch requires admin role", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("should not enqueue");
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/-/rescan-batch", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ batchSize: 25 }),
}),
);
expect(response.status).toBe(403);
await expect(response.text()).resolves.toBe("Admin role required.");
expect(runMutation).toHaveBeenCalledTimes(1);
});
it("bulk skill rescan batch enqueues via admin API", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
mode: "all-active-latest",
queued: 2,
alreadyQueued: 1,
skipped: 0,
jobIds: ["securityScanJobs:1", "securityScanJobs:2", "securityScanJobs:3"],
nextCursor: "cursor-2",
done: false,
sampleSlugs: ["demo"],
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/-/rescan-batch", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ batchSize: 25, cursor: null, dryRun: false }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
queued: 2,
alreadyQueued: 1,
nextCursor: "cursor-2",
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { securityScan: Record<string, unknown> }).securityScan
.enqueueBulkSkillRescanBatchForAdminInternal,
{
actorUserId: "users:admin",
cursor: null,
batchSize: 25,
dryRun: false,
},
);
});
it("bulk skill rescan status aggregates via admin API", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
total: 2,
queued: 0,
running: 1,
succeeded: 1,
failed: 0,
missing: 0,
terminal: 1,
done: false,
failedJobIds: [],
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery }),
new Request("https://example.com/api/v1/skills/-/rescan-batch/status", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ jobIds: ["securityScanJobs:1", "securityScanJobs:2"] }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
total: 2,
running: 1,
done: false,
});
expect(runQuery).toHaveBeenCalledWith(
(internal as unknown as { securityScan: Record<string, unknown> }).securityScan
.getBulkSkillRescanBatchStatusForAdminInternal,
{
actorUserId: "users:admin",
jobIds: ["securityScanJobs:1", "securityScanJobs:2"],
},
);
});
it("VT pending repair requires admin role", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("should not repair");
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/-/repair-vt-pending", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ batchSize: 25, dryRun: true }),
}),
);
expect(response.status).toBe(403);
await expect(response.text()).resolves.toBe("Admin role required.");
expect(runMutation).toHaveBeenCalledTimes(1);
});
it("VT pending repair invokes the internal repair action via admin API", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runAction = vi.fn(async () => ({
dryRun: true,
total: 2,
wouldUpdate: 2,
updated: 0,
noResults: 0,
noDecisiveStats: 0,
errors: 0,
done: false,
cursor: "cursor-2",
statusCounts: { clean: 2 },
sampleUpdated: [{ slug: "demo", status: "clean" }],
}));
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runAction }),
new Request("https://example.com/api/v1/skills/-/repair-vt-pending", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ batchSize: 25, cursor: null, dryRun: true }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
dryRun: true,
wouldUpdate: 2,
cursor: "cursor-2",
});
expect(runAction).toHaveBeenCalledWith(
(internal as unknown as { vt: Record<string, unknown> }).vt.repairPendingSkillVtAnalysis,
{
dryRun: true,
cursor: null,
batchSize: 25,
},
);
});
it("package rescan enqueues owner-authorized ClawScan jobs", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
name: "@scope/demo",
version: "1.2.3",
packageId: "packages:1",
packageReleaseId: "packageReleases:1",
jobId: "securityScanJobs:1",
alreadyQueued: false,
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40scope%2Fdemo/rescan", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.2.3" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
name: "@scope/demo",
version: "1.2.3",
jobId: "securityScanJobs:1",
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { securityScan: Record<string, unknown> }).securityScan
.requestPackageRescanForUserInternal,
{
actorUserId: "users:1",
name: "@scope/demo",
version: "1.2.3",
},
);
});
it("package rescan rejects malformed JSON", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
throw new Error("should not enqueue");
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40scope%2Fdemo/rescan", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: "{",
}),
);
expect(response.status).toBe(400);
await expect(response.text()).resolves.toBe("Invalid JSON");
expect(runMutation).toHaveBeenCalledTimes(1);
});
it("transfer request requires auth", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "alice" }),
}),
);
expect(response.status).toBe(401);
});
it("transfer request succeeds", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
transferId: "skillOwnershipTransfers:1",
toUserHandle: "alice",
expiresAt: 123,
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "@Alice" }),
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
skillId: "skills:1",
toUserHandle: "@Alice",
}),
);
});
it("transfer request forwards ownerHandle to skill resolution", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true, transferId: "skillOwnershipTransfers:1", toUserHandle: "alice" };
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer?ownerHandle=openclaw", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "alice" }),
}),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
internal.skills.getSkillBySlugInternal,
expect.objectContaining({ slug: "demo", ownerHandle: "openclaw" }),
);
});
it("allows platform admins to transfer soft-deleted skills", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", handle: "admin", role: "admin" },
} as never);
const runQuery = vi
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ _id: "skills:deleted", slug: "deleted-demo", softDeletedAt: 123 })
.mockResolvedValueOnce({ _id: "publishers:team", kind: "org", handle: "team" });
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
transferred: true,
skillSlug: "deleted-demo",
toPublisherHandle: "team",
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/deleted-demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "@team", message: "Publisher recovery" }),
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
slug: "deleted-demo",
toOwner: "@team",
reason: "Publisher recovery",
}),
);
});
it("prefers a live merged-alias target before admin soft-delete recovery lookup", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", handle: "admin", role: "admin" },
} as never);
const runQuery = vi
.fn()
.mockResolvedValueOnce({ _id: "skills:canonical", slug: "canonical-demo" })
.mockResolvedValueOnce({ _id: "publishers:team", kind: "org", handle: "team" });
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
transferred: true,
skillSlug: "canonical-demo",
toPublisherHandle: "team",
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/merged-demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toOwner: "@team" }),
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
slug: "canonical-demo",
toOwner: "@team",
}),
);
expect(runQuery).toHaveBeenCalledTimes(2);
});
it("requires an audit reason for soft-deleted skill transfers", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", handle: "admin", role: "admin" },
} as never);
const runQuery = vi
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ _id: "skills:deleted", slug: "deleted-demo", softDeletedAt: 123 });
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error(`unexpected mutation ${JSON.stringify(args)}`);
});
const ctx = makeCtx({ runQuery, runMutation });
const missingReason = await __handlers.skillsPostRouterV1Handler(
ctx,
new Request("https://example.com/api/v1/skills/deleted-demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toOwner: "@team" }),
}),
);
expect(missingReason.status).toBe(400);
expect(await missingReason.text()).toBe("message required for soft-deleted skill transfer");
});
it("skill transfer maps ownership denials to 403", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:stranger",
user: { handle: "stranger" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error("Forbidden: Only owners can transfer this skill.");
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "alice" }),
}),
);
expect(response.status).toBe(403);
expect(await response.text()).toBe("Forbidden: Only owners can transfer this skill.");
});
it("transfers a skill directly to an org publisher when the target handle is an org", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
if ("handle" in args) return { _id: "publishers:org", kind: "org", handle: "team" };
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
transferred: true,
skillSlug: "demo",
toPublisherHandle: "team",
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "@team" }),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
ok: true,
transferred: true,
toPublisherHandle: "team",
});
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
slug: "demo",
toOwner: "@team",
}),
);
});
it("transfers a skill directly to the actor's personal publisher", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
if ("handle" in args) {
return {
_id: "publishers:self",
kind: "user",
handle: "steipete",
linkedUserId: "users:1",
};
}
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
transferred: true,
skillSlug: "demo",
toPublisherHandle: "steipete",
};
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ toUserHandle: "@steipete" }),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
ok: true,
transferred: true,
toPublisherHandle: "steipete",
});
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
slug: "demo",
toOwner: "@steipete",
}),
);
});
it("transfer accept returns 404 when no pending request exists", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer/accept", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(404);
});
it("transfer accept maps committed cancellation failures to an error response", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("slug" in args) return { _id: "skills:1", slug: "demo" };
if ("toUserId" in args) {
return {
_id: "skillOwnershipTransfers:1",
skillId: "skills:1",
toUserId: "users:1",
status: "pending",
};
}
return null;
});
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: false, error: "Skill is under moderation" };
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/skills/demo/transfer/accept", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Skill is under moderation");
});
it("rename endpoint forwards to renameOwnedSkillInternal", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true, slug: "demo-new", previousSlug: "demo" };
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo/rename", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({ newSlug: "demo-new", ownerHandle: "alice" }),
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
slug: "demo",
newSlug: "demo-new",
ownerHandle: "alice",
}),
);
});
it("merge endpoint forwards to mergeOwnedSkillIntoCanonicalInternal", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true, sourceSlug: "demo-old", targetSlug: "demo" };
});
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/skills/demo-old/merge", {
method: "POST",
headers: { Authorization: "Bearer clh_test", "content-type": "application/json" },
body: JSON.stringify({
targetSlug: "demo",
sourceOwnerHandle: "alice",
targetOwnerHandle: "alice",
}),
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
sourceSlug: "demo-old",
targetSlug: "demo",
sourceOwnerHandle: "alice",
targetOwnerHandle: "alice",
}),
);
});
it("transfer list returns incoming transfers", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
if ("userId" in args) {
return [
{
_id: "skillOwnershipTransfers:1",
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
fromUser: { _id: "users:2", handle: "alice", displayName: "Alice" },
requestedAt: 100,
expiresAt: 200,
},
];
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.transfersGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/transfers/incoming", {
method: "GET",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
const payload = await response.json();
expect(payload.transfers).toHaveLength(1);
expect(payload.transfers[0]?.skill?.slug).toBe("demo");
});
it("ban user requires auth", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/users/ban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo" }),
}),
);
expect(response.status).toBe(401);
});
it("ban user succeeds with handle", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "users:2" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, alreadyBanned: false, deletedSkills: 2 });
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/ban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo" }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.deletedSkills).toBe(2);
});
it("ban user forwards reason", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "users:2" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, alreadyBanned: false, deletedSkills: 0 });
await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/ban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo", reason: "malware" }),
}),
);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
targetUserId: "users:2",
reason: "malware",
}),
);
});
it("unban user requires auth", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/users/unban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo" }),
}),
);
expect(response.status).toBe(401);
});
it("unban user succeeds with handle", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "users:2" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, alreadyUnbanned: false, restoredSkills: 2 });
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/unban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo" }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.restoredSkills).toBe(2);
});
it("unban user forwards reason", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "users:2" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, alreadyUnbanned: false, restoredSkills: 0 });
await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/unban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo", reason: "appeal accepted" }),
}),
);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
targetUserId: "users:2",
reason: "appeal accepted",
}),
);
});
it("reclassify ban requires admin", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:actor",
user: { _id: "users:actor", role: "user" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/users/reclassify-ban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo", reason: "bulk publishing spam" }),
}),
);
expect(response.status).toBe(403);
});
it("reclassify ban forwards admin payload", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runMutation = vi.fn().mockResolvedValueOnce(okRate()).mockResolvedValueOnce({
ok: true,
dryRun: false,
userId: "users:target",
handle: "demo",
previousReason: "malware auto-ban",
nextReason: "bulk publishing spam",
changed: true,
});
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/users/reclassify-ban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
dryRun: false,
userId: "users:target",
reason: "bulk publishing spam",
}),
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
targetUserId: "users:target",
dryRun: false,
reason: "bulk publishing spam",
}),
);
});
it("reclassify ban resolves banned users by handle", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "users:target", deletedAt: 123 });
const runMutation = vi.fn().mockResolvedValueOnce(okRate()).mockResolvedValueOnce({
ok: true,
dryRun: true,
userId: "users:target",
handle: "demo",
previousReason: "malware auto-ban",
nextReason: "bulk publishing spam",
changed: true,
});
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/reclassify-ban", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
handle: "demo",
reason: "bulk publishing spam",
}),
}),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ handle: "demo" }),
);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
targetUserId: "users:target",
dryRun: true,
}),
);
});
it("staff email uses the configured ClawHub noreply sender", async () => {
vi.stubEnv("RESEND_API_KEY", "resend_test");
vi.stubEnv("CLAWHUB_NOREPLY_FROM", "ClawHub <noreply@notifications.openclaw.ai>");
vi.stubEnv("NOREPLY_EMAIL_FROM", "Legacy <legacy@example.com>");
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runQuery = vi.fn().mockResolvedValueOnce({
_id: "users:recipient",
handle: "demo",
email: "demo@example.com",
});
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({
auditLogId: "auditLogs:staff-email",
})
.mockResolvedValueOnce({ ok: true });
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ id: "email_123" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/email", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
userHandle: "demo",
subject: "Notice",
body: "Hello",
confirmUserRequest: true,
confirmUserSignoff: true,
}),
}),
);
expect(response.status).toBe(200);
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
expect(resendBody.from).not.toBe("Legacy <legacy@example.com>");
expect(resendBody).not.toHaveProperty("replyTo");
});
it("staff email ignores legacy noreply sender env names", async () => {
vi.stubEnv("RESEND_API_KEY", "resend_test");
vi.stubEnv("CLAWHUB_NOREPLY_FROM", "");
vi.stubEnv("NOREPLY_EMAIL_FROM", "Legacy <legacy@example.com>");
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runQuery = vi.fn().mockResolvedValueOnce({
_id: "users:recipient",
handle: "demo",
email: "demo@example.com",
});
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({
auditLogId: "auditLogs:staff-email",
})
.mockResolvedValueOnce({ ok: true });
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ id: "email_123" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/email", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
userHandle: "demo",
subject: "Notice",
body: "Hello",
confirmUserRequest: true,
confirmUserSignoff: true,
}),
}),
);
expect(response.status).toBe(200);
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
expect(resendBody).not.toHaveProperty("replyTo");
});
it("set role requires auth", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/users/role", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo", role: "moderator" }),
}),
);
expect(response.status).toBe(401);
});
it("set role succeeds with handle", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "users:2" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, role: "moderator" });
const response = await __handlers.usersPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/users/role", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ handle: "demo", role: "moderator" }),
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.role).toBe("moderator");
});
it("stars require auth", async () => {
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.starsPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/stars/demo", { method: "POST" }),
);
expect(response.status).toBe(401);
});
it("stars add succeeds", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "skills:1" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, starred: true, alreadyStarred: false });
const response = await __handlers.starsPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/stars/demo?ownerHandle=openclaw", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(true);
expect(json.starred).toBe(true);
expect(runQuery).toHaveBeenCalledWith(
internal.skills.getSkillBySlugInternal,
expect.objectContaining({ slug: "demo", ownerHandle: "openclaw" }),
);
});
it("stars delete succeeds", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runQuery = vi.fn().mockResolvedValue({ _id: "skills:1" });
const runMutation = vi
.fn()
.mockResolvedValueOnce(okRate())
.mockResolvedValueOnce({ ok: true, unstarred: true, alreadyUnstarred: false });
const response = await __handlers.starsDeleteRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/stars/demo?ownerHandle=openclaw", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.ok).toBe(true);
expect(json.unstarred).toBe(true);
expect(runQuery).toHaveBeenCalledWith(
internal.skills.getSkillBySlugInternal,
expect.objectContaining({ slug: "demo", ownerHandle: "openclaw" }),
);
});
it("packages search ignores retired execution and capability filters", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/packages/search?q=test&executesCode=true&capabilityTag=tools&limit=5",
),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
query: "test",
limit: 5,
}),
);
expect(runQuery.mock.calls.map(([, args]) => args)).not.toContainEqual(
expect.objectContaining({
executesCode: expect.anything(),
}),
);
expect(runQuery.mock.calls.map(([, args]) => args)).not.toContainEqual(
expect.objectContaining({
capabilityTag: expect.anything(),
}),
);
expect(findRateLimitCallArgs(runMutation)).toMatchObject({
key: expect.stringMatching(/^ip:/),
limit: RATE_LIMITS.read.ip,
});
expect(response.headers.get("RateLimit-Limit")).toBeTruthy();
});
it("packages search ignores retired environment capability shorthands", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=test&requiresBrowser=true"),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runQuery.mock.calls.map(([, args]) => args)).not.toContainEqual(
expect.objectContaining({
capabilityTag: expect.anything(),
}),
);
});
it("packages search ignores retired artifact capability shorthands", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=test&artifactKind=npm-pack"),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runQuery.mock.calls.map(([, args]) => args)).not.toContainEqual(
expect.objectContaining({
capabilityTag: expect.anything(),
}),
);
});
it("packages search rejects invalid known filters", async () => {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
for (const [param, message] of [
["family=bad", "Invalid family query parameter"],
["channel=bad", "Invalid channel query parameter"],
["isOfficial=maybe", "Invalid isOfficial query parameter"],
["featured=maybe", "Invalid featured query parameter"],
]) {
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/packages/search?q=test&${param}`),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe(message);
}
expect(runQuery).not.toHaveBeenCalled();
});
it("packages list supports family=skill and topics on the generic route", async () => {
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?family=skill&limit=7&topic=calendar"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
paginationOpts: { cursor: null, numItems: 7 },
topic: "calendar",
}),
);
});
it("packages list forwards topics to both unified catalog sources", async () => {
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?limit=7&topic=calendar"),
);
expect(response.status).toBe(200);
expect(runQuery.mock.calls.map(([, args]) => args)).toEqual(
expect.arrayContaining([
expect.objectContaining({ topic: "calendar" }),
expect.objectContaining({ topic: "calendar" }),
]),
);
});
it("packages list rejects invalid known filters but ignores unknown params", async () => {
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
const runMutation = vi.fn().mockResolvedValue(okRate());
const invalid = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?family=bad"),
);
expect(invalid.status).toBe(400);
expect(await invalid.text()).toBe("Invalid family query parameter");
const unknown = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?unknown=bad&limit=7"),
);
expect(unknown.status).toBe(200);
expect(runQuery).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
paginationOpts: { cursor: null, numItems: 7 },
}),
);
});
it("packages list supports category when scoped to a plugin family", async () => {
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?family=code-plugin&category=tools&limit=7"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
family: "code-plugin",
category: "tools",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
});
it("packages list install sort merges package and skill rows by installs", async () => {
const pluginPackage = makeCatalogItem("plugin-installed", {
family: "code-plugin",
updatedAt: 200,
stats: { downloads: 100, installs: 5, stars: 0, versions: 1 },
});
const skillPackage = makeCatalogItem("skill-installed", {
family: "skill",
updatedAt: 100,
stats: { downloads: 1, installs: 40, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
expect(args).toEqual(expect.objectContaining({ sort: "installs" }));
if (Object.hasOwn(args, "viewerUserId")) {
return { page: [pluginPackage], isDone: true, continueCursor: "" };
}
return { page: [skillPackage], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?limit=2&sort=installs"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"skill-installed",
"plugin-installed",
]);
expect(runQuery).toHaveBeenCalledTimes(2);
});
it("packages list recommended sort merges package and skill rows by recommendation score", async () => {
const pluginPackage = makeCatalogItem("plugin-downloaded", {
family: "code-plugin",
updatedAt: 200,
stats: { downloads: 1_000, installs: 0, stars: 0, versions: 1 },
});
const skillPackage = makeCatalogItem("skill-installed", {
family: "skill",
updatedAt: 100,
stats: { downloads: 1, installs: 500, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return false;
expect(args).toEqual(expect.objectContaining({ sort: "recommended" }));
if (Object.hasOwn(args, "viewerUserId")) {
return { page: [pluginPackage], isDone: true, continueCursor: "" };
}
return { page: [skillPackage], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?limit=2&sort=recommended"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"skill-installed",
"plugin-downloaded",
]);
expect(runQuery).toHaveBeenCalledTimes(4);
});
it("packages list recommended fallback merges package and skill rows by installs", async () => {
const pluginPackage = makeCatalogItem("plugin-low-install-score", {
family: "code-plugin",
updatedAt: 300,
stats: { downloads: 1, installs: 1, stars: 0, versions: 1 },
});
const skillPackage = makeCatalogItem("skill-high-install-score", {
family: "skill",
updatedAt: 200,
stats: { downloads: 10, installs: 100, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return true;
if (Object.hasOwn(args, "viewerUserId")) {
expect(args).toEqual(expect.objectContaining({ sort: "installs" }));
return { page: [pluginPackage], isDone: false, continueCursor: "packages-next" };
}
expect(args).toEqual(expect.objectContaining({ sort: "installs" }));
return { page: [skillPackage], isDone: false, continueCursor: "skills-next" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?limit=1&sort=recommended"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"skill-high-install-score",
]);
expect(json.nextCursor).toContain('"recommendedFallback":"installs"');
expect(runQuery).toHaveBeenCalledTimes(4);
});
it("packages list keeps legacy updated fallback sort from recommended pagination cursors", async () => {
const fallbackCursor = `pkgcatalog:${JSON.stringify({
packages: { cursor: "legacy-package-next", offset: 0, pageSize: 1, done: false },
skills: { cursor: null, offset: 0, pageSize: 1, done: true },
recommendedFallback: "updated",
})}`;
const pluginPackage = makeCatalogItem("plugin-next", {
family: "code-plugin",
updatedAt: 100,
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) {
throw new Error("readiness should come from the pagination cursor");
}
expect(args).toEqual(
expect.objectContaining({
sort: "updated",
paginationOpts: expect.objectContaining({ cursor: "legacy-package-next" }),
}),
);
return { page: [pluginPackage], isDone: false, continueCursor: "packages-next" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/packages?limit=1&sort=recommended&cursor=${encodeURIComponent(
fallbackCursor,
)}`,
),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual(["plugin-next"]);
expect(json.nextCursor).toContain('"recommendedFallback":"updated"');
});
it("plugins list defaults to plugin package families", async () => {
const codePlugin = {
name: "code-plugin",
displayName: "Code Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
createdAt: 20,
updatedAt: 200,
};
const bundlePlugin = {
name: "bundle-plugin",
displayName: "Bundle Plugin",
family: "bundle-plugin",
channel: "community",
isOfficial: false,
createdAt: 10,
updatedAt: 100,
};
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) {
return 2;
}
if (hasPluginRecommendedScoreReadinessArgs(args)) {
return false;
}
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin") {
return { page: [bundlePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=7"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"code-plugin",
"bundle-plugin",
]);
expect(json.totalCount).toBe(2);
const families = runQuery.mock.calls
.map(([, args]) => (args as { family?: string }).family)
.filter(Boolean);
expect(families).toEqual(["code-plugin", "bundle-plugin"]);
for (const [, args] of runQuery.mock.calls) {
if (!("family" in (args as Record<string, unknown>))) continue;
expect(args).toEqual(
expect.objectContaining({
category: undefined,
sort: "recommended",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
});
it("plugins list omits the global total count for topic-filtered results", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) {
throw new Error("global count must not be queried for topic filters");
}
if (hasPluginRecommendedScoreReadinessArgs(args)) {
return false;
}
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?topic=calendar&limit=7"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json).not.toHaveProperty("totalCount");
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(expect.objectContaining({ topic: "calendar" }));
}
});
it("plugins list forwards category to both plugin families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) {
return false;
}
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?category=tools&limit=7"),
);
expect(response.status).toBe(200);
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(
expect.objectContaining({
category: "tools",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
});
it("plugins list maps retired v1 category filters to controlled categories", async () => {
const aliases = {
"mcp-tooling": "tools",
data: "tools",
observability: "gateway",
automation: "tools",
deployment: "gateway",
"dev-tools": "runtime",
} as const;
for (const [legacyCategory, controlledCategory] of Object.entries(aliases)) {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) {
return false;
}
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/plugins?category=${legacyCategory}&limit=7`),
);
expect(response.status).toBe(200);
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(
expect.objectContaining({
category: controlledCategory,
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
}
});
it("plugins list fills official-first pages from the community phase", async () => {
const officialPlugin = {
...makeCatalogItem("official-memory", { family: "code-plugin", updatedAt: 2 }),
isOfficial: true,
};
const communityPlugin = {
...makeCatalogItem("community-memory", { family: "code-plugin", updatedAt: 1 }),
isOfficial: false,
};
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
if (args.family === "bundle-plugin") {
return { page: [], isDone: true, continueCursor: "" };
}
if (args.family === "code-plugin") {
const cursor = (args.paginationOpts as { cursor: string | null }).cursor;
if (cursor === null) {
return {
page: [officialPlugin],
isDone: false,
continueCursor: "community-phase",
};
}
expect(cursor).toBe("community-phase");
return { page: [communityPlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?category=memory&officialFirst=true&limit=2"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"official-memory",
"community-memory",
]);
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(expect.objectContaining({ officialFirst: true }));
}
});
it("plugin and package lists reject invalid downloads sort", async () => {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const packageResponse = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/packages?family=code-plugin&sort=downloads&cursor=invalid-download-cursor",
),
);
const pluginResponse = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/plugins?sort=downloads&cursor=${encodeURIComponent(
`pkgplugins:${JSON.stringify({
codePlugins: {
cursor: "invalid-code-download-cursor",
offset: 0,
pageSize: 25,
done: false,
},
bundlePlugins: {
cursor: "invalid-bundle-download-cursor",
offset: 0,
pageSize: 25,
done: false,
},
})}`,
)}`,
),
);
expect(packageResponse.status).toBe(400);
expect(await packageResponse.text()).toBe("Invalid sort query parameter");
expect(pluginResponse.status).toBe(400);
expect(await pluginResponse.text()).toBe("Invalid sort query parameter");
expect(runQuery).not.toHaveBeenCalled();
});
it("plugins list defaults filtered browse to installs sort", async () => {
const readinessCalls: unknown[] = [];
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) {
readinessCalls.push(args);
return false;
}
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?category=data&limit=7"),
);
expect(response.status).toBe(200);
expect(readinessCalls).toEqual([]);
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(
expect.objectContaining({
category: "tools",
sort: "installs",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
});
it("plugins list defaults featured browse to installs sort", async () => {
const readinessCalls: unknown[] = [];
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) {
readinessCalls.push(args);
return false;
}
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?featured=true&limit=7"),
);
expect(response.status).toBe(200);
expect(readinessCalls).toEqual([]);
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(
expect.objectContaining({
highlightedOnly: true,
sort: "installs",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
});
it("plugins list install sort forwards to both plugin families and merges by installs", async () => {
const codePlugin = makeCatalogItem("code-installed", {
family: "code-plugin",
updatedAt: 100,
stats: { downloads: 1, installs: 50, stars: 0, versions: 1 },
});
const bundlePlugin = makeCatalogItem("bundle-installed", {
family: "bundle-plugin",
updatedAt: 200,
stats: { downloads: 100, installs: 5, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 2;
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
expect(args).toEqual(expect.objectContaining({ sort: "installs" }));
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin") {
return { page: [bundlePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=2&sort=installs"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"code-installed",
"bundle-installed",
]);
});
it("plugins list recommended sort uses weighted scores across plugin families", async () => {
const codePlugin = makeCatalogItem("code-starred", {
family: "code-plugin",
updatedAt: 100,
stats: { downloads: 10, installs: 20, stars: 5, versions: 1 },
});
const bundlePlugin = makeCatalogItem("bundle-downloaded", {
family: "bundle-plugin",
updatedAt: 200,
stats: { downloads: 1_000, installs: 0, stars: 1, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 2;
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
expect(args).toEqual(expect.objectContaining({ sort: "recommended" }));
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin") {
return { page: [bundlePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=2&sort=recommended"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"bundle-downloaded",
"code-starred",
]);
});
it("plugins list recommended sort lets strong downloads beat smaller installs", async () => {
const codePlugin = makeCatalogItem("code-downloaded", {
family: "code-plugin",
updatedAt: 100,
stats: { downloads: 43_080, installs: 2, stars: 0, versions: 1 },
});
const bundlePlugin = makeCatalogItem("bundle-installed", {
family: "bundle-plugin",
updatedAt: 200,
stats: { downloads: 393, installs: 74, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 2;
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
expect(args).toEqual(expect.objectContaining({ sort: "recommended" }));
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin") {
return { page: [bundlePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=2&sort=recommended"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"code-downloaded",
"bundle-installed",
]);
});
it("plugins list falls back to installs sort while recommendation scores backfill", async () => {
const codePlugin = makeCatalogItem("code-older-high-score", {
family: "code-plugin",
updatedAt: 100,
stats: { downloads: 50_000, installs: 500, stars: 10, versions: 1 },
});
const bundlePlugin = makeCatalogItem("bundle-newer-low-score", {
family: "bundle-plugin",
updatedAt: 200,
stats: { downloads: 1, installs: 0, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 2;
if (hasPluginRecommendedScoreReadinessArgs(args)) return true;
expect(args).toEqual(expect.objectContaining({ sort: "installs" }));
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin") {
return { page: [bundlePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=2&sort=recommended"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual([
"code-older-high-score",
"bundle-newer-low-score",
]);
});
it("plugins list keeps installs fallback sort from recommended pagination cursors", async () => {
const fallbackCursor = `pkgplugins:${JSON.stringify({
codePlugins: { cursor: null, offset: 0, pageSize: 1, done: false },
bundlePlugins: { cursor: null, offset: 0, pageSize: 1, done: true },
recommendedFallback: "installs",
})}`;
const codePlugin = makeCatalogItem("code-next", {
family: "code-plugin",
updatedAt: 100,
stats: { downloads: 50_000, installs: 500, stars: 10, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 1;
if (hasPluginRecommendedScoreReadinessArgs(args)) {
throw new Error("readiness should come from the pagination cursor");
}
expect(args).toEqual(expect.objectContaining({ sort: "installs" }));
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/plugins?limit=1&sort=recommended&cursor=${encodeURIComponent(
fallbackCursor,
)}`,
),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual(["code-next"]);
});
it("plugins list keeps legacy updated fallback sort from recommended pagination cursors", async () => {
const fallbackCursor = `pkgplugins:${JSON.stringify({
codePlugins: { cursor: "legacy-code-next", offset: 0, pageSize: 1, done: false },
bundlePlugins: { cursor: null, offset: 0, pageSize: 1, done: true },
recommendedFallback: "updated",
})}`;
const codePlugin = makeCatalogItem("code-next", {
family: "code-plugin",
updatedAt: 100,
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 1;
if (hasPluginRecommendedScoreReadinessArgs(args)) {
throw new Error("readiness should come from the pagination cursor");
}
expect(args).toEqual(
expect.objectContaining({
family: "code-plugin",
sort: "updated",
paginationOpts: expect.objectContaining({ cursor: "legacy-code-next" }),
}),
);
return { page: [codePlugin], isDone: false, continueCursor: "code-next" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/plugins?limit=1&sort=recommended&cursor=${encodeURIComponent(
fallbackCursor,
)}`,
),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual(["code-next"]);
expect(json.nextCursor).toContain('"recommendedFallback":"updated"');
});
it("plugins list keeps legacy recommended cursors on recommended sort", async () => {
const legacyCursor = `pkgplugins:${JSON.stringify({
codePlugins: { cursor: "legacy-code-next", offset: 0, pageSize: 1, done: false },
bundlePlugins: { cursor: null, offset: 0, pageSize: 1, done: true },
})}`;
const codePlugin = makeCatalogItem("code-next", {
family: "code-plugin",
updatedAt: 100,
stats: { downloads: 50_000, installs: 500, stars: 10, versions: 1 },
});
const readinessCalls: unknown[] = [];
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 1;
if (hasPluginRecommendedScoreReadinessArgs(args)) {
readinessCalls.push(args);
return true;
}
expect(args).toEqual(
expect.objectContaining({
family: "code-plugin",
sort: "recommended",
paginationOpts: expect.objectContaining({ cursor: "legacy-code-next" }),
}),
);
return { page: [codePlugin], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/plugins?limit=1&sort=recommended&cursor=${encodeURIComponent(
legacyCursor,
)}`,
),
);
expect(response.status).toBe(200);
expect(readinessCalls).toEqual([]);
const json = await response.json();
expect(json.items.map((entry: { name: string }) => entry.name)).toEqual(["code-next"]);
});
it("plugins list rejects invalid categories", async () => {
for (const category of ["not-a-category", "constructor", "toString"]) {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/plugins?category=${category}`),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Invalid plugin category");
expect(runQuery).not.toHaveBeenCalled();
}
});
it("plugins list paginates with separate plugin family cursors", async () => {
const codeNewest = makeCatalogItem("code-newest", {
family: "code-plugin",
updatedAt: 300,
});
const codeOlder = makeCatalogItem("code-older", {
family: "code-plugin",
updatedAt: 100,
});
const bundleMiddle = makeCatalogItem("bundle-middle", {
family: "bundle-plugin",
updatedAt: 200,
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 3;
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
const pagination = args.paginationOpts as { cursor: string | null };
if (args.family === "code-plugin" && pagination.cursor === null) {
return { page: [codeNewest], isDone: false, continueCursor: "code-cursor" };
}
if (args.family === "code-plugin" && pagination.cursor === "code-cursor") {
return { page: [codeOlder], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin" && pagination.cursor === null) {
return { page: [bundleMiddle], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected args ${JSON.stringify(args)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const firstResponse = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=1"),
);
expect(firstResponse.status).toBe(200);
const firstJson = await firstResponse.json();
expect(firstJson.items.map((entry: { name: string }) => entry.name)).toEqual(["code-newest"]);
expect(firstJson.nextCursor).toMatch(/^pkgplugins:/);
const secondUrl = new URL("https://example.com/api/v1/plugins");
secondUrl.searchParams.set("limit", "1");
secondUrl.searchParams.set("cursor", firstJson.nextCursor);
const secondResponse = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(secondUrl),
);
expect(secondResponse.status).toBe(200);
const secondJson = await secondResponse.json();
expect(secondJson.items.map((entry: { name: string }) => entry.name)).toEqual([
"bundle-middle",
]);
const packageCalls = runQuery.mock.calls
.map(([, args]) => args as { family?: string; paginationOpts?: { cursor: string | null } })
.filter((args) => args.family === "code-plugin" || args.family === "bundle-plugin");
expect(
packageCalls.map((args) => ({
family: args.family,
cursor: args.paginationOpts?.cursor ?? null,
})),
).toEqual([
{ family: "code-plugin", cursor: null },
{ family: "bundle-plugin", cursor: null },
{ family: "code-plugin", cursor: "code-cursor" },
{ family: "bundle-plugin", cursor: null },
]);
});
it("plugins list ignores stale plugin search cursors", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (Object.keys(args).length === 0) return 0;
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const staleSearchCursor = `pkgpluginsearch:${JSON.stringify({
codePlugins: { cursor: "code-search", offset: 0, pageSize: 2, done: false },
bundlePlugins: { cursor: null, offset: 0, pageSize: 2, done: true },
})}`;
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/plugins?limit=7&cursor=${encodeURIComponent(staleSearchCursor)}`,
),
);
expect(response.status).toBe(200);
const packageCalls = runQuery.mock.calls
.map(([, args]) => args as { family?: string; paginationOpts?: { cursor: string | null } })
.filter((args) => args.family === "code-plugin" || args.family === "bundle-plugin");
expect(packageCalls.map((args) => args.paginationOpts?.cursor ?? null)).toEqual([null, null]);
});
it("package and plugin lists ignore stale skill cursors", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) return false;
return { page: [], isDone: true, continueCursor: "" };
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const staleSkillCursor = `skillcat:${JSON.stringify({
cursor: "skill-cursor",
offset: 0,
pageSize: 20,
done: false,
})}`;
const packagesResponse = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/packages?limit=7&cursor=${encodeURIComponent(staleSkillCursor)}`,
),
);
const pluginsResponse = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
`https://example.com/api/v1/plugins?limit=7&cursor=${encodeURIComponent(staleSkillCursor)}`,
),
);
expect(packagesResponse.status).toBe(200);
expect(pluginsResponse.status).toBe(200);
const cursors = runQuery.mock.calls
.map(([, args]) => (args as { paginationOpts?: { cursor: string | null } }).paginationOpts)
.filter(Boolean)
.map((pagination) => pagination?.cursor ?? null);
expect(cursors).toEqual(cursors.map(() => null));
});
it("packages search supports family=skill and topics on the generic route", async () => {
const runQuery = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=demo&family=skill&topic=calendar"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: "demo",
topic: "calendar",
}),
);
});
it("packages search forwards topics to both unified catalog sources", async () => {
const runQuery = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=demo&topic=calendar"),
);
expect(response.status).toBe(200);
expect(runQuery.mock.calls.map(([, args]) => args)).toEqual(
expect.arrayContaining([
expect.objectContaining({ query: "demo", topic: "calendar" }),
expect.objectContaining({ query: "demo", topic: "calendar" }),
]),
);
});
it("packages search supports category when scoped to a plugin family", async () => {
const runQuery = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/packages/search?q=api&family=code-plugin&category=tools",
),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: "api",
family: "code-plugin",
category: "tools",
}),
);
});
it("plugins search defaults to plugin package families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return [
{
score: 10,
package: {
name: "weather-code",
displayName: "Weather Code",
family: "code-plugin",
channel: "community",
isOfficial: false,
createdAt: 10,
updatedAt: 100,
},
},
];
}
if (args.family === "bundle-plugin") {
return [
{
score: 10,
package: {
name: "weather-bundle",
displayName: "Weather Bundle",
family: "bundle-plugin",
channel: "community",
isOfficial: false,
createdAt: 20,
updatedAt: 200,
},
},
];
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins/search?q=weather&category=tools&limit=7"),
);
expect(response.status).toBe(200);
expect(
(await response.json()).results.map(
(entry: { package: { name: string } }) => entry.package.name,
),
).toEqual(["weather-bundle", "weather-code"]);
const families = runQuery.mock.calls.map(([, args]) => (args as { family?: string }).family);
expect(families).toEqual(["code-plugin", "bundle-plugin"]);
for (const [, args] of runQuery.mock.calls) {
expect(args).toEqual(
expect.objectContaining({
query: "weather",
category: "tools",
limit: 7,
}),
);
}
});
it("plugins search maps retired v1 category filters to controlled categories", async () => {
const runQuery = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/plugins/search?q=metrics&category=observability&limit=7",
),
);
expect(response.status).toBe(200);
for (const [, args] of runQuery.mock.calls) {
expect(args).toEqual(
expect.objectContaining({
query: "metrics",
category: "gateway",
limit: 7,
}),
);
}
});
it("plugins search dedupes and sorts results from both plugin families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return [
{
score: 10,
package: makeCatalogItem("shared-plugin", { family: "code-plugin", updatedAt: 100 }),
},
{
score: 50,
package: makeCatalogItem("plugin-code", { family: "code-plugin", updatedAt: 50 }),
},
];
}
if (args.family === "bundle-plugin") {
return [
{
score: 70,
package: makeCatalogItem("plugin-bundle", { family: "bundle-plugin", updatedAt: 80 }),
},
{
score: 10,
package: makeCatalogItem("shared-plugin", {
family: "bundle-plugin",
updatedAt: 60,
}),
},
];
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins/search?q=plugin&limit=3"),
);
expect(response.status).toBe(200);
expect(
(await response.json()).results.map(
(entry: { score: number; package: { family: string; name: string } }) => ({
family: entry.package.family,
name: entry.package.name,
}),
),
).toEqual([
{ family: "bundle-plugin", name: "plugin-bundle" },
{ family: "code-plugin", name: "plugin-code" },
{ family: "code-plugin", name: "shared-plugin" },
]);
});
it("plugins search ignores client-only sort and cursor params", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
expect(args).not.toHaveProperty("sort");
expect(args).not.toHaveProperty("cursor");
return [];
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const url = new URL("https://example.com/api/v1/plugins/search");
url.searchParams.set("q", "plugin");
url.searchParams.set("sort", "name");
url.searchParams.set("limit", "2");
url.searchParams.set("cursor", "pkgplugins:stale");
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(url),
);
expect(response.status).toBe(200);
expect((await response.json()).results).toEqual([]);
});
it("plugins search rejects invalid categories", async () => {
for (const category of ["not-a-category", "constructor", "toString"]) {
const runQuery = vi.fn();
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(`https://example.com/api/v1/plugins/search?q=plugin&category=${category}`),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Invalid plugin category");
expect(runQuery).not.toHaveBeenCalled();
}
});
it("plugins search sorts by rank tier before score without exposing rank metadata", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return [
{
score: 20,
rankTier: 3,
package: makeCatalogItem("summary-plugin", { family: "code-plugin", updatedAt: 100 }),
},
];
}
if (args.family === "bundle-plugin") {
return [
{
score: 10,
rankTier: 1,
package: makeCatalogItem("name-plugin", { family: "bundle-plugin", updatedAt: 50 }),
},
];
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins/search?q=plugin&limit=2"),
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.results.map((entry: { package: { name: string } }) => entry.package.name)).toEqual([
"name-plugin",
"summary-plugin",
]);
expect(body.results[0]).not.toHaveProperty("rankTier");
expect(body.results[0]).not.toHaveProperty("matchReason");
});
it("packages list forwards viewerUserId for authenticated private package browsing", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?channel=private&limit=7"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
channel: "private",
viewerUserId: "users:owner",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
});
it("packages search forwards viewerUserId for authenticated private package search", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("userId" in args) return { _id: args.userId };
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=secret&channel=private"),
);
expect(response.status).toBe(200);
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
query: "secret",
channel: "private",
viewerUserId: "users:owner",
}),
);
});
it("packages list falls back to anonymous when cookie auth resolution fails", async () => {
vi.mocked(getAuthUserId).mockRejectedValue(new Error("stale session"));
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPackagesV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages?isOfficial=true&limit=7"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
isOfficial: true,
viewerUserId: undefined,
paginationOpts: { cursor: null, numItems: 7 },
}),
);
});
it("packages search falls back to anonymous when cookie auth resolves to an invalid user", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:broken" as never);
const runQuery = vi.fn(async (query: unknown, args: Record<string, unknown>) => {
if (query === internal.users.getByIdInternal) {
throw new Error("Table mismatch");
}
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=secret&channel=community"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
internal.users.getByIdInternal,
expect.objectContaining({ userId: "users:broken" }),
);
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
query: "secret",
channel: "community",
viewerUserId: undefined,
}),
);
});
it("packages detail falls back to public skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-1",
tags: { latest: "skillVersions:demo-1" },
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
_id: "skillVersions:demo-1",
skillId: "skills:demo",
version: "1.0.0",
createdAt: 3,
changelog: "init",
files: [],
},
owner: { handle: "steipete", displayName: "Peter" },
};
}
if ("versionIds" in args) {
return [{ _id: "skillVersions:demo-1", version: "1.0.0" }];
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo"),
);
if (response.status !== 200) throw new Error(await response.text());
await expect(response.json()).resolves.toMatchObject({
package: {
name: "demo",
family: "skill",
latestVersion: "1.0.0",
channel: "community",
},
owner: {
handle: "steipete",
},
});
});
it("packages detail returns not found for invalid package lookup names", async () => {
const runQuery = vi.fn(async () => {
throw new Error("unexpected package lookup");
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/openclaw%2Fdiscord"),
);
expect(response.status).toBe(404);
await expect(response.text()).resolves.toBe("Package not found");
expect(runQuery).not.toHaveBeenCalled();
});
it("packages detail returns stats for plugins", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Plugin summary",
latestVersion: "1.2.3",
capabilityTags: ["tools"],
executesCode: true,
capabilities: {
executesCode: true,
toolNames: ["demoTool"],
capabilityTags: ["tools"],
},
stats: { downloads: 7, installs: 3, stars: 2, versions: 4 },
createdAt: 1,
updatedAt: 2,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner", displayName: "Owner" },
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin"),
);
if (response.status !== 200) throw new Error(await response.text());
const json = await response.json();
expect(json).toMatchObject({
package: {
name: "demo-plugin",
latestVersion: "1.2.3",
stats: { downloads: 7, installs: 3, stars: 2, versions: 4 },
},
owner: {
handle: "owner",
},
});
expect(json.package).not.toHaveProperty("capabilityTags");
expect(json.package).not.toHaveProperty("capabilities");
expect(json.package).not.toHaveProperty("executesCode");
});
it("packages detail accepts double-encoded scoped package names", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
expect(args.name).toBe("@openclaw/demo-plugin");
return {
package: {
_id: "packages:demo-plugin",
name: "@openclaw/demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Plugin summary",
latestVersion: "1.2.3",
stats: { downloads: 7, installs: 3, stars: 2, versions: 4 },
createdAt: 1,
updatedAt: 2,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner", displayName: "Owner" },
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/%2540openclaw%2Fdemo-plugin"),
);
if (response.status !== 200) throw new Error(await response.text());
await expect(response.json()).resolves.toMatchObject({
package: {
name: "@openclaw/demo-plugin",
latestVersion: "1.2.3",
},
});
});
it("packages file serves SKILL.md for skill README requests", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-1",
tags: { latest: "skillVersions:demo-1" },
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "steipete" },
};
}
if ("versionId" in args) {
return {
_id: "skillVersions:demo-1",
skillId: "skills:demo",
version: "1.0.0",
createdAt: 3,
changelog: "init",
files: [
{
path: "SKILL.md",
size: 11,
sha256: "abc",
storageId: "storage:skill",
contentType: "text/markdown",
},
],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = {
get: vi.fn().mockResolvedValue(new Blob(["# Demo skill"])),
};
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/packages/demo/file?path=README.md"),
);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("# Demo skill");
expect(storage.get).toHaveBeenCalledWith("storage:skill");
});
it("packages file blocks skill compatibility files for malware-blocked skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-1",
tags: { latest: "skillVersions:demo-1" },
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "steipete" },
moderationInfo: {
isMalwareBlocked: true,
isPendingScan: false,
isHiddenByMod: false,
isRemoved: false,
},
};
}
if (args.versionId === "skillVersions:demo-1") {
return {
_id: "skillVersions:demo-1",
skillId: "skills:demo",
version: "1.0.0",
files: [{ path: "SKILL.md", size: 5, storageId: "storage:skill", sha256: "skill" }],
softDeletedAt: undefined,
};
}
throw new Error("unexpected version lookup");
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = { get: vi.fn() };
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/packages/demo/file?path=README.md"),
);
expect(response.status).toBe(403);
expect(await response.text()).toContain("flagged as malicious");
expect(storage.get).not.toHaveBeenCalled();
});
it("packages file does not serve skill tags pointing at another skill's version", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-1",
tags: { latest: "skillVersions:demo-1", old: "skillVersions:other" },
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "steipete" },
moderationInfo: null,
};
}
if (args.versionId === "skillVersions:other") {
return {
_id: "skillVersions:other",
skillId: "skills:other",
version: "9.9.9",
createdAt: 9,
changelog: "other",
files: [
{
path: "SKILL.md",
size: 11,
sha256: "abc",
storageId: "storage:other",
contentType: "text/markdown",
},
],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const storage = { get: vi.fn() };
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage }),
new Request("https://example.com/api/v1/packages/demo/file?path=README.md&tag=old"),
);
expect(response.status).toBe(404);
expect(await response.text()).toBe("Version not found");
expect(storage.get).not.toHaveBeenCalled();
});
it("packages download redirects skills to the skill download endpoint", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-1",
tags: { latest: "skillVersions:demo-1" },
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "steipete" },
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo/download?version=1.0.0"),
);
expect(response.status).toBe(307);
expect(response.headers.get("Location")).toBe(
"https://example.com/api/v1/download?slug=demo&ownerHandle=steipete&version=1.0.0",
);
});
it("packages detail hides private packages from anonymous requests", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue(null);
const runQuery = vi.fn().mockResolvedValue(null);
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/private-plugin"),
);
expect(response.status).toBe(404);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
name: "private-plugin",
viewerUserId: undefined,
}),
);
});
it("package skill compatibility versions return recovered empty pages for stale skill cursors", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return null;
}
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-1",
tags: {},
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "owner" },
};
}
if ("skillId" in args && "cursor" in args) {
return { items: [], nextCursor: null };
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo/versions?limit=1&cursor=legacy-cursor"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ items: [], nextCursor: null });
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
skillId: "skills:demo",
limit: 1,
cursor: "legacy-cursor",
}),
);
});
it("packages detail allows private packages for browser-session owners", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
const runQuery = vi.fn().mockResolvedValue({
package: {
_id: "packages:private",
name: "private-plugin",
displayName: "Private Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "private",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner" },
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/private-plugin"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
name: "private-plugin",
viewerUserId: "users:owner",
}),
);
});
it("packages version detail returns security scan fields for plugins", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
reportCount: 7,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [
{
path: "README.md",
size: 10,
sha256: "file-sha",
storageId: "storage:1",
contentType: "text/markdown",
},
],
verification: {
tier: "source-linked",
scope: "artifact-only",
scanStatus: "malicious",
},
sha256hash: "a".repeat(64),
vtAnalysis: {
status: "clean",
verdict: "benign",
checkedAt: 1,
},
llmAnalysis: {
status: "clean",
verdict: "clean",
summary: "Looks safe.",
checkedAt: 1,
},
staticScan: {
status: "malicious",
reasonCodes: ["malicious.static_fixture"],
findings: [],
summary: "Static fixture only.",
engineVersion: "1",
checkedAt: 1,
},
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0"),
);
if (response.status !== 200) throw new Error(await response.text());
await expect(response.json()).resolves.toMatchObject({
package: {
name: "demo-plugin",
family: "code-plugin",
},
version: {
version: "1.0.0",
sha256hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
vtAnalysis: {
status: "clean",
verdict: "benign",
},
llmAnalysis: {
status: "clean",
verdict: "clean",
},
verification: {
scanStatus: "clean",
},
staticScan: {
status: "malicious",
summary: "Static fixture only.",
},
},
});
});
it("packages version detail blocks skill compatibility metadata for malware-blocked skills", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
return {
skill: {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
summary: "Skill summary",
latestVersionId: "skillVersions:demo-2",
tags: { latest: "skillVersions:demo-2" },
badges: {},
createdAt: 1,
updatedAt: 2,
},
latestVersion: null,
owner: { handle: "steipete" },
moderationInfo: {
isPendingScan: false,
isMalwareBlocked: true,
isHiddenByMod: false,
isRemoved: false,
sourceVersionId: "skillVersions:demo-1",
},
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:demo-1",
skillId: "skills:demo",
version: "1.0.0",
createdAt: 3,
changelog: "init",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo/versions/1.0.0"),
);
expect(response.status).toBe(403);
expect(await response.text()).toContain("flagged as malicious");
});
it("packages version detail blocks moderated skills that are unavailable publicly", async () => {
let slugLookupCount = 0;
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
if ("slug" in args) {
slugLookupCount += 1;
if (slugLookupCount === 1) return null;
return {
_id: "skills:demo",
slug: "demo",
displayName: "Demo Skill",
latestVersionId: "skillVersions:demo-1",
tags: { latest: "skillVersions:demo-1" },
moderationStatus: "hidden",
moderationReason: "pending.scan",
moderationFlags: [],
moderationSourceVersionId: "skillVersions:demo-1",
};
}
if ("skillId" in args && "version" in args) {
return {
_id: "skillVersions:demo-1",
skillId: "skills:demo",
version: "1.0.0",
createdAt: 3,
changelog: "init",
files: [],
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo/versions/1.0.0"),
);
expect(response.status).toBe(423);
expect(await response.text()).toContain("pending a ClawScan security review");
});
it("packages version detail returns ClawPack artifact metadata", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
clawpackSha256: "c".repeat(64),
clawpackSize: 123,
clawpackFormat: "tgz",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
npmUnpackedSize: 456,
npmFileCount: 3,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.package).not.toHaveProperty("reportCount");
expect(json).toMatchObject({
version: {
artifact: {
kind: "npm-pack",
sha256: "c".repeat(64),
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
},
},
});
});
it("package artifact endpoint exposes ClawPack resolver URLs", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
reportCount: 7,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
clawpackSha256: "c".repeat(64),
clawpackSize: 123,
clawpackFormat: "tgz",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/artifact"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.package).not.toHaveProperty("reportCount");
expect(json).toMatchObject({
artifact: {
kind: "npm-pack",
tarballUrl: "https://example.com/api/npm/demo-plugin/-/demo-plugin-1.0.0.tgz",
legacyDownloadUrl: "https://example.com/api/v1/packages/demo-plugin/download?version=1.0.0",
},
});
});
it("package artifact endpoint exposes legacy zip resolver compatibility aliases", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "legacy-zip",
integritySha256: "a".repeat(64),
sha256hash: "b".repeat(64),
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/artifact"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
package: { name: "demo-plugin" },
version: "1.0.0",
artifact: {
kind: "legacy-zip",
sha256: "b".repeat(64),
format: "zip",
source: "clawhub",
artifactKind: "legacy-zip",
artifactSha256: "b".repeat(64),
packageName: "demo-plugin",
version: "1.0.0",
downloadUrl: "https://example.com/api/v1/packages/demo-plugin/download?version=1.0.0",
legacyDownloadUrl: "https://example.com/api/v1/packages/demo-plugin/download?version=1.0.0",
},
});
});
it("package security endpoint uses the canonical npm artifact hash", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
sha256hash: "c".repeat(64),
clawpackSha256: "e".repeat(64),
clawpackSize: 123,
clawpackFormat: "tgz",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
verification: { scanStatus: "malicious" },
manualModeration: { state: "quarantined", reason: "private reviewer note" },
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/security"),
);
expect(response.status).toBe(200);
const packageLookupArgs = runQuery.mock.calls
.map(([, args]) => args)
.filter(hasPackageNameArgs);
expect(packageLookupArgs).toContainEqual(
expect.objectContaining({ name: "demo-plugin", version: "1.0.0" }),
);
expect(packageLookupArgs.some((args) => !("version" in args))).toBe(false);
const json = await response.json();
expect(json.trust).not.toHaveProperty("moderationReason");
expect(json).toEqual({
package: {
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
release: {
releaseId: "packageReleases:1",
version: "1.0.0",
artifactKind: "npm-pack",
artifactSha256: "e".repeat(64),
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
createdAt: 1,
},
trust: {
scanStatus: "malicious",
moderationState: "quarantined",
blockedFromDownload: true,
reasons: ["manual:quarantined", "scan:malicious"],
pending: false,
stale: false,
},
});
});
it("package security endpoint includes package-level public download blocks", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
scanStatus: "malicious",
publicDownloadBlocked: true,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
sha256hash: "c".repeat(64),
verification: { scanStatus: "clean" },
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/security"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.trust).toMatchObject({
scanStatus: "clean",
blockedFromDownload: true,
reasons: ["package:malicious"],
pending: false,
stale: false,
});
});
it("package security endpoint does not use file-set integrity as npm artifact hash", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
integritySha256: "a".repeat(64),
sha256hash: "b".repeat(64),
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
verification: { scanStatus: "clean" },
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/security"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.release).not.toHaveProperty("artifactSha256");
expect(json.release).toMatchObject({
artifactKind: "npm-pack",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
});
});
it("package security endpoint does not use file-set integrity as legacy artifact hash", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "legacy-zip",
integritySha256: "a".repeat(64),
verification: { scanStatus: "clean" },
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/security"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.release).not.toHaveProperty("artifactSha256");
expect(json.release).toMatchObject({
artifactKind: "legacy-zip",
version: "1.0.0",
});
});
it.each([
{
name: "clean",
release: { verification: { scanStatus: "clean" } },
expected: { scanStatus: "clean", blockedFromDownload: false, reasons: [], pending: false },
},
{
name: "pending",
release: { sha256hash: "b".repeat(64) },
expected: {
scanStatus: "pending",
blockedFromDownload: false,
reasons: ["scan:pending"],
pending: true,
},
},
{
name: "stale",
release: { sha256hash: "b".repeat(64), vtAnalysis: { status: "stale", checkedAt: 123 } },
expected: {
scanStatus: "pending",
blockedFromDownload: false,
reasons: ["scan:pending"],
pending: true,
stale: true,
},
},
{
name: "suspicious",
release: {
vtAnalysis: {
status: "suspicious",
source: "engines",
engineStats: { suspicious: 1 },
checkedAt: 123,
},
},
expected: {
scanStatus: "pending",
blockedFromDownload: false,
reasons: ["scan:pending"],
pending: true,
},
},
{
name: "malicious",
release: {
llmAnalysis: {
status: "malicious",
verdict: "malicious",
summary: "ClawScan found malicious behavior.",
checkedAt: 123,
},
},
expected: {
scanStatus: "malicious",
blockedFromDownload: true,
reasons: ["scan:malicious"],
pending: false,
},
},
{
name: "static-only",
release: {
sha256hash: "b".repeat(64),
staticScan: {
status: "malicious",
reasonCodes: ["malicious.test"],
findings: [],
summary: "Detected: malicious.test",
engineVersion: "v1",
checkedAt: 123,
},
},
expected: {
scanStatus: "pending",
blockedFromDownload: false,
reasons: ["scan:pending"],
pending: true,
},
},
{
name: "quarantined",
release: {
verification: { scanStatus: "clean" },
manualModeration: { state: "quarantined", reason: "private reviewer note" },
},
expected: {
scanStatus: "malicious",
moderationState: "quarantined",
blockedFromDownload: true,
reasons: ["manual:quarantined", "scan:malicious"],
pending: false,
},
},
{
name: "revoked",
release: {
verification: { scanStatus: "clean" },
manualModeration: { state: "revoked", reason: "unsafe artifact" },
},
expected: {
scanStatus: "malicious",
moderationState: "revoked",
blockedFromDownload: true,
reasons: ["manual:revoked", "scan:malicious"],
pending: false,
},
},
])("package security endpoint reports $name trust state", async ({ release, expected }) => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "legacy-zip",
sha256hash: "b".repeat(64),
...release,
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/security"),
);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.release).toMatchObject({
releaseId: "packageReleases:1",
version: "1.0.0",
artifactKind: "legacy-zip",
artifactSha256: "b".repeat(64),
createdAt: 1,
});
expect(json.trust).toMatchObject({
moderationState: null,
stale: false,
...expected,
});
expect(json.trust).not.toHaveProperty("moderationReason");
});
it("package artifact endpoint omits legacy zip archive aliases when archive hash is missing", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "legacy-zip",
integritySha256: "a".repeat(64),
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/artifact"),
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toMatchObject({
artifact: {
kind: "legacy-zip",
format: "zip",
source: "clawhub",
artifactKind: "legacy-zip",
packageName: "demo-plugin",
version: "1.0.0",
},
});
expect(body.artifact).not.toHaveProperty("sha256");
expect(body.artifact).not.toHaveProperty("artifactSha256");
});
it("package artifact endpoint accepts split scoped package paths", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "@scope/demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "@scope/demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
clawpackSha256: "c".repeat(64),
clawpackSize: 123,
clawpackFormat: "tgz",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "scope-demo-plugin-1.0.0.tgz",
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/@scope/demo-plugin/versions/1.0.0/artifact"),
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ name: "@scope/demo-plugin", version: "1.0.0" }),
);
await expect(response.json()).resolves.toMatchObject({
package: { name: "@scope/demo-plugin" },
artifact: {
kind: "npm-pack",
tarballUrl: "https://example.com/api/npm/@scope/demo-plugin/-/scope-demo-plugin-1.0.0.tgz",
},
});
});
it("package readiness reports official OpenClaw blockers", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
latestVersion: "1.0.0",
channel: "community",
isOfficial: false,
compatibility: {
pluginApiRange: "^1.0.0",
builtWithOpenClawVersion: "2026.3.14",
},
capabilities: {
executesCode: true,
hostTargets: ["darwin-arm64"],
capabilityTags: ["environment:declared"],
},
verification: {
tier: "source-linked",
scope: "artifact-only",
sourceRepo: "openclaw/demo-plugin",
sourceCommit: "abc123",
scanStatus: "clean",
},
artifact: {
kind: "legacy-zip",
sha256: "a".repeat(64),
format: "zip",
},
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/readiness"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ready: false,
blockers: ["official", "clawpack"],
checks: expect.arrayContaining([
expect.objectContaining({ id: "official", status: "fail" }),
expect.objectContaining({ id: "clawpack", status: "fail" }),
expect.objectContaining({ id: "compatibility", status: "pass" }),
expect.objectContaining({ id: "scan", status: "pass" }),
]),
});
});
it("package release moderation posts state changes", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
packageId: "packages:demo-plugin",
releaseId: "packageReleases:1",
state: "quarantined",
scanStatus: "malicious",
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/moderation", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
state: "quarantined",
reason: "manual review",
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
state: "quarantined",
scanStatus: "malicious",
});
expect(runMutation).toHaveBeenCalledWith(
internal.packages.moderatePackageReleaseForUserInternal,
{
actorUserId: "users:moderator",
name: "demo-plugin",
version: "1.0.0",
state: "quarantined",
reason: "manual review",
},
);
});
it("package moderation queue lists releases for moderators", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
packageId: "packages:demo-plugin",
releaseId: "packageReleases:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
version: "1.0.0",
createdAt: 1,
artifactKind: "npm-pack",
scanStatus: "malicious",
moderationState: "quarantined",
moderationReason: "manual review",
sourceRepo: "openclaw/demo-plugin",
sourceCommit: "abc123",
reportCount: 0,
lastReportedAt: null,
reasons: ["manual:quarantined", "scan:malicious"],
},
],
nextCursor: "cursor-1",
done: false,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/moderation/queue?status=blocked&limit=20", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [
{
name: "demo-plugin",
version: "1.0.0",
scanStatus: "malicious",
moderationState: "quarantined",
},
],
nextCursor: "cursor-1",
done: false,
});
expect(runQuery).toHaveBeenCalledWith(internal.packages.listPackageModerationQueueInternal, {
actorUserId: "users:moderator",
cursor: null,
limit: 20,
status: "blocked",
});
});
it("package report posts authenticated reports", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:reporter",
user: { _id: "users:reporter", role: "user" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
reported: true,
alreadyReported: false,
packageId: "packages:1",
releaseId: "packageReleases:1",
reportCount: 1,
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40scope%2Fdemo/report", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
reason: "suspicious native payload",
version: "1.2.3",
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
reported: true,
reportCount: 1,
});
expect(runMutation).toHaveBeenCalledWith(internal.packages.reportPackageForUserInternal, {
actorUserId: "users:reporter",
name: "@scope/demo",
reason: "suspicious native payload",
version: "1.2.3",
});
});
it("package reports lists moderator report intake", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
reportId: "packageReports:1",
packageId: "packages:1",
releaseId: "packageReleases:1",
name: "@scope/demo",
displayName: "Demo",
family: "code-plugin",
version: "1.2.3",
reason: "suspicious",
status: "open",
createdAt: 123,
reporter: { userId: "users:reporter", handle: "reporter", displayName: "Reporter" },
triagedAt: null,
triagedBy: null,
triageNote: null,
},
],
nextCursor: null,
done: true,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/reports?status=open&limit=10", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [{ reportId: "packageReports:1", name: "@scope/demo" }],
});
expect(runQuery).toHaveBeenCalledWith(internal.packages.listPackageReportsInternal, {
actorUserId: "users:moderator",
cursor: null,
limit: 10,
status: "open",
});
});
it("package migrations lists official migration rows", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
migrationId: "officialPluginMigrations:1",
bundledPluginId: "core.search",
packageName: "@scope/demo",
packageId: "packages:1",
owner: "platform",
sourceRepo: "openclaw/openclaw",
sourcePath: "plugins/search",
sourceCommit: "abc123",
phase: "ready-for-openclaw",
blockers: [],
hostTargetsComplete: true,
scanClean: true,
moderationApproved: true,
runtimeBundlesReady: false,
notes: null,
createdAt: 100,
updatedAt: 200,
},
],
nextCursor: null,
done: true,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/migrations?phase=all&limit=10", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [{ bundledPluginId: "core.search", phase: "ready-for-openclaw" }],
});
expect(runQuery).toHaveBeenCalledWith(internal.packages.listOfficialPluginMigrationsInternal, {
actorUserId: "users:moderator",
cursor: null,
limit: 10,
phase: "all",
});
});
it("package migrations upserts official migration rows", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
migration: {
migrationId: "officialPluginMigrations:1",
bundledPluginId: "core.search",
packageName: "@scope/demo",
packageId: "packages:1",
owner: "platform",
sourceRepo: "openclaw/openclaw",
sourcePath: "plugins/search",
sourceCommit: null,
phase: "blocked",
blockers: ["missing ClawPack"],
hostTargetsComplete: true,
scanClean: false,
moderationApproved: false,
runtimeBundlesReady: false,
notes: null,
createdAt: 100,
updatedAt: 200,
},
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/migrations", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
bundledPluginId: "core.search",
packageName: "@scope/demo",
owner: "platform",
sourceRepo: "openclaw/openclaw",
sourcePath: "plugins/search",
phase: "blocked",
blockers: ["missing ClawPack"],
hostTargetsComplete: true,
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
ok: true,
migration: { bundledPluginId: "core.search", phase: "blocked" },
});
expect(runMutation).toHaveBeenCalledWith(
internal.packages.upsertOfficialPluginMigrationForUserInternal,
expect.objectContaining({
actorUserId: "users:admin",
bundledPluginId: "core.search",
packageName: "@scope/demo",
}),
);
});
it("package report triage posts moderator decisions", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
reportId: "packageReports:1",
packageId: "packages:1",
status: "confirmed",
reportCount: 0,
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/reports/packageReports%3A1/triage", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ status: "confirmed", note: "handled", finalAction: "quarantine" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ status: "confirmed" });
expect(runMutation).toHaveBeenCalledWith(internal.packages.triagePackageReportForUserInternal, {
actorUserId: "users:moderator",
reportId: "packageReports:1",
status: "confirmed",
note: "handled",
finalAction: "quarantine",
});
});
it("package moderation status returns owner diagnostics", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:owner",
user: { _id: "users:owner", role: "user" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
package: {
packageId: "packages:1",
name: "@scope/demo",
displayName: "Demo",
family: "code-plugin",
channel: "community",
isOfficial: false,
reportCount: 2,
lastReportedAt: 456,
scanStatus: "malicious",
},
latestRelease: {
releaseId: "packageReleases:1",
version: "1.2.3",
artifactKind: "npm-pack",
scanStatus: "malicious",
moderationState: "quarantined",
moderationReason: "manual review",
blockedFromDownload: true,
reasons: ["manual:quarantined", "scan:malicious", "reports:2"],
createdAt: 123,
},
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/%40scope%2Fdemo/moderation", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
package: { name: "@scope/demo", reportCount: 2 },
latestRelease: { blockedFromDownload: true },
});
expect(runQuery).toHaveBeenCalledWith(
internal.packages.getPackageModerationStatusForUserInternal,
{
actorUserId: "users:owner",
name: "@scope/demo",
},
);
});
it("package appeal posts owner appeal requests", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:owner",
user: { _id: "users:owner", role: "user" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
submitted: true,
alreadyOpen: false,
appealId: "packageAppeals:1",
packageId: "packages:1",
releaseId: "packageReleases:1",
status: "open",
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40scope%2Fdemo/appeal", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.2.3", message: "please review" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
submitted: true,
appealId: "packageAppeals:1",
});
expect(runMutation).toHaveBeenCalledWith(internal.packages.submitPackageAppealForUserInternal, {
actorUserId: "users:owner",
name: "@scope/demo",
version: "1.2.3",
message: "please review",
});
});
it("package appeals lists moderator appeal intake", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
appealId: "packageAppeals:1",
packageId: "packages:1",
releaseId: "packageReleases:1",
name: "@scope/demo",
displayName: "Demo",
family: "code-plugin",
version: "1.2.3",
message: "please review",
status: "open",
createdAt: 123,
submitter: { userId: "users:owner", handle: "owner", displayName: "Owner" },
resolvedAt: null,
resolvedBy: null,
resolutionNote: null,
},
],
nextCursor: null,
done: true,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/appeals?status=open&limit=10", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [{ appealId: "packageAppeals:1", name: "@scope/demo" }],
});
expect(runQuery).toHaveBeenCalledWith(internal.packages.listPackageAppealsInternal, {
actorUserId: "users:moderator",
cursor: null,
limit: 10,
status: "open",
});
});
it("package appeal resolve posts moderator decisions", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
appealId: "packageAppeals:1",
packageId: "packages:1",
releaseId: "packageReleases:1",
status: "accepted",
actionTaken: "approve",
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/appeals/packageAppeals%3A1/resolve", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
status: "accepted",
note: "scanner finding cleared",
finalAction: "approve",
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
status: "accepted",
actionTaken: "approve",
});
expect(runMutation).toHaveBeenCalledWith(
internal.packages.resolvePackageAppealForUserInternal,
{
actorUserId: "users:moderator",
appealId: "packageAppeals:1",
status: "accepted",
note: "scanner finding cleared",
finalAction: "approve",
},
);
});
it("npm mirror packument lists only ClawPack-backed releases", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("paginationOpts" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Demo package",
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("paginationOpts" in args) {
return {
page: [
{
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
extractedPackageJson: { dependencies: { semver: "^7.0.0" } },
},
{
_id: "packageReleases:legacy",
packageId: "packages:demo-plugin",
version: "0.9.0",
createdAt: 1,
changelog: "Legacy",
distTags: [],
files: [],
artifactKind: "legacy-zip",
},
],
isDone: true,
continueCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.npmMirrorGetHandler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/npm/demo-plugin"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
name: "demo-plugin",
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
dist: {
tarball: "https://example.com/api/npm/demo-plugin/-/demo-plugin-1.0.0.tgz",
integrity: "sha512-demo",
shasum: "d".repeat(40),
},
dependencies: { semver: "^7.0.0" },
},
},
});
});
it("npm mirror uses the public host when requests arrive through Convex rewrites", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("paginationOpts" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Demo package",
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("paginationOpts" in args) {
return {
page: [
{
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
},
],
isDone: true,
continueCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.npmMirrorGetHandler(
makeCtx({ runQuery, runMutation }),
new Request("https://wry-manatee-359.convex.site/api/npm/demo-plugin", {
headers: {
"x-forwarded-host": "clawhub.ai",
"x-forwarded-proto": "https",
},
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
versions: {
"1.0.0": {
dist: {
tarball: "https://clawhub.ai/api/npm/demo-plugin/-/demo-plugin-1.0.0.tgz",
},
},
},
});
});
it("npm mirror falls back to clawhub.ai for production Convex artifact URLs", async () => {
vi.stubEnv("CONVEX_DEPLOYMENT", "prod:wry-manatee-359");
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("paginationOpts" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Demo package",
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("paginationOpts" in args) {
return {
page: [
{
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
},
],
isDone: true,
continueCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.npmMirrorGetHandler(
makeCtx({ runQuery, runMutation }),
new Request("https://wry-manatee-359.convex.site/api/npm/demo-plugin"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
versions: {
"1.0.0": {
dist: {
tarball: "https://clawhub.ai/api/npm/demo-plugin/-/demo-plugin-1.0.0.tgz",
},
},
},
});
});
it("npm mirror tarball downloads record package installs and download metrics", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("paginationOpts" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Demo package",
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("paginationOpts" in args) {
return {
page: [
{
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
},
],
isDone: true,
continueCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.npmMirrorGetHandler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn(async () => new Blob(["tarball"], { type: "application/octet-stream" })),
},
}),
new Request("https://example.com/api/npm/demo-plugin/-/demo-plugin-1.0.0.tgz", {
headers: { "cf-connecting-ip": "203.0.113.10" },
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
internal.packages.recordPackageInstallInternal,
expect.objectContaining({
packageId: "packages:demo-plugin",
identityKind: "ip",
identityHash: expect.stringMatching(/^[a-f0-9]{64}$/),
dayStart: expect.any(Number),
occurredAt: expect.any(Number),
}),
);
expect(runMutation).toHaveBeenCalledWith(
internal.downloadMetrics.recordDownloadMetricInternal,
expect.objectContaining({
target: { kind: "package", id: "packages:demo-plugin" },
identityKind: "ip",
identityHash: expect.stringMatching(/^[a-f0-9]{64}$/),
dayStart: expect.any(Number),
occurredAt: expect.any(Number),
}),
);
});
it("npm mirror returns not found for invalid package lookup names", async () => {
const runQuery = vi.fn(async () => {
throw new Error("unexpected package lookup");
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.npmMirrorGetHandler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/npm/openclaw%2Fdiscord"),
);
expect(response.status).toBe(404);
await expect(response.text()).resolves.toBe("Package not found");
expect(runQuery).not.toHaveBeenCalled();
});
it("npm mirror accepts encoded scoped package packument paths", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("paginationOpts" in args)) {
expect(args.name).toBe("@scope/demo-plugin");
return {
package: {
_id: "packages:demo-plugin",
name: "@scope/demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
summary: "Demo package",
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("paginationOpts" in args) {
expect(args.name).toBe("@scope/demo-plugin");
return {
page: [
{
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "npm-pack",
clawpackStorageId: "storage:clawpack",
npmIntegrity: "sha512-demo",
npmShasum: "d".repeat(40),
npmTarballName: "scope-demo-plugin-1.0.0.tgz",
},
],
isDone: true,
continueCursor: null,
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.npmMirrorGetHandler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/npm/@scope%2Fdemo-plugin"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
name: "@scope/demo-plugin",
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
dist: {
tarball: "https://example.com/api/npm/@scope/demo-plugin/-/scope-demo-plugin-1.0.0.tgz",
},
},
},
});
});
it("returns 400 for /packages/search without q", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn();
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search"),
);
expect(response.status).toBe(400);
await expect(response.text()).resolves.toBe("Missing q query parameter");
expect(runQuery).not.toHaveBeenCalled();
});
it("returns 400 for /packages/search with blank q", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn();
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=%20%20"),
);
expect(response.status).toBe(400);
await expect(response.text()).resolves.toBe("Missing q query parameter");
expect(runQuery).not.toHaveBeenCalled();
});
it("routes /packages/search with q to catalog search only", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async () => []);
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search?q=demo"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ results: [] });
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: "demo",
}),
);
expect(runQuery).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
name: "search",
}),
);
});
it("does not treat nested /packages/search paths as catalog search", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) return null;
return [];
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/search/extra?q=demo"),
);
expect(response.status).toBe(404);
expect(runQuery).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.any(String),
}),
);
});
it("package download uses download rate limiting", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn().mockResolvedValue(new Blob(["{}"], { type: "application/json" })),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/download"),
);
expect(response.status).toBe(200);
expect(response.headers.get("RateLimit-Limit")).toBeTruthy();
expect(findRateLimitCallArgs(runMutation)).toMatchObject({
key: expect.stringMatching(/^ip:/),
limit: RATE_LIMITS.download.ip,
});
});
it("package file uses read rate limiting", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "README.md",
size: 5,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "text/markdown",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn().mockResolvedValue(new Blob(["hello"], { type: "text/markdown" })),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/file?path=README.md"),
);
expect(response.status).toBe(200);
expect(response.headers.get("RateLimit-Limit")).toBeTruthy();
expect(findRateLimitCallArgs(runMutation)).toMatchObject({
key: expect.stringMatching(/^ip:/),
limit: RATE_LIMITS.read.ip,
});
});
it("package file resolves lowercase readme variants from the canonical request path", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "readme.md",
size: 5,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "text/markdown",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn().mockResolvedValue(new Blob(["hello"], { type: "text/markdown" })),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/file?path=README.md"),
);
expect(response.status).toBe(200);
expect(await response.text()).toBe("hello");
});
it("package download uses a package/ root without registry metadata", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner" },
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
{
path: "dist/index.js",
size: 17,
sha256: "b".repeat(64),
storageId: "storage:2",
contentType: "text/javascript",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn(async (storageId: string) => {
if (storageId === "storage:1") {
return new Blob(["{}"], { type: "application/json" });
}
return new Blob(["export default {}"], { type: "text/javascript" });
}),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/download", {
headers: { "cf-connecting-ip": "203.0.113.20" },
}),
);
const zipEntries = unzipSync(new Uint8Array(await response.arrayBuffer()));
expect(Object.keys(zipEntries).sort()).toEqual([
"package/dist/index.js",
"package/package.json",
]);
expect(zipEntries["_meta.json"]).toBeUndefined();
expect(runMutation).toHaveBeenCalledWith(
internal.downloadMetrics.recordDownloadMetricInternal,
{
target: { kind: "package", id: "packages:1" },
identityKind: "ip",
identityHash: expect.any(String),
dayStart: expect.any(Number),
occurredAt: expect.any(Number),
},
);
});
it("package download metrics prefer API token user identity over IP", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:viewer" as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner" },
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn(async () => new Blob(["{}"], { type: "application/json" })),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/download", {
headers: {
authorization: "Bearer clh_test",
"cf-connecting-ip": "203.0.113.20",
},
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
internal.downloadMetrics.recordDownloadMetricInternal,
expect.objectContaining({
target: { kind: "package", id: "packages:1" },
identityKind: "user",
identityHash: expect.stringMatching(/^[a-f0-9]{64}$/),
}),
);
});
it("package downloads succeed and record download metrics", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner" },
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn(async () => new Blob(["{}"], { type: "application/json" })),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/download", {
headers: { "cf-connecting-ip": "203.0.113.20" },
}),
);
expect(response.status).toBe(200);
const mutationArgs = runMutation.mock.calls.map(([, args]) => args);
expect(
mutationArgs.filter((args) => hasPackageDownloadMetricTarget(args, "packages:1")),
).toHaveLength(1);
});
it("package download fails when any stored file is missing", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "users:owner", handle: "owner" },
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
{
path: "dist/index.js",
size: 2,
sha256: "b".repeat(64),
storageId: "storage:missing",
contentType: "text/javascript",
},
],
};
}
return null;
});
const storageGet = vi.fn(async (storageId: string) => {
if (storageId === "storage:1") return new Blob(["{}"], { type: "application/json" });
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: { get: storageGet },
}),
new Request("https://example.com/api/v1/packages/demo-plugin/download"),
);
expect(response.status).toBe(500);
expect(await response.text()).toBe("Missing stored file: dist/index.js");
});
it("allows package downloads while VT scan is pending", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
sha256hash: "a".repeat(64),
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
],
};
}
return null;
});
const storageGet = vi.fn(async () => new Blob(['{"name":"demo-plugin"}']));
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: storageGet } }),
new Request("https://example.com/api/v1/packages/demo-plugin/download"),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("application/zip");
expect(storageGet).toHaveBeenCalledWith("storage:1");
});
it("allows package downloads when verification is clean even without cached vtAnalysis", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
sha256hash: "a".repeat(64),
verification: { scanStatus: "clean" },
files: [
{
path: "package.json",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "application/json",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({
runQuery,
runMutation,
storage: {
get: vi.fn(async () => new Blob(["{}"], { type: "application/json" })),
},
}),
new Request("https://example.com/api/v1/packages/demo-plugin/download"),
);
expect(response.status).toBe(200);
});
it("blocks package file access when release is malicious", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args) {
return {
_id: "packageReleases:1",
version: "1.0.0",
createdAt: 1,
changelog: "init",
verification: { scanStatus: "malicious" },
files: [
{
path: "README.md",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "text/markdown",
},
],
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/packages/demo-plugin/file?path=README.md"),
);
expect(response.status).toBe(403);
expect(await response.text()).toContain("flagged as malicious");
});
it("blocks file and download access to soft-deleted package releases", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:deleted" },
latestReleaseId: "packageReleases:deleted",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("releaseId" in args || "version" in args) {
return {
_id: "packageReleases:deleted",
version: "1.0.0",
createdAt: 1,
changelog: "init",
distTags: ["latest"],
softDeletedAt: 10,
files: [
{
path: "README.md",
size: 2,
sha256: "a".repeat(64),
storageId: "storage:1",
contentType: "text/markdown",
},
],
};
}
return null;
});
const fileResponse = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request(
"https://example.com/api/v1/packages/demo-plugin/file?version=1.0.0&path=README.md",
),
);
const downloadResponse = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
new Request("https://example.com/api/v1/packages/demo-plugin/download?tag=latest"),
);
expect(fileResponse.status).toBe(404);
expect(await fileResponse.text()).toBe("Version not found");
expect(downloadResponse.status).toBe(404);
expect(await downloadResponse.text()).toBe("Version not found");
});
it("package publish uses write rate limiting", async () => {
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi
.fn()
.mockResolvedValue({ ok: true, packageId: "pkg:1", releaseId: "rel:1" });
const form = packagePublishForm(
packagePublishMetadata({
ownerHandle: "openclaw",
bundle: { hostTargets: ["desktop"] },
}),
);
form.append("files", new File(["{}"], "openclaw.plugin.json", { type: "application/json" }));
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: {
store: vi.fn(async (entry: File) => `storage:${entry.name}`),
},
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("RateLimit-Limit")).toBeTruthy();
expect(findRateLimitCallArgs(runMutation)).toMatchObject({
key: "user:users:1:write",
limit: RATE_LIMITS.write.key,
});
expect(runAction).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
payload: expect.objectContaining({ ownerHandle: "openclaw" }),
}),
);
});
it("package publish returns retryable status for transient Convex contention", async () => {
vi.mocked(getOptionalApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi
.fn()
.mockRejectedValue(
new Error(
'Documents read from or written to the "publishers" table changed while this mutation was being run and on every subsequent retry.',
),
);
const form = packagePublishForm(
packagePublishMetadata({
ownerHandle: "openclaw",
bundle: { hostTargets: ["desktop"] },
}),
);
const pack = npmPackFixture({
"package/package.json": JSON.stringify({ name: "demo-plugin", version: "1.0.0" }),
"package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }),
"package/dist/index.js": "export const demo = true;\n",
});
form.append(
"clawpack",
new File([bytesToArrayBuffer(pack)], "demo-plugin-1.0.0.tgz", {
type: "application/octet-stream",
}),
);
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { store: vi.fn(async (_entry: Blob) => "storage:1") },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: {
Authorization: "Bearer clh_test",
},
body: form,
}),
);
expect(response.status).toBe(503);
expect(response.headers.get("Retry-After")).toBe("1");
await expect(response.text()).resolves.toContain("Transient ClawHub write contention");
});
it("package publish rejects JSON request bodies before publish actions run", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi.fn();
const response = await __handlers.publishPackageV1Handler(
makeCtx({ runAction, runMutation }),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify(packagePublishMetadata()),
}),
);
expect(response.status).toBe(415);
expect(await response.text()).toBe("Package publish requires multipart/form-data");
expect(runAction).not.toHaveBeenCalled();
});
it("package publish rejects browser session auth when token auth is not an API token", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:session" as never);
vi.mocked(requirePackagePublishAuth).mockRejectedValue(new Error("Unauthorized"));
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi.fn();
const form = packagePublishForm(packagePublishMetadata());
form.append("files", new File(["{}"], "openclaw.plugin.json", { type: "application/json" }));
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { store: vi.fn(async () => "storage:plugin") },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer convex-session-token" },
body: form,
}),
);
expect(response.status).toBe(401);
expect(await response.text()).toBe("Unauthorized");
expect(runAction).not.toHaveBeenCalled();
});
it("multipart package publish ignores macOS junk files", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi
.fn()
.mockResolvedValue({ ok: true, packageId: "pkg:1", releaseId: "rel:1" });
const storageStore = vi.fn(async () => "storage:plugin");
const form = new FormData();
form.set(
"payload",
JSON.stringify({
name: "demo-plugin",
family: "bundle-plugin",
version: "1.0.0",
changelog: "init",
bundle: { hostTargets: ["desktop"] },
}),
);
form.append("files", new File(["{}"], ".DS_Store", { type: "application/octet-stream" }));
form.append("files", new File(["{}"], "openclaw.plugin.json", { type: "application/json" }));
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { store: storageStore },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(200);
expect(storageStore).toHaveBeenCalledTimes(1);
expect(runAction).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
payload: expect.objectContaining({
files: [
{
path: "openclaw.plugin.json",
size: 2,
storageId: "storage:plugin",
sha256: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
contentType: "application/json",
},
],
}),
}),
);
const actionCall = runAction.mock.calls[0];
expect(actionCall).toBeTruthy();
expect(actionCall[1]).toEqual(
expect.objectContaining({
payload: expect.not.objectContaining({ artifact: expect.anything() }),
}),
);
});
it("multipart ClawPack publish stores the tarball and extracted file metadata", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi
.fn()
.mockResolvedValue({ ok: true, packageId: "pkg:1", releaseId: "rel:1" });
const storageStore = vi.fn(async (_entry: Blob) => `storage:${storageStore.mock.calls.length}`);
const pack = npmPackFixture({
"package/package.json": JSON.stringify({ name: "demo-plugin", version: "1.0.0" }),
"package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }),
"package/dist/index.js": "export const demo = true;\n",
"package/assets/viewer-runtime.js": "x".repeat(MAX_PUBLISH_FILE_BYTES + 1),
});
const form = new FormData();
form.set(
"payload",
JSON.stringify({
name: "demo-plugin",
family: "code-plugin",
version: "1.0.0",
changelog: "init",
}),
);
form.append(
"clawpack",
new File([bytesToArrayBuffer(pack)], "demo-plugin-1.0.0.tgz", {
type: "application/octet-stream",
}),
);
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { store: storageStore },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(200);
expect(storageStore).toHaveBeenCalledTimes(5);
expect(runAction).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
payload: expect.objectContaining({
artifact: expect.objectContaining({
kind: "npm-pack",
storageId: "storage:1",
size: pack.byteLength,
npmFileCount: 4,
}),
files: [
expect.objectContaining({ path: "package.json", storageId: "storage:2" }),
expect.objectContaining({ path: "openclaw.plugin.json", storageId: "storage:3" }),
expect.objectContaining({ path: "dist/index.js", storageId: "storage:4" }),
expect.objectContaining({
path: "assets/viewer-runtime.js",
storageId: "storage:5",
size: MAX_PUBLISH_FILE_BYTES + 1,
}),
],
}),
}),
);
const actionCall = runAction.mock.calls[0];
expect(actionCall).toBeTruthy();
const payload = (actionCall[1] as { payload?: { files?: Array<{ path: string }> } }).payload;
expect(payload?.files?.map((file) => file.path)).toContain("dist/index.js");
});
it("staged ClawPack publish derives artifact metadata from stored bytes", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi
.fn()
.mockResolvedValue({ ok: true, packageId: "pkg:1", releaseId: "rel:1" });
const pack = npmPackFixture({
"package/package.json": JSON.stringify({ name: "demo-plugin", version: "1.0.0" }),
"package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }),
"package/dist/index.js": "export const demo = true;\n",
});
const storageGet = vi.fn(async (storageId: string) =>
storageId === "storage:clawpack"
? new Blob([bytesToArrayBuffer(pack)], { type: "application/octet-stream" })
: null,
);
const storageStore = vi.fn(async (_entry: Blob) => `storage:${storageStore.mock.calls.length}`);
const form = packagePublishForm(
packagePublishMetadata({
family: "code-plugin",
}),
);
form.set("clawpack", "storage:clawpack");
form.set("clawpackUploadTicket", "packagePublishUploadTickets:1");
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { get: storageGet, store: storageStore },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
uploadTicket: "packagePublishUploadTickets:1",
storageId: "storage:clawpack",
auth: { kind: "user", userId: "users:1" },
}),
);
expect(storageGet).toHaveBeenCalledWith("storage:clawpack");
expect(storageStore).toHaveBeenCalledTimes(3);
expect(runAction).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
payload: expect.objectContaining({
artifact: expect.objectContaining({
kind: "npm-pack",
storageId: "storage:clawpack",
size: pack.byteLength,
npmFileCount: 3,
}),
files: [
expect.objectContaining({ path: "package.json", storageId: "storage:1" }),
expect.objectContaining({ path: "openclaw.plugin.json", storageId: "storage:2" }),
expect.objectContaining({ path: "dist/index.js", storageId: "storage:3" }),
],
}),
}),
);
});
it("staged ClawPack publish rejects storage ids without upload tickets", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi.fn();
const storageGet = vi.fn();
const form = packagePublishForm(packagePublishMetadata({ family: "code-plugin" }));
form.set("clawpack", "storage:clawpack");
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { get: storageGet, store: vi.fn() },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Package tarball upload ticket required");
expect(storageGet).not.toHaveBeenCalled();
expect(runAction).not.toHaveBeenCalled();
});
it("multipart package publish rejects files and tarball together", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi.fn();
const pack = npmPackFixture({
"package/package.json": JSON.stringify({ name: "demo-plugin", version: "1.0.0" }),
"package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }),
});
const form = packagePublishForm(packagePublishMetadata({ family: "code-plugin" }));
form.append("files", new File(["{}"], "openclaw.plugin.json", { type: "application/json" }));
form.append(
"clawpack",
new File([bytesToArrayBuffer(pack)], "demo-plugin-1.0.0.tgz", {
type: "application/octet-stream",
}),
);
const response = await __handlers.publishPackageV1Handler(
makeCtx({ runAction, runMutation, storage: { store: vi.fn() } }),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe(
"Upload either a package tarball or individual files, not both",
);
expect(runAction).not.toHaveBeenCalled();
});
it.each(["files[]", "tarball", "artifact", "extraMetadata"])(
"multipart package publish rejects unsupported field %s",
async (field) => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi.fn();
const form = packagePublishForm(packagePublishMetadata());
form.append("files", new File(["{}"], "openclaw.plugin.json", { type: "application/json" }));
form.append(field, new File(["{}"], "ignored.json", { type: "application/json" }));
const response = await __handlers.publishPackageV1Handler(
makeCtx({ runAction, runMutation, storage: { store: vi.fn() } }),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe(`Unsupported package publish form field: ${field}`);
expect(runAction).not.toHaveBeenCalled();
},
);
it.each(["files", "artifact"])(
"multipart package publish rejects caller-supplied %s metadata",
async (field) => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "user",
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi.fn();
const form = packagePublishForm(
packagePublishMetadata({
[field]:
field === "files"
? [
{
path: "openclaw.plugin.json",
size: 2,
storageId: "storage:attacker",
sha256: "a".repeat(64),
},
]
: {
kind: "npm-pack",
storageId: "storage:attacker",
sha256: "a".repeat(64),
size: 2,
format: "tgz",
npmIntegrity: "sha512-attacker",
npmShasum: "a".repeat(40),
npmTarballName: "demo-plugin-1.0.0.tgz",
npmUnpackedSize: 2,
npmFileCount: 1,
},
}),
);
form.append("files", new File(["{}"], "openclaw.plugin.json", { type: "application/json" }));
const response = await __handlers.publishPackageV1Handler(
makeCtx({ runAction, runMutation, storage: { store: vi.fn() } }),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: form,
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toContain(`Package publish payload: ${field}`);
expect(runAction).not.toHaveBeenCalled();
},
);
it("package publish routes GitHub Actions auth through the trusted publisher action", async () => {
vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never);
vi.mocked(requirePackagePublishAuth).mockResolvedValue({
kind: "github-actions",
publishToken: { _id: "packagePublishTokens:1" },
} as never);
const runMutation = vi.fn().mockResolvedValue(okRate());
const runAction = vi
.fn()
.mockResolvedValue({ ok: true, packageId: "pkg:1", releaseId: "rel:1" });
const form = packagePublishForm(
packagePublishMetadata({
bundle: { hostTargets: ["desktop"] },
}),
);
const pack = npmPackFixture({
"package/package.json": JSON.stringify({ name: "demo-plugin", version: "1.0.0" }),
"package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }),
"package/dist/index.js": "export const demo = true;\n",
});
form.append(
"clawpack",
new File([bytesToArrayBuffer(pack)], "demo-plugin-1.0.0.tgz", {
type: "application/octet-stream",
}),
);
const response = await __handlers.publishPackageV1Handler(
makeCtx({
runAction,
runMutation,
storage: { store: vi.fn(async (_entry: Blob) => "storage:1") },
}),
new Request("https://example.com/api/v1/packages", {
method: "POST",
headers: {
Authorization: "Bearer clh_publish",
},
body: form,
}),
);
expect(response.status).toBe(200);
expect(runAction).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
publishTokenId: "packagePublishTokens:1",
}),
);
});
it("returns trusted publisher config for a package", async () => {
const runMutation = vi.fn().mockResolvedValue(okRate());
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
package: {
_id: "packages:1",
name: "@openclaw/demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: {},
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: null,
};
}
if ("packageId" in args) {
return {
_id: "packageTrustedPublishers:1",
packageId: "packages:1",
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
createdAt: 1,
updatedAt: 1,
};
}
return null;
});
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher",
),
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
trustedPublisher: {
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
},
});
});
it("mints a short-lived publish token after verifying GitHub OIDC", async () => {
vi.mocked(verifyGitHubActionsTrustedPublishJwt).mockResolvedValue({
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
runId: "101",
runAttempt: "1",
sha: "abc123",
ref: "refs/heads/main",
refType: "branch",
actor: "onur",
actorId: "42",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return "mutation:ok";
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
_id: "packages:1",
name: "@openclaw/demo-plugin",
ownerUserId: "users:owner",
};
}
if ("packageId" in args) {
return {
_id: "packageTrustedPublishers:1",
packageId: "packages:1",
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
};
}
return null;
});
const response = await __handlers.mintPublishTokenV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/publish/token/mint", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
packageName: "@openclaw/demo-plugin",
version: "1.0.0",
githubOidcToken: "gh.jwt",
}),
}),
);
if (response.status !== 200) throw new Error(await response.text());
const body = await response.json();
expect(body.token).toEqual(expect.any(String));
expect(body.expiresAt).toEqual(expect.any(Number));
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
key: "ip:unknown:trustedPublish",
limit: RATE_LIMITS.trustedPublish.ip,
}),
);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
packageId: "packages:1",
version: "1.0.0",
repository: "openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
runId: "101",
sha: "abc123",
}),
);
});
it("mints a short-lived publish token without environment when none is pinned", async () => {
vi.mocked(verifyGitHubActionsTrustedPublishJwt).mockResolvedValue({
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
runId: "101",
runAttempt: "1",
sha: "abc123",
ref: "refs/heads/main",
refType: "branch",
actor: "onur",
actorId: "42",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return "mutation:ok";
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args) {
return {
_id: "packages:1",
name: "@openclaw/demo-plugin",
ownerUserId: "users:owner",
};
}
if ("packageId" in args) {
return {
_id: "packageTrustedPublishers:1",
packageId: "packages:1",
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
};
}
return null;
});
const response = await __handlers.mintPublishTokenV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/publish/token/mint", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
packageName: "@openclaw/demo-plugin",
version: "1.0.0",
githubOidcToken: "gh.jwt",
}),
}),
);
if (response.status !== 200) throw new Error(await response.text());
const body = await response.json();
expect(body.token).toEqual(expect.any(String));
expect(body.expiresAt).toEqual(expect.any(Number));
const createCall = runMutation.mock.calls.find(
([, args]) =>
typeof args === "object" && args !== null && "packageId" in args && "tokenHash" in args,
);
expect(createCall?.[1]).toEqual(
expect.objectContaining({
packageId: "packages:1",
version: "1.0.0",
repository: "openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
runId: "101",
sha: "abc123",
}),
);
expect(createCall?.[1]).not.toHaveProperty("environment");
});
it("sets trusted publisher config for a package", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
vi.mocked(fetchGitHubRepositoryIdentity).mockResolvedValue({
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
_id: "packageTrustedPublishers:1",
packageId: "packages:1",
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request(
"https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher",
{
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({
repository: "https://github.com/openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
}),
},
),
);
if (response.status !== 200) throw new Error(await response.text());
expect(fetchGitHubRepositoryIdentity).toHaveBeenCalledWith(
"https://github.com/openclaw/openclaw",
);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
packageName: "@openclaw/demo-plugin",
repository: "openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
environment: "clawhub-release",
}),
);
});
it("transfers a package through the public package transfer endpoint", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:vincent",
user: { _id: "users:vincent", handle: "vincentkoc" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
packageId: "packages:opik",
name: "@opik/opik-openclaw",
ownerUserId: "users:vincent",
ownerPublisherId: "publishers:opik",
channel: "community",
isOfficial: false,
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40opik%2Fopik-openclaw/transfer", {
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({ toOwner: "opik" }),
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(await response.json()).toEqual({
ok: true,
packageId: "packages:opik",
name: "@opik/opik-openclaw",
ownerUserId: "users:vincent",
ownerPublisherId: "publishers:opik",
channel: "community",
isOfficial: false,
});
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:vincent",
name: "@opik/opik-openclaw",
toOwner: "opik",
}),
);
});
it("dry-runs package name repair without mutating packages", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-15T12:00:00Z"));
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin", handle: "patrick" },
} as never);
const sourcePackage = {
_id: "packages:source",
name: "@openclaw/openviking",
normalizedName: "@openclaw/openviking",
runtimeId: "openviking",
ownerUserId: "users:lin",
ownerPublisherId: "publishers:lin",
channel: "community",
softDeletedAt: undefined,
};
const targetPackage = {
_id: "packages:target",
name: "@openviking/openclaw-plugin",
normalizedName: "@openviking/openclaw-plugin",
runtimeId: "openviking-openclaw-plugin-placeholder",
ownerUserId: "users:openviking",
ownerPublisherId: "publishers:openviking",
channel: "private",
softDeletedAt: undefined,
};
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
if (args.name === "@openclaw/openviking") return sourcePackage;
if (args.name === "@openviking/openclaw-plugin") return targetPackage;
if (args.name === "@openviking/openclaw-plugin-retired-20260515") return null;
return null;
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fopenviking/repair-name", {
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({
nextName: "@openviking/openclaw-plugin",
retireTarget: true,
reason: "Admin repair for openclaw/clawhub#2133",
dryRun: true,
}),
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(await response.json()).toMatchObject({
ok: true,
dryRun: true,
source: { packageId: "packages:source", name: "@openclaw/openviking" },
target: { packageId: "packages:target", name: "@openviking/openclaw-plugin" },
retiredName: "@openviking/openclaw-plugin-retired-20260515",
operations: [
{
action: "retire-target",
from: "@openviking/openclaw-plugin",
to: "@openviking/openclaw-plugin-retired-20260515",
},
{
action: "rename-source",
from: "@openclaw/openviking",
to: "@openviking/openclaw-plugin",
},
],
});
expect(runMutation).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ name: "@openviking/openclaw-plugin" }),
);
vi.useRealTimers();
});
it("applies package name repair by retiring the occupied target first", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-15T12:00:00Z"));
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin", handle: "patrick" },
} as never);
const sourcePackage = {
_id: "packages:source",
name: "@openclaw/openviking",
normalizedName: "@openclaw/openviking",
runtimeId: "openviking",
ownerUserId: "users:lin",
ownerPublisherId: "publishers:lin",
channel: "community",
softDeletedAt: undefined,
};
const targetPackage = {
_id: "packages:target",
name: "@openviking/openclaw-plugin",
normalizedName: "@openviking/openclaw-plugin",
runtimeId: "openviking-openclaw-plugin-placeholder",
ownerUserId: "users:openviking",
ownerPublisherId: "publishers:openviking",
channel: "private",
softDeletedAt: undefined,
};
const ownerPublisher = {
_id: "publishers:openviking",
handle: "openviking",
kind: "org",
deletedAt: undefined,
};
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true, packageId: "packages:source" };
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
if (args.name === "@openclaw/openviking") return sourcePackage;
if (args.name === "@openviking/openclaw-plugin") return targetPackage;
if (args.name === "@openviking/openclaw-plugin-retired-20260515") return null;
if (args.handle === "openviking") return ownerPublisher;
return null;
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fopenviking/repair-name", {
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({
nextName: "@openviking/openclaw-plugin",
retireTarget: true,
owner: "openviking",
reason: "Admin repair for openclaw/clawhub#2133",
dryRun: false,
}),
}),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runMutation).toHaveBeenNthCalledWith(
2,
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
name: "@openviking/openclaw-plugin",
nextName: "@openviking/openclaw-plugin-retired-20260515",
}),
);
expect(runMutation).toHaveBeenNthCalledWith(
3,
expect.anything(),
expect.objectContaining({
userId: "users:admin",
name: "@openviking/openclaw-plugin-retired-20260515",
}),
);
expect(runMutation).toHaveBeenNthCalledWith(
4,
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
name: "@openclaw/openviking",
nextName: "@openviking/openclaw-plugin",
}),
);
expect(runMutation).toHaveBeenNthCalledWith(
5,
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
name: "@openviking/openclaw-plugin",
ownerUserId: "users:lin",
ownerPublisherId: "publishers:openviking",
channel: "community",
}),
);
vi.useRealTimers();
});
it("dry-runs package runtime id repair without mutating packages", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin", handle: "patrick" },
} as never);
const sourcePackage = {
_id: "packages:stepfun",
name: "@hengm3467/stepfun-openclaw-plugin",
normalizedName: "@hengm3467/stepfun-openclaw-plugin",
runtimeId: "stepfun",
ownerUserId: "users:hengm",
ownerPublisherId: "publishers:hengm",
channel: "community",
softDeletedAt: undefined,
};
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
if (args.name === "@hengm3467/stepfun-openclaw-plugin") return sourcePackage;
return null;
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/packages/%40hengm3467%2Fstepfun-openclaw-plugin/repair-runtime-id",
{
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({
nextRuntimeId: "stepfun-2",
reason: "Release official StepFun runtime id claim",
dryRun: true,
}),
},
),
);
if (response.status !== 200) throw new Error(await response.text());
expect(await response.json()).toMatchObject({
ok: true,
dryRun: true,
source: {
packageId: "packages:stepfun",
name: "@hengm3467/stepfun-openclaw-plugin",
runtimeId: "stepfun",
},
operations: [
{
action: "repair-runtime-id",
packageId: "packages:stepfun",
from: "stepfun",
to: "stepfun-2",
},
],
});
expect(runMutation).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
name: "@hengm3467/stepfun-openclaw-plugin",
nextRuntimeId: "stepfun-2",
}),
);
});
it("applies package runtime id repair through the admin repair mutation", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin", handle: "patrick" },
} as never);
const sourcePackage = {
_id: "packages:stepfun",
name: "@hengm3467/stepfun-openclaw-plugin",
normalizedName: "@hengm3467/stepfun-openclaw-plugin",
runtimeId: "stepfun",
ownerUserId: "users:hengm",
ownerPublisherId: "publishers:hengm",
channel: "community",
softDeletedAt: undefined,
};
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
ok: true,
packageId: "packages:stepfun",
name: "@hengm3467/stepfun-openclaw-plugin",
runtimeId: "stepfun-2",
};
});
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
if (args.name === "@hengm3467/stepfun-openclaw-plugin") return sourcePackage;
return null;
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(
"https://example.com/api/v1/packages/%40hengm3467%2Fstepfun-openclaw-plugin/repair-runtime-id",
{
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({
nextRuntimeId: "stepfun-2",
reason: "Release official StepFun runtime id claim",
dryRun: false,
}),
},
),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:admin",
name: "@hengm3467/stepfun-openclaw-plugin",
nextRuntimeId: "stepfun-2",
reason: "Release official StepFun runtime id claim",
}),
);
});
it("package transfer maps ownership denials to 403", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:stranger",
user: { _id: "users:stranger", handle: "stranger" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error("Forbidden: Only owners can transfer this package.");
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40opik%2Fopik-openclaw/transfer", {
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({ toOwner: "opik" }),
}),
);
expect(response.status).toBe(403);
expect(await response.text()).toBe("Forbidden: Only owners can transfer this package.");
});
it("sets trusted publisher config for a package without environment", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
vi.mocked(fetchGitHubRepositoryIdentity).mockResolvedValue({
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return {
_id: "packageTrustedPublishers:1",
packageId: "packages:1",
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request(
"https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher",
{
method: "POST",
headers: {
Authorization: "Bearer clh_test",
"content-type": "application/json",
},
body: JSON.stringify({
repository: "https://github.com/openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
}),
},
),
);
if (response.status !== 200) throw new Error(await response.text());
expect(fetchGitHubRepositoryIdentity).toHaveBeenCalledWith(
"https://github.com/openclaw/openclaw",
);
expect(await response.json()).toEqual({
trustedPublisher: {
provider: "github-actions",
repository: "openclaw/openclaw",
repositoryId: "1",
repositoryOwner: "openclaw",
repositoryOwnerId: "2",
workflowFilename: "plugin-clawhub-release.yml",
},
});
const setCall = runMutation.mock.calls.find(
([, args]) =>
typeof args === "object" && args !== null && "packageName" in args && "actorUserId" in args,
);
expect(setCall?.[1]).toEqual(
expect.objectContaining({
actorUserId: "users:1",
packageName: "@openclaw/demo-plugin",
repository: "openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
}),
);
expect(setCall?.[1]).not.toHaveProperty("environment");
});
it("deletes a package", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
userId: "users:1",
name: "@openclaw/demo-plugin",
}),
);
});
it("deletes one package version through the authenticated package delete route", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: " 1.2.3 " }),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true });
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
name: "@openclaw/demo-plugin",
version: "1.2.3",
}),
);
const versionDeleteArgs = runMutation.mock.calls.find(
([, args]) => typeof args === "object" && args !== null && "version" in args,
)?.[1];
expect(versionDeleteArgs).not.toHaveProperty("userId");
});
it("uses the package version route when a redirect drops the request body", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
const mutationArgs = runMutation.mock.calls.find(
([, args]) => typeof args === "object" && args !== null && !("key" in args),
)?.[1];
expect(mutationArgs).toMatchObject({
actorUserId: "users:1",
name: "@openclaw/demo-plugin",
version: "1.2.3",
});
expect(mutationArgs).not.toHaveProperty("userId");
});
it("rejects conflicting package version selectors across query, body, and path", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
for (const { bodyVersion, queryVersion } of [
{ bodyVersion: "1.2.3", queryVersion: "9.9.9" },
{ bodyVersion: "9.9.9", queryVersion: "1.2.3" },
]) {
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request(
`https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3?version=${queryVersion}`,
{
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: bodyVersion }),
},
),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Version does not match request target");
}
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects a body-only package version selector on the whole-package route", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.2.3" }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toContain("/versions/1.2.3");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects an empty package version without deleting the whole package", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: " " }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Version cannot be empty");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects a non-string package version without deleting the whole package", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: 123 }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Version must be a non-empty string");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("rejects malformed package version delete JSON", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: "{",
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Invalid JSON");
expect(runMutation.mock.calls.filter(([, args]) => !("key" in args))).toHaveLength(0);
});
it("preserves latest-release replacement guidance from package version deletion", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const message = "Publish a replacement release before deleting the current latest release.";
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error(message);
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/1.2.3", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ version: "1.2.3" }),
}),
);
expect(response.status).toBe(400);
expect(await response.text()).toBe(message);
});
it("undeletes a package", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { ok: true };
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/undelete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
userId: "users:1",
name: "@openclaw/demo-plugin",
}),
);
});
it("package delete and undelete map ownership denials to 403", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:stranger",
user: { _id: "users:stranger", handle: "stranger" },
} as never);
const runMutationForbidden = vi.fn(
async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error("Forbidden: This package belongs to another owner.");
},
);
const deleteResponse = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation: runMutationForbidden }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(deleteResponse.status).toBe(403);
expect(await deleteResponse.text()).toBe("Forbidden: This package belongs to another owner.");
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:stranger",
user: { _id: "users:stranger", handle: "stranger" },
} as never);
const undeleteResponse = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation: runMutationForbidden }),
new Request("https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/undelete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(undeleteResponse.status).toBe(403);
expect(await undeleteResponse.text()).toBe("Forbidden: This package belongs to another owner.");
});
it("deletes trusted publisher config for a package", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { _id: "users:1", handle: "p" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
return { deleted: true };
});
const response = await __handlers.packagesDeleteRouterV1Handler(
makeCtx({ runMutation }),
new Request(
"https://example.com/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher",
{
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
},
),
);
expect(response.status).toBe(200);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
actorUserId: "users:1",
packageName: "@openclaw/demo-plugin",
}),
);
});
it("delete/undelete map forbidden/not-found/unknown to 403/404/500", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutationForbidden = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error("Forbidden");
});
const forbidden = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation: runMutationForbidden }),
new Request("https://example.com/api/v1/skills/demo", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(forbidden.status).toBe(403);
expect(await forbidden.text()).toBe("Forbidden");
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutationNotFound = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error("Skill not found");
});
const notFound = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation: runMutationNotFound }),
new Request("https://example.com/api/v1/skills/demo/undelete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(notFound.status).toBe(404);
expect(await notFound.text()).toBe("Skill not found");
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:1",
user: { handle: "p" },
} as never);
const runMutationUnknown = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
throw new Error("boom");
});
const unknown = await __handlers.skillsDeleteRouterV1Handler(
makeCtx({ runMutation: runMutationUnknown }),
new Request("https://example.com/api/v1/skills/demo", {
method: "DELETE",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(unknown.status).toBe(500);
expect(await unknown.text()).toBe("Internal Server Error");
});
// Regression: owner undelete gate throws a ConvexError prefixed with
// "Forbidden:" so the HTTP layer returns a deterministic 403 and surfaces
// the actionable reason ("hidden by moderation") instead of falling through
// to a generic 500.
it("owner undelete denial returns 403 with moderation reason in body", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:owner",
user: { handle: "p" },
} as never);
const moderationMessage =
"Forbidden: This skill was hidden by moderation and cannot be restored by the owner. Please contact a moderator.";
const runMutationModerationDenied = vi.fn(
async (_query: unknown, args: Record<string, unknown>) => {
if ("key" in args) return okRate();
// Mirror ConvexError shape: Error subclass whose message carries the
// "Forbidden:" sentinel so softDeleteErrorToResponse routes to 403.
throw new Error(moderationMessage);
},
);
const response = await __handlers.skillsPostRouterV1Handler(
makeCtx({ runMutation: runMutationModerationDenied }),
new Request("https://example.com/api/v1/skills/demo/undelete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(403);
expect(await response.text()).toBe(moderationMessage);
});
});