mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
* test: fix indexed row helper lint * fix: preserve legacy package zip hashes
1452 lines
42 KiB
TypeScript
1452 lines
42 KiB
TypeScript
/* @vitest-environment node */
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { buildDeterministicZip } from "./lib/skillZip";
|
|
import {
|
|
__test,
|
|
fetchResults,
|
|
pollPendingScans,
|
|
pollPackageReleaseScanResults,
|
|
repairPendingSkillVtAnalysis,
|
|
scanWithVirusTotal,
|
|
scanPackageReleaseWithVirusTotal,
|
|
} from "./vt";
|
|
|
|
type WrappedHandler<TArgs, TResult> = {
|
|
_handler: (ctx: unknown, args: TArgs) => Promise<TResult>;
|
|
};
|
|
|
|
const scanWithVirusTotalHandler = (
|
|
scanWithVirusTotal as unknown as WrappedHandler<{ versionId: string }, void>
|
|
)._handler;
|
|
|
|
const scanPackageReleaseWithVirusTotalHandler = (
|
|
scanPackageReleaseWithVirusTotal as unknown as WrappedHandler<
|
|
{ releaseId: string; attempt?: number },
|
|
void
|
|
>
|
|
)._handler;
|
|
|
|
const pollPackageReleaseScanResultsHandler = (
|
|
pollPackageReleaseScanResults as unknown as WrappedHandler<
|
|
{ releaseId: string; attempt?: number },
|
|
void
|
|
>
|
|
)._handler;
|
|
|
|
const fetchResultsHandler = (
|
|
fetchResults as unknown as WrappedHandler<
|
|
{ sha256hash?: string },
|
|
{ status: string; message?: string; url?: string }
|
|
>
|
|
)._handler;
|
|
|
|
const pollPendingScansHandler = (
|
|
pollPendingScans as unknown as WrappedHandler<
|
|
{ batchSize?: number },
|
|
{ processed: number; updated: number; staled?: number; healthy: boolean; queueSize?: number }
|
|
>
|
|
)._handler;
|
|
|
|
const repairPendingSkillVtAnalysisHandler = (
|
|
repairPendingSkillVtAnalysis as unknown as WrappedHandler<
|
|
{ dryRun: boolean; batchSize?: number; cursor?: string | null },
|
|
{
|
|
dryRun: boolean;
|
|
total: number;
|
|
wouldUpdate: number;
|
|
updated: number;
|
|
noResults: number;
|
|
noDecisiveStats: number;
|
|
errors: number;
|
|
done: boolean;
|
|
cursor: string | null;
|
|
statusCounts: Record<string, number>;
|
|
sampleUpdated: Array<{ slug: string; status: string }>;
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const originalVtApiKey = process.env.VT_API_KEY;
|
|
|
|
function mutationPayloads(mock: ReturnType<typeof vi.fn>) {
|
|
return mock.mock.calls.map((call) => call[1] as Record<string, unknown>);
|
|
}
|
|
|
|
function expectVtAnalysisMutation(
|
|
mock: ReturnType<typeof vi.fn>,
|
|
expected: Record<string, unknown>,
|
|
) {
|
|
expect(mutationPayloads(mock)).toContainEqual(
|
|
expect.objectContaining({
|
|
vtAnalysis: expect.objectContaining(expected),
|
|
}),
|
|
);
|
|
}
|
|
|
|
afterEach(() => {
|
|
if (originalVtApiKey === undefined) {
|
|
delete process.env.VT_API_KEY;
|
|
} else {
|
|
process.env.VT_API_KEY = originalVtApiKey;
|
|
}
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("vt unavailable fallback", () => {
|
|
it("does not activate a skill when VT is not configured", async () => {
|
|
delete process.env.VT_API_KEY;
|
|
const ctx = {
|
|
runQuery: vi.fn(),
|
|
runMutation: vi.fn(),
|
|
};
|
|
|
|
await scanWithVirusTotalHandler(ctx as never, { versionId: "skillVersions:demo" });
|
|
|
|
expect(ctx.runQuery).not.toHaveBeenCalled();
|
|
expect(ctx.runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("marks stale pending scans without activating hidden skills", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn().mockResolvedValue({ status: 404, ok: false });
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
queueSize: 1,
|
|
staleCount: 0,
|
|
veryStaleCount: 0,
|
|
oldestAgeMinutes: 5,
|
|
healthy: true,
|
|
})
|
|
.mockResolvedValueOnce([
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:pending",
|
|
sha256hash: "a".repeat(64),
|
|
checkCount: 9,
|
|
},
|
|
]);
|
|
const runMutation = vi.fn(async () => null);
|
|
|
|
const result = await pollPendingScansHandler({ runQuery, runMutation } as never, {
|
|
batchSize: 1,
|
|
});
|
|
|
|
expect(result).toMatchObject({ processed: 1, updated: 0, staled: 1 });
|
|
expect(runMutation).toHaveBeenCalledTimes(3);
|
|
expect(runMutation).toHaveBeenNthCalledWith(1, expect.anything(), {
|
|
skillId: "skills:pending",
|
|
});
|
|
expect(runMutation).toHaveBeenNthCalledWith(2, expect.anything(), {
|
|
versionId: "skillVersions:pending",
|
|
vtAnalysis: { status: "stale", checkedAt: expect.any(Number) },
|
|
});
|
|
expect(runMutation).toHaveBeenNthCalledWith(3, expect.anything(), {
|
|
versionId: "skillVersions:pending",
|
|
source: "vt-update",
|
|
waitForVtMs: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("skill VT scans", () => {
|
|
it("hashes and uploads source files without generated Skill Cards", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const skillBytes = new TextEncoder().encode("# Demo Skill");
|
|
const cardBytes = new TextEncoder().encode("# Generated card");
|
|
const expectedZip = buildDeterministicZip([{ path: "SKILL.md", bytes: skillBytes }], {
|
|
ownerId: "users:owner",
|
|
slug: "demo-skill",
|
|
version: "1.0.0",
|
|
publishedAt: 123,
|
|
});
|
|
const expectedSha = await __test.sha256Hex(expectedZip);
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(new Response("", { status: 404 }))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ data: { id: "analysis" } })));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
await scanWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "skillVersions:demo",
|
|
skillId: "skills:demo",
|
|
version: "1.0.0",
|
|
createdAt: 123,
|
|
files: [
|
|
{ path: "SKILL.md", storageId: "storage:skill" },
|
|
{ path: "skill-card.md", storageId: "storage:card" },
|
|
],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "skills:demo",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:owner",
|
|
})
|
|
.mockResolvedValueOnce([{ fingerprint: "bundle-fingerprint", kind: "generated-bundle" }]),
|
|
runMutation,
|
|
storage: {
|
|
get: vi.fn(async (storageId) => {
|
|
if (storageId === "storage:skill") return new Blob([skillBytes]);
|
|
if (storageId === "storage:card") return new Blob([cardBytes]);
|
|
return null;
|
|
}),
|
|
},
|
|
} as never,
|
|
{ versionId: "skillVersions:demo" },
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:demo",
|
|
sha256hash: expectedSha,
|
|
}),
|
|
);
|
|
const uploadedFile = (
|
|
fetchMock.mock.calls[1]?.[1] as { body?: FormData } | undefined
|
|
)?.body?.get("file") as File | null;
|
|
expect(uploadedFile?.name).toBe("skill.zip");
|
|
expect(new Uint8Array((await uploadedFile?.arrayBuffer()) ?? [])).toEqual(expectedZip);
|
|
});
|
|
});
|
|
|
|
describe("vt AV engine fallback verdicts", () => {
|
|
it("strips unsupported VT stat keys before caching", () => {
|
|
expect(
|
|
__test.normalizeVtEngineStats({
|
|
"confirmed-timeout": 0,
|
|
failure: 2,
|
|
harmless: 0,
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
timeout: 0,
|
|
"type-unsupported": 10,
|
|
undetected: 64,
|
|
} as never),
|
|
).toEqual({
|
|
harmless: 0,
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
undetected: 64,
|
|
});
|
|
});
|
|
|
|
it("maps engine verdicts in severity order", () => {
|
|
expect(
|
|
__test.statusFromAvStats({
|
|
malicious: 1,
|
|
suspicious: 2,
|
|
harmless: 10,
|
|
undetected: 40,
|
|
}),
|
|
).toBe("malicious");
|
|
|
|
expect(
|
|
__test.statusFromAvStats({
|
|
malicious: 0,
|
|
suspicious: 1,
|
|
harmless: 10,
|
|
undetected: 40,
|
|
}),
|
|
).toBe("suspicious");
|
|
|
|
expect(
|
|
__test.statusFromAvStats({
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 1,
|
|
undetected: 40,
|
|
}),
|
|
).toBe("clean");
|
|
});
|
|
|
|
it("treats undetected-only engine results as clean no-detections telemetry", () => {
|
|
expect(
|
|
__test.statusFromAvStats({
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 40,
|
|
}),
|
|
).toBe("clean");
|
|
});
|
|
});
|
|
|
|
describe("vt result lookup", () => {
|
|
it("rejects non-SHA-256 lookup input before calling VirusTotal", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const result = await fetchResultsHandler({} as never, { sha256hash: "../domains/google.com" });
|
|
|
|
expect(result).toEqual({ status: "error", message: "Invalid SHA-256 hash" });
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("looks up only the intended VirusTotal file endpoint for valid hashes", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "a".repeat(64);
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 1,
|
|
undetected: 20,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const result = await fetchResultsHandler({} as never, { sha256hash: hash });
|
|
|
|
expect(result.status).toBe("clean");
|
|
expect(fetchMock).toHaveBeenCalledWith(`https://www.virustotal.com/api/v3/files/${hash}`, {
|
|
method: "GET",
|
|
headers: { "x-apikey": "test-key" },
|
|
});
|
|
});
|
|
|
|
it("ignores VirusTotal Code Insight results and reports engine stats only", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "b".repeat(64);
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
crowdsourced_ai_results: [
|
|
{
|
|
category: "code_insight",
|
|
verdict: "malicious",
|
|
analysis: "AI-only claim that should be ignored.",
|
|
source: "palm",
|
|
},
|
|
],
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 2,
|
|
undetected: 20,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const result = await fetchResultsHandler({} as never, { sha256hash: hash });
|
|
|
|
expect(result).toMatchObject({
|
|
status: "clean",
|
|
source: "engines",
|
|
metadata: {
|
|
stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 2,
|
|
undetected: 20,
|
|
},
|
|
},
|
|
});
|
|
expect((result as { metadata?: Record<string, unknown> }).metadata).not.toHaveProperty(
|
|
"aiVerdict",
|
|
);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("package VT retries", () => {
|
|
it("retries package scan when release files are not readable yet", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
files: [{ path: "package.json", storageId: "storage:pkg" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
}),
|
|
runMutation: vi.fn(async () => null),
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(async () => null),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo", attempt: 2 },
|
|
);
|
|
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(5 * 60 * 1000, expect.anything(), {
|
|
releaseId: "packageReleases:demo",
|
|
attempt: 3,
|
|
});
|
|
});
|
|
|
|
it("retries package upload when VT upload fails", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(new Response("", { status: 404 }))
|
|
.mockResolvedValueOnce(new Response("rate limited", { status: 429 }));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
files: [{ path: "package.json", storageId: "storage:pkg" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(
|
|
async () => new Blob(['{"name":"demo-plugin"}'], { type: "application/json" }),
|
|
),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo" },
|
|
);
|
|
|
|
expect(mutationPayloads(runMutation).every((payload) => !("sha256hash" in payload))).toBe(true);
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(5 * 60 * 1000, expect.anything(), {
|
|
releaseId: "packageReleases:demo",
|
|
attempt: 2,
|
|
});
|
|
});
|
|
|
|
it("uploads the exact ClawPack tarball for package scans", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const clawpackBytes = new TextEncoder().encode("exact clawpack tgz bytes");
|
|
const clawpackSha256 = await __test.sha256Hex(clawpackBytes);
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(new Response("", { status: 404 }))
|
|
.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ data: { id: "analysis-clawpack" } }), { status: 200 }),
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.2.3",
|
|
artifactKind: "npm-pack",
|
|
clawpackStorageId: "storage:clawpack",
|
|
npmTarballName: "demo-plugin-1.2.3.tgz",
|
|
files: [
|
|
{ path: "package.json", storageId: "storage:pkg" },
|
|
{ path: "openclaw.plugin.json", storageId: "storage:plugin" },
|
|
],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(async (storageId) => {
|
|
if (storageId === "storage:clawpack") {
|
|
return new Blob([clawpackBytes], { type: "application/gzip" });
|
|
}
|
|
return null;
|
|
}),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo" },
|
|
);
|
|
|
|
expect(mutationPayloads(runMutation).every((payload) => !("sha256hash" in payload))).toBe(true);
|
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
1,
|
|
`https://www.virustotal.com/api/v3/files/${clawpackSha256}`,
|
|
expect.objectContaining({ method: "GET" }),
|
|
);
|
|
const uploadOptions = fetchMock.mock.calls[1]?.[1] as { body?: FormData } | undefined;
|
|
const uploadedFile = uploadOptions?.body?.get("file") as File | null;
|
|
expect(uploadedFile?.name).toBe("demo-plugin-1.2.3.tgz");
|
|
expect(await uploadedFile?.text()).toBe("exact clawpack tgz bytes");
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(5 * 60 * 1000, expect.anything(), {
|
|
releaseId: "packageReleases:demo",
|
|
attempt: 1,
|
|
});
|
|
});
|
|
|
|
it("uses VirusTotal large-file upload URLs above the direct upload limit", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ data: "https://upload.example.test/vt" }), { status: 200 }),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ data: { id: "analysis-large" } }), { status: 200 }),
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const response = await __test.uploadFileToVirusTotal(
|
|
"test-key",
|
|
new Uint8Array(__test.VIRUSTOTAL_DIRECT_UPLOAD_LIMIT_BYTES + 1),
|
|
"large.tgz",
|
|
"application/gzip",
|
|
);
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
1,
|
|
"https://www.virustotal.com/api/v3/files/upload_url",
|
|
expect.objectContaining({ method: "GET" }),
|
|
);
|
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
2,
|
|
"https://upload.example.test/vt",
|
|
expect.objectContaining({ method: "POST" }),
|
|
);
|
|
});
|
|
|
|
it("uses existing AV engine verdicts for packages without re-uploading", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 1,
|
|
harmless: 10,
|
|
undetected: 40,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
verification: { tier: "source-linked" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
files: [{ path: "package.json", storageId: "storage:pkg" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(
|
|
async () => new Blob(['{"name":"demo-plugin"}'], { type: "application/json" }),
|
|
),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo" },
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
releaseId: "packageReleases:demo",
|
|
vtAnalysis: expect.objectContaining({ status: "suspicious", source: "engines" }),
|
|
}),
|
|
);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores undetected-only package VT telemetry even when static scans are suspicious", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { id: "analysis-123" } }),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
verification: { tier: "source-linked" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "suspicious" },
|
|
files: [{ path: "package.json", storageId: "storage:pkg" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(
|
|
async () => new Blob(['{"name":"demo-plugin"}'], { type: "application/json" }),
|
|
),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo" },
|
|
);
|
|
|
|
expectVtAnalysisMutation(runMutation, {
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores undetected-only official package VT telemetry as clean engine results", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
verification: { tier: "source-linked" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
files: [{ path: "package.json", storageId: "storage:pkg" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(
|
|
async () => new Blob(['{"name":"demo-plugin"}'], { type: "application/json" }),
|
|
),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo" },
|
|
);
|
|
|
|
expectVtAnalysisMutation(runMutation, {
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores undetected-only community package VT telemetry as clean engine results", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await scanPackageReleaseWithVirusTotalHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
verification: { tier: "source-linked" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
files: [{ path: "package.json", storageId: "storage:pkg" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
isOfficial: false,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
storage: {
|
|
get: vi.fn(
|
|
async () => new Blob(['{"name":"demo-plugin"}'], { type: "application/json" }),
|
|
),
|
|
},
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo" },
|
|
);
|
|
|
|
expectVtAnalysisMutation(runMutation, {
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retries package poll when VT lookup throws", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn().mockRejectedValue(new Error("network error"));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await pollPackageReleaseScanResultsHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
artifactKind: "npm-pack",
|
|
sha256hash: "legacy-zip-sha",
|
|
clawpackSha256: "tgz-sha",
|
|
}),
|
|
runMutation: vi.fn(async () => null),
|
|
scheduler,
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo", attempt: 3 },
|
|
);
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"https://www.virustotal.com/api/v3/files/tgz-sha",
|
|
expect.objectContaining({ method: "GET" }),
|
|
);
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(5 * 60 * 1000, expect.anything(), {
|
|
releaseId: "packageReleases:demo",
|
|
attempt: 4,
|
|
});
|
|
});
|
|
|
|
it("does not poll a legacy ZIP hash when an npm-pack artifact hash is missing", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi.fn();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await pollPackageReleaseScanResultsHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
artifactKind: "npm-pack",
|
|
sha256hash: "legacy-zip-sha",
|
|
}),
|
|
runMutation: vi.fn(async () => null),
|
|
scheduler: { runAfter: vi.fn(async () => null) },
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo", attempt: 3 },
|
|
);
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores undetected-only package VT telemetry during polling even when static scan is suspicious", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await pollPackageReleaseScanResultsHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
sha256hash: "abc123",
|
|
verification: { tier: "source-linked" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "suspicious" },
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo", attempt: 3 },
|
|
);
|
|
|
|
expectVtAnalysisMutation(runMutation, {
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
});
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores undetected-only community package VT telemetry during polling", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await pollPackageReleaseScanResultsHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
sha256hash: "abc123",
|
|
verification: { tier: "source-linked" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
family: "code-plugin",
|
|
isOfficial: false,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo", attempt: 3 },
|
|
);
|
|
|
|
expectVtAnalysisMutation(runMutation, {
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
});
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores undetected-only package VT telemetry without requiring trusted verification", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const scheduler = { runAfter: vi.fn(async () => null) };
|
|
await pollPackageReleaseScanResultsHandler(
|
|
{
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
sha256hash: "abc123",
|
|
verification: { tier: "artifact-only" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:demo",
|
|
family: "code-plugin",
|
|
isOfficial: false,
|
|
}),
|
|
runMutation,
|
|
scheduler,
|
|
} as never,
|
|
{ releaseId: "packageReleases:demo", attempt: 3 },
|
|
);
|
|
|
|
expectVtAnalysisMutation(runMutation, {
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("vt pending polling", () => {
|
|
it("does not request VirusTotal reanalysis to trigger Code Insight", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "c".repeat(64);
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
queueSize: 1,
|
|
staleCount: 0,
|
|
veryStaleCount: 0,
|
|
oldestAgeMinutes: 5,
|
|
healthy: true,
|
|
})
|
|
.mockResolvedValueOnce([
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:pending",
|
|
sha256hash: hash,
|
|
checkCount: 9,
|
|
},
|
|
]);
|
|
const runMutation = vi.fn(async () => null);
|
|
|
|
const result = await pollPendingScansHandler({ runQuery, runMutation } as never, {
|
|
batchSize: 1,
|
|
});
|
|
|
|
expect(result).toMatchObject({ processed: 1, updated: 1, staled: 0 });
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:pending",
|
|
vtAnalysis: expect.objectContaining({
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(fetchMock).toHaveBeenCalledWith(`https://www.virustotal.com/api/v3/files/${hash}`, {
|
|
method: "GET",
|
|
headers: { "x-apikey": "test-key" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("vt pending repair", () => {
|
|
it("dry-runs completed undetected-only pending skills without writing", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "d".repeat(64);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:pending",
|
|
slug: "pending-skill",
|
|
sha256hash: hash,
|
|
},
|
|
],
|
|
cursor: "next-page",
|
|
done: false,
|
|
}),
|
|
runMutation,
|
|
} as never,
|
|
{ dryRun: true, batchSize: 1, cursor: "start-page" },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
dryRun: true,
|
|
total: 1,
|
|
wouldUpdate: 1,
|
|
updated: 0,
|
|
done: false,
|
|
cursor: "next-page",
|
|
statusCounts: { clean: 1 },
|
|
sampleUpdated: [{ slug: "pending-skill", status: "clean" }],
|
|
});
|
|
expect(runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("repairs completed undetected-only pending skills and recomputes moderation", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "e".repeat(64);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 0,
|
|
undetected: 66,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:pending",
|
|
slug: "pending-skill",
|
|
sha256hash: hash,
|
|
},
|
|
],
|
|
cursor: null,
|
|
done: true,
|
|
}),
|
|
runMutation,
|
|
} as never,
|
|
{ dryRun: false, batchSize: 1 },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
dryRun: false,
|
|
total: 1,
|
|
wouldUpdate: 1,
|
|
updated: 1,
|
|
done: true,
|
|
cursor: null,
|
|
statusCounts: { clean: 1 },
|
|
});
|
|
expect(mutationPayloads(runMutation)).toContainEqual(
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:pending",
|
|
sha256hash: hash,
|
|
vtAnalysis: expect.objectContaining({
|
|
status: "clean",
|
|
source: "engines",
|
|
engineStats: expect.objectContaining({ undetected: 66 }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(mutationPayloads(runMutation)).toContainEqual(
|
|
expect.objectContaining({ skillId: "skills:pending" }),
|
|
);
|
|
expect(mutationPayloads(runMutation)).not.toContainEqual(
|
|
expect.objectContaining({ source: "vt-update" }),
|
|
);
|
|
});
|
|
|
|
it("queues ClawScan follow-up when repaired VT telemetry is suspicious", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "f".repeat(64);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 1,
|
|
harmless: 0,
|
|
undetected: 65,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:pending",
|
|
slug: "pending-skill",
|
|
sha256hash: hash,
|
|
},
|
|
],
|
|
cursor: null,
|
|
done: true,
|
|
}),
|
|
runMutation,
|
|
} as never,
|
|
{ dryRun: false, batchSize: 100 },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
wouldUpdate: 1,
|
|
updated: 1,
|
|
statusCounts: { suspicious: 1 },
|
|
});
|
|
expect(mutationPayloads(runMutation)).toContainEqual(
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:pending",
|
|
source: "vt-update",
|
|
waitForVtMs: 0,
|
|
}),
|
|
);
|
|
expect(mutationPayloads(runMutation)).not.toContainEqual(
|
|
expect.objectContaining({ skillId: "skills:pending" }),
|
|
);
|
|
});
|
|
|
|
it("repairs historical pending VT cache rows without recomputing latest moderation", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "a".repeat(64);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 0,
|
|
harmless: 2,
|
|
undetected: 64,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:historical",
|
|
slug: "pending-skill",
|
|
sha256hash: hash,
|
|
isLatest: false,
|
|
},
|
|
],
|
|
cursor: null,
|
|
done: true,
|
|
}),
|
|
runMutation,
|
|
} as never,
|
|
{ dryRun: false, batchSize: 100 },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
wouldUpdate: 1,
|
|
updated: 1,
|
|
statusCounts: { clean: 1 },
|
|
});
|
|
expect(mutationPayloads(runMutation)).toContainEqual(
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:historical",
|
|
sha256hash: hash,
|
|
vtAnalysis: expect.objectContaining({ status: "clean" }),
|
|
}),
|
|
);
|
|
expect(mutationPayloads(runMutation)).not.toContainEqual(
|
|
expect.objectContaining({ skillId: "skills:pending" }),
|
|
);
|
|
expect(mutationPayloads(runMutation)).not.toContainEqual(
|
|
expect.objectContaining({ source: "vt-update" }),
|
|
);
|
|
});
|
|
|
|
it("does not enqueue ClawScan follow-up for suspicious historical VT cache rows", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
const hash = "b".repeat(64);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
attributes: {
|
|
last_analysis_stats: {
|
|
malicious: 0,
|
|
suspicious: 1,
|
|
harmless: 1,
|
|
undetected: 64,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:historical",
|
|
slug: "pending-skill",
|
|
sha256hash: hash,
|
|
isLatest: false,
|
|
},
|
|
],
|
|
cursor: null,
|
|
done: true,
|
|
}),
|
|
runMutation,
|
|
} as never,
|
|
{ dryRun: false, batchSize: 100 },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
wouldUpdate: 1,
|
|
updated: 1,
|
|
statusCounts: { suspicious: 1 },
|
|
});
|
|
expect(mutationPayloads(runMutation)).toContainEqual(
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:historical",
|
|
sha256hash: hash,
|
|
vtAnalysis: expect.objectContaining({ status: "suspicious" }),
|
|
}),
|
|
);
|
|
expect(mutationPayloads(runMutation)).not.toContainEqual(
|
|
expect.objectContaining({ source: "vt-update" }),
|
|
);
|
|
});
|
|
|
|
it("returns pagination cursor when unresolved pending VT rows are skipped", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
}),
|
|
);
|
|
|
|
const runMutation = vi.fn(async () => null);
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{
|
|
skillId: "skills:pending",
|
|
versionId: "skillVersions:pending",
|
|
slug: "pending-skill",
|
|
sha256hash: "a".repeat(64),
|
|
},
|
|
],
|
|
cursor: "next-page",
|
|
done: false,
|
|
}),
|
|
runMutation,
|
|
} as never,
|
|
{ dryRun: true, batchSize: 1 },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
total: 1,
|
|
wouldUpdate: 0,
|
|
noResults: 1,
|
|
done: false,
|
|
cursor: "next-page",
|
|
});
|
|
expect(runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports done only when no pending VT skills are selected", async () => {
|
|
process.env.VT_API_KEY = "test-key";
|
|
|
|
const result = await repairPendingSkillVtAnalysisHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({ skills: [], cursor: null, done: true }),
|
|
runMutation: vi.fn(async () => null),
|
|
} as never,
|
|
{ dryRun: true, batchSize: 100 },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
total: 0,
|
|
wouldUpdate: 0,
|
|
done: true,
|
|
});
|
|
});
|
|
});
|