Files
clawhub/convex/skillVersionRevocation.test.ts
T

211 lines
5.8 KiB
TypeScript

/* @vitest-environment node */
import { describe, expect, it, vi } from "vitest";
import { buildSkillVersionRevocationPlan, revokeSkillVersionForUser } from "./skills";
function makeVersion(id: string, version: string, createdAt: number) {
return {
_id: id,
skillId: "skills:demo",
version,
changelog: `${version} changes`,
changelogSource: "user",
parsed: {
frontmatter: {
name: `Demo ${version}`,
description: `Description ${version}`,
},
clawdis: { version },
},
icon: `lucide:${version}`,
createdAt,
};
}
function makeSkill() {
return {
_id: "skills:demo",
slug: "demo",
displayName: "Demo 2.0.0",
summary: "Description 2.0.0",
icon: "lucide:2.0.0",
latestVersionId: "skillVersions:v2",
latestVersionSummary: {
version: "2.0.0",
createdAt: 20,
changelog: "2.0.0 changes",
},
tags: {
latest: "skillVersions:v2",
stable: "skillVersions:v2",
legacy: "skillVersions:v1",
},
moderationStatus: "hidden",
moderationReason: "user.banned",
softDeletedAt: 100,
};
}
describe("buildSkillVersionRevocationPlan", () => {
it("moves latest pointers to the highest available replacement without lifting a parent hold", () => {
const plan = buildSkillVersionRevocationPlan({
actorUserId: "users:moderator" as never,
skill: makeSkill() as never,
target: makeVersion("skillVersions:v2", "2.0.0", 20) as never,
replacement: makeVersion("skillVersions:v1", "1.0.0", 10) as never,
reason: "confirmed unsafe artifact",
now: 200,
});
expect(plan.versionPatch).toEqual({
softDeletedAt: 200,
manualRevocation: {
reason: "confirmed unsafe artifact",
reviewerUserId: "users:moderator",
revokedAt: 200,
},
});
expect(plan.skillPatch).toMatchObject({
latestVersionId: "skillVersions:v1",
displayName: "Demo 1.0.0",
summary: "Description 1.0.0",
icon: "lucide:1.0.0",
tags: {
latest: "skillVersions:v1",
legacy: "skillVersions:v1",
},
});
expect(plan.skillPatch).not.toHaveProperty("softDeletedAt");
expect(plan.skillPatch).not.toHaveProperty("moderationReason");
});
it("creates an independent hold when the revoked latest has no available replacement", () => {
const skill = {
...makeSkill(),
tags: {
latest: "skillVersions:v2",
stable: "skillVersions:v2",
},
};
const plan = buildSkillVersionRevocationPlan({
actorUserId: "users:moderator" as never,
skill: skill as never,
target: makeVersion("skillVersions:v2", "2.0.0", 20) as never,
replacement: null,
reason: "confirmed unsafe artifact",
now: 200,
});
expect(plan.skillPatch).toMatchObject({
latestVersionId: undefined,
latestVersionSummary: undefined,
tags: {},
softDeletedAt: 200,
moderationStatus: "hidden",
moderationReason: "manual.version_revoked",
moderationNotes: "confirmed unsafe artifact",
hiddenAt: 200,
hiddenBy: "users:moderator",
manualOverride: undefined,
});
});
it("does not change skill pointers when a non-latest version is revoked", () => {
const plan = buildSkillVersionRevocationPlan({
actorUserId: "users:moderator" as never,
skill: makeSkill() as never,
target: makeVersion("skillVersions:v1", "1.0.0", 10) as never,
replacement: null,
reason: "confirmed unsafe artifact",
now: 200,
});
expect(plan.skillPatch).toEqual({
tags: {
latest: "skillVersions:v2",
stable: "skillVersions:v2",
},
updatedAt: 200,
});
});
});
describe("revokeSkillVersionForUser", () => {
it("rejects non-moderator actors before resolving the skill", async () => {
const ctx = {
db: {
get: vi.fn().mockResolvedValue({
_id: "users:publisher",
role: "user",
deletedAt: undefined,
deactivatedAt: undefined,
}),
},
};
await expect(
revokeSkillVersionForUser(ctx as never, {
actorUserId: "users:publisher" as never,
slug: "demo",
version: "1.0.0",
reason: "confirmed unsafe artifact",
}),
).rejects.toThrow("Forbidden");
expect(ctx.db.get).toHaveBeenCalledTimes(1);
});
it("resolves already-hidden skills so their exact versions can be revoked before unban", async () => {
const hiddenSkill = {
_id: "skills:hidden",
slug: "hidden-demo",
softDeletedAt: 100,
moderationStatus: "hidden",
moderationReason: "user.banned",
tags: {},
};
const ctx = {
db: {
get: vi.fn().mockResolvedValue({
_id: "users:moderator",
role: "moderator",
deletedAt: undefined,
deactivatedAt: undefined,
}),
query: vi.fn((table: string) => {
if (table === "skills") {
return {
withIndex: vi.fn(() => ({
take: vi.fn().mockResolvedValue([hiddenSkill]),
})),
};
}
if (table === "skillSlugAliases") {
return {
withIndex: vi.fn(() => ({
take: vi.fn().mockResolvedValue([]),
})),
};
}
if (table === "skillVersions") {
return {
withIndex: vi.fn(() => ({
unique: vi.fn().mockResolvedValue(null),
})),
};
}
throw new Error(`Unexpected table ${table}`);
}),
},
};
await expect(
revokeSkillVersionForUser(ctx as never, {
actorUserId: "users:moderator" as never,
slug: "hidden-demo",
version: "1.0.0",
reason: "confirmed unsafe artifact",
}),
).rejects.toThrow("Skill version not found");
});
});