mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
655 lines
18 KiB
TypeScript
655 lines
18 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("./lib/access", async () => {
|
|
const actual = await vi.importActual<typeof import("./lib/access")>("./lib/access");
|
|
return {
|
|
...actual,
|
|
requireUser: vi.fn(),
|
|
};
|
|
});
|
|
|
|
const { requireUser } = await import("./lib/access");
|
|
const { setSkillManualOverride, clearSkillManualOverride, updateVersionLlmAnalysisInternal } =
|
|
await import("./skills");
|
|
|
|
type WrappedHandler<TArgs, TResult = unknown> = {
|
|
_handler: (ctx: unknown, args: TArgs) => Promise<TResult>;
|
|
};
|
|
|
|
const setSkillManualOverrideHandler = (
|
|
setSkillManualOverride as unknown as WrappedHandler<{
|
|
skillId: string;
|
|
note: string;
|
|
}>
|
|
)._handler;
|
|
|
|
const clearSkillManualOverrideHandler = (
|
|
clearSkillManualOverride as unknown as WrappedHandler<{
|
|
skillId: string;
|
|
note: string;
|
|
}>
|
|
)._handler;
|
|
|
|
const updateVersionLlmAnalysisInternalHandler = (
|
|
updateVersionLlmAnalysisInternal as unknown as WrappedHandler<{
|
|
versionId: string;
|
|
moderationMode?: "normal" | "preserve";
|
|
llmAnalysis: Record<string, unknown>;
|
|
}>
|
|
)._handler;
|
|
|
|
function makeCtx(params: { skill: Record<string, unknown>; version?: Record<string, unknown> }) {
|
|
const patch = vi.fn(async () => {});
|
|
const insert = vi.fn(async () => "auditLogs:1");
|
|
const query = vi.fn((table: string) => {
|
|
if (table === "globalStats") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => ({ _id: "globalStats:1", activeSkillsCount: 1 })),
|
|
})),
|
|
};
|
|
}
|
|
|
|
if (table === "skills") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn(async () => [params.skill]),
|
|
})),
|
|
};
|
|
}
|
|
|
|
throw new Error(`Unexpected query table: ${table}`);
|
|
});
|
|
const get = vi.fn(async (id: string) => {
|
|
if (id === params.skill._id) return params.skill;
|
|
if (params.version && id === params.version._id) return params.version;
|
|
if (params.version && id === params.skill.latestVersionId) return params.version;
|
|
return null;
|
|
});
|
|
|
|
return {
|
|
ctx: {
|
|
db: { get, patch, insert, query, normalizeId: vi.fn() },
|
|
} as never,
|
|
patch,
|
|
insert,
|
|
get,
|
|
query,
|
|
};
|
|
}
|
|
|
|
function findPatchForId(patch: ReturnType<typeof vi.fn>, id: string) {
|
|
return (patch.mock.calls as Array<[string, Record<string, unknown>]>).find(
|
|
([patchedId]) => patchedId === id,
|
|
)?.[1];
|
|
}
|
|
|
|
describe("skills manual overrides", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.mocked(requireUser).mockReset();
|
|
});
|
|
|
|
it("applies a skill-level override and preserves scan metadata", async () => {
|
|
const now = 1_700_000_000_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
vi.mocked(requireUser).mockResolvedValue({
|
|
userId: "users:moderator",
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
} as never);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
latestVersionId: "skillVersions:1",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "active",
|
|
moderationReason: "scanner.vt.suspicious",
|
|
moderationVerdict: "suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
moderationReasonCodes: ["suspicious.vt_suspicious"],
|
|
moderationEvidence: [
|
|
{ code: "x", severity: "warn", file: "SKILL.md", line: 1, message: "x", evidence: "x" },
|
|
],
|
|
moderationEngineVersion: "v2.0.0",
|
|
moderationSourceVersionId: "skillVersions:1",
|
|
};
|
|
|
|
const { ctx, patch, insert } = makeCtx({ skill });
|
|
|
|
await setSkillManualOverrideHandler(ctx, {
|
|
skillId: "skills:1",
|
|
note: "reviewed locally",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:1",
|
|
expect.objectContaining({
|
|
manualOverride: expect.objectContaining({
|
|
verdict: "clean",
|
|
note: "reviewed locally",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: now,
|
|
}),
|
|
moderationReason: "manual.override.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
moderationReasonCodes: ["suspicious.vt_suspicious"],
|
|
moderationEngineVersion: "v2.0.0",
|
|
isSuspicious: false,
|
|
}),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "skill.manual_override.set",
|
|
targetType: "skill",
|
|
targetId: "skills:1",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("increments global public count when an override restores a hidden skill", async () => {
|
|
const now = 1_700_000_050_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
vi.mocked(requireUser).mockResolvedValue({
|
|
userId: "users:moderator",
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
} as never);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
latestVersionId: "skillVersions:1",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "scanner.vt.suspicious",
|
|
moderationVerdict: "suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
moderationReasonCodes: ["suspicious.vt_suspicious"],
|
|
moderationSourceVersionId: "skillVersions:1",
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill });
|
|
|
|
await setSkillManualOverrideHandler(ctx, {
|
|
skillId: "skills:1",
|
|
note: "reviewed and okay to list",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"globalStats:1",
|
|
expect.objectContaining({
|
|
activeSkillsCount: 2,
|
|
updatedAt: now,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("clears a skill-level override and restores scanner-derived suspicious state", async () => {
|
|
const now = 1_700_000_100_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
vi.mocked(requireUser).mockResolvedValue({
|
|
userId: "users:moderator",
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
} as never);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
latestVersionId: "skillVersions:3",
|
|
moderationReason: "manual.override.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
manualOverride: {
|
|
verdict: "clean",
|
|
note: "reviewed locally",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: now - 10_000,
|
|
},
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:3",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: {
|
|
status: "suspicious",
|
|
source: "engines",
|
|
engineStats: { malicious: 0, suspicious: 1, undetected: 64 },
|
|
checkedAt: now - 1000,
|
|
},
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch, insert } = makeCtx({ skill, version });
|
|
|
|
await clearSkillManualOverrideHandler(ctx, {
|
|
skillId: "skills:1",
|
|
note: "scanner is fixed now",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:1",
|
|
expect.objectContaining({
|
|
manualOverride: undefined,
|
|
updatedAt: now,
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:1",
|
|
expect.objectContaining({
|
|
moderationReason: "scanner.aggregate.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
isSuspicious: false,
|
|
}),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "skill.manual_override.clear",
|
|
targetType: "skill",
|
|
targetId: "skills:1",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("clears a skill-level override and restores hidden malicious state", async () => {
|
|
const now = 1_700_000_200_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
vi.mocked(requireUser).mockResolvedValue({
|
|
userId: "users:moderator",
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
} as never);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:4",
|
|
moderationStatus: "active",
|
|
moderationReason: "manual.override.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
manualOverride: {
|
|
verdict: "clean",
|
|
note: "reviewed locally",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: now - 10_000,
|
|
},
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:4",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: {
|
|
status: "malicious",
|
|
source: "engines",
|
|
engineStats: { malicious: 1, suspicious: 0, undetected: 64 },
|
|
checkedAt: now - 1000,
|
|
},
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill, version });
|
|
|
|
await clearSkillManualOverrideHandler(ctx, {
|
|
skillId: "skills:1",
|
|
note: "restoring scanner verdict",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:1",
|
|
expect.objectContaining({
|
|
moderationStatus: "active",
|
|
moderationReason: "scanner.aggregate.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
hiddenAt: undefined,
|
|
lastReviewedAt: undefined,
|
|
isSuspicious: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects override notes longer than the max length", async () => {
|
|
vi.mocked(requireUser).mockResolvedValue({
|
|
userId: "users:moderator",
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
} as never);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
latestVersionId: "skillVersions:1",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "active",
|
|
moderationReason: "scanner.vt.suspicious",
|
|
moderationVerdict: "suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
};
|
|
|
|
const { ctx, patch, insert } = makeCtx({ skill });
|
|
|
|
await expect(
|
|
setSkillManualOverrideHandler(ctx, {
|
|
skillId: "skills:1",
|
|
note: "x".repeat(1201),
|
|
}),
|
|
).rejects.toThrow("Audit note must be at most 1200 characters.");
|
|
expect(patch).not.toHaveBeenCalled();
|
|
expect(insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects manual overrides for malware-blocked skills", async () => {
|
|
vi.mocked(requireUser).mockResolvedValue({
|
|
userId: "users:moderator",
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
} as never);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
latestVersionId: "skillVersions:1",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "manual.override.clean",
|
|
moderationVerdict: "malicious",
|
|
moderationFlags: ["blocked.malware"],
|
|
};
|
|
|
|
const { ctx, patch, insert } = makeCtx({ skill });
|
|
|
|
await expect(
|
|
setSkillManualOverrideHandler(ctx, {
|
|
skillId: "skills:1",
|
|
note: "trying to reactivate blocked malware",
|
|
}),
|
|
).rejects.toThrow("Skill is not currently suspicious.");
|
|
expect(patch).not.toHaveBeenCalled();
|
|
expect(insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not let llm scan sync clear an existing quality quarantine", async () => {
|
|
vi.mocked(requireUser).mockReset();
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:7",
|
|
moderationStatus: "hidden",
|
|
moderationReason: "quality.low",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:7",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: { status: "clean", checkedAt: 100 },
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill, version });
|
|
|
|
await updateVersionLlmAnalysisInternalHandler(ctx, {
|
|
versionId: "skillVersions:7",
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
checkedAt: 200,
|
|
},
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledTimes(1);
|
|
expect(patch).toHaveBeenCalledWith("skillVersions:7", {
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
checkedAt: 200,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("can store llm backfill results without syncing moderation", async () => {
|
|
const now = 1_700_000_250_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:7",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "active",
|
|
moderationReason: undefined,
|
|
moderationVerdict: undefined,
|
|
moderationFlags: undefined,
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:7",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: undefined,
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch, get, query } = makeCtx({ skill, version });
|
|
|
|
await updateVersionLlmAnalysisInternalHandler(ctx, {
|
|
versionId: "skillVersions:7",
|
|
moderationMode: "preserve",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
verdict: "malicious",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledTimes(1);
|
|
expect(patch).toHaveBeenCalledWith("skillVersions:7", {
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
verdict: "malicious",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
expect(get).toHaveBeenCalledTimes(1);
|
|
expect(get).toHaveBeenCalledWith("skillVersions:7");
|
|
expect(get).not.toHaveBeenCalledWith("skills:1");
|
|
expect(query).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not bump public updatedAt when llm scan sync updates moderation", async () => {
|
|
const originalUpdatedAt = 1_700_000_100_000;
|
|
const now = 1_700_000_300_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:8",
|
|
softDeletedAt: undefined,
|
|
updatedAt: originalUpdatedAt,
|
|
moderationStatus: "active",
|
|
moderationReason: "scanner.vt.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:8",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: { status: "clean", checkedAt: now - 100 },
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill, version });
|
|
|
|
await updateVersionLlmAnalysisInternalHandler(ctx, {
|
|
versionId: "skillVersions:8",
|
|
llmAnalysis: {
|
|
status: "suspicious",
|
|
verdict: "suspicious",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
|
|
const skillPatch = findPatchForId(patch, "skills:1");
|
|
expect(skillPatch).toEqual(
|
|
expect.objectContaining({
|
|
moderationEvaluatedAt: now,
|
|
moderationSourceVersionId: "skillVersions:8",
|
|
}),
|
|
);
|
|
expect(skillPatch).not.toHaveProperty("updatedAt");
|
|
});
|
|
|
|
it("does not bump public updatedAt when llm scan sync preserves a manual override", async () => {
|
|
const originalUpdatedAt = 1_700_000_100_000;
|
|
const overrideUpdatedAt = 1_700_000_200_000;
|
|
const now = 1_700_000_300_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:8",
|
|
softDeletedAt: undefined,
|
|
updatedAt: originalUpdatedAt,
|
|
manualOverride: {
|
|
verdict: "clean",
|
|
note: "reviewed",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: overrideUpdatedAt,
|
|
},
|
|
moderationStatus: "active",
|
|
moderationReason: "manual.override.clean",
|
|
moderationVerdict: "clean",
|
|
moderationFlags: undefined,
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:8",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: { status: "clean", checkedAt: now - 100 },
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill, version });
|
|
|
|
await updateVersionLlmAnalysisInternalHandler(ctx, {
|
|
versionId: "skillVersions:8",
|
|
llmAnalysis: {
|
|
status: "suspicious",
|
|
verdict: "suspicious",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
|
|
const skillPatch = findPatchForId(patch, "skills:1");
|
|
expect(skillPatch).toEqual(
|
|
expect.objectContaining({
|
|
moderationReason: "manual.override.clean",
|
|
moderationEvaluatedAt: overrideUpdatedAt,
|
|
moderationSourceVersionId: "skillVersions:8",
|
|
}),
|
|
);
|
|
expect(skillPatch).not.toHaveProperty("updatedAt");
|
|
});
|
|
|
|
it("updates global public count when llm scan sync restores a skill to active", async () => {
|
|
const now = 1_700_000_300_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:8",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "scanner.llm.suspicious",
|
|
moderationVerdict: "suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:8",
|
|
skillId: "skills:1",
|
|
staticScan: undefined,
|
|
vtAnalysis: undefined,
|
|
llmAnalysis: { status: "suspicious", checkedAt: now - 100 },
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill, version });
|
|
|
|
await updateVersionLlmAnalysisInternalHandler(ctx, {
|
|
versionId: "skillVersions:8",
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"globalStats:1",
|
|
expect.objectContaining({
|
|
activeSkillsCount: 2,
|
|
updatedAt: now,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("clears legacy suspicious state when LLM corroborates clean VT telemetry", async () => {
|
|
const now = 1_700_000_400_000;
|
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
|
|
const skill = {
|
|
_id: "skills:1",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:9",
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "scanner.vt.suspicious",
|
|
moderationVerdict: "suspicious",
|
|
moderationFlags: ["flagged.suspicious"],
|
|
};
|
|
const version = {
|
|
_id: "skillVersions:9",
|
|
skillId: "skills:1",
|
|
staticScan: {
|
|
status: "clean",
|
|
reasonCodes: [],
|
|
findings: [],
|
|
summary: "",
|
|
engineVersion: "v2.1.1",
|
|
checkedAt: now - 200,
|
|
},
|
|
vtAnalysis: {
|
|
status: "suspicious",
|
|
scanner: "legacy-ai",
|
|
engineStats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 12,
|
|
undetected: 54,
|
|
},
|
|
checkedAt: now - 100,
|
|
},
|
|
llmAnalysis: undefined,
|
|
};
|
|
|
|
const { ctx, patch } = makeCtx({ skill, version });
|
|
|
|
await updateVersionLlmAnalysisInternalHandler(ctx, {
|
|
versionId: "skillVersions:9",
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:1",
|
|
expect.objectContaining({
|
|
moderationStatus: "active",
|
|
moderationReason: "scanner.aggregate.clean",
|
|
moderationFlags: undefined,
|
|
moderationVerdict: "clean",
|
|
moderationReasonCodes: undefined,
|
|
isSuspicious: false,
|
|
}),
|
|
);
|
|
});
|
|
});
|