mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
Ships the fail-closed skills.sh catalog control plane validated by the bounded 500-row permanent Test gate. No production ingestion, schedule, visibility, or bulk scanning is enabled.
6202 lines
187 KiB
TypeScript
6202 lines
187 KiB
TypeScript
import { getAuthUserId } from "@convex-dev/auth/server";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
appendGitHubSkillScanRequestFilesInternal,
|
|
cancelQueuedVtUpdateJobsInternal,
|
|
claimCodexScanJobs,
|
|
claimCodexScanJobLeases,
|
|
clearQueuedBackfillJobsForLocalDev,
|
|
claimQueuedJobsInternal,
|
|
completeCodexScanJob,
|
|
enqueueBulkSkillRescanBatchForAdminInternal,
|
|
enqueuePackageReleaseScanInternal,
|
|
enqueueSkillVersionScanInternal,
|
|
failCodexScanJob,
|
|
failJobInternal,
|
|
finalizeGitHubSkillScanRequestInternal,
|
|
getCodexScanQueueHealth,
|
|
getCodexScanQueueHealthInternal,
|
|
getJobTargetInternal,
|
|
getBulkSkillRescanBatchStatusForAdminInternal,
|
|
getSkillScanRequestForUserInternal,
|
|
getStoredScanReportForUserInternal,
|
|
logCodexScanQueueHealthInternal,
|
|
prepareGitHubSkillScanRequestInternal,
|
|
pruneExpiredSkillScanRequestsInternal,
|
|
requeueCodexScanJobLease,
|
|
requeueExpiredCodexScanJobsInternal,
|
|
requeueFailedSecurityScanJobsInternal,
|
|
requeueJobLeaseInternal,
|
|
recordGitHubSkillScanResultInternal,
|
|
recordSkillScanRequestFailedInternal,
|
|
requestPackageRescanForUserInternal,
|
|
requestPackageRescan,
|
|
requestSkillRescanForUserInternal,
|
|
requestSkillRescan,
|
|
hydrateCodexScanJob,
|
|
} from "./securityScan";
|
|
|
|
vi.mock("@convex-dev/auth/server", () => ({
|
|
getAuthUserId: vi.fn(),
|
|
authTables: {},
|
|
}));
|
|
|
|
type WrappedHandler<TArgs, TResult = unknown> = {
|
|
_handler: (ctx: unknown, args: TArgs) => Promise<TResult>;
|
|
};
|
|
|
|
const claimCodexScanJobsHandler = (
|
|
claimCodexScanJobs as unknown as WrappedHandler<
|
|
{ token: string; workerId: string; limit?: number },
|
|
Array<unknown>
|
|
>
|
|
)._handler;
|
|
|
|
const claimCodexScanJobLeasesHandler = (
|
|
claimCodexScanJobLeases as unknown as WrappedHandler<
|
|
{
|
|
token: string;
|
|
workerId: string;
|
|
lane?: "priority" | "shared" | "catalog";
|
|
limit?: number;
|
|
leaseMs?: number;
|
|
},
|
|
Array<ScanJob & { leaseToken: string; workerId: string }>
|
|
>
|
|
)._handler;
|
|
|
|
const hydrateCodexScanJobHandler = (
|
|
hydrateCodexScanJob as unknown as WrappedHandler<{
|
|
token: string;
|
|
workerId: string;
|
|
jobId: string;
|
|
leaseToken: string;
|
|
}>
|
|
)._handler;
|
|
|
|
const claimQueuedJobsInternalHandler = (
|
|
claimQueuedJobsInternal as unknown as WrappedHandler<
|
|
{ workerId: string; lane?: "priority" | "shared" | "catalog"; limit: number; leaseMs?: number },
|
|
Array<ScanJob & { leaseToken: string; workerId: string }>
|
|
>
|
|
)._handler;
|
|
|
|
const requeueExpiredCodexScanJobsInternalHandler = (
|
|
requeueExpiredCodexScanJobsInternal as unknown as WrappedHandler<
|
|
{ limit?: number },
|
|
{ requeued: number }
|
|
>
|
|
)._handler;
|
|
|
|
const requeueFailedSecurityScanJobsInternalHandler = (
|
|
requeueFailedSecurityScanJobsInternal as unknown as WrappedHandler<
|
|
{ failedAfter: number; failedBefore: number; dryRun: boolean; limit?: number },
|
|
{
|
|
dryRun: boolean;
|
|
matched: number;
|
|
requeued: number;
|
|
hasMore: boolean;
|
|
bySource: Record<string, number>;
|
|
byTargetKind: Record<string, number>;
|
|
sampleJobIds: string[];
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const requeueJobLeaseInternalHandler = (
|
|
requeueJobLeaseInternal as unknown as WrappedHandler<
|
|
{ jobId: string; leaseToken: string; workerId: string },
|
|
{ ok: true; nextRunAt: number }
|
|
>
|
|
)._handler;
|
|
|
|
const requeueCodexScanJobLeaseHandler = (
|
|
requeueCodexScanJobLease as unknown as WrappedHandler<
|
|
{ token: string; jobId: string; leaseToken: string; workerId: string },
|
|
{ ok: true; nextRunAt: number }
|
|
>
|
|
)._handler;
|
|
|
|
const failCodexScanJobHandler = (
|
|
failCodexScanJob as unknown as WrappedHandler<
|
|
{ token: string; jobId: string; leaseToken: string; error: string },
|
|
{ ok: true; retry: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const failJobInternalHandler = (
|
|
failJobInternal as unknown as WrappedHandler<
|
|
{ jobId: string; leaseToken: string; error: string },
|
|
{ ok: true; retry: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const getCodexScanQueueHealthInternalHandler = (
|
|
getCodexScanQueueHealthInternal as unknown as WrappedHandler<
|
|
Record<string, never>,
|
|
{
|
|
snapshotAt: number;
|
|
queueDepth: number;
|
|
queueDepthIsEstimate: boolean;
|
|
readyQueueDepth: number;
|
|
readyQueueDepthIsEstimate: boolean;
|
|
oldestReadyJobAgeSeconds: number;
|
|
oldestReadyJobNextRunAt: number | null;
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const getCodexScanQueueHealthHandler = (
|
|
getCodexScanQueueHealth as unknown as WrappedHandler<
|
|
{ token: string },
|
|
{
|
|
snapshotAt: number;
|
|
queueDepth: number;
|
|
queueDepthIsEstimate: boolean;
|
|
readyQueueDepth: number;
|
|
readyQueueDepthIsEstimate: boolean;
|
|
oldestReadyJobAgeSeconds: number;
|
|
oldestReadyJobNextRunAt: number | null;
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const logCodexScanQueueHealthInternalHandler = (
|
|
logCodexScanQueueHealthInternal as unknown as WrappedHandler<
|
|
Record<string, never>,
|
|
{
|
|
snapshotAt: number;
|
|
queueDepth: number;
|
|
queueDepthIsEstimate: boolean;
|
|
readyQueueDepth: number;
|
|
readyQueueDepthIsEstimate: boolean;
|
|
oldestReadyJobAgeSeconds: number;
|
|
oldestReadyJobNextRunAt: number | null;
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const recordSkillScanRequestFailedInternalHandler = (
|
|
recordSkillScanRequestFailedInternal as unknown as WrappedHandler<
|
|
{ scanId: string; error: string; llmAnalysis?: { status: string; checkedAt: number } },
|
|
{ ok: true }
|
|
>
|
|
)._handler;
|
|
|
|
const recordGitHubSkillScanResultInternalHandler = (
|
|
recordGitHubSkillScanResultInternal as unknown as WrappedHandler<
|
|
{
|
|
githubSkillScanId: string;
|
|
scanStatus: "clean" | "suspicious" | "malicious" | "pending" | "failed";
|
|
error?: string;
|
|
},
|
|
{ ok: true; skipped?: string }
|
|
>
|
|
)._handler;
|
|
|
|
function fakeLeakyWorkerError() {
|
|
return (
|
|
`Download failed 403: https://signed.example.invalid/file?token=secret&X-Amz-Signature=abc123 ` +
|
|
`Authorization: Bearer abc OPENAI_API_KEY=openai-runtime-secret ` +
|
|
`GITHUB_TOKEN=github-runtime-secret CONVEX_DEPLOY_KEY=convex-deploy-secret ` +
|
|
`api_key=plugin-api-token sha256=${"a".repeat(64)}`
|
|
);
|
|
}
|
|
|
|
function expectNoLeakedWorkerErrorSecrets(error: string) {
|
|
expect(error).toContain("Download failed 403");
|
|
expect(error).not.toContain("https://");
|
|
expect(error).not.toContain("signed.example.invalid");
|
|
expect(error).not.toContain("token=secret");
|
|
expect(error).not.toContain("X-Amz-Signature");
|
|
expect(error).not.toContain("Authorization");
|
|
expect(error).not.toContain("Bearer abc");
|
|
expect(error).not.toContain("openai-runtime-secret");
|
|
expect(error).not.toContain("github-runtime-secret");
|
|
expect(error).not.toContain("convex-deploy-secret");
|
|
expect(error).not.toContain("plugin-api-token");
|
|
expect(error).toContain("OPENAI_API_KEY=[redacted-secret]");
|
|
expect(error).toContain("GITHUB_TOKEN=[redacted-secret]");
|
|
expect(error).toContain("CONVEX_DEPLOY_KEY=[redacted-secret]");
|
|
expect(error).toContain("api_key=[redacted-secret]");
|
|
}
|
|
|
|
function makeFailurePersistenceCtx(docs: Record<string, Record<string, unknown>>) {
|
|
const records = new Map<string, Record<string, unknown>>(Object.entries(docs));
|
|
const patches: Array<{ id: string; patch: Record<string, unknown> }> = [];
|
|
const get = vi.fn(async (id: string) => records.get(id) ?? null);
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
const id = `${table}:inserted-${records.size + 1}`;
|
|
records.set(id, { _id: id, ...doc });
|
|
return id;
|
|
});
|
|
const patch = vi.fn(async (id: string, next: Record<string, unknown>) => {
|
|
patches.push({ id, patch: next });
|
|
const doc = records.get(id);
|
|
if (!doc) return;
|
|
for (const [key, value] of Object.entries(next)) {
|
|
if (value === undefined) delete doc[key];
|
|
else doc[key] = value;
|
|
}
|
|
});
|
|
const replace = vi.fn(async (id: string, doc: Record<string, unknown>) => {
|
|
records.set(id, { _id: id, ...doc });
|
|
});
|
|
const deleteDoc = vi.fn(async (id: string) => {
|
|
records.delete(id);
|
|
});
|
|
const query = vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn(async () => []),
|
|
take: vi.fn(async () => []),
|
|
unique: vi.fn(async () => null),
|
|
})),
|
|
}));
|
|
const normalizeId = vi.fn((table: string, id: string) =>
|
|
id.startsWith(`${table}:`) ? id : null,
|
|
);
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
get,
|
|
insert,
|
|
patch,
|
|
query,
|
|
replace,
|
|
delete: deleteDoc,
|
|
normalizeId,
|
|
system: {},
|
|
},
|
|
},
|
|
patches,
|
|
records,
|
|
};
|
|
}
|
|
|
|
const completeCodexScanJobHandler = (
|
|
completeCodexScanJob as unknown as WrappedHandler<
|
|
{
|
|
token: string;
|
|
jobId: string;
|
|
leaseToken: string;
|
|
llmAnalysis: {
|
|
status: string;
|
|
verdict?: string;
|
|
checkedAt: number;
|
|
};
|
|
skillSpectorAnalysis?: {
|
|
status: string;
|
|
issueCount: number;
|
|
issues: Array<{
|
|
issueId: string;
|
|
severity: string;
|
|
explanation: string;
|
|
finding?: string;
|
|
codeSnippet?: string;
|
|
}>;
|
|
checkedAt: number;
|
|
};
|
|
runId?: string;
|
|
},
|
|
{ ok: true }
|
|
>
|
|
)._handler;
|
|
|
|
const prepareGitHubSkillScanRequestInternalHandler = (
|
|
prepareGitHubSkillScanRequestInternal as unknown as WrappedHandler<
|
|
{
|
|
skillId: string;
|
|
contentHash: string;
|
|
commit: string;
|
|
force?: boolean;
|
|
parsed: { frontmatter: Record<string, unknown> };
|
|
staticScan: {
|
|
status: "clean" | "suspicious" | "malicious";
|
|
reasonCodes: string[];
|
|
findings: [];
|
|
summary: string;
|
|
engineVersion: string;
|
|
checkedAt: number;
|
|
};
|
|
},
|
|
{ ok: true; prepared?: true; scanId?: string; requestId?: string }
|
|
>
|
|
)._handler;
|
|
|
|
const appendGitHubSkillScanRequestFilesInternalHandler = (
|
|
appendGitHubSkillScanRequestFilesInternal as unknown as WrappedHandler<
|
|
{
|
|
requestId: string;
|
|
chunkIndex: number;
|
|
files: Array<{ path: string; size: number; storageId: string; sha256: string }>;
|
|
},
|
|
{ ok: true; appended: true }
|
|
>
|
|
)._handler;
|
|
|
|
const finalizeGitHubSkillScanRequestInternalHandler = (
|
|
finalizeGitHubSkillScanRequestInternal as unknown as WrappedHandler<
|
|
{ requestId: string; force?: boolean },
|
|
{ ok: true; queued?: true; scanId?: string; requestId?: string; jobId?: string }
|
|
>
|
|
)._handler;
|
|
|
|
const getJobTargetInternalHandler = (
|
|
getJobTargetInternal as unknown as WrappedHandler<{ jobId: string }>
|
|
)._handler;
|
|
|
|
type CancelArgs = {
|
|
dryRun: boolean;
|
|
createdBefore: number;
|
|
scanLimit?: number;
|
|
deleteLimit?: number;
|
|
};
|
|
|
|
type CancelResult = {
|
|
dryRun: boolean;
|
|
scanned: number;
|
|
matched: number;
|
|
deleted: number;
|
|
wouldDelete: number;
|
|
skippedByReason: Record<string, number>;
|
|
oldestScannedCreatedAt: number | null;
|
|
newestScannedCreatedAt: number | null;
|
|
oldestScannedNextRunAt: number | null;
|
|
newestScannedNextRunAt: number | null;
|
|
sampleMatchedJobIds: string[];
|
|
sampleDeletedJobIds: string[];
|
|
};
|
|
|
|
type ScanJob = {
|
|
_id: string;
|
|
_creationTime: number;
|
|
status: string;
|
|
targetKind: string;
|
|
skillVersionId?: string;
|
|
packageReleaseId?: string;
|
|
skillScanRequestId?: string;
|
|
source: string;
|
|
priority: number;
|
|
hasMaliciousSignal: boolean;
|
|
waitForVtUntil: number;
|
|
nextRunAt: number;
|
|
attempts: number;
|
|
leaseToken?: string;
|
|
leaseExpiresAt?: number;
|
|
workerId?: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
};
|
|
|
|
const cancelQueuedVtUpdateJobsInternalHandler = (
|
|
cancelQueuedVtUpdateJobsInternal as unknown as WrappedHandler<CancelArgs, CancelResult>
|
|
)._handler;
|
|
const clearQueuedBackfillJobsForLocalDevHandler = (
|
|
clearQueuedBackfillJobsForLocalDev as unknown as WrappedHandler<
|
|
{ dryRun?: boolean; limit?: number },
|
|
{ dryRun: boolean; matched: number; deleted: number; sampleDeletedJobIds: string[] }
|
|
>
|
|
)._handler;
|
|
const pruneExpiredSkillScanRequestsInternalHandler = (
|
|
pruneExpiredSkillScanRequestsInternal as unknown as WrappedHandler<
|
|
{ batchSize?: number },
|
|
{
|
|
ok: true;
|
|
deletedRequests: number;
|
|
deferredRequests: number;
|
|
deletedJobs: number;
|
|
deletedFiles: number;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const requestSkillRescanHandler = (
|
|
requestSkillRescan as unknown as WrappedHandler<
|
|
{ skillId: string; version?: string },
|
|
{ jobId?: string; scheduled?: boolean; alreadyQueued: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const requestPackageRescanHandler = (
|
|
requestPackageRescan as unknown as WrappedHandler<
|
|
{ packageId: string; version?: string },
|
|
{ jobId: string; alreadyQueued: boolean; packageReleaseId: string }
|
|
>
|
|
)._handler;
|
|
|
|
const requestSkillRescanForUserInternalHandler = (
|
|
requestSkillRescanForUserInternal as unknown as WrappedHandler<
|
|
{ actorUserId: string; slug: string; ownerHandle?: string; version?: string },
|
|
{ jobId?: string; scheduled?: boolean; alreadyQueued: boolean; skillVersionId?: string }
|
|
>
|
|
)._handler;
|
|
|
|
const requestPackageRescanForUserInternalHandler = (
|
|
requestPackageRescanForUserInternal as unknown as WrappedHandler<
|
|
{ actorUserId: string; name: string; version?: string },
|
|
{ jobId: string; alreadyQueued: boolean; packageReleaseId: string }
|
|
>
|
|
)._handler;
|
|
|
|
const enqueueSkillVersionScanInternalHandler = (
|
|
enqueueSkillVersionScanInternal as unknown as WrappedHandler<
|
|
{
|
|
versionId: string;
|
|
source: "publish" | "vt-update" | "backfill" | "bulk-rescan" | "manual";
|
|
priority?: number;
|
|
waitForVtMs?: number;
|
|
preserveActiveJob?: boolean;
|
|
preserveExistingJob?: boolean;
|
|
},
|
|
{ ok: true; skipped?: string; jobId?: string; alreadyQueued?: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const enqueuePackageReleaseScanInternalHandler = (
|
|
enqueuePackageReleaseScanInternal as unknown as WrappedHandler<
|
|
{
|
|
releaseId: string;
|
|
source: "publish" | "vt-update" | "backfill" | "bulk-rescan" | "manual";
|
|
priority?: number;
|
|
waitForVtMs?: number;
|
|
},
|
|
{ ok: true; skipped?: string; jobId?: string; alreadyQueued?: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const enqueueBulkSkillRescanBatchForAdminInternalHandler = (
|
|
enqueueBulkSkillRescanBatchForAdminInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
mode?: "all-active-latest";
|
|
cursor?: string | null;
|
|
batchSize?: number;
|
|
dryRun?: boolean;
|
|
},
|
|
{
|
|
ok: true;
|
|
queued: number;
|
|
alreadyQueued: number;
|
|
skipped: number;
|
|
jobIds: string[];
|
|
nextCursor: string | null;
|
|
done: boolean;
|
|
sampleSlugs: string[];
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const getBulkSkillRescanBatchStatusForAdminInternalHandler = (
|
|
getBulkSkillRescanBatchStatusForAdminInternal as unknown as WrappedHandler<
|
|
{ actorUserId: string; jobIds: string[] },
|
|
{
|
|
ok: true;
|
|
total: number;
|
|
queued: number;
|
|
running: number;
|
|
succeeded: number;
|
|
failed: number;
|
|
missing: number;
|
|
terminal: number;
|
|
done: boolean;
|
|
failedJobIds: string[];
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const getSkillScanRequestForUserInternalHandler = (
|
|
getSkillScanRequestForUserInternal as unknown as WrappedHandler<
|
|
{ actorUserId: string; scanId: string },
|
|
{
|
|
ok: true;
|
|
scanId: string;
|
|
jobId?: string;
|
|
status: string;
|
|
queue: {
|
|
queuedAhead: number;
|
|
queuedAheadIsEstimate?: boolean;
|
|
position: number | null;
|
|
running: number;
|
|
runningIsEstimate?: boolean;
|
|
note: string;
|
|
};
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const getStoredScanReportForUserInternalHandler = (
|
|
getStoredScanReportForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
kind: "skill" | "plugin";
|
|
name: string;
|
|
version: string;
|
|
},
|
|
{
|
|
ok: true;
|
|
status: string;
|
|
artifact: Record<string, unknown>;
|
|
report: {
|
|
clawscan: Record<string, unknown> | null;
|
|
skillspector: Record<string, unknown> | null;
|
|
staticAnalysis: Record<string, unknown> | null;
|
|
virustotal: Record<string, unknown> | null;
|
|
};
|
|
}
|
|
>
|
|
)._handler;
|
|
|
|
const claimedJob = {
|
|
_id: "securityScanJobs:1",
|
|
_creationTime: 1,
|
|
status: "running",
|
|
targetKind: "skillVersion",
|
|
skillVersionId: "skillVersions:1",
|
|
source: "publish",
|
|
priority: 0,
|
|
hasMaliciousSignal: true,
|
|
waitForVtUntil: 0,
|
|
nextRunAt: 0,
|
|
attempts: 1,
|
|
leaseToken: "lease-token",
|
|
};
|
|
|
|
function makeScanJob(overrides: Partial<ScanJob> = {}): ScanJob {
|
|
const suffix = (overrides._id ?? "matched").split(":").at(-1) ?? "matched";
|
|
return {
|
|
_id: `securityScanJobs:${suffix}`,
|
|
_creationTime: 1,
|
|
status: "queued",
|
|
targetKind: "skillVersion",
|
|
skillVersionId: `skillVersions:${suffix}`,
|
|
source: "vt-update",
|
|
priority: 0,
|
|
hasMaliciousSignal: false,
|
|
waitForVtUntil: 0,
|
|
nextRunAt: 100,
|
|
attempts: 0,
|
|
createdAt: 50,
|
|
updatedAt: 50,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeQueueHealthCtx(jobs: ScanJob[]) {
|
|
const query = vi.fn((table: string) => {
|
|
expect(table).toBe("securityScanJobs");
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
buildRange: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
expect(indexName).toBe("by_status_and_next_run_at");
|
|
const equals = new Map<string, unknown>();
|
|
const range = {
|
|
eq(field: string, value: unknown) {
|
|
equals.set(field, value);
|
|
return range;
|
|
},
|
|
};
|
|
buildRange(range);
|
|
const matched = [...jobs]
|
|
.filter((job) =>
|
|
Array.from(equals.entries()).every(([field, value]) => {
|
|
return job[field as keyof ScanJob] === value;
|
|
}),
|
|
)
|
|
.sort(
|
|
(a, b) =>
|
|
a.nextRunAt - b.nextRunAt ||
|
|
a._creationTime - b._creationTime ||
|
|
a._id.localeCompare(b._id),
|
|
);
|
|
return {
|
|
order: vi.fn((direction: string) => {
|
|
expect(direction).toBe("asc");
|
|
return {
|
|
take: vi.fn(async (limit: number) => matched.slice(0, limit)),
|
|
};
|
|
}),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
});
|
|
|
|
return { db: { query } };
|
|
}
|
|
|
|
function makeFailedScanRecoveryCtx(
|
|
jobs: ScanJob[],
|
|
docs: Record<string, Record<string, unknown>> = {},
|
|
) {
|
|
const patches: Array<{ id: string; patch: Record<string, unknown> }> = [];
|
|
const inserts: Array<{ table: string; doc: Record<string, unknown> }> = [];
|
|
const patch = vi.fn(
|
|
async (
|
|
tableOrId: string,
|
|
idOrValue: string | Record<string, unknown>,
|
|
maybeValue?: Record<string, unknown>,
|
|
) => {
|
|
const id = maybeValue ? (idOrValue as string) : tableOrId;
|
|
const value = maybeValue ?? (idOrValue as Record<string, unknown>);
|
|
patches.push({ id, patch: value });
|
|
docs[id] = { ...docs[id], ...value };
|
|
},
|
|
);
|
|
const query = vi.fn((table: string) => {
|
|
if (table !== "securityScanJobs") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn(async () => []),
|
|
order: vi.fn(() => ({ take: vi.fn(async () => []) })),
|
|
take: vi.fn(async () => []),
|
|
unique: vi.fn(async () => null),
|
|
})),
|
|
};
|
|
}
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
buildRange: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
gte: (field: string, value: number) => unknown;
|
|
lt: (field: string, value: number) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
expect(indexName).toBe("by_status_and_updated_at");
|
|
const equals = new Map<string, unknown>();
|
|
let updatedAfter = Number.NEGATIVE_INFINITY;
|
|
let updatedBefore = Number.POSITIVE_INFINITY;
|
|
const range = {
|
|
eq(field: string, value: unknown) {
|
|
equals.set(field, value);
|
|
return range;
|
|
},
|
|
gte(field: string, value: number) {
|
|
expect(field).toBe("updatedAt");
|
|
updatedAfter = value;
|
|
return range;
|
|
},
|
|
lt(field: string, value: number) {
|
|
expect(field).toBe("updatedAt");
|
|
updatedBefore = value;
|
|
return range;
|
|
},
|
|
};
|
|
buildRange(range);
|
|
const matched = jobs
|
|
.filter(
|
|
(job) =>
|
|
Array.from(equals.entries()).every(
|
|
([field, value]) => job[field as keyof ScanJob] === value,
|
|
) &&
|
|
job.updatedAt >= updatedAfter &&
|
|
job.updatedAt < updatedBefore,
|
|
)
|
|
.sort((a, b) => a.updatedAt - b.updatedAt || a._id.localeCompare(b._id));
|
|
let filtered = matched;
|
|
const builder = {
|
|
filter: vi.fn(
|
|
(
|
|
predicate: (q: {
|
|
field: (field: string) => { field: string };
|
|
neq: (left: { field: string }, right: unknown) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
predicate({
|
|
field: (field) => ({ field }),
|
|
neq: (left, right) => {
|
|
filtered = filtered.filter((job) => job[left.field as keyof ScanJob] !== right);
|
|
return true;
|
|
},
|
|
});
|
|
return builder;
|
|
},
|
|
),
|
|
order: vi.fn((direction: string) => {
|
|
expect(direction).toBe("asc");
|
|
return {
|
|
take: vi.fn(async (limit: number) => filtered.slice(0, limit)),
|
|
};
|
|
}),
|
|
};
|
|
return builder;
|
|
},
|
|
),
|
|
};
|
|
});
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
delete: vi.fn(),
|
|
get: vi.fn(async (id: string) => docs[id] ?? null),
|
|
insert: vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
inserts.push({ table, doc });
|
|
return `${table}:inserted`;
|
|
}),
|
|
normalizeId: vi.fn((_table: string, id: string) => id),
|
|
patch,
|
|
query,
|
|
replace: vi.fn(),
|
|
system: {},
|
|
},
|
|
},
|
|
patches,
|
|
inserts,
|
|
};
|
|
}
|
|
|
|
function makeTarget(llmStatus?: string) {
|
|
if (!llmStatus) return {};
|
|
return {
|
|
llmAnalysis: {
|
|
status: llmStatus,
|
|
checkedAt: 123,
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeRescanCtx(options: {
|
|
actorId: string;
|
|
actorRole?: "admin" | "moderator" | "user";
|
|
docs: Record<string, Record<string, unknown>>;
|
|
activeJobs?: Array<Record<string, unknown>>;
|
|
membership?: Record<string, unknown> | null;
|
|
}) {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(options.actorId as never);
|
|
const docs = new Map<string, Record<string, unknown>>(
|
|
Object.entries({
|
|
[options.actorId]: {
|
|
_id: options.actorId,
|
|
role: options.actorRole ?? "user",
|
|
},
|
|
...options.docs,
|
|
...Object.fromEntries((options.activeJobs ?? []).map((job) => [String(job._id), job])),
|
|
}),
|
|
);
|
|
const inserts: Array<{ table: string; doc: Record<string, unknown> }> = [];
|
|
const patches: Array<{ id: string; patch: Record<string, unknown> }> = [];
|
|
const get = vi.fn(async (id: string) => docs.get(id) ?? null);
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
const id = `${table}:${inserts.length + 1}`;
|
|
inserts.push({ table, doc });
|
|
docs.set(id, { _id: id, _creationTime: Date.now(), ...doc });
|
|
return id;
|
|
});
|
|
const patch = vi.fn(async (id: string, doc: Record<string, unknown>) => {
|
|
patches.push({ id, patch: doc });
|
|
});
|
|
const query = vi.fn((table: string) => ({
|
|
withIndex: vi.fn((_indexName: string, buildRange: (q: { eq: typeof eq }) => unknown) => {
|
|
const equals = new Map<string, unknown>();
|
|
function eq(field: string, value: unknown) {
|
|
equals.set(field, value);
|
|
return { eq };
|
|
}
|
|
buildRange({ eq });
|
|
return {
|
|
collect: vi.fn(async () => {
|
|
if (table === "securityScanJobs") {
|
|
return Array.from(docs.values()).filter((doc) =>
|
|
String(doc._id).startsWith("securityScanJobs:"),
|
|
);
|
|
}
|
|
return [];
|
|
}),
|
|
order: vi.fn(() => ({
|
|
first: vi.fn(async () => {
|
|
if (table !== "securityScanJobs") return null;
|
|
return (
|
|
Array.from(docs.values())
|
|
.filter(
|
|
(doc) =>
|
|
String(doc._id).startsWith("securityScanJobs:") &&
|
|
(!equals.has("status") || doc.status === equals.get("status")),
|
|
)
|
|
.sort(
|
|
(a, b) =>
|
|
Number(a.nextRunAt ?? 0) - Number(b.nextRunAt ?? 0) ||
|
|
Number(a._creationTime ?? 0) - Number(b._creationTime ?? 0),
|
|
)[0] ?? null
|
|
);
|
|
}),
|
|
})),
|
|
take: vi.fn(async () => {
|
|
if (table === "skills") {
|
|
return Array.from(docs.values()).filter((doc) => {
|
|
if (!doc._id?.toString().startsWith("skills:")) return false;
|
|
if (doc.slug !== equals.get("slug")) return false;
|
|
const ownerPublisherId = equals.get("ownerPublisherId");
|
|
return !ownerPublisherId || doc.ownerPublisherId === ownerPublisherId;
|
|
});
|
|
}
|
|
return [];
|
|
}),
|
|
unique: vi.fn(async () => {
|
|
if (table === "publisherMembers") return options.membership ?? null;
|
|
if (table === "publishers") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
doc._id?.toString().startsWith("publishers:") &&
|
|
doc.handle === equals.get("handle"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "skills") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
doc._id?.toString().startsWith("skills:") &&
|
|
doc.slug === equals.get("slug") &&
|
|
(!equals.has("ownerPublisherId") ||
|
|
doc.ownerPublisherId === equals.get("ownerPublisherId")),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "packages") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
doc._id?.toString().startsWith("packages:") &&
|
|
doc.normalizedName === equals.get("normalizedName"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "skillVersions") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
doc._id?.toString().startsWith("skillVersions:") &&
|
|
doc.skillId === equals.get("skillId") &&
|
|
doc.version === equals.get("version"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "packageReleases") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
doc._id?.toString().startsWith("packageReleases:") &&
|
|
doc.packageId === equals.get("packageId") &&
|
|
doc.version === equals.get("version"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "githubSkillScans") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
doc._id?.toString().startsWith("githubSkillScans:") &&
|
|
doc.skillId === equals.get("skillId") &&
|
|
doc.contentHash === equals.get("contentHash"),
|
|
) ?? null
|
|
);
|
|
}
|
|
return null;
|
|
}),
|
|
};
|
|
}),
|
|
}));
|
|
const scheduler = {
|
|
runAfter: vi.fn(async () => undefined),
|
|
runAt: vi.fn(async () => "_scheduled_functions:1"),
|
|
};
|
|
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
get,
|
|
insert,
|
|
patch,
|
|
query,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
scheduler,
|
|
},
|
|
inserts,
|
|
patches,
|
|
get,
|
|
insert,
|
|
patch,
|
|
query,
|
|
scheduler,
|
|
};
|
|
}
|
|
|
|
function makeBulkRescanCtx(options: {
|
|
actorId?: string;
|
|
actorRole?: "admin" | "moderator" | "user";
|
|
skills: Array<Record<string, unknown>>;
|
|
versions: Array<Record<string, unknown>>;
|
|
jobs?: Array<Record<string, unknown>>;
|
|
}) {
|
|
const actorId = options.actorId ?? "users:admin";
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[
|
|
actorId,
|
|
{
|
|
_id: actorId,
|
|
role: options.actorRole ?? "admin",
|
|
},
|
|
],
|
|
...options.skills.map((skill) => [String(skill._id), skill] as const),
|
|
...options.versions.map((version) => [String(version._id), version] as const),
|
|
...(options.jobs ?? []).map((job) => [String(job._id), job] as const),
|
|
]);
|
|
const inserts: Array<{ table: string; doc: Record<string, unknown> }> = [];
|
|
const patches: Array<{ id: string; patch: Record<string, unknown> }> = [];
|
|
const get = vi.fn(async (id: string) => docs.get(id) ?? null);
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
const id = `${table}:${inserts.filter((entry) => entry.table === table).length + 1}`;
|
|
const inserted = { _id: id, _creationTime: Date.now(), ...doc };
|
|
docs.set(id, inserted);
|
|
inserts.push({ table, doc });
|
|
return id;
|
|
});
|
|
const patch = vi.fn(async (id: string, doc: Record<string, unknown>) => {
|
|
patches.push({ id, patch: doc });
|
|
docs.set(id, { ...(docs.get(id) ?? { _id: id }), ...doc });
|
|
});
|
|
const query = vi.fn((table: string) => ({
|
|
withIndex: vi.fn((indexName: string, buildRange: (q: { eq: typeof eq }) => unknown) => {
|
|
const equals = new Map<string, unknown>();
|
|
function eq(field: string, value: unknown) {
|
|
equals.set(field, value);
|
|
return { eq };
|
|
}
|
|
buildRange({ eq });
|
|
|
|
if (table === "skills") {
|
|
return {
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn(
|
|
async ({ cursor, numItems }: { cursor: string | null; numItems: number }) => {
|
|
expect(indexName).toBe("by_active_created");
|
|
const start = cursor ? Number.parseInt(cursor, 10) : 0;
|
|
const allSkills = options.skills.filter(
|
|
(skill) => skill.softDeletedAt === equals.get("softDeletedAt"),
|
|
);
|
|
const page = allSkills.slice(start, start + numItems);
|
|
const next = start + page.length;
|
|
return {
|
|
page,
|
|
isDone: next >= allSkills.length,
|
|
continueCursor: next >= allSkills.length ? "" : String(next),
|
|
};
|
|
},
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
|
|
return {
|
|
collect: vi.fn(async () => {
|
|
if (table !== "securityScanJobs") return [];
|
|
return (options.jobs ?? []).filter((job) => {
|
|
if (
|
|
equals.has("skillVersionId") &&
|
|
job.skillVersionId !== equals.get("skillVersionId")
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}),
|
|
};
|
|
}),
|
|
}));
|
|
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
get,
|
|
insert,
|
|
patch,
|
|
query,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
},
|
|
inserts,
|
|
patches,
|
|
get,
|
|
insert,
|
|
patch,
|
|
query,
|
|
};
|
|
}
|
|
|
|
function makeCancelCtx(jobs: ScanJob[], targets: Map<string, unknown> = new Map()) {
|
|
const deleted: string[] = [];
|
|
const deleteDoc = vi.fn(async (id: string) => {
|
|
deleted.push(id);
|
|
});
|
|
const get = vi.fn(async (id: string) => targets.get(id) ?? null);
|
|
const noopWrite = vi.fn(async () => undefined);
|
|
const take = vi.fn(async (limit: number) => jobs.slice(0, limit));
|
|
const order = vi.fn(() => ({ take }));
|
|
const indexBuilder: {
|
|
eq: ReturnType<typeof vi.fn>;
|
|
lt: ReturnType<typeof vi.fn>;
|
|
} = {
|
|
eq: vi.fn(() => indexBuilder),
|
|
lt: vi.fn(() => indexBuilder),
|
|
};
|
|
const withIndex = vi.fn((indexName: string, buildRange: (q: typeof indexBuilder) => unknown) => {
|
|
expect(indexName).toBe("by_status_source_created_at");
|
|
buildRange(indexBuilder);
|
|
expect(indexBuilder.eq).toHaveBeenCalledWith("status", "queued");
|
|
expect(indexBuilder.eq).toHaveBeenCalledWith("source", "vt-update");
|
|
expect(indexBuilder.lt).toHaveBeenCalledWith("createdAt", 1000);
|
|
return { order };
|
|
});
|
|
const query = vi.fn((tableName: string) => {
|
|
expect(tableName).toBe("securityScanJobs");
|
|
return { withIndex };
|
|
});
|
|
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
query,
|
|
get,
|
|
delete: deleteDoc,
|
|
insert: noopWrite,
|
|
patch: noopWrite,
|
|
replace: noopWrite,
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
},
|
|
deleted,
|
|
deleteDoc,
|
|
get,
|
|
take,
|
|
};
|
|
}
|
|
|
|
function makeClaimCtx(
|
|
jobs: ScanJob[],
|
|
docs: Record<string, Record<string, unknown>> = {},
|
|
catalogControl: Record<string, unknown> | null = null,
|
|
) {
|
|
const patches: Array<{ id: string; patch: Record<string, unknown> }> = [];
|
|
const patch = vi.fn(async (id: string, doc: Record<string, unknown>) => {
|
|
patches.push({ id, patch: doc });
|
|
docs[id] = { ...docs[id], ...doc };
|
|
});
|
|
const query = vi.fn((tableName: string) => {
|
|
if (tableName === "skillsShCatalogControls") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => catalogControl),
|
|
})),
|
|
};
|
|
}
|
|
if (tableName === "skillsShCatalogScanAttempts") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
buildRange: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
const equals = new Map<string, unknown>();
|
|
const range = {
|
|
eq(field: string, value: unknown) {
|
|
equals.set(field, value);
|
|
return range;
|
|
},
|
|
};
|
|
buildRange(range);
|
|
const attempts = Object.values(docs).filter(
|
|
(doc) =>
|
|
typeof doc._id === "string" &&
|
|
doc._id.startsWith("skillsShCatalogScanAttempts:") &&
|
|
Array.from(equals.entries()).every(([field, value]) => doc[field] === value),
|
|
);
|
|
return {
|
|
take: vi.fn(async (limit: number) => attempts.slice(0, limit)),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
expect(tableName).toBe("securityScanJobs");
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
buildRange: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
lte: (field: string, value: number) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
const eqFilters = new Map<string, unknown>();
|
|
const lteFilters = new Map<string, number>();
|
|
const indexBuilder = {
|
|
eq(field: string, value: unknown) {
|
|
eqFilters.set(field, value);
|
|
return indexBuilder;
|
|
},
|
|
lte(field: string, value: number) {
|
|
lteFilters.set(field, value);
|
|
return indexBuilder;
|
|
},
|
|
};
|
|
buildRange(indexBuilder);
|
|
const select = () =>
|
|
jobs
|
|
.filter((job) => {
|
|
for (const [field, value] of eqFilters) {
|
|
if ((job as unknown as Record<string, unknown>)[field] !== value) return false;
|
|
}
|
|
for (const [field, value] of lteFilters) {
|
|
const fieldValue = (job as unknown as Record<string, unknown>)[field];
|
|
if (typeof fieldValue !== "number" || fieldValue > value) return false;
|
|
}
|
|
return true;
|
|
})
|
|
.sort((a, b) => {
|
|
if (indexName.includes("next_run_at")) return a.nextRunAt - b.nextRunAt;
|
|
if (indexName.includes("lease_expires_at")) {
|
|
return (
|
|
((a as unknown as { leaseExpiresAt?: number }).leaseExpiresAt ?? 0) -
|
|
((b as unknown as { leaseExpiresAt?: number }).leaseExpiresAt ?? 0)
|
|
);
|
|
}
|
|
return a.createdAt - b.createdAt;
|
|
});
|
|
const take = vi.fn(async (limit: number) => select().slice(0, limit));
|
|
return {
|
|
take,
|
|
order: vi.fn(() => ({ take })),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
});
|
|
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
query,
|
|
patch,
|
|
get: vi.fn(async (id: string) => docs[id] ?? null),
|
|
insert: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
},
|
|
patches,
|
|
patch,
|
|
query,
|
|
};
|
|
}
|
|
|
|
function makeSkillScanStatusCtx(options: {
|
|
actor: Record<string, unknown>;
|
|
request: Record<string, unknown>;
|
|
jobs: ScanJob[];
|
|
}) {
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[String(options.actor._id), options.actor],
|
|
[String(options.request._id), options.request],
|
|
...options.jobs.map((job) => [job._id, job] as const),
|
|
]);
|
|
const get = vi.fn(async (id: string) => docs.get(id) ?? null);
|
|
const query = vi.fn((tableName: string) => {
|
|
expect(tableName).toBe("securityScanJobs");
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
buildRange: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
lte: (field: string, value: number) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
const eqFilters = new Map<string, unknown>();
|
|
const lteFilters = new Map<string, number>();
|
|
const indexBuilder = {
|
|
eq(field: string, value: unknown) {
|
|
eqFilters.set(field, value);
|
|
return indexBuilder;
|
|
},
|
|
lte(field: string, value: number) {
|
|
lteFilters.set(field, value);
|
|
return indexBuilder;
|
|
},
|
|
};
|
|
buildRange(indexBuilder);
|
|
const select = () =>
|
|
options.jobs
|
|
.filter((job) => {
|
|
for (const [field, value] of eqFilters) {
|
|
if ((job as unknown as Record<string, unknown>)[field] !== value) return false;
|
|
}
|
|
for (const [field, value] of lteFilters) {
|
|
const fieldValue = (job as unknown as Record<string, unknown>)[field];
|
|
if (typeof fieldValue !== "number" || fieldValue > value) return false;
|
|
}
|
|
return true;
|
|
})
|
|
.sort((a, b) => {
|
|
if (indexName.includes("next_run_at")) {
|
|
if (a.nextRunAt !== b.nextRunAt) return a.nextRunAt - b.nextRunAt;
|
|
if (a._creationTime !== b._creationTime) {
|
|
return a._creationTime - b._creationTime;
|
|
}
|
|
return a._id.localeCompare(b._id);
|
|
}
|
|
return a.createdAt - b.createdAt;
|
|
});
|
|
const collect = vi.fn(async () => select());
|
|
const take = vi.fn(async (limit: number) => select().slice(0, limit));
|
|
return {
|
|
collect,
|
|
take,
|
|
order: vi.fn(() => ({ collect, take })),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
});
|
|
|
|
return {
|
|
db: {
|
|
get,
|
|
query,
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeStoredScanReportCtx(options: {
|
|
actor: Record<string, unknown>;
|
|
docs: Record<string, Record<string, unknown>>;
|
|
membership?: Record<string, unknown> | null;
|
|
}) {
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[String(options.actor._id), options.actor],
|
|
...Object.entries(options.docs),
|
|
]);
|
|
const get = vi.fn(async (id: string) => docs.get(id) ?? null);
|
|
const query = vi.fn((table: string) => ({
|
|
withIndex: vi.fn((_indexName: string, buildRange: (q: { eq: typeof eq }) => unknown) => {
|
|
const equals = new Map<string, unknown>();
|
|
function eq(field: string, value: unknown) {
|
|
equals.set(field, value);
|
|
return { eq };
|
|
}
|
|
buildRange({ eq });
|
|
return {
|
|
unique: vi.fn(async () => {
|
|
if (table === "publisherMembers") return options.membership ?? null;
|
|
if (table === "skills") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) => String(doc._id).startsWith("skills:") && doc.slug === equals.get("slug"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "skillVersions") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
String(doc._id).startsWith("skillVersions:") &&
|
|
doc.skillId === equals.get("skillId") &&
|
|
doc.version === equals.get("version"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "packages") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
String(doc._id).startsWith("packages:") &&
|
|
doc.normalizedName === equals.get("normalizedName"),
|
|
) ?? null
|
|
);
|
|
}
|
|
if (table === "packageReleases") {
|
|
return (
|
|
Array.from(docs.values()).find(
|
|
(doc) =>
|
|
String(doc._id).startsWith("packageReleases:") &&
|
|
doc.packageId === equals.get("packageId") &&
|
|
doc.version === equals.get("version"),
|
|
) ?? null
|
|
);
|
|
}
|
|
return null;
|
|
}),
|
|
};
|
|
}),
|
|
}));
|
|
|
|
return {
|
|
db: {
|
|
get,
|
|
query,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("securityScan", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.unstubAllEnvs();
|
|
vi.mocked(getAuthUserId).mockReset();
|
|
});
|
|
|
|
it("reports claimable queue depth and oldest overdue age", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(1_000_000);
|
|
const ctx = makeQueueHealthCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:oldest-ready",
|
|
nextRunAt: 100_000,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:ready",
|
|
nextRunAt: 900_000,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:future",
|
|
nextRunAt: 1_100_000,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:running",
|
|
status: "running",
|
|
nextRunAt: 1,
|
|
}),
|
|
]);
|
|
|
|
const result = await getCodexScanQueueHealthInternalHandler(ctx, {});
|
|
|
|
expect(result).toEqual({
|
|
snapshotAt: 1_000_000,
|
|
queueDepth: 3,
|
|
queueDepthIsEstimate: false,
|
|
readyQueueDepth: 2,
|
|
readyQueueDepthIsEstimate: false,
|
|
oldestReadyJobAgeSeconds: 900,
|
|
oldestReadyJobNextRunAt: 100_000,
|
|
});
|
|
});
|
|
|
|
it("marks capped queue health counts as estimates", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(1_000_000);
|
|
const ctx = makeQueueHealthCtx(
|
|
Array.from({ length: 513 }, (_, index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:queued-${index}`,
|
|
_creationTime: index,
|
|
nextRunAt: index,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const result = await getCodexScanQueueHealthInternalHandler(ctx, {});
|
|
|
|
expect(result).toMatchObject({
|
|
queueDepth: 512,
|
|
queueDepthIsEstimate: true,
|
|
readyQueueDepth: 512,
|
|
readyQueueDepthIsEstimate: true,
|
|
});
|
|
});
|
|
|
|
it("logs the queue health snapshot as a structured observability event", async () => {
|
|
const snapshot = {
|
|
snapshotAt: 1_000_000,
|
|
queueDepth: 4,
|
|
queueDepthIsEstimate: false,
|
|
readyQueueDepth: 2,
|
|
readyQueueDepthIsEstimate: false,
|
|
oldestReadyJobAgeSeconds: 901,
|
|
oldestReadyJobNextRunAt: 99_000,
|
|
};
|
|
const runQuery = vi.fn(async () => snapshot);
|
|
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
|
|
const result = await logCodexScanQueueHealthInternalHandler({ runQuery }, {});
|
|
|
|
expect(result).toEqual(snapshot);
|
|
expect(runQuery).toHaveBeenCalledWith(expect.anything(), {});
|
|
expect(log).toHaveBeenCalledWith(
|
|
JSON.stringify({
|
|
event: "security_scan_queue.snapshot",
|
|
...snapshot,
|
|
}),
|
|
);
|
|
log.mockRestore();
|
|
});
|
|
|
|
it("exposes queue health to the authenticated security worker", async () => {
|
|
const workerAuth = "fixture";
|
|
const rejectedAuth = "rejected-fixture";
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", workerAuth);
|
|
const snapshot = {
|
|
snapshotAt: 1_000_000,
|
|
queueDepth: 4,
|
|
queueDepthIsEstimate: false,
|
|
readyQueueDepth: 2,
|
|
readyQueueDepthIsEstimate: false,
|
|
oldestReadyJobAgeSeconds: 901,
|
|
oldestReadyJobNextRunAt: 99_000,
|
|
};
|
|
const runQuery = vi.fn(async () => snapshot);
|
|
|
|
await expect(
|
|
getCodexScanQueueHealthHandler({ runQuery }, { token: workerAuth }),
|
|
).resolves.toEqual(snapshot);
|
|
expect(runQuery).toHaveBeenCalledWith(expect.anything(), {});
|
|
await expect(
|
|
getCodexScanQueueHealthHandler({ runQuery }, { ["token"]: rejectedAuth }),
|
|
).rejects.toThrow("Unauthorized");
|
|
});
|
|
|
|
it("does not enqueue a duplicate publish scan after the backup delay if the first scan already finished", async () => {
|
|
const existingJob = makeScanJob({
|
|
_id: "securityScanJobs:fast-publish",
|
|
status: "succeeded",
|
|
source: "publish",
|
|
skillVersionId: "skillVersions:fast-publish",
|
|
});
|
|
const { ctx, inserts, patches } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"skillVersions:fast-publish": {
|
|
_id: "skillVersions:fast-publish",
|
|
skillId: "skills:fast-publish",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
activeJobs: [existingJob],
|
|
});
|
|
|
|
const result = await enqueueSkillVersionScanInternalHandler(ctx, {
|
|
versionId: "skillVersions:fast-publish",
|
|
source: "publish",
|
|
preserveExistingJob: true,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
jobId: "securityScanJobs:fast-publish",
|
|
alreadyQueued: true,
|
|
});
|
|
expect(inserts.filter((entry) => entry.table === "securityScanJobs")).toEqual([]);
|
|
expect(patches).toEqual([]);
|
|
});
|
|
|
|
it("keeps an unscanned skill publish at publish priority when VirusTotal finishes", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(1_000_000);
|
|
const existingJob = makeScanJob({
|
|
_id: "securityScanJobs:skill-publish",
|
|
source: "publish",
|
|
skillVersionId: "skillVersions:skill-publish",
|
|
waitForVtUntil: 1_500_000,
|
|
nextRunAt: 1_500_000,
|
|
});
|
|
const { ctx, patches } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"skillVersions:skill-publish": {
|
|
_id: "skillVersions:skill-publish",
|
|
skillId: "skills:skill-publish",
|
|
version: "1.0.0",
|
|
vtAnalysis: { status: "clean", checkedAt: 1_000_000 },
|
|
},
|
|
},
|
|
activeJobs: [existingJob],
|
|
});
|
|
|
|
await enqueueSkillVersionScanInternalHandler(ctx, {
|
|
versionId: "skillVersions:skill-publish",
|
|
source: "vt-update",
|
|
waitForVtMs: 0,
|
|
});
|
|
|
|
expect(patches).toContainEqual({
|
|
id: "securityScanJobs:skill-publish",
|
|
patch: expect.objectContaining({
|
|
source: "publish",
|
|
nextRunAt: 1_000_000,
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("keeps an unscanned package publish at publish priority when VirusTotal finishes", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(2_000_000);
|
|
const existingJob = makeScanJob({
|
|
_id: "securityScanJobs:package-publish",
|
|
targetKind: "packageRelease",
|
|
skillVersionId: undefined,
|
|
packageReleaseId: "packageReleases:package-publish",
|
|
source: "publish",
|
|
waitForVtUntil: 2_500_000,
|
|
nextRunAt: 2_500_000,
|
|
});
|
|
const { ctx, patches } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"packageReleases:package-publish": {
|
|
_id: "packageReleases:package-publish",
|
|
packageId: "packages:package-publish",
|
|
version: "1.0.0",
|
|
vtAnalysis: { status: "clean", checkedAt: 2_000_000 },
|
|
},
|
|
},
|
|
activeJobs: [existingJob],
|
|
});
|
|
|
|
await enqueuePackageReleaseScanInternalHandler(ctx, {
|
|
releaseId: "packageReleases:package-publish",
|
|
source: "vt-update",
|
|
waitForVtMs: 0,
|
|
});
|
|
|
|
expect(patches).toContainEqual({
|
|
id: "securityScanJobs:package-publish",
|
|
patch: expect.objectContaining({
|
|
source: "publish",
|
|
nextRunAt: 2_000_000,
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("requests an immediate worker dispatch when a publish scan becomes claimable", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(3_000_000);
|
|
vi.stubEnv("SECURITY_SCAN_EVENT_DISPATCH_ENABLED", "1");
|
|
vi.stubEnv("GITHUB_APP_ID", "configured");
|
|
vi.stubEnv("GITHUB_APP_INSTALLATION_ID", "configured");
|
|
vi.stubEnv("GITHUB_APP_PRIVATE_KEY", "configured");
|
|
const { ctx, scheduler } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"skillVersions:dispatch": {
|
|
_id: "skillVersions:dispatch",
|
|
skillId: "skills:dispatch",
|
|
version: "1.0.0",
|
|
vtAnalysis: { status: "clean", checkedAt: 3_000_000 },
|
|
},
|
|
},
|
|
});
|
|
|
|
await enqueueSkillVersionScanInternalHandler(ctx, {
|
|
versionId: "skillVersions:dispatch",
|
|
source: "publish",
|
|
});
|
|
|
|
expect(scheduler.runAt).toHaveBeenCalledWith(3_000_000, expect.anything(), {
|
|
scheduleToken: expect.any(String),
|
|
});
|
|
});
|
|
|
|
it("lets platform moderators request skill rescans", async () => {
|
|
const { ctx, inserts } = makeRescanCtx({
|
|
actorId: "users:moderator",
|
|
actorRole: "moderator",
|
|
docs: {
|
|
"skills:1": {
|
|
_id: "skills:1",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:1",
|
|
},
|
|
"skillVersions:1": {
|
|
_id: "skillVersions:1",
|
|
skillId: "skills:1",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanHandler(ctx, {
|
|
skillId: "skills:1",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
jobId: "securityScanJobs:1",
|
|
alreadyQueued: false,
|
|
});
|
|
expect(inserts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
table: "securityScanJobs",
|
|
doc: expect.objectContaining({
|
|
targetKind: "skillVersion",
|
|
skillVersionId: "skillVersions:1",
|
|
source: "manual",
|
|
priority: 100,
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
actorUserId: "users:moderator",
|
|
action: "skill.clawscan.rescan",
|
|
targetType: "skillVersion",
|
|
targetId: "skillVersions:1",
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("lets platform moderators force-rescan GitHub-backed skills", async () => {
|
|
const { ctx, inserts, scheduler } = makeRescanCtx({
|
|
actorId: "users:moderator",
|
|
actorRole: "moderator",
|
|
docs: {
|
|
"skills:github": {
|
|
_id: "skills:github",
|
|
slug: "github-demo",
|
|
ownerUserId: "users:owner",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:github",
|
|
githubPath: "skills/github-demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
latestVersionSummary: { version: "1.2.3" },
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanHandler(ctx, {
|
|
skillId: "skills:github",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
scheduled: true,
|
|
alreadyQueued: false,
|
|
githubContentHash: "content-hash",
|
|
});
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(0, expect.anything(), {
|
|
skillId: "skills:github",
|
|
contentHash: "content-hash",
|
|
force: true,
|
|
});
|
|
const durableScanInsert = inserts.find((entry) => entry.table === "githubSkillScans");
|
|
expect(durableScanInsert).toBeDefined();
|
|
expect(Object.values(durableScanInsert?.doc ?? {})).not.toContain(undefined);
|
|
expect(inserts).toContainEqual(
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
action: "skill.clawscan.rescan",
|
|
targetType: "skill",
|
|
targetId: "skills:github",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not schedule another GitHub verification action while the content scan is active", async () => {
|
|
const { ctx, inserts, scheduler } = makeRescanCtx({
|
|
actorId: "users:moderator",
|
|
actorRole: "moderator",
|
|
docs: {
|
|
"skills:github": {
|
|
_id: "skills:github",
|
|
slug: "github-demo",
|
|
ownerUserId: "users:owner",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:github",
|
|
githubPath: "skills/github-demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
},
|
|
"githubSkillScans:github": {
|
|
_id: "githubSkillScans:github",
|
|
skillId: "skills:github",
|
|
contentHash: "content-hash",
|
|
status: "pending",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
},
|
|
"skillScanRequests:github": {
|
|
_id: "skillScanRequests:github",
|
|
securityScanJobId: "securityScanJobs:github",
|
|
},
|
|
"securityScanJobs:github": {
|
|
_id: "securityScanJobs:github",
|
|
status: "running",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanHandler(ctx, {
|
|
skillId: "skills:github",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
scheduled: false,
|
|
alreadyQueued: true,
|
|
jobId: "securityScanJobs:github",
|
|
});
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
expect(inserts).toContainEqual(
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
alreadyQueued: true,
|
|
jobId: "securityScanJobs:github",
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("promotes an already queued GitHub verification job to manual priority", async () => {
|
|
const { ctx, patches, scheduler } = makeRescanCtx({
|
|
actorId: "users:moderator",
|
|
actorRole: "moderator",
|
|
docs: {
|
|
"skills:github": {
|
|
_id: "skills:github",
|
|
slug: "github-demo",
|
|
ownerUserId: "users:owner",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:github",
|
|
githubPath: "skills/github-demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
},
|
|
"githubSkillScans:github": {
|
|
_id: "githubSkillScans:github",
|
|
skillId: "skills:github",
|
|
contentHash: "content-hash",
|
|
status: "pending",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
},
|
|
"skillScanRequests:github": {
|
|
_id: "skillScanRequests:github",
|
|
securityScanJobId: "securityScanJobs:github",
|
|
},
|
|
"securityScanJobs:github": {
|
|
_id: "securityScanJobs:github",
|
|
status: "queued",
|
|
source: "publish",
|
|
priority: 0,
|
|
nextRunAt: Date.now() + 60_000,
|
|
waitForVtUntil: Date.now() + 60_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanHandler(ctx, {
|
|
skillId: "skills:github",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
scheduled: false,
|
|
alreadyQueued: true,
|
|
jobId: "securityScanJobs:github",
|
|
});
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
expect(patches).toContainEqual({
|
|
id: "securityScanJobs:github",
|
|
patch: expect.objectContaining({
|
|
source: "manual",
|
|
priority: 100,
|
|
nextRunAt: expect.any(Number),
|
|
waitForVtUntil: expect.any(Number),
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("does not schedule another GitHub verification action while a recent action is pending", async () => {
|
|
const { ctx, patches, scheduler } = makeRescanCtx({
|
|
actorId: "users:moderator",
|
|
actorRole: "moderator",
|
|
docs: {
|
|
"skills:github": {
|
|
_id: "skills:github",
|
|
slug: "github-demo",
|
|
ownerUserId: "users:owner",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:github",
|
|
githubPath: "skills/github-demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
},
|
|
"githubSkillScans:github": {
|
|
_id: "githubSkillScans:github",
|
|
skillId: "skills:github",
|
|
githubSourceId: "githubSkillSources:github",
|
|
contentHash: "content-hash",
|
|
commit: "a".repeat(40),
|
|
path: "skills/github-demo",
|
|
status: "pending",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
},
|
|
"skillScanRequests:github": {
|
|
_id: "skillScanRequests:github",
|
|
sourceKind: "github",
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanHandler(ctx, {
|
|
skillId: "skills:github",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
scheduled: false,
|
|
alreadyQueued: true,
|
|
});
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
expect(patches).toContainEqual({
|
|
id: "skillScanRequests:github",
|
|
patch: expect.objectContaining({
|
|
requestedJobSource: "manual",
|
|
requestedJobPriority: 100,
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("returns stored scan reports for hidden skill versions to the owner", async () => {
|
|
const ctx = makeStoredScanReportCtx({
|
|
actor: { _id: "users:owner", role: "user" },
|
|
docs: {
|
|
"skills:hidden": {
|
|
_id: "skills:hidden",
|
|
slug: "hidden-skill",
|
|
displayName: "Hidden Skill",
|
|
ownerUserId: "users:owner",
|
|
},
|
|
"skillVersions:hidden": {
|
|
_id: "skillVersions:hidden",
|
|
skillId: "skills:hidden",
|
|
version: "1.2.3",
|
|
softDeletedAt: 1_700_000_100_000,
|
|
files: [],
|
|
sha256hash: "abc123",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
summary: "Attempts to exfiltrate credentials.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
staticScan: {
|
|
status: "malicious",
|
|
reasonCodes: ["network.exfiltration"],
|
|
findings: [],
|
|
summary: "Credential exfiltration pattern.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
createdAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
const report = await getStoredScanReportForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
kind: "skill",
|
|
name: "hidden-skill",
|
|
version: "1.2.3",
|
|
});
|
|
|
|
expect(report).toMatchObject({
|
|
ok: true,
|
|
status: "succeeded",
|
|
artifact: {
|
|
kind: "skill",
|
|
slug: "hidden-skill",
|
|
displayName: "Hidden Skill",
|
|
version: "1.2.3",
|
|
},
|
|
report: {
|
|
clawscan: {
|
|
status: "malicious",
|
|
summary: "Attempts to exfiltrate credentials.",
|
|
},
|
|
staticAnalysis: {
|
|
status: "malicious",
|
|
summary: "Credential exfiltration pattern.",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("returns stored scan reports for hidden org skill versions to publisher-role uploaders", async () => {
|
|
const ctx = makeStoredScanReportCtx({
|
|
actor: { _id: "users:member", role: "user" },
|
|
membership: {
|
|
_id: "publisherMembers:member",
|
|
publisherId: "publishers:org",
|
|
userId: "users:member",
|
|
role: "publisher",
|
|
},
|
|
docs: {
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
},
|
|
"skills:hidden": {
|
|
_id: "skills:hidden",
|
|
slug: "hidden-skill",
|
|
displayName: "Hidden Skill",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
},
|
|
"skillVersions:hidden": {
|
|
_id: "skillVersions:hidden",
|
|
skillId: "skills:hidden",
|
|
version: "1.2.3",
|
|
softDeletedAt: 1_700_000_100_000,
|
|
files: [],
|
|
sha256hash: "abc123",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
summary: "Attempts to exfiltrate credentials.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
createdAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
const report = await getStoredScanReportForUserInternalHandler(ctx, {
|
|
actorUserId: "users:member",
|
|
kind: "skill",
|
|
name: "hidden-skill",
|
|
version: "1.2.3",
|
|
});
|
|
|
|
expect(report).toMatchObject({
|
|
ok: true,
|
|
artifact: {
|
|
kind: "skill",
|
|
slug: "hidden-skill",
|
|
displayName: "Hidden Skill",
|
|
version: "1.2.3",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("denies stored scan reports to non-owners", async () => {
|
|
const ctx = makeStoredScanReportCtx({
|
|
actor: { _id: "users:intruder", role: "user" },
|
|
docs: {
|
|
"skills:hidden": {
|
|
_id: "skills:hidden",
|
|
slug: "hidden-skill",
|
|
displayName: "Hidden Skill",
|
|
ownerUserId: "users:owner",
|
|
},
|
|
"skillVersions:hidden": {
|
|
_id: "skillVersions:hidden",
|
|
skillId: "skills:hidden",
|
|
version: "1.2.3",
|
|
softDeletedAt: 1,
|
|
files: [],
|
|
llmAnalysis: { status: "malicious", checkedAt: 1 },
|
|
createdAt: 1,
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
getStoredScanReportForUserInternalHandler(ctx, {
|
|
actorUserId: "users:intruder",
|
|
kind: "skill",
|
|
name: "hidden-skill",
|
|
version: "1.2.3",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("returns stored scan reports for hidden plugin releases to platform moderators", async () => {
|
|
const ctx = makeStoredScanReportCtx({
|
|
actor: { _id: "users:moderator", role: "moderator" },
|
|
docs: {
|
|
"packages:plugin": {
|
|
_id: "packages:plugin",
|
|
name: "@scope/demo",
|
|
normalizedName: "@scope/demo",
|
|
displayName: "Demo Plugin",
|
|
ownerUserId: "users:owner",
|
|
},
|
|
"packageReleases:hidden": {
|
|
_id: "packageReleases:hidden",
|
|
packageId: "packages:plugin",
|
|
version: "2.0.0",
|
|
softDeletedAt: 1_700_000_100_000,
|
|
files: [],
|
|
integritySha256: "def456",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
summary: "Runs unexpected shell commands.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
createdAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
const report = await getStoredScanReportForUserInternalHandler(ctx, {
|
|
actorUserId: "users:moderator",
|
|
kind: "plugin",
|
|
name: "@scope/demo",
|
|
version: "2.0.0",
|
|
});
|
|
|
|
expect(report).toMatchObject({
|
|
ok: true,
|
|
status: "succeeded",
|
|
artifact: {
|
|
kind: "plugin",
|
|
name: "@scope/demo",
|
|
displayName: "Demo Plugin",
|
|
version: "2.0.0",
|
|
},
|
|
report: {
|
|
clawscan: {
|
|
status: "malicious",
|
|
summary: "Runs unexpected shell commands.",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("returns stored scan reports for hidden org plugin releases to publisher-role uploaders", async () => {
|
|
const ctx = makeStoredScanReportCtx({
|
|
actor: { _id: "users:member", role: "user" },
|
|
membership: {
|
|
_id: "publisherMembers:member",
|
|
publisherId: "publishers:org",
|
|
userId: "users:member",
|
|
role: "publisher",
|
|
},
|
|
docs: {
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
},
|
|
"packages:plugin": {
|
|
_id: "packages:plugin",
|
|
name: "@org/demo",
|
|
normalizedName: "@org/demo",
|
|
displayName: "Org Plugin",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
},
|
|
"packageReleases:hidden": {
|
|
_id: "packageReleases:hidden",
|
|
packageId: "packages:plugin",
|
|
version: "2.0.0",
|
|
softDeletedAt: 1_700_000_100_000,
|
|
files: [],
|
|
integritySha256: "def456",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
summary: "Runs unexpected shell commands.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
createdAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
const report = await getStoredScanReportForUserInternalHandler(ctx, {
|
|
actorUserId: "users:member",
|
|
kind: "plugin",
|
|
name: "@org/demo",
|
|
version: "2.0.0",
|
|
});
|
|
|
|
expect(report).toMatchObject({
|
|
ok: true,
|
|
artifact: {
|
|
kind: "plugin",
|
|
name: "@org/demo",
|
|
displayName: "Org Plugin",
|
|
version: "2.0.0",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("lets skill owners request skill rescans through the API helper", async () => {
|
|
const { ctx, inserts } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"skills:1": {
|
|
_id: "skills:1",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:owner",
|
|
latestVersionId: "skillVersions:1",
|
|
},
|
|
"skillVersions:1": {
|
|
_id: "skillVersions:1",
|
|
skillId: "skills:1",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
slug: "demo-skill",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
skillVersionId: "skillVersions:1",
|
|
jobId: "securityScanJobs:1",
|
|
alreadyQueued: false,
|
|
});
|
|
expect(inserts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
actorUserId: "users:owner",
|
|
action: "skill.clawscan.rescan",
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("scopes API helper rescans by owner handle", async () => {
|
|
const { ctx } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"publishers:owner": {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
"publishers:other": {
|
|
_id: "publishers:other",
|
|
kind: "user",
|
|
handle: "other",
|
|
linkedUserId: "users:other",
|
|
},
|
|
"skills:owner": {
|
|
_id: "skills:owner",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
latestVersionId: "skillVersions:owner",
|
|
},
|
|
"skills:other": {
|
|
_id: "skills:other",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:other",
|
|
latestVersionId: "skillVersions:other",
|
|
},
|
|
"skillVersions:owner": {
|
|
_id: "skillVersions:owner",
|
|
skillId: "skills:owner",
|
|
version: "1.0.0",
|
|
},
|
|
"skillVersions:other": {
|
|
_id: "skillVersions:other",
|
|
skillId: "skills:other",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestSkillRescanForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
slug: "demo-skill",
|
|
ownerHandle: "owner",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
skillId: "skills:owner",
|
|
skillVersionId: "skillVersions:owner",
|
|
});
|
|
});
|
|
|
|
it("fails slug-only API helper rescans with controlled ambiguity", async () => {
|
|
const { ctx } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"publishers:owner": {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
"publishers:other": {
|
|
_id: "publishers:other",
|
|
kind: "user",
|
|
handle: "other",
|
|
linkedUserId: "users:other",
|
|
},
|
|
"skills:owner": {
|
|
_id: "skills:owner",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
latestVersionId: "skillVersions:owner",
|
|
},
|
|
"skills:other": {
|
|
_id: "skills:other",
|
|
slug: "demo-skill",
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:other",
|
|
latestVersionId: "skillVersions:other",
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
requestSkillRescanForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
slug: "demo-skill",
|
|
}),
|
|
).rejects.toThrow("Slug is used by multiple publishers");
|
|
});
|
|
|
|
it("queues bulk rescans for active latest skill versions as low-priority jobs", async () => {
|
|
const { ctx, inserts } = makeBulkRescanCtx({
|
|
skills: [
|
|
{
|
|
_id: "skills:active-1",
|
|
slug: "active-one",
|
|
moderationStatus: "active",
|
|
latestVersionId: "skillVersions:active-1",
|
|
},
|
|
{
|
|
_id: "skills:hidden",
|
|
slug: "hidden-skill",
|
|
moderationStatus: "hidden",
|
|
latestVersionId: "skillVersions:hidden",
|
|
},
|
|
{
|
|
_id: "skills:active-2",
|
|
slug: "active-two",
|
|
moderationStatus: "active",
|
|
latestVersionId: "skillVersions:active-2",
|
|
},
|
|
],
|
|
versions: [
|
|
{ _id: "skillVersions:active-1", skillId: "skills:active-1", version: "1.0.0" },
|
|
{ _id: "skillVersions:hidden", skillId: "skills:hidden", version: "1.0.0" },
|
|
{ _id: "skillVersions:active-2", skillId: "skills:active-2", version: "1.0.0" },
|
|
],
|
|
});
|
|
|
|
const result = await enqueueBulkSkillRescanBatchForAdminInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
batchSize: 3,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
queued: 2,
|
|
alreadyQueued: 0,
|
|
skipped: 1,
|
|
done: true,
|
|
});
|
|
expect(result.jobIds).toEqual(["securityScanJobs:1", "securityScanJobs:2"]);
|
|
expect(inserts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
table: "securityScanJobs",
|
|
doc: expect.objectContaining({
|
|
targetKind: "skillVersion",
|
|
skillVersionId: "skillVersions:active-1",
|
|
source: "bulk-rescan",
|
|
priority: 0,
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
action: "skill.clawscan.bulk_rescan_batch",
|
|
targetType: "securityScanBatch",
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("treats missing moderation status as active during bulk rescans", async () => {
|
|
const { ctx } = makeBulkRescanCtx({
|
|
skills: [
|
|
{
|
|
_id: "skills:legacy-active",
|
|
slug: "legacy-active",
|
|
latestVersionId: "skillVersions:legacy-active",
|
|
},
|
|
],
|
|
versions: [
|
|
{
|
|
_id: "skillVersions:legacy-active",
|
|
skillId: "skills:legacy-active",
|
|
version: "1.0.0",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await enqueueBulkSkillRescanBatchForAdminInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
batchSize: 1,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
queued: 1,
|
|
alreadyQueued: 0,
|
|
skipped: 0,
|
|
jobIds: ["securityScanJobs:1"],
|
|
});
|
|
});
|
|
|
|
it("does not demote existing active jobs during bulk rescans", async () => {
|
|
const { ctx, inserts, patch } = makeBulkRescanCtx({
|
|
skills: [
|
|
{
|
|
_id: "skills:active-1",
|
|
slug: "active-one",
|
|
moderationStatus: "active",
|
|
latestVersionId: "skillVersions:active-1",
|
|
},
|
|
],
|
|
versions: [{ _id: "skillVersions:active-1", skillId: "skills:active-1", version: "1.0.0" }],
|
|
jobs: [
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual",
|
|
skillVersionId: "skillVersions:active-1",
|
|
source: "manual",
|
|
priority: 100,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await enqueueBulkSkillRescanBatchForAdminInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
batchSize: 1,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
queued: 0,
|
|
alreadyQueued: 1,
|
|
skipped: 0,
|
|
jobIds: ["securityScanJobs:manual"],
|
|
});
|
|
expect(patch).not.toHaveBeenCalled();
|
|
expect(inserts).toEqual(
|
|
expect.not.arrayContaining([expect.objectContaining({ table: "securityScanJobs" })]),
|
|
);
|
|
});
|
|
|
|
it("dry-runs bulk rescans without inserting jobs", async () => {
|
|
const { ctx, inserts } = makeBulkRescanCtx({
|
|
skills: [
|
|
{
|
|
_id: "skills:active-1",
|
|
slug: "active-one",
|
|
moderationStatus: "active",
|
|
latestVersionId: "skillVersions:active-1",
|
|
},
|
|
],
|
|
versions: [{ _id: "skillVersions:active-1", skillId: "skills:active-1", version: "1.0.0" }],
|
|
});
|
|
|
|
const result = await enqueueBulkSkillRescanBatchForAdminInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
batchSize: 1,
|
|
dryRun: true,
|
|
});
|
|
|
|
expect(result).toMatchObject({ queued: 1, alreadyQueued: 0, skipped: 0, jobIds: [] });
|
|
expect(inserts).toEqual([]);
|
|
});
|
|
|
|
it("aggregates bulk rescan batch status", async () => {
|
|
const { ctx } = makeBulkRescanCtx({
|
|
skills: [],
|
|
versions: [],
|
|
jobs: [
|
|
makeScanJob({ _id: "securityScanJobs:queued", status: "queued" }),
|
|
makeScanJob({ _id: "securityScanJobs:running", status: "running" }),
|
|
makeScanJob({ _id: "securityScanJobs:succeeded", status: "succeeded" }),
|
|
makeScanJob({ _id: "securityScanJobs:failed", status: "failed" }),
|
|
],
|
|
});
|
|
|
|
const result = await getBulkSkillRescanBatchStatusForAdminInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
jobIds: [
|
|
"securityScanJobs:queued",
|
|
"securityScanJobs:running",
|
|
"securityScanJobs:succeeded",
|
|
"securityScanJobs:failed",
|
|
"securityScanJobs:missing",
|
|
],
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
total: 5,
|
|
queued: 1,
|
|
running: 1,
|
|
succeeded: 1,
|
|
failed: 1,
|
|
missing: 1,
|
|
terminal: 3,
|
|
done: false,
|
|
failedJobIds: ["securityScanJobs:failed"],
|
|
});
|
|
});
|
|
|
|
it("dry-runs failed security scan recovery without changing jobs", async () => {
|
|
const { ctx, patches } = makeFailedScanRecoveryCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:before-window",
|
|
status: "failed",
|
|
updatedAt: 99,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:publish-failed",
|
|
status: "failed",
|
|
source: "publish",
|
|
updatedAt: 100,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual-failed",
|
|
status: "failed",
|
|
source: "manual",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:manual",
|
|
updatedAt: 101,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:succeeded",
|
|
status: "succeeded",
|
|
updatedAt: 102,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:after-window",
|
|
status: "failed",
|
|
updatedAt: 103,
|
|
}),
|
|
]);
|
|
|
|
const result = await requeueFailedSecurityScanJobsInternalHandler(ctx, {
|
|
dryRun: true,
|
|
failedAfter: 100,
|
|
failedBefore: 103,
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
dryRun: true,
|
|
matched: 2,
|
|
requeued: 0,
|
|
hasMore: false,
|
|
bySource: { manual: 1, publish: 1 },
|
|
byTargetKind: { skillScanRequest: 1, skillVersion: 1 },
|
|
sampleJobIds: ["securityScanJobs:publish-failed", "securityScanJobs:manual-failed"],
|
|
});
|
|
expect(patches).toEqual([]);
|
|
});
|
|
|
|
it("requeues failed jobs and resets uploaded scan-request state", async () => {
|
|
const firstCtx = makeFailedScanRecoveryCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:publish-failed",
|
|
status: "failed",
|
|
source: "publish",
|
|
attempts: 3,
|
|
updatedAt: 100,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual-failed",
|
|
status: "failed",
|
|
source: "manual",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:manual",
|
|
attempts: 3,
|
|
updatedAt: 101,
|
|
}),
|
|
]);
|
|
|
|
const result = await requeueFailedSecurityScanJobsInternalHandler(firstCtx.ctx, {
|
|
dryRun: false,
|
|
failedAfter: 100,
|
|
failedBefore: 200,
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
dryRun: false,
|
|
matched: 1,
|
|
requeued: 1,
|
|
hasMore: true,
|
|
sampleJobIds: ["securityScanJobs:publish-failed"],
|
|
});
|
|
expect(firstCtx.patches).toHaveLength(1);
|
|
expect(firstCtx.patches[0]).toMatchObject({
|
|
id: "securityScanJobs:publish-failed",
|
|
patch: {
|
|
status: "queued",
|
|
attempts: 0,
|
|
lastError: undefined,
|
|
completedAt: undefined,
|
|
},
|
|
});
|
|
|
|
const secondCtx = makeFailedScanRecoveryCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual-failed",
|
|
status: "failed",
|
|
source: "manual",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:manual",
|
|
attempts: 3,
|
|
updatedAt: 101,
|
|
}),
|
|
]);
|
|
const second = await requeueFailedSecurityScanJobsInternalHandler(secondCtx.ctx, {
|
|
dryRun: false,
|
|
failedAfter: 100,
|
|
failedBefore: 200,
|
|
});
|
|
|
|
expect(second).toMatchObject({ matched: 1, requeued: 1, hasMore: false });
|
|
expect(secondCtx.patches).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "securityScanJobs:manual-failed",
|
|
patch: expect.objectContaining({
|
|
status: "queued",
|
|
attempts: 0,
|
|
lastError: undefined,
|
|
completedAt: undefined,
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
id: "skillScanRequests:manual",
|
|
patch: expect.objectContaining({
|
|
status: "queued",
|
|
lastError: undefined,
|
|
completedAt: undefined,
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("does not revive catalog attempts through generic failed-job recovery", async () => {
|
|
const ctx = makeFailedScanRecoveryCtx(
|
|
[
|
|
makeScanJob({
|
|
_id: "securityScanJobs:catalog-failed",
|
|
status: "failed",
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
attempts: 3,
|
|
updatedAt: 150,
|
|
}),
|
|
],
|
|
{
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "failed",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
status: "failed",
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = await requeueFailedSecurityScanJobsInternalHandler(ctx.ctx, {
|
|
dryRun: false,
|
|
failedAfter: 100,
|
|
failedBefore: 200,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
matched: 0,
|
|
requeued: 0,
|
|
hasMore: false,
|
|
});
|
|
expect(ctx.patches).toEqual([]);
|
|
});
|
|
|
|
it("does not let catalog failures hide native recovery candidates", async () => {
|
|
const ctx = makeFailedScanRecoveryCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:catalog-failed",
|
|
status: "failed",
|
|
source: "skills-sh-catalog-test",
|
|
updatedAt: 100,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:native-failed",
|
|
status: "failed",
|
|
source: "manual",
|
|
updatedAt: 101,
|
|
}),
|
|
]);
|
|
|
|
const result = await requeueFailedSecurityScanJobsInternalHandler(ctx.ctx, {
|
|
dryRun: false,
|
|
failedAfter: 100,
|
|
failedBefore: 200,
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
matched: 1,
|
|
requeued: 1,
|
|
hasMore: false,
|
|
sampleJobIds: ["securityScanJobs:native-failed"],
|
|
});
|
|
expect(ctx.patches).toEqual([
|
|
expect.objectContaining({
|
|
id: "securityScanJobs:native-failed",
|
|
patch: expect.objectContaining({ status: "queued" }),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("restores failed GitHub-backed scans to pending while requeueing", async () => {
|
|
const ctx = makeFailedScanRecoveryCtx(
|
|
[
|
|
makeScanJob({
|
|
_id: "securityScanJobs:github-failed",
|
|
status: "failed",
|
|
source: "github",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
attempts: 3,
|
|
updatedAt: 150,
|
|
}),
|
|
],
|
|
{
|
|
"skillScanRequests:github": {
|
|
_id: "skillScanRequests:github",
|
|
githubSkillScanId: "githubSkillScans:github",
|
|
status: "failed",
|
|
},
|
|
"githubSkillScans:github": {
|
|
_id: "githubSkillScans:github",
|
|
skillId: "skills:github",
|
|
contentHash: "content-hash",
|
|
status: "failed",
|
|
},
|
|
"skills:github": {
|
|
_id: "skills:github",
|
|
installKind: "github",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
slug: "github-skill",
|
|
displayName: "GitHub Skill",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
comments: 0,
|
|
versions: 0,
|
|
},
|
|
moderationStatus: "hidden",
|
|
moderationReason: "scanner.failed",
|
|
moderationFlags: [],
|
|
isSuspicious: false,
|
|
createdAt: 1,
|
|
updatedAt: 150,
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = await requeueFailedSecurityScanJobsInternalHandler(ctx.ctx, {
|
|
dryRun: false,
|
|
failedAfter: 100,
|
|
failedBefore: 200,
|
|
});
|
|
|
|
expect(result).toMatchObject({ matched: 1, requeued: 1, hasMore: false });
|
|
expect(ctx.patches).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "githubSkillScans:github",
|
|
patch: expect.objectContaining({
|
|
status: "pending",
|
|
lastError: undefined,
|
|
completedAt: undefined,
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
id: "skills:github",
|
|
patch: expect.objectContaining({
|
|
githubScanStatus: "pending",
|
|
moderationStatus: "active",
|
|
moderationReason: "pending.scan",
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("lets platform moderators request package rescans", async () => {
|
|
const { ctx, inserts } = makeRescanCtx({
|
|
actorId: "users:moderator",
|
|
actorRole: "moderator",
|
|
docs: {
|
|
"packages:1": {
|
|
_id: "packages:1",
|
|
name: "@acme/demo-plugin",
|
|
normalizedName: "@acme/demo-plugin",
|
|
family: "plugin",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:1",
|
|
},
|
|
"packageReleases:1": {
|
|
_id: "packageReleases:1",
|
|
packageId: "packages:1",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestPackageRescanHandler(ctx, {
|
|
packageId: "packages:1",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
packageReleaseId: "packageReleases:1",
|
|
jobId: "securityScanJobs:1",
|
|
alreadyQueued: false,
|
|
});
|
|
expect(inserts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
table: "securityScanJobs",
|
|
doc: expect.objectContaining({
|
|
targetKind: "packageRelease",
|
|
packageReleaseId: "packageReleases:1",
|
|
source: "manual",
|
|
priority: 100,
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
actorUserId: "users:moderator",
|
|
action: "package.clawscan.rescan",
|
|
targetType: "packageRelease",
|
|
targetId: "packageReleases:1",
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("lets package owners request package rescans through the API helper", async () => {
|
|
const { ctx, inserts } = makeRescanCtx({
|
|
actorId: "users:owner",
|
|
docs: {
|
|
"packages:1": {
|
|
_id: "packages:1",
|
|
name: "@acme/demo-plugin",
|
|
normalizedName: "@acme/demo-plugin",
|
|
family: "code-plugin",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:1",
|
|
},
|
|
"packageReleases:1": {
|
|
_id: "packageReleases:1",
|
|
packageId: "packages:1",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await requestPackageRescanForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
name: "@acme/demo-plugin",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
packageReleaseId: "packageReleases:1",
|
|
jobId: "securityScanJobs:1",
|
|
alreadyQueued: false,
|
|
});
|
|
expect(inserts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
table: "auditLogs",
|
|
doc: expect.objectContaining({
|
|
actorUserId: "users:owner",
|
|
action: "package.clawscan.rescan",
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("rejects unrelated package rescan callers", async () => {
|
|
const { ctx, insert } = makeRescanCtx({
|
|
actorId: "users:random",
|
|
actorRole: "user",
|
|
docs: {
|
|
"packages:1": {
|
|
_id: "packages:1",
|
|
name: "@acme/demo-plugin",
|
|
normalizedName: "@acme/demo-plugin",
|
|
family: "plugin",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:1",
|
|
},
|
|
"packageReleases:1": {
|
|
_id: "packageReleases:1",
|
|
packageId: "packages:1",
|
|
version: "1.0.0",
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
requestPackageRescanHandler(ctx, {
|
|
packageId: "packages:1",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
expect(insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails claimed jobs when an artifact file URL is unavailable", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("limit" in args) return [claimedJob];
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("jobId" in args) {
|
|
return {
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
files: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 12,
|
|
sha256: "a".repeat(64),
|
|
storageId: "storage:skill",
|
|
},
|
|
{
|
|
path: "payload.js",
|
|
size: 24,
|
|
sha256: "b".repeat(64),
|
|
storageId: "storage:missing",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
if ("skillVersionId" in args) return [];
|
|
throw new Error(`Unexpected query args: ${JSON.stringify(args)}`);
|
|
});
|
|
const getUrl = vi.fn(async (storageId: string) =>
|
|
storageId === "storage:skill" ? "https://storage.example/SKILL.md" : null,
|
|
);
|
|
|
|
const result = await claimCodexScanJobsHandler(
|
|
{ runMutation, runQuery, storage: { getUrl } },
|
|
{ token: "worker-secret", workerId: "worker-1", limit: 10 },
|
|
);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(runMutation).toHaveBeenLastCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: "Artifact file unavailable: payload.js",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("omits generated Skill Card files from claimed skill scan files", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("limit" in args) return [claimedJob];
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("jobId" in args) {
|
|
return {
|
|
job: claimedJob,
|
|
skill: {
|
|
_id: "skills:1",
|
|
slug: "demo",
|
|
},
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
files: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 12,
|
|
sha256: "a".repeat(64),
|
|
storageId: "storage:skill",
|
|
contentType: "text/markdown",
|
|
},
|
|
{
|
|
path: "skill-card.md",
|
|
size: 24,
|
|
sha256: "b".repeat(64),
|
|
storageId: "storage:card",
|
|
contentType: "text/markdown",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
if ("skillVersionId" in args) {
|
|
return [{ fingerprint: "bundle-fingerprint", kind: "generated-bundle" }];
|
|
}
|
|
throw new Error(`Unexpected query args: ${JSON.stringify(args)}`);
|
|
});
|
|
const getUrl = vi.fn(async (storageId: string) => `https://storage.example/${storageId}`);
|
|
|
|
const result = (await claimCodexScanJobsHandler(
|
|
{ runMutation, runQuery, storage: { getUrl } },
|
|
{ token: "worker-secret", workerId: "worker-1", limit: 10 },
|
|
)) as Array<{ target: { files: Array<{ path: string }> } }>;
|
|
|
|
expect(result[0]?.target.files.map((file) => file.path)).toEqual(["SKILL.md"]);
|
|
expect(getUrl).toHaveBeenCalledWith("storage:skill");
|
|
expect(getUrl).not.toHaveBeenCalledWith("storage:card");
|
|
});
|
|
|
|
it("keeps publisher-authored Skill Card files in claimed skill scans", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("limit" in args) return [claimedJob];
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("jobId" in args) {
|
|
return {
|
|
job: claimedJob,
|
|
skill: {
|
|
_id: "skills:1",
|
|
slug: "demo",
|
|
},
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
files: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 12,
|
|
sha256: "a".repeat(64),
|
|
storageId: "storage:skill",
|
|
contentType: "text/markdown",
|
|
},
|
|
{
|
|
path: "skill-card.md",
|
|
size: 24,
|
|
sha256: "b".repeat(64),
|
|
storageId: "storage:card",
|
|
contentType: "text/markdown",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
if ("skillVersionId" in args) return [];
|
|
throw new Error(`Unexpected query args: ${JSON.stringify(args)}`);
|
|
});
|
|
const getUrl = vi.fn(async (storageId: string) => `https://storage.example/${storageId}`);
|
|
|
|
const result = (await claimCodexScanJobsHandler(
|
|
{ runMutation, runQuery, storage: { getUrl } },
|
|
{ token: "worker-secret", workerId: "worker-1", limit: 10 },
|
|
)) as Array<{ target: { files: Array<{ path: string }> } }>;
|
|
|
|
expect(result[0]?.target.files.map((file) => file.path)).toEqual(["SKILL.md", "skill-card.md"]);
|
|
expect(getUrl).toHaveBeenCalledWith("storage:skill");
|
|
expect(getUrl).toHaveBeenCalledWith("storage:card");
|
|
});
|
|
|
|
it("claims GitHub scan request files stored in bounded child chunks", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const githubJob = {
|
|
...claimedJob,
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
};
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("limit" in args) return [githubJob];
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
job: githubJob,
|
|
scanRequest: {
|
|
_id: "skillScanRequests:github",
|
|
sourceKind: "github",
|
|
files: [],
|
|
},
|
|
scanRequestFiles: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 12,
|
|
sha256: "a".repeat(64),
|
|
storageId: "storage:skill",
|
|
},
|
|
],
|
|
}));
|
|
const getUrl = vi.fn(async (storageId: string) => `https://storage.example/${storageId}`);
|
|
|
|
const result = (await claimCodexScanJobsHandler(
|
|
{ runMutation, runQuery, storage: { getUrl } },
|
|
{ token: "worker-secret", workerId: "worker-1", limit: 10 },
|
|
)) as Array<{ target: { files: Array<{ path: string }> } }>;
|
|
|
|
expect(result[0]?.target.files.map((file) => file.path)).toEqual(["SKILL.md"]);
|
|
expect(getUrl).toHaveBeenCalledWith("storage:skill");
|
|
});
|
|
|
|
it("claims several lightweight leases in one mutation without hydrating signed URLs", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
const leases = Array.from({ length: 4 }, (_, index) => ({
|
|
...claimedJob,
|
|
_id: `securityScanJobs:${index}`,
|
|
leaseToken: `lease-${index}`,
|
|
}));
|
|
const runMutation = vi.fn(async () => leases);
|
|
const runQuery = vi.fn();
|
|
const getUrl = vi.fn();
|
|
|
|
const result = await claimCodexScanJobLeasesHandler(
|
|
{ runMutation, runQuery, storage: { getUrl } },
|
|
{
|
|
token: "worker-secret",
|
|
workerId: "worker-1",
|
|
lane: "shared",
|
|
limit: 4,
|
|
},
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
workerId: "worker-1",
|
|
lane: "shared",
|
|
limit: 4,
|
|
}),
|
|
);
|
|
expect(result).toEqual(leases);
|
|
expect(runQuery).not.toHaveBeenCalled();
|
|
expect(getUrl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refuses to hydrate a lease owned by a different worker", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
...claimedJob,
|
|
workerId: "worker-2",
|
|
},
|
|
}));
|
|
const getUrl = vi.fn();
|
|
|
|
await expect(
|
|
hydrateCodexScanJobHandler(
|
|
{ runMutation: vi.fn(), runQuery, storage: { getUrl } },
|
|
{
|
|
token: "worker-secret",
|
|
workerId: "worker-1",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
},
|
|
),
|
|
).rejects.toThrow("Lease mismatch");
|
|
expect(getUrl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("exposes a worker-authenticated retry-safe lease requeue action", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
const runMutation = vi.fn(async () => ({ ok: true, nextRunAt: 1234 }));
|
|
|
|
await expect(
|
|
requeueCodexScanJobLeaseHandler(
|
|
{ runMutation },
|
|
{
|
|
token: "worker-secret",
|
|
workerId: "worker-1",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
},
|
|
),
|
|
).resolves.toEqual({ ok: true, nextRunAt: 1234 });
|
|
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
|
workerId: "worker-1",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
});
|
|
});
|
|
|
|
it("hydrates only the declared bounded GitHub file chunks", async () => {
|
|
const take = vi.fn(async () => [
|
|
{
|
|
_id: "skillScanRequestFileChunks:1",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
chunkIndex: 0,
|
|
files: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 12,
|
|
sha256: "a".repeat(64),
|
|
storageId: "storage:skill",
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
const query = vi.fn((table: string) => {
|
|
expect(table).toBe("skillScanRequestFileChunks");
|
|
return {
|
|
withIndex: vi.fn(() => ({ take })),
|
|
};
|
|
});
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[
|
|
"securityScanJobs:github",
|
|
{
|
|
_id: "securityScanJobs:github",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
},
|
|
],
|
|
[
|
|
"skillScanRequests:github",
|
|
{
|
|
_id: "skillScanRequests:github",
|
|
sourceKind: "github",
|
|
githubSkillScanId: "githubSkillScans:github",
|
|
fileChunkCount: 1,
|
|
files: [],
|
|
},
|
|
],
|
|
]);
|
|
|
|
const result = await getJobTargetInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => docs.get(id) ?? null),
|
|
query,
|
|
},
|
|
},
|
|
{ jobId: "securityScanJobs:github" },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
scanRequestFiles: [expect.objectContaining({ path: "SKILL.md" })],
|
|
});
|
|
expect(take).toHaveBeenCalledWith(expect.any(Number));
|
|
});
|
|
|
|
it("rejects GitHub file chunks that exceed the cumulative manifest hydration budget", async () => {
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[
|
|
"skillScanRequests:github",
|
|
{
|
|
_id: "skillScanRequests:github",
|
|
sourceKind: "github",
|
|
githubSkillScanId: "githubSkillScans:github",
|
|
fileChunkCount: 1,
|
|
fileManifestBytes: 4 * 1024 * 1024,
|
|
},
|
|
],
|
|
[
|
|
"githubSkillScans:github",
|
|
{
|
|
_id: "githubSkillScans:github",
|
|
status: "pending",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
},
|
|
],
|
|
]);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => docs.get(id) ?? null),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => null),
|
|
})),
|
|
})),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
appendGitHubSkillScanRequestFilesInternalHandler(ctx as never, {
|
|
requestId: "skillScanRequests:github",
|
|
chunkIndex: 1,
|
|
files: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 10,
|
|
storageId: "storage:1",
|
|
sha256: "a".repeat(64),
|
|
},
|
|
],
|
|
}),
|
|
).rejects.toThrow(/manifest exceeds the hydration limit/i);
|
|
|
|
expect(ctx.db.insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("clears only queued backfill jobs in local dev", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "local-dev-worker-token");
|
|
const jobs = [
|
|
makeScanJob({ _id: "securityScanJobs:backfill-1", source: "backfill" }),
|
|
makeScanJob({ _id: "securityScanJobs:backfill-2", source: "backfill" }),
|
|
];
|
|
const deleted: string[] = [];
|
|
const take = vi.fn(async () => jobs);
|
|
const order = vi.fn(() => ({ take }));
|
|
const indexBuilder = {
|
|
eq: vi.fn(() => indexBuilder),
|
|
};
|
|
const withIndex = vi.fn(
|
|
(indexName: string, buildRange: (q: typeof indexBuilder) => unknown) => {
|
|
expect(indexName).toBe("by_status_source_created_at");
|
|
buildRange(indexBuilder);
|
|
expect(indexBuilder.eq).toHaveBeenCalledWith("status", "queued");
|
|
expect(indexBuilder.eq).toHaveBeenCalledWith("source", "backfill");
|
|
return { order };
|
|
},
|
|
);
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((tableName: string) => {
|
|
expect(tableName).toBe("securityScanJobs");
|
|
return { withIndex };
|
|
}),
|
|
insert: vi.fn(async () => "noop"),
|
|
patch: vi.fn(async () => undefined),
|
|
replace: vi.fn(async () => undefined),
|
|
delete: vi.fn(async (id: string) => {
|
|
deleted.push(id);
|
|
}),
|
|
get: vi.fn(async () => null),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
};
|
|
|
|
const result = await clearQueuedBackfillJobsForLocalDevHandler(ctx as never, {});
|
|
|
|
expect(result).toEqual({
|
|
dryRun: false,
|
|
matched: 2,
|
|
deleted: 2,
|
|
sampleDeletedJobIds: ["securityScanJobs:backfill-1", "securityScanJobs:backfill-2"],
|
|
});
|
|
expect(deleted).toEqual(["securityScanJobs:backfill-1", "securityScanJobs:backfill-2"]);
|
|
});
|
|
|
|
it("prunes expired uploaded and GitHub scan request blobs without deleting published files", async () => {
|
|
const requests = [
|
|
{
|
|
_id: "skillScanRequests:upload",
|
|
sourceKind: "upload",
|
|
securityScanJobId: "securityScanJobs:upload",
|
|
files: [{ storageId: "storage:upload-1" }, { storageId: "storage:upload-2" }],
|
|
},
|
|
{
|
|
_id: "skillScanRequests:published",
|
|
sourceKind: "published",
|
|
securityScanJobId: "securityScanJobs:published",
|
|
files: [{ storageId: "storage:published-version-file" }],
|
|
},
|
|
{
|
|
_id: "skillScanRequests:github",
|
|
sourceKind: "github",
|
|
securityScanJobId: "securityScanJobs:github",
|
|
files: [{ storageId: "storage:github-1" }],
|
|
},
|
|
{
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
files: [{ storageId: "storage:catalog-1" }],
|
|
},
|
|
];
|
|
const githubFileChunk = {
|
|
_id: "skillScanRequestFileChunks:github",
|
|
skillScanRequestId: "skillScanRequests:github",
|
|
files: [{ storageId: "storage:github-2" }],
|
|
};
|
|
const deletedDocs: string[] = [];
|
|
const deletedStorage: string[] = [];
|
|
const take = vi.fn(async () => requests);
|
|
const indexBuilder = {
|
|
lt: vi.fn(() => indexBuilder),
|
|
};
|
|
const withIndex = vi.fn(
|
|
(indexName: string, buildRange: (q: typeof indexBuilder) => unknown) => {
|
|
expect(indexName).toBe("by_expires_at");
|
|
buildRange(indexBuilder);
|
|
expect(indexBuilder.lt).toHaveBeenCalledWith("expiresAt", expect.any(Number));
|
|
return { take };
|
|
},
|
|
);
|
|
const docs: Record<string, Record<string, unknown>> = {
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
entryId: "skillsShCatalogEntries:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
sourceContentHash: "source-hash",
|
|
status: "queued",
|
|
},
|
|
"skillsShCatalogEntries:catalog": {
|
|
_id: "skillsShCatalogEntries:catalog",
|
|
sourceContentHash: "source-hash",
|
|
scanStatus: "queued",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
counts: {
|
|
scansCompleted: 0,
|
|
scansCanceled: 0,
|
|
},
|
|
operations: {
|
|
functionCalls: 0,
|
|
dbReads: 0,
|
|
dbWrites: 0,
|
|
},
|
|
},
|
|
};
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((tableName: string) => {
|
|
if (tableName === "skillScanRequests") return { withIndex };
|
|
expect(tableName).toBe("skillScanRequestFileChunks");
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
take: vi.fn(async () => [githubFileChunk]),
|
|
})),
|
|
};
|
|
}),
|
|
insert: vi.fn(async () => "noop"),
|
|
patch: vi.fn(async (id: string, value: Record<string, unknown>) => {
|
|
docs[id] = { ...docs[id], ...value };
|
|
}),
|
|
replace: vi.fn(async () => undefined),
|
|
get: vi.fn(
|
|
async (id: string) =>
|
|
docs[id] ?? {
|
|
_id: id,
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
),
|
|
delete: vi.fn(async (id: string) => {
|
|
deletedDocs.push(id);
|
|
}),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
storage: {
|
|
delete: vi.fn(async (id: string) => {
|
|
deletedStorage.push(id);
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await pruneExpiredSkillScanRequestsInternalHandler(ctx as never, {
|
|
batchSize: 10,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
deletedRequests: 4,
|
|
deferredRequests: 0,
|
|
deletedJobs: 4,
|
|
deletedFiles: 5,
|
|
done: true,
|
|
});
|
|
expect(deletedStorage).toEqual([
|
|
"storage:upload-1",
|
|
"storage:upload-2",
|
|
"storage:github-1",
|
|
"storage:github-2",
|
|
"storage:catalog-1",
|
|
]);
|
|
expect(deletedDocs).toEqual([
|
|
"securityScanJobs:upload",
|
|
"skillScanRequests:upload",
|
|
"securityScanJobs:published",
|
|
"skillScanRequests:published",
|
|
"securityScanJobs:github",
|
|
"skillScanRequestFileChunks:github",
|
|
"skillScanRequests:github",
|
|
"securityScanJobs:catalog",
|
|
"skillScanRequests:catalog",
|
|
]);
|
|
expect(docs["skillsShCatalogScanAttempts:catalog"]).toMatchObject({
|
|
status: "failed",
|
|
verdict: "failed",
|
|
completedAt: expect.any(Number),
|
|
});
|
|
expect(docs["skillsShCatalogEntries:catalog"]).toMatchObject({
|
|
scanStatus: "failed",
|
|
});
|
|
expect(docs["skillsShCatalogRuns:catalog"]).toMatchObject({
|
|
counts: {
|
|
scansCompleted: 1,
|
|
scansCanceled: 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("prunes one bounded GitHub file chunk before deleting the parent request", async () => {
|
|
const request = {
|
|
_id: "skillScanRequests:github",
|
|
sourceKind: "github",
|
|
securityScanJobId: "securityScanJobs:github",
|
|
files: [],
|
|
};
|
|
const chunks = [
|
|
{
|
|
_id: "skillScanRequestFileChunks:first",
|
|
skillScanRequestId: request._id,
|
|
files: [{ storageId: "storage:github-1" }],
|
|
},
|
|
{
|
|
_id: "skillScanRequestFileChunks:second",
|
|
skillScanRequestId: request._id,
|
|
files: [{ storageId: "storage:github-2" }],
|
|
},
|
|
];
|
|
const deletedDocs: string[] = [];
|
|
const deletedStorage: string[] = [];
|
|
const scheduler = { runAfter: vi.fn(async () => undefined) };
|
|
const requestTake = vi.fn(async () => [request]);
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((tableName: string) => {
|
|
if (tableName === "skillScanRequests") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
take: requestTake,
|
|
})),
|
|
};
|
|
}
|
|
expect(tableName).toBe("skillScanRequestFileChunks");
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
take: vi.fn(async () => chunks),
|
|
})),
|
|
};
|
|
}),
|
|
insert: vi.fn(async () => "noop"),
|
|
patch: vi.fn(async () => undefined),
|
|
replace: vi.fn(async () => undefined),
|
|
get: vi.fn(async (id: string) => ({
|
|
_id: id,
|
|
targetKind: "skillScanRequest",
|
|
})),
|
|
delete: vi.fn(async (id: string) => {
|
|
deletedDocs.push(id);
|
|
}),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
scheduler,
|
|
storage: {
|
|
delete: vi.fn(async (id: string) => {
|
|
deletedStorage.push(id);
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await pruneExpiredSkillScanRequestsInternalHandler(ctx as never, {
|
|
batchSize: 250,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
deletedRequests: 0,
|
|
deferredRequests: 1,
|
|
deletedJobs: 1,
|
|
deletedFiles: 1,
|
|
done: false,
|
|
});
|
|
expect(deletedStorage).toEqual(["storage:github-1"]);
|
|
expect(deletedDocs).toEqual(["securityScanJobs:github", "skillScanRequestFileChunks:first"]);
|
|
expect(requestTake).toHaveBeenCalledWith(10);
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(0, expect.anything(), { batchSize: 10 });
|
|
});
|
|
|
|
it("fails claimed package jobs when the ClawPack URL is unavailable", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("limit" in args) return [claimedJob];
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
release: {
|
|
files: [],
|
|
clawpackStorageId: "storage:clawpack",
|
|
},
|
|
}));
|
|
const getUrl = vi.fn(async () => null);
|
|
|
|
const result = await claimCodexScanJobsHandler(
|
|
{ runMutation, runQuery, storage: { getUrl } },
|
|
{ token: "worker-secret", workerId: "worker-1", limit: 10 },
|
|
);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(runMutation).toHaveBeenLastCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: "ClawPack artifact unavailable",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("claims manual rescans and malicious signals before ordinary backlog", async () => {
|
|
const { ctx, patches } = makeClaimCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:old-publish",
|
|
source: "publish",
|
|
createdAt: 10,
|
|
nextRunAt: 10,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:older-vt-update",
|
|
source: "vt-update",
|
|
createdAt: 20,
|
|
nextRunAt: 20,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:malicious-publish",
|
|
source: "publish",
|
|
hasMaliciousSignal: true,
|
|
createdAt: 30,
|
|
nextRunAt: 30,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:backfill",
|
|
source: "backfill",
|
|
createdAt: 50,
|
|
nextRunAt: 50,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual",
|
|
source: "manual",
|
|
priority: 100,
|
|
createdAt: 1000,
|
|
nextRunAt: 1000,
|
|
}),
|
|
]);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "worker-1",
|
|
limit: 4,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual([
|
|
"securityScanJobs:manual",
|
|
"securityScanJobs:malicious-publish",
|
|
"securityScanJobs:old-publish",
|
|
"securityScanJobs:backfill",
|
|
]);
|
|
expect(patches.map((entry) => entry.id)).toEqual(claimed.map((job) => job._id));
|
|
});
|
|
|
|
it("claims bulk rescans after every supported source", async () => {
|
|
const { ctx } = makeClaimCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:bulk-rescan",
|
|
source: "bulk-rescan",
|
|
createdAt: 1,
|
|
nextRunAt: 1,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:publish",
|
|
source: "publish",
|
|
createdAt: 20,
|
|
nextRunAt: 20,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:vt-update",
|
|
source: "vt-update",
|
|
createdAt: 30,
|
|
nextRunAt: 30,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:backfill",
|
|
source: "backfill",
|
|
createdAt: 50,
|
|
nextRunAt: 50,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual",
|
|
source: "manual",
|
|
priority: 100,
|
|
createdAt: 100,
|
|
nextRunAt: 100,
|
|
}),
|
|
]);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "worker-1",
|
|
limit: 6,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual([
|
|
"securityScanJobs:manual",
|
|
"securityScanJobs:publish",
|
|
"securityScanJobs:backfill",
|
|
"securityScanJobs:vt-update",
|
|
"securityScanJobs:bulk-rescan",
|
|
]);
|
|
});
|
|
|
|
it("reserves the priority lane for manual, malicious, and publish work", async () => {
|
|
const { ctx } = makeClaimCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:vt-update",
|
|
source: "vt-update",
|
|
createdAt: 1,
|
|
nextRunAt: 1,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:publish",
|
|
source: "publish",
|
|
createdAt: 2,
|
|
nextRunAt: 2,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual",
|
|
source: "manual",
|
|
createdAt: 3,
|
|
nextRunAt: 3,
|
|
}),
|
|
]);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "priority-worker",
|
|
lane: "priority",
|
|
limit: 4,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual([
|
|
"securityScanJobs:manual",
|
|
"securityScanJobs:publish",
|
|
]);
|
|
});
|
|
|
|
it("lets shared workers help priority work before consuming bulk backlog", async () => {
|
|
const { ctx } = makeClaimCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:vt-update",
|
|
source: "vt-update",
|
|
createdAt: 1,
|
|
nextRunAt: 1,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:publish",
|
|
source: "publish",
|
|
createdAt: 2,
|
|
nextRunAt: 2,
|
|
}),
|
|
]);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "shared-worker",
|
|
lane: "shared",
|
|
limit: 1,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual(["securityScanJobs:publish"]);
|
|
});
|
|
|
|
it("lets the catalog lane claim only the lowest-priority catalog source", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_DISABLE_CRONS", "1");
|
|
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
|
|
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
|
|
const { ctx } = makeClaimCtx(
|
|
[
|
|
makeScanJob({
|
|
_id: "securityScanJobs:manual",
|
|
source: "manual",
|
|
priority: 100,
|
|
createdAt: 1,
|
|
nextRunAt: 1,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:publish",
|
|
source: "publish",
|
|
priority: 0,
|
|
createdAt: 2,
|
|
nextRunAt: 2,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:catalog",
|
|
source: "skills-sh-catalog-test",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
priority: -100,
|
|
createdAt: 3,
|
|
nextRunAt: 3,
|
|
}),
|
|
],
|
|
{
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "queued",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
},
|
|
},
|
|
{
|
|
key: "global",
|
|
mode: "staging-live",
|
|
paused: false,
|
|
scanAdmissionEnabled: true,
|
|
maxNativeQueued: 10,
|
|
maxNativeInFlight: 10,
|
|
maxCatalogQueued: 10,
|
|
maxCatalogInFlight: 1,
|
|
},
|
|
);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "catalog-worker",
|
|
lane: "catalog",
|
|
limit: 3,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual(["securityScanJobs:catalog"]);
|
|
});
|
|
|
|
it("skips paused catalog runs without starving later runnable jobs", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_DISABLE_CRONS", "1");
|
|
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
|
|
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
|
|
const jobs = ["paused", "active"].map((kind, index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:${kind}`,
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: `skillScanRequests:${kind}`,
|
|
createdAt: index,
|
|
nextRunAt: index,
|
|
}),
|
|
);
|
|
const docs = Object.fromEntries(
|
|
jobs.flatMap((job, index) => {
|
|
const kind = index === 0 ? "paused" : "active";
|
|
return [
|
|
[
|
|
`skillScanRequests:${kind}`,
|
|
{
|
|
_id: `skillScanRequests:${kind}`,
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: `skillsShCatalogScanAttempts:${kind}`,
|
|
},
|
|
],
|
|
[
|
|
`skillsShCatalogScanAttempts:${kind}`,
|
|
{
|
|
_id: `skillsShCatalogScanAttempts:${kind}`,
|
|
runId: `skillsShCatalogRuns:${kind}`,
|
|
skillScanRequestId: `skillScanRequests:${kind}`,
|
|
securityScanJobId: job._id,
|
|
status: "queued",
|
|
},
|
|
],
|
|
[
|
|
`skillsShCatalogRuns:${kind}`,
|
|
{
|
|
_id: `skillsShCatalogRuns:${kind}`,
|
|
status: kind === "paused" ? "paused" : "completed",
|
|
},
|
|
],
|
|
];
|
|
}),
|
|
);
|
|
const { ctx } = makeClaimCtx(jobs, docs, {
|
|
key: "global",
|
|
mode: "staging-live",
|
|
paused: false,
|
|
scanAdmissionEnabled: true,
|
|
maxNativeQueued: 10,
|
|
maxNativeInFlight: 10,
|
|
maxCatalogQueued: 1,
|
|
maxCatalogInFlight: 1,
|
|
});
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "catalog-worker",
|
|
lane: "catalog",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual(["securityScanJobs:active"]);
|
|
});
|
|
|
|
it("stops catalog claims when the native queue is unhealthy", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_DISABLE_CRONS", "1");
|
|
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
|
|
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
|
|
const { ctx } = makeClaimCtx(
|
|
[
|
|
makeScanJob({
|
|
_id: "securityScanJobs:native",
|
|
source: "manual",
|
|
status: "queued",
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:catalog",
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
}),
|
|
],
|
|
{
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "queued",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
},
|
|
},
|
|
{
|
|
key: "global",
|
|
mode: "staging-live",
|
|
paused: false,
|
|
scanAdmissionEnabled: true,
|
|
maxNativeQueued: 0,
|
|
maxNativeInFlight: 10,
|
|
maxCatalogQueued: 10,
|
|
maxCatalogInFlight: 1,
|
|
},
|
|
);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "catalog-worker",
|
|
lane: "catalog",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(claimed).toEqual([]);
|
|
});
|
|
|
|
it("ignores deterministic attempts when checking real catalog claim capacity", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_DISABLE_CRONS", "1");
|
|
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
|
|
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
|
|
const catalogJob = makeScanJob({
|
|
_id: "securityScanJobs:catalog",
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
});
|
|
const { ctx } = makeClaimCtx(
|
|
[catalogJob],
|
|
{
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
dispatchKind: "real",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: catalogJob._id,
|
|
status: "queued",
|
|
},
|
|
"skillsShCatalogScanAttempts:deterministic-queued": {
|
|
_id: "skillsShCatalogScanAttempts:deterministic-queued",
|
|
dispatchKind: "deterministic",
|
|
status: "queued",
|
|
},
|
|
"skillsShCatalogScanAttempts:deterministic-running": {
|
|
_id: "skillsShCatalogScanAttempts:deterministic-running",
|
|
dispatchKind: "deterministic",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
},
|
|
},
|
|
{
|
|
key: "global",
|
|
mode: "staging-live",
|
|
paused: false,
|
|
scanAdmissionEnabled: true,
|
|
maxNativeQueued: 10,
|
|
maxNativeInFlight: 10,
|
|
maxCatalogQueued: 1,
|
|
maxCatalogInFlight: 1,
|
|
},
|
|
);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "catalog-worker",
|
|
lane: "catalog",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual(["securityScanJobs:catalog"]);
|
|
});
|
|
|
|
it("drains an admitted catalog backlog above a lowered queue cap", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_DISABLE_CRONS", "1");
|
|
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
|
|
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
|
|
const jobs = [0, 1].map((index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:catalog-${index}`,
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: `skillScanRequests:catalog-${index}`,
|
|
createdAt: index,
|
|
nextRunAt: index,
|
|
}),
|
|
);
|
|
const docs = Object.fromEntries(
|
|
jobs.flatMap((job, index) => [
|
|
[
|
|
`skillScanRequests:catalog-${index}`,
|
|
{
|
|
_id: `skillScanRequests:catalog-${index}`,
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: `skillsShCatalogScanAttempts:catalog-${index}`,
|
|
},
|
|
],
|
|
[
|
|
`skillsShCatalogScanAttempts:catalog-${index}`,
|
|
{
|
|
_id: `skillsShCatalogScanAttempts:catalog-${index}`,
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
dispatchKind: "real",
|
|
skillScanRequestId: `skillScanRequests:catalog-${index}`,
|
|
securityScanJobId: job._id,
|
|
status: "queued",
|
|
},
|
|
],
|
|
]),
|
|
);
|
|
docs["skillsShCatalogRuns:catalog"] = {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
};
|
|
const { ctx } = makeClaimCtx(jobs, docs, {
|
|
key: "global",
|
|
mode: "staging-live",
|
|
paused: false,
|
|
scanAdmissionEnabled: true,
|
|
maxNativeQueued: 10,
|
|
maxNativeInFlight: 10,
|
|
maxCatalogQueued: 1,
|
|
maxCatalogInFlight: 1,
|
|
});
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "catalog-worker",
|
|
lane: "catalog",
|
|
limit: 2,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual(["securityScanJobs:catalog-0"]);
|
|
expect(docs["skillsShCatalogScanAttempts:catalog-0"]).toMatchObject({
|
|
status: "running",
|
|
});
|
|
expect(docs["skillsShCatalogScanAttempts:catalog-1"]).toMatchObject({
|
|
status: "queued",
|
|
});
|
|
});
|
|
|
|
it("caps catalog claims at the configured in-flight capacity", async () => {
|
|
vi.stubEnv("CLAWHUB_ENV", "test");
|
|
vi.stubEnv("CLAWHUB_DISABLE_CRONS", "1");
|
|
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
|
|
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
|
|
const jobs = [0, 1].map((index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:catalog-${index}`,
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: `skillScanRequests:catalog-${index}`,
|
|
createdAt: index,
|
|
nextRunAt: index,
|
|
}),
|
|
);
|
|
const docs = Object.fromEntries(
|
|
jobs.flatMap((job, index) => [
|
|
[
|
|
`skillScanRequests:catalog-${index}`,
|
|
{
|
|
_id: `skillScanRequests:catalog-${index}`,
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: `skillsShCatalogScanAttempts:catalog-${index}`,
|
|
},
|
|
],
|
|
[
|
|
`skillsShCatalogScanAttempts:catalog-${index}`,
|
|
{
|
|
_id: `skillsShCatalogScanAttempts:catalog-${index}`,
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: `skillScanRequests:catalog-${index}`,
|
|
securityScanJobId: job._id,
|
|
status: "queued",
|
|
},
|
|
],
|
|
]),
|
|
);
|
|
docs["skillsShCatalogRuns:catalog"] = {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
};
|
|
const { ctx } = makeClaimCtx(jobs, docs, {
|
|
key: "global",
|
|
mode: "staging-live",
|
|
paused: false,
|
|
scanAdmissionEnabled: true,
|
|
maxNativeQueued: 10,
|
|
maxNativeInFlight: 10,
|
|
maxCatalogQueued: 10,
|
|
maxCatalogInFlight: 1,
|
|
});
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "catalog-worker",
|
|
lane: "catalog",
|
|
limit: 2,
|
|
});
|
|
|
|
expect(claimed).toHaveLength(1);
|
|
});
|
|
|
|
it("caps each Codex scan claim request", async () => {
|
|
const { ctx } = makeClaimCtx(
|
|
Array.from({ length: 600 }, (_, index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:manual-${index}`,
|
|
source: "manual",
|
|
priority: 100,
|
|
createdAt: index,
|
|
nextRunAt: index,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "worker-1",
|
|
limit: 10_000,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed).toHaveLength(512);
|
|
});
|
|
|
|
it("claims requested jobs even when many other scans are already active", async () => {
|
|
const activeJobs = Array.from({ length: 80 }, (_, index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:running-${index}`,
|
|
status: "running",
|
|
leaseExpiresAt: Date.now() + 60_000,
|
|
source: "bulk-rescan",
|
|
}),
|
|
);
|
|
const queuedJobs = Array.from({ length: 3 }, (_, index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:manual-${index}`,
|
|
source: "manual",
|
|
priority: 100,
|
|
createdAt: index,
|
|
nextRunAt: index,
|
|
}),
|
|
);
|
|
const { ctx } = makeClaimCtx([...activeJobs, ...queuedJobs]);
|
|
|
|
const claimed = await claimQueuedJobsInternalHandler(ctx, {
|
|
workerId: "worker-1",
|
|
limit: 3,
|
|
leaseMs: 60_000,
|
|
});
|
|
|
|
expect(claimed.map((job) => job._id)).toEqual([
|
|
"securityScanJobs:manual-0",
|
|
"securityScanJobs:manual-1",
|
|
"securityScanJobs:manual-2",
|
|
]);
|
|
});
|
|
|
|
it("recovers expired leases separately from normal claim transactions", async () => {
|
|
const { ctx, patches } = makeClaimCtx([
|
|
makeScanJob({
|
|
_id: "securityScanJobs:expired",
|
|
status: "running",
|
|
leaseToken: "expired-lease",
|
|
leaseExpiresAt: Date.now() - 1,
|
|
workerId: "stale-worker",
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:active",
|
|
status: "running",
|
|
leaseToken: "active-lease",
|
|
leaseExpiresAt: Date.now() + 60_000,
|
|
workerId: "active-worker",
|
|
}),
|
|
]);
|
|
|
|
await expect(requeueExpiredCodexScanJobsInternalHandler(ctx, {})).resolves.toEqual({
|
|
requeued: 1,
|
|
});
|
|
expect(patches).toEqual([
|
|
expect.objectContaining({
|
|
id: "securityScanJobs:expired",
|
|
patch: expect.objectContaining({
|
|
status: "queued",
|
|
leaseToken: undefined,
|
|
workerId: undefined,
|
|
}),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("requeues the existing catalog attempt when its worker lease expires", async () => {
|
|
const { ctx, patches } = makeClaimCtx(
|
|
[
|
|
makeScanJob({
|
|
_id: "securityScanJobs:catalog-expired",
|
|
status: "running",
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
leaseToken: "expired",
|
|
leaseExpiresAt: Date.now() - 1,
|
|
workerId: "stale-worker",
|
|
}),
|
|
],
|
|
{
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog-expired",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(requeueExpiredCodexScanJobsInternalHandler(ctx, {})).resolves.toEqual({
|
|
requeued: 1,
|
|
});
|
|
expect(patches).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "skillScanRequests:catalog",
|
|
patch: expect.objectContaining({ status: "queued" }),
|
|
}),
|
|
expect.objectContaining({
|
|
id: "skillsShCatalogScanAttempts:catalog",
|
|
patch: expect.objectContaining({ status: "queued" }),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("terminalizes an active catalog attempt when an expired lease belongs to a canceled run", async () => {
|
|
const { ctx, patches } = makeClaimCtx(
|
|
[
|
|
makeScanJob({
|
|
_id: "securityScanJobs:catalog-expired-canceled",
|
|
status: "running",
|
|
source: "skills-sh-catalog-test",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
leaseToken: "expired",
|
|
leaseExpiresAt: Date.now() - 1,
|
|
workerId: "stale-worker",
|
|
}),
|
|
],
|
|
{
|
|
"skillsShCatalogEntries:catalog": {
|
|
_id: "skillsShCatalogEntries:catalog",
|
|
sourceContentHash: "source-hash",
|
|
scanStatus: "queued",
|
|
publicVisible: false,
|
|
},
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
entryId: "skillsShCatalogEntries:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
sourceContentHash: "source-hash",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog-expired-canceled",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "canceled",
|
|
counts: {
|
|
scansCompleted: 0,
|
|
scansCanceled: 0,
|
|
},
|
|
operations: {
|
|
functionCalls: 0,
|
|
dbReads: 0,
|
|
dbWrites: 0,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(requeueExpiredCodexScanJobsInternalHandler(ctx, {})).resolves.toEqual({
|
|
requeued: 0,
|
|
});
|
|
expect(patches).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "skillsShCatalogScanAttempts:catalog",
|
|
patch: expect.objectContaining({
|
|
status: "canceled",
|
|
completedAt: expect.any(Number),
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
id: "skillsShCatalogEntries:catalog",
|
|
patch: expect.objectContaining({
|
|
scanStatus: "canceled",
|
|
publicVisible: false,
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("requeues hydration failures without consuming a scanner attempt", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_EVENT_DISPATCH_ENABLED", "0");
|
|
const { ctx, patches } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:1": {
|
|
...claimedJob,
|
|
workerId: "worker-1",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:1",
|
|
attempts: 2,
|
|
},
|
|
"skillScanRequests:1": {
|
|
_id: "skillScanRequests:1",
|
|
status: "running",
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
requeueJobLeaseInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
workerId: "worker-1",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
expect(patches).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "securityScanJobs:1",
|
|
patch: expect.objectContaining({
|
|
status: "queued",
|
|
attempts: 1,
|
|
leaseToken: undefined,
|
|
workerId: undefined,
|
|
}),
|
|
}),
|
|
expect.objectContaining({
|
|
id: "skillScanRequests:1",
|
|
patch: expect.objectContaining({ status: "queued" }),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("requeues hydration failures on the existing catalog attempt", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_EVENT_DISPATCH_ENABLED", "0");
|
|
const { ctx, patches } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:catalog": {
|
|
...claimedJob,
|
|
_id: "securityScanJobs:catalog",
|
|
source: "skills-sh-catalog-test",
|
|
leaseToken: "lease",
|
|
workerId: "worker-1",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
attempts: 2,
|
|
},
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
requeueJobLeaseInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease",
|
|
workerId: "worker-1",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
expect(patches).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "skillScanRequests:catalog",
|
|
patch: expect.objectContaining({ status: "queued" }),
|
|
}),
|
|
expect.objectContaining({
|
|
id: "skillsShCatalogScanAttempts:catalog",
|
|
patch: expect.objectContaining({ status: "queued" }),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("terminalizes an active catalog attempt when hydration retry is denied by cancellation", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_EVENT_DISPATCH_ENABLED", "0");
|
|
const { ctx, records } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:catalog": {
|
|
...claimedJob,
|
|
_id: "securityScanJobs:catalog",
|
|
source: "skills-sh-catalog-test",
|
|
leaseToken: "lease",
|
|
workerId: "worker-1",
|
|
targetKind: "skillScanRequest",
|
|
skillVersionId: undefined,
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
attempts: 2,
|
|
},
|
|
"skillsShCatalogEntries:catalog": {
|
|
_id: "skillsShCatalogEntries:catalog",
|
|
sourceContentHash: "source-hash",
|
|
scanStatus: "queued",
|
|
publicVisible: false,
|
|
},
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
entryId: "skillsShCatalogEntries:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
sourceContentHash: "source-hash",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "canceled",
|
|
counts: {
|
|
scansCompleted: 0,
|
|
scansCanceled: 0,
|
|
},
|
|
operations: {
|
|
functionCalls: 0,
|
|
dbReads: 0,
|
|
dbWrites: 0,
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
requeueJobLeaseInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease",
|
|
workerId: "worker-1",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
expect(records.get("securityScanJobs:catalog")).toMatchObject({ status: "failed" });
|
|
expect(records.get("skillsShCatalogScanAttempts:catalog")).toMatchObject({
|
|
status: "canceled",
|
|
completedAt: expect.any(Number),
|
|
});
|
|
expect(records.get("skillsShCatalogEntries:catalog")).toMatchObject({
|
|
scanStatus: "canceled",
|
|
publicVisible: false,
|
|
});
|
|
});
|
|
|
|
it("reports queued scan position for manual scan requests", async () => {
|
|
const targetJob = makeScanJob({
|
|
_id: "securityScanJobs:target",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:target",
|
|
source: "manual",
|
|
createdAt: 300,
|
|
nextRunAt: 300,
|
|
});
|
|
const ctx = makeSkillScanStatusCtx({
|
|
actor: { _id: "users:owner", role: "user" },
|
|
request: {
|
|
_id: "skillScanRequests:target",
|
|
actorUserId: "users:owner",
|
|
sourceKind: "upload",
|
|
update: false,
|
|
writtenBack: false,
|
|
status: "queued",
|
|
securityScanJobId: targetJob._id,
|
|
files: [],
|
|
expiresAt: 1000,
|
|
createdAt: 300,
|
|
updatedAt: 300,
|
|
},
|
|
jobs: [
|
|
makeScanJob({
|
|
_id: "securityScanJobs:older",
|
|
source: "manual",
|
|
createdAt: 100,
|
|
nextRunAt: 100,
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:running",
|
|
status: "running",
|
|
source: "manual",
|
|
createdAt: 200,
|
|
nextRunAt: 200,
|
|
}),
|
|
targetJob,
|
|
makeScanJob({
|
|
_id: "securityScanJobs:bulk",
|
|
source: "bulk-rescan",
|
|
createdAt: 1,
|
|
nextRunAt: 1,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const status = await getSkillScanRequestForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
scanId: "skillScanRequests:target",
|
|
});
|
|
|
|
expect(status.queue).toEqual({
|
|
queuedAhead: 1,
|
|
queuedAheadIsEstimate: false,
|
|
position: 2,
|
|
running: 1,
|
|
runningIsEstimate: false,
|
|
note: "Scans are asynchronous and may take time to complete.",
|
|
});
|
|
});
|
|
|
|
it("uses claim-order tie-breaks for same-timestamp queued scan positions", async () => {
|
|
const targetJob = makeScanJob({
|
|
_id: "securityScanJobs:target",
|
|
_creationTime: 2,
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:target",
|
|
source: "manual",
|
|
createdAt: 300,
|
|
nextRunAt: 300,
|
|
});
|
|
const ctx = makeSkillScanStatusCtx({
|
|
actor: { _id: "users:owner", role: "user" },
|
|
request: {
|
|
_id: "skillScanRequests:target",
|
|
actorUserId: "users:owner",
|
|
sourceKind: "upload",
|
|
update: false,
|
|
writtenBack: false,
|
|
status: "queued",
|
|
securityScanJobId: targetJob._id,
|
|
files: [],
|
|
expiresAt: 1000,
|
|
createdAt: 300,
|
|
updatedAt: 300,
|
|
},
|
|
jobs: [
|
|
makeScanJob({
|
|
_id: "securityScanJobs:first",
|
|
_creationTime: 1,
|
|
source: "manual",
|
|
createdAt: 300,
|
|
nextRunAt: 300,
|
|
}),
|
|
targetJob,
|
|
makeScanJob({
|
|
_id: "securityScanJobs:last",
|
|
_creationTime: 3,
|
|
source: "manual",
|
|
createdAt: 300,
|
|
nextRunAt: 300,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const status = await getSkillScanRequestForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
scanId: "skillScanRequests:target",
|
|
});
|
|
|
|
expect(status.queue).toMatchObject({
|
|
queuedAhead: 1,
|
|
queuedAheadIsEstimate: false,
|
|
position: 2,
|
|
});
|
|
});
|
|
|
|
it("bounds large queue position scans and marks the count as estimated", async () => {
|
|
const targetJob = makeScanJob({
|
|
_id: "securityScanJobs:target",
|
|
targetKind: "skillScanRequest",
|
|
skillScanRequestId: "skillScanRequests:target",
|
|
source: "manual",
|
|
createdAt: 1_000,
|
|
nextRunAt: 1_000,
|
|
});
|
|
const ctx = makeSkillScanStatusCtx({
|
|
actor: { _id: "users:owner", role: "user" },
|
|
request: {
|
|
_id: "skillScanRequests:target",
|
|
actorUserId: "users:owner",
|
|
sourceKind: "upload",
|
|
update: false,
|
|
writtenBack: false,
|
|
status: "queued",
|
|
securityScanJobId: targetJob._id,
|
|
files: [],
|
|
expiresAt: 1000,
|
|
createdAt: 1_000,
|
|
updatedAt: 1_000,
|
|
},
|
|
jobs: [
|
|
...Array.from({ length: 300 }, (_, index) =>
|
|
makeScanJob({
|
|
_id: `securityScanJobs:older-${index}`,
|
|
source: "manual",
|
|
createdAt: index,
|
|
nextRunAt: index,
|
|
}),
|
|
),
|
|
targetJob,
|
|
],
|
|
});
|
|
|
|
const status = await getSkillScanRequestForUserInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
scanId: "skillScanRequests:target",
|
|
});
|
|
|
|
expect(status.queue).toEqual({
|
|
queuedAhead: 250,
|
|
queuedAheadIsEstimate: true,
|
|
position: null,
|
|
running: 0,
|
|
runningIsEstimate: false,
|
|
note: "Scans are asynchronous and may take time to complete.",
|
|
});
|
|
});
|
|
|
|
it("caps SkillSpector findings before storing completed scan results", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
const longSnippet = "sensitive SkillSpector artifact text ".repeat(200);
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillVersion",
|
|
leaseToken: "lease-token",
|
|
},
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
},
|
|
}));
|
|
const runMutation = vi.fn(async (_ref: unknown, _args: Record<string, unknown>) => ({
|
|
ok: true,
|
|
}));
|
|
|
|
await completeCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
llmAnalysis: {
|
|
status: "suspicious",
|
|
checkedAt: 123,
|
|
},
|
|
skillSpectorAnalysis: {
|
|
status: "suspicious",
|
|
issueCount: 30,
|
|
checkedAt: 123,
|
|
issues: Array.from({ length: 30 }, (_, index) => ({
|
|
issueId: `SDI-${index + 1}`,
|
|
severity: "HIGH",
|
|
explanation: `Issue ${index + 1}: ${longSnippet}`,
|
|
finding: longSnippet,
|
|
codeSnippet: longSnippet,
|
|
})),
|
|
},
|
|
},
|
|
);
|
|
|
|
const skillSpectorCall = runMutation.mock.calls.find(
|
|
([, args]) => "skillSpectorAnalysis" in (args as Record<string, unknown>),
|
|
);
|
|
expect(skillSpectorCall).toBeDefined();
|
|
if (!skillSpectorCall) throw new Error("Expected SkillSpector persistence call");
|
|
const stored = skillSpectorCall[1].skillSpectorAnalysis as {
|
|
issueCount: number;
|
|
issues: Array<{ codeSnippet?: string; finding?: string }>;
|
|
};
|
|
expect(stored.issueCount).toBe(30);
|
|
expect(stored.issues).toHaveLength(25);
|
|
expect(stored.issues[0]?.codeSnippet).toContain("...[truncated ");
|
|
expect(stored.issues[0]?.finding?.length).toBeLessThan(longSnippet.length);
|
|
});
|
|
|
|
it("keeps the scan request and job retryable when catalog result persistence fails", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "placeholder");
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:catalog",
|
|
targetKind: "skillScanRequest",
|
|
leaseToken: "placeholder",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
sha256hash: "artifact-hash",
|
|
},
|
|
}));
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("attemptId" in args) throw new Error("catalog result unavailable");
|
|
return { ok: true };
|
|
});
|
|
|
|
await expect(
|
|
completeCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "placeholder",
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "placeholder",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
},
|
|
),
|
|
).rejects.toThrow("catalog result unavailable");
|
|
|
|
expect(runMutation).toHaveBeenCalledTimes(1);
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
attemptId: "skillsShCatalogScanAttempts:catalog",
|
|
artifactContentHash: "artifact-hash",
|
|
verdict: "clean",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("terminalizes a catalog attempt, request, and job through one mutation", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "placeholder");
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:catalog",
|
|
targetKind: "skillScanRequest",
|
|
leaseToken: "lease-token",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
sha256hash: "artifact-hash",
|
|
},
|
|
}));
|
|
const runMutation = vi.fn(async () => ({ ok: true }));
|
|
|
|
await completeCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "placeholder",
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease-token",
|
|
runId: "clawscan-run",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
},
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledTimes(2);
|
|
expect(runMutation).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
attemptId: "skillsShCatalogScanAttempts:catalog",
|
|
scanId: "skillScanRequests:catalog",
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease-token",
|
|
artifactContentHash: "artifact-hash",
|
|
verdict: "clean",
|
|
runId: "clawscan-run",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
}),
|
|
);
|
|
expect(runMutation).toHaveBeenNthCalledWith(2, expect.anything(), {});
|
|
});
|
|
|
|
it("acknowledges a committed catalog completion when queue refill fails", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "placeholder");
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:catalog",
|
|
targetKind: "skillScanRequest",
|
|
leaseToken: "lease-token",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
sha256hash: "artifact-hash",
|
|
},
|
|
}));
|
|
const runMutation = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ ok: true, applied: true })
|
|
.mockRejectedValueOnce(new Error("dispatch unavailable"));
|
|
|
|
await expect(
|
|
completeCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "placeholder",
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease-token",
|
|
runId: "clawscan-run",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
},
|
|
),
|
|
).resolves.toEqual({ ok: true, applied: true });
|
|
|
|
expect(runMutation).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("routes a repeated terminal catalog callback to idempotent completion", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "placeholder");
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:catalog",
|
|
targetKind: "skillScanRequest",
|
|
status: "succeeded",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
sha256hash: "artifact-hash",
|
|
},
|
|
}));
|
|
const runMutation = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ ok: true, applied: true, publicVisible: false })
|
|
.mockResolvedValueOnce({ ok: true });
|
|
|
|
await expect(
|
|
completeCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "placeholder",
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "expired-lease-token",
|
|
runId: "clawscan-run",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
},
|
|
),
|
|
).resolves.toEqual({ ok: true, applied: true, publicVisible: false });
|
|
|
|
expect(runMutation).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("clears legacy plugin SkillSpector results when no new analysis is produced", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:plugin",
|
|
targetKind: "packageRelease",
|
|
leaseToken: "lease-token",
|
|
},
|
|
release: {
|
|
_id: "packageReleases:plugin",
|
|
},
|
|
}));
|
|
const runMutation = vi.fn(async (..._args: unknown[]) => ({ ok: true }));
|
|
|
|
await completeCodexScanJobHandler(
|
|
{ runQuery, runMutation },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:plugin",
|
|
leaseToken: "lease-token",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
},
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.anything(),
|
|
expect.objectContaining({ releaseId: "packageReleases:plugin" }),
|
|
);
|
|
const scanPatch = runMutation.mock.calls[0]?.[1] as Record<string, unknown>;
|
|
expect(scanPatch).not.toHaveProperty("skillSpectorAnalysis");
|
|
});
|
|
|
|
it("persists an error ClawScan result when worker retries are exhausted", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("error" in args) return { ok: true, retry: false };
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillVersion",
|
|
},
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
},
|
|
}));
|
|
|
|
const result = await failCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error:
|
|
"Download failed https://signed.example.invalid/file?token=secret Authorization: Bearer auth-secret",
|
|
},
|
|
);
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
expect(runMutation).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
}),
|
|
);
|
|
expect(runMutation).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:1",
|
|
moderationMode: "preserve",
|
|
llmAnalysis: expect.objectContaining({
|
|
confidence: "low",
|
|
status: "error",
|
|
summary: expect.stringContaining("could not complete"),
|
|
}),
|
|
}),
|
|
);
|
|
const llmAnalysis = runMutation.mock.calls[1]?.[1]?.llmAnalysis as
|
|
| { findings?: string }
|
|
| undefined;
|
|
expect(llmAnalysis?.findings).toContain("Worker error");
|
|
expect(llmAnalysis?.findings).not.toContain("token=secret");
|
|
expect(llmAnalysis?.findings).not.toContain("Bearer auth-secret");
|
|
});
|
|
|
|
it("completes skill scans without directly enqueueing duplicate Skill Card jobs", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillVersion",
|
|
leaseToken: "lease-token",
|
|
},
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
},
|
|
}));
|
|
const runMutation = vi.fn(async () => ({ ok: true }));
|
|
|
|
await completeCodexScanJobHandler(
|
|
{ runQuery, runMutation },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
},
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledTimes(3);
|
|
expect(runMutation).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
versionId: "skillVersions:1",
|
|
llmAnalysis: { status: "clean", checkedAt: 123 },
|
|
}),
|
|
);
|
|
expect(runMutation).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
}),
|
|
);
|
|
expect(runMutation).toHaveBeenNthCalledWith(3, expect.anything(), {});
|
|
});
|
|
|
|
it.each([
|
|
{ priorStatus: "failed", force: undefined, expectedSource: "publish", expectedPriority: 0 },
|
|
{ priorStatus: "clean", force: true, expectedSource: "manual", expectedPriority: 100 },
|
|
{
|
|
priorStatus: "failed",
|
|
force: undefined,
|
|
requestedJobSource: "manual",
|
|
requestedJobPriority: 100,
|
|
expectedSource: "manual",
|
|
expectedPriority: 100,
|
|
},
|
|
] as const)(
|
|
"requeues a $priorStatus GitHub-backed scan for the same content hash",
|
|
async ({
|
|
priorStatus,
|
|
force,
|
|
requestedJobSource,
|
|
requestedJobPriority,
|
|
expectedSource,
|
|
expectedPriority,
|
|
}) => {
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[
|
|
"skills:1",
|
|
{
|
|
_id: "skills:1",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:new",
|
|
githubPath: "skills/demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
ownerUserId: "users:1",
|
|
slug: "demo",
|
|
displayName: "Demo",
|
|
},
|
|
],
|
|
[
|
|
"githubSkillScans:1",
|
|
{
|
|
_id: "githubSkillScans:1",
|
|
skillId: "skills:1",
|
|
githubSourceId: "githubSkillSources:old",
|
|
contentHash: "content-hash",
|
|
commit: "a".repeat(40),
|
|
path: "skills/demo",
|
|
status: priorStatus,
|
|
llmAnalysis: { status: "error", checkedAt: 1 },
|
|
lastError: "worker failed",
|
|
completedAt: 1,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
},
|
|
],
|
|
]);
|
|
const inserts: Array<{ table: string; doc: Record<string, unknown> }> = [];
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
const id = `${table}:new-${inserts.length + 1}`;
|
|
docs.set(id, { _id: id, ...doc });
|
|
inserts.push({ table, doc });
|
|
return id;
|
|
});
|
|
const patch = vi.fn(async (id: string, next: Record<string, unknown>) => {
|
|
const doc = docs.get(id);
|
|
if (!doc) return;
|
|
for (const [key, value] of Object.entries(next)) {
|
|
if (value === undefined) delete doc[key];
|
|
else doc[key] = value;
|
|
}
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => docs.get(id) ?? null),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "skillScanRequestFileChunks") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => null),
|
|
take: vi.fn(async () =>
|
|
Array.from(docs.values())
|
|
.filter((doc) =>
|
|
doc._id?.toString().startsWith("skillScanRequestFileChunks:"),
|
|
)
|
|
.slice(0, 1),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
expect(table).toBe("githubSkillScans");
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => docs.get("githubSkillScans:1")),
|
|
})),
|
|
};
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
};
|
|
|
|
const prepared = await prepareGitHubSkillScanRequestInternalHandler(ctx, {
|
|
skillId: "skills:1",
|
|
contentHash: "content-hash",
|
|
commit: "a".repeat(40),
|
|
force,
|
|
parsed: { frontmatter: {} },
|
|
staticScan: {
|
|
status: "clean",
|
|
reasonCodes: [],
|
|
findings: [],
|
|
summary: "No static findings.",
|
|
engineVersion: "test",
|
|
checkedAt: 2,
|
|
},
|
|
});
|
|
expect(prepared).toMatchObject({
|
|
ok: true,
|
|
prepared: true,
|
|
scanId: "githubSkillScans:1",
|
|
});
|
|
if (!prepared.requestId) throw new Error("missing prepared request");
|
|
await appendGitHubSkillScanRequestFilesInternalHandler(ctx, {
|
|
requestId: prepared.requestId,
|
|
chunkIndex: 0,
|
|
files: [
|
|
{
|
|
path: "SKILL.md",
|
|
size: 10,
|
|
storageId: "storage:1",
|
|
sha256: "sha256",
|
|
},
|
|
],
|
|
});
|
|
Object.assign(docs.get(prepared.requestId) ?? {}, {
|
|
requestedJobSource,
|
|
requestedJobPriority,
|
|
});
|
|
const result = await finalizeGitHubSkillScanRequestInternalHandler(ctx, {
|
|
requestId: prepared.requestId,
|
|
force,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
queued: true,
|
|
scanId: "githubSkillScans:1",
|
|
});
|
|
expect(inserts.map((entry) => entry.table)).toEqual([
|
|
"skillScanRequests",
|
|
"skillScanRequestFileChunks",
|
|
"securityScanJobs",
|
|
]);
|
|
expect(inserts[0]?.doc.files).toEqual([]);
|
|
expect(inserts[0]?.doc).toMatchObject({
|
|
fileChunkCount: 0,
|
|
fileManifestBytes: 0,
|
|
});
|
|
expect(inserts[1]?.doc).toMatchObject({
|
|
skillScanRequestId: expect.stringMatching(/^skillScanRequests:/),
|
|
chunkIndex: 0,
|
|
files: [{ path: "SKILL.md", storageId: "storage:1" }],
|
|
});
|
|
expect(inserts[2]?.doc).toMatchObject({
|
|
source: expectedSource,
|
|
priority: expectedPriority,
|
|
});
|
|
expect(docs.get("githubSkillScans:1")).toMatchObject({
|
|
githubSourceId: "githubSkillSources:new",
|
|
status: "pending",
|
|
skillScanRequestId: expect.stringMatching(/^skillScanRequests:/),
|
|
});
|
|
expect(docs.get("githubSkillScans:1")).not.toHaveProperty("llmAnalysis");
|
|
expect(docs.get("githubSkillScans:1")).not.toHaveProperty("lastError");
|
|
expect(docs.get("githubSkillScans:1")).not.toHaveProperty("completedAt");
|
|
expect(docs.get(prepared.requestId)).toMatchObject({
|
|
fileChunkCount: 1,
|
|
fileManifestBytes: expect.any(Number),
|
|
});
|
|
},
|
|
);
|
|
|
|
it("lets forced GitHub-backed rescans recover incomplete pending requests without jobs", async () => {
|
|
const now = 1_781_570_600_000;
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(now);
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[
|
|
"skills:1",
|
|
{
|
|
_id: "skills:1",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:new",
|
|
githubPath: "skills/demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "a".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
ownerUserId: "users:1",
|
|
slug: "demo",
|
|
displayName: "Demo",
|
|
},
|
|
],
|
|
[
|
|
"githubSkillScans:1",
|
|
{
|
|
_id: "githubSkillScans:1",
|
|
skillId: "skills:1",
|
|
githubSourceId: "githubSkillSources:new",
|
|
contentHash: "content-hash",
|
|
commit: "a".repeat(40),
|
|
path: "skills/demo",
|
|
status: "pending",
|
|
skillScanRequestId: "skillScanRequests:stale",
|
|
createdAt: now - 1_000,
|
|
updatedAt: now - 1_000,
|
|
},
|
|
],
|
|
[
|
|
"skillScanRequests:stale",
|
|
{
|
|
_id: "skillScanRequests:stale",
|
|
sourceKind: "github",
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
status: "queued",
|
|
fileChunkCount: 0,
|
|
fileManifestBytes: 0,
|
|
createdAt: now - 1_000,
|
|
updatedAt: now - 1_000,
|
|
},
|
|
],
|
|
]);
|
|
const inserts: Array<{ table: string; doc: Record<string, unknown> }> = [];
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
const id = `${table}:new-${inserts.length + 1}`;
|
|
docs.set(id, { _id: id, ...doc });
|
|
inserts.push({ table, doc });
|
|
return id;
|
|
});
|
|
const patch = vi.fn(async (id: string, next: Record<string, unknown>) => {
|
|
Object.assign(docs.get(id) ?? {}, next);
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => docs.get(id) ?? null),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => docs.get("githubSkillScans:1")),
|
|
})),
|
|
})),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
};
|
|
|
|
try {
|
|
const prepared = await prepareGitHubSkillScanRequestInternalHandler(ctx as never, {
|
|
skillId: "skills:1",
|
|
contentHash: "content-hash",
|
|
commit: "a".repeat(40),
|
|
force: true,
|
|
parsed: { frontmatter: {} },
|
|
staticScan: {
|
|
status: "clean",
|
|
reasonCodes: [],
|
|
findings: [],
|
|
summary: "No static findings.",
|
|
engineVersion: "test",
|
|
checkedAt: now,
|
|
},
|
|
});
|
|
|
|
expect(prepared).toMatchObject({
|
|
ok: true,
|
|
prepared: true,
|
|
scanId: "githubSkillScans:1",
|
|
requestId: expect.stringMatching(/^skillScanRequests:new-/),
|
|
});
|
|
expect(inserts).toHaveLength(1);
|
|
expect(inserts[0]).toMatchObject({
|
|
table: "skillScanRequests",
|
|
doc: expect.objectContaining({
|
|
sourceKind: "github",
|
|
fileChunkCount: 0,
|
|
fileManifestBytes: 0,
|
|
}),
|
|
});
|
|
expect(docs.get("githubSkillScans:1")).toMatchObject({
|
|
skillScanRequestId: expect.stringMatching(/^skillScanRequests:new-/),
|
|
});
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("reassociates a reused GitHub scan with the skill's current source", async () => {
|
|
const docs = new Map<string, Record<string, unknown>>([
|
|
[
|
|
"skills:1",
|
|
{
|
|
_id: "skills:1",
|
|
installKind: "github",
|
|
githubSourceId: "githubSkillSources:new",
|
|
githubPath: "skills/demo",
|
|
githubCurrentStatus: "present",
|
|
githubCurrentCommit: "b".repeat(40),
|
|
githubCurrentContentHash: "content-hash",
|
|
ownerUserId: "users:1",
|
|
slug: "demo",
|
|
displayName: "Demo",
|
|
},
|
|
],
|
|
[
|
|
"githubSkillScans:1",
|
|
{
|
|
_id: "githubSkillScans:1",
|
|
skillId: "skills:1",
|
|
githubSourceId: "githubSkillSources:deleted",
|
|
contentHash: "content-hash",
|
|
commit: "a".repeat(40),
|
|
path: "skills/old-demo",
|
|
status: "clean",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
},
|
|
],
|
|
]);
|
|
const patch = vi.fn(async (id: string, next: Record<string, unknown>) => {
|
|
Object.assign(docs.get(id) ?? {}, next);
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => docs.get(id) ?? null),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn(async () => docs.get("githubSkillScans:1")),
|
|
})),
|
|
})),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
system: {},
|
|
},
|
|
};
|
|
const staticScan = {
|
|
status: "clean" as const,
|
|
reasonCodes: [],
|
|
findings: [] as [],
|
|
summary: "No static findings.",
|
|
engineVersion: "test",
|
|
checkedAt: 2,
|
|
};
|
|
|
|
const result = await prepareGitHubSkillScanRequestInternalHandler(ctx as never, {
|
|
skillId: "skills:1",
|
|
contentHash: "content-hash",
|
|
commit: "b".repeat(40),
|
|
parsed: { frontmatter: {} },
|
|
staticScan,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
reused: true,
|
|
scanId: "githubSkillScans:1",
|
|
scanStatus: "clean",
|
|
});
|
|
expect(docs.get("githubSkillScans:1")).toMatchObject({
|
|
githubSourceId: "githubSkillSources:new",
|
|
commit: "b".repeat(40),
|
|
path: "skills/demo",
|
|
staticScan,
|
|
});
|
|
});
|
|
|
|
it("writes completed GitHub-backed scan results to the durable content-hash record", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillScanRequest",
|
|
leaseToken: "lease-token",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:1",
|
|
sourceKind: "github",
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
},
|
|
githubScan: {
|
|
_id: "githubSkillScans:1",
|
|
skillId: "skills:1",
|
|
contentHash: "content-hash",
|
|
},
|
|
}));
|
|
const runMutation = vi.fn(async () => ({ ok: true }));
|
|
|
|
await completeCodexScanJobHandler(
|
|
{ runQuery, runMutation },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
llmAnalysis: { status: "clean", verdict: "benign", checkedAt: 123 },
|
|
skillSpectorAnalysis: {
|
|
status: "clean",
|
|
issueCount: 0,
|
|
issues: [],
|
|
checkedAt: 123,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
scanStatus: "clean",
|
|
llmAnalysis: { status: "clean", verdict: "benign", checkedAt: 123 },
|
|
skillSpectorAnalysis: expect.objectContaining({ status: "clean", issueCount: 0 }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("marks the durable GitHub-backed scan failed when worker retries are exhausted", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) =>
|
|
"error" in args ? { ok: true, retry: false } : { ok: true },
|
|
);
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:1",
|
|
sourceKind: "github",
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
},
|
|
githubScan: {
|
|
_id: "githubSkillScans:1",
|
|
skillId: "skills:1",
|
|
contentHash: "content-hash",
|
|
},
|
|
}));
|
|
|
|
await failCodexScanJobHandler(
|
|
{ runQuery, runMutation },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: "worker failed",
|
|
},
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
scanStatus: "failed",
|
|
error: "worker failed",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("sanitizes worker errors before patching failed job and scan request records", async () => {
|
|
const { ctx, records } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:1": {
|
|
_id: "securityScanJobs:1",
|
|
attempts: 3,
|
|
leaseToken: "lease-token",
|
|
nextRunAt: 123,
|
|
skillScanRequestId: "skillScanRequests:1",
|
|
status: "running",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
"skillScanRequests:1": {
|
|
_id: "skillScanRequests:1",
|
|
status: "running",
|
|
},
|
|
});
|
|
|
|
const result = await failJobInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: fakeLeakyWorkerError(),
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
const jobError = String(records.get("securityScanJobs:1")?.lastError);
|
|
const requestError = String(records.get("skillScanRequests:1")?.lastError);
|
|
expectNoLeakedWorkerErrorSecrets(jobError);
|
|
expectNoLeakedWorkerErrorSecrets(requestError);
|
|
});
|
|
|
|
it("retries catalog worker failures on the same admitted attempt", async () => {
|
|
const { ctx, records } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:catalog": {
|
|
_id: "securityScanJobs:catalog",
|
|
attempts: 1,
|
|
leaseToken: "lease",
|
|
nextRunAt: 123,
|
|
source: "skills-sh-catalog-test",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
status: "running",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
},
|
|
});
|
|
|
|
const result = await failJobInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease",
|
|
error: "transient worker failure",
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true, retry: true });
|
|
expect(records.get("skillsShCatalogScanAttempts:catalog")).toMatchObject({
|
|
status: "queued",
|
|
});
|
|
expect(records.get("skillScanRequests:catalog")).toMatchObject({
|
|
status: "queued",
|
|
});
|
|
});
|
|
|
|
it("does not apply a second catalog terminal transition after retry exhaustion", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "placeholder");
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("jobId" in args && "leaseToken" in args && "error" in args) {
|
|
return { ok: true, retry: false };
|
|
}
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:catalog",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
sha256hash: "artifact-hash",
|
|
},
|
|
}));
|
|
|
|
await failCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "placeholder",
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "placeholder",
|
|
error: "terminal worker failure",
|
|
},
|
|
);
|
|
|
|
expect(
|
|
runMutation.mock.calls.some(([, args]) => "attemptId" in (args as Record<string, unknown>)),
|
|
).toBe(false);
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
scanId: "skillScanRequests:catalog",
|
|
error: "terminal worker failure",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("terminalizes the linked catalog attempt when the worker retry budget is exhausted", async () => {
|
|
const { ctx, records } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:catalog": {
|
|
_id: "securityScanJobs:catalog",
|
|
attempts: 3,
|
|
leaseToken: "lease",
|
|
nextRunAt: 123,
|
|
source: "skills-sh-catalog-test",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
status: "running",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
"skillsShCatalogEntries:catalog": {
|
|
_id: "skillsShCatalogEntries:catalog",
|
|
sourceContentHash: "source-hash",
|
|
scanStatus: "queued",
|
|
publicVisible: false,
|
|
},
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
entryId: "skillsShCatalogEntries:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
sourceContentHash: "source-hash",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "completed",
|
|
counts: {
|
|
scansCompleted: 0,
|
|
scansCanceled: 0,
|
|
},
|
|
operations: {
|
|
functionCalls: 0,
|
|
dbReads: 0,
|
|
dbWrites: 0,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await failJobInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease",
|
|
error: "terminal worker failure",
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
expect(records.get("skillsShCatalogScanAttempts:catalog")).toMatchObject({
|
|
status: "failed",
|
|
verdict: "failed",
|
|
completedAt: expect.any(Number),
|
|
});
|
|
expect(records.get("skillsShCatalogEntries:catalog")).toMatchObject({
|
|
scanStatus: "failed",
|
|
publicVisible: false,
|
|
});
|
|
expect(records.get("skillsShCatalogRuns:catalog")).toMatchObject({
|
|
counts: expect.objectContaining({ scansCompleted: 1 }),
|
|
});
|
|
});
|
|
|
|
it("does not retry catalog work after its run is canceled", async () => {
|
|
const { ctx, records } = makeFailurePersistenceCtx({
|
|
"securityScanJobs:catalog": {
|
|
_id: "securityScanJobs:catalog",
|
|
attempts: 1,
|
|
leaseToken: "lease",
|
|
nextRunAt: 123,
|
|
source: "skills-sh-catalog-test",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
status: "running",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
"skillScanRequests:catalog": {
|
|
_id: "skillScanRequests:catalog",
|
|
sourceKind: "skills-sh-catalog",
|
|
skillsShCatalogAttemptId: "skillsShCatalogScanAttempts:catalog",
|
|
status: "running",
|
|
},
|
|
"skillsShCatalogScanAttempts:catalog": {
|
|
_id: "skillsShCatalogScanAttempts:catalog",
|
|
runId: "skillsShCatalogRuns:catalog",
|
|
skillScanRequestId: "skillScanRequests:catalog",
|
|
securityScanJobId: "securityScanJobs:catalog",
|
|
status: "canceled",
|
|
},
|
|
"skillsShCatalogRuns:catalog": {
|
|
_id: "skillsShCatalogRuns:catalog",
|
|
status: "canceled",
|
|
},
|
|
});
|
|
|
|
const result = await failJobInternalHandler(ctx, {
|
|
jobId: "securityScanJobs:catalog",
|
|
leaseToken: "lease",
|
|
error: "worker stopped after cancellation",
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
expect(records.get("securityScanJobs:catalog")).toMatchObject({ status: "failed" });
|
|
expect(records.get("skillScanRequests:catalog")).toMatchObject({ status: "failed" });
|
|
expect(records.get("skillsShCatalogScanAttempts:catalog")).toMatchObject({
|
|
status: "canceled",
|
|
});
|
|
});
|
|
|
|
it("sanitizes worker errors before patching failed scan result records", async () => {
|
|
const { ctx, records } = makeFailurePersistenceCtx({
|
|
"githubSkillScans:1": {
|
|
_id: "githubSkillScans:1",
|
|
contentHash: "content-hash",
|
|
skillId: "skills:missing",
|
|
status: "pending",
|
|
},
|
|
"skillScanRequests:1": {
|
|
_id: "skillScanRequests:1",
|
|
status: "running",
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
recordSkillScanRequestFailedInternalHandler(ctx, {
|
|
scanId: "skillScanRequests:1",
|
|
error: fakeLeakyWorkerError(),
|
|
}),
|
|
).resolves.toEqual({ ok: true });
|
|
|
|
await expect(
|
|
recordGitHubSkillScanResultInternalHandler(ctx, {
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
scanStatus: "failed",
|
|
error: fakeLeakyWorkerError(),
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
|
|
const requestError = String(records.get("skillScanRequests:1")?.lastError);
|
|
const githubScanError = String(records.get("githubSkillScans:1")?.lastError);
|
|
expectNoLeakedWorkerErrorSecrets(requestError);
|
|
expectNoLeakedWorkerErrorSecrets(githubScanError);
|
|
});
|
|
|
|
it("redacts signed artifact URLs from persisted worker failure fields", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const leakyError =
|
|
`Download failed 403: https://signed.example.invalid/file?token=secret&X-Amz-Signature=abc123 ` +
|
|
`Authorization: Bearer abc OPENAI_API_KEY=openai-runtime-secret ` +
|
|
`GITHUB_TOKEN=github-runtime-secret CONVEX_DEPLOY_KEY=convex-deploy-secret ` +
|
|
"api_key=plugin-api-token";
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) =>
|
|
"error" in args ? { ok: true, retry: false } : { ok: true },
|
|
);
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillScanRequest",
|
|
},
|
|
scanRequest: {
|
|
_id: "skillScanRequests:1",
|
|
sourceKind: "github",
|
|
githubSkillScanId: "githubSkillScans:1",
|
|
},
|
|
githubScan: {
|
|
_id: "githubSkillScans:1",
|
|
skillId: "skills:1",
|
|
contentHash: "content-hash",
|
|
},
|
|
}));
|
|
|
|
await failCodexScanJobHandler(
|
|
{ runQuery, runMutation },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: leakyError,
|
|
},
|
|
);
|
|
|
|
const persistedErrorArgs = runMutation.mock.calls
|
|
.map(([, args]) => args)
|
|
.filter((args): args is Record<string, unknown> => {
|
|
return typeof args === "object" && args !== null && "error" in args;
|
|
});
|
|
expect(persistedErrorArgs).toHaveLength(3);
|
|
for (const args of persistedErrorArgs) {
|
|
expect(args.error).toBeTypeOf("string");
|
|
const error = String(args.error);
|
|
expect(error).toContain("Download failed 403");
|
|
expect(error).not.toContain("https://");
|
|
expect(error).not.toContain("signed.example.invalid");
|
|
expect(error).not.toContain("token=secret");
|
|
expect(error).not.toContain("X-Amz-Signature");
|
|
expect(error).not.toContain("Authorization");
|
|
expect(error).not.toContain("Bearer abc");
|
|
expect(error).not.toContain("openai-runtime-secret");
|
|
expect(error).not.toContain("github-runtime-secret");
|
|
expect(error).not.toContain("convex-deploy-secret");
|
|
expect(error).not.toContain("plugin-api-token");
|
|
expect(error).toContain("OPENAI_API_KEY=[redacted-secret]");
|
|
expect(error).toContain("GITHUB_TOKEN=[redacted-secret]");
|
|
expect(error).toContain("CONVEX_DEPLOY_KEY=[redacted-secret]");
|
|
expect(error).toContain("api_key=[redacted-secret]");
|
|
}
|
|
|
|
const llmAnalyses = runMutation.mock.calls
|
|
.map(([, args]) => {
|
|
if (!args || typeof args !== "object" || !("llmAnalysis" in args)) return undefined;
|
|
const analysis = args.llmAnalysis;
|
|
if (!analysis || typeof analysis !== "object" || !("findings" in analysis)) {
|
|
return undefined;
|
|
}
|
|
return typeof analysis.findings === "string" ? analysis : undefined;
|
|
})
|
|
.filter((analysis) => analysis !== undefined);
|
|
for (const analysis of llmAnalyses) {
|
|
expect(analysis?.findings).toContain("Download failed 403");
|
|
expect(analysis?.findings).not.toContain("https://");
|
|
expect(analysis?.findings).not.toContain("signed.example.invalid");
|
|
expect(analysis?.findings).not.toContain("token=secret");
|
|
expect(analysis?.findings).not.toContain("X-Amz-Signature");
|
|
expect(analysis?.findings).not.toContain("Authorization");
|
|
expect(analysis?.findings).not.toContain("Bearer abc");
|
|
expect(analysis?.findings).not.toContain("openai-runtime-secret");
|
|
expect(analysis?.findings).not.toContain("github-runtime-secret");
|
|
expect(analysis?.findings).not.toContain("convex-deploy-secret");
|
|
expect(analysis?.findings).not.toContain("plugin-api-token");
|
|
expect(analysis?.findings).toContain("OPENAI_API_KEY=[redacted-secret]");
|
|
expect(analysis?.findings).toContain("GITHUB_TOKEN=[redacted-secret]");
|
|
expect(analysis?.findings).toContain("CONVEX_DEPLOY_KEY=[redacted-secret]");
|
|
expect(analysis?.findings).toContain("api_key=[redacted-secret]");
|
|
}
|
|
});
|
|
|
|
it("redacts signed artifact URLs from package failure analysis fields", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) =>
|
|
"error" in args ? { ok: true, retry: false } : { ok: true },
|
|
);
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "packageRelease",
|
|
},
|
|
release: {
|
|
_id: "packageReleases:1",
|
|
},
|
|
}));
|
|
|
|
await failCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: fakeLeakyWorkerError(),
|
|
},
|
|
);
|
|
|
|
const packageFailureCall = runMutation.mock.calls.find(([, args]) => {
|
|
return args && typeof args === "object" && "releaseId" in args && "llmAnalysis" in args;
|
|
});
|
|
expect(packageFailureCall).toBeDefined();
|
|
const llmAnalysis = packageFailureCall?.[1].llmAnalysis as { findings?: string } | undefined;
|
|
expect(llmAnalysis?.findings).toBeTypeOf("string");
|
|
expectNoLeakedWorkerErrorSecrets(llmAnalysis?.findings ?? "");
|
|
});
|
|
|
|
it("preserves a prior blocking skill ClawScan verdict when worker retries are exhausted", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("error" in args) return { ok: true, retry: false };
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "skillVersion",
|
|
},
|
|
version: {
|
|
_id: "skillVersions:1",
|
|
llmAnalysis: {
|
|
status: "suspicious",
|
|
checkedAt: 123,
|
|
},
|
|
},
|
|
}));
|
|
|
|
const result = await failCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: "Codex worker failed",
|
|
},
|
|
);
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
expect(runMutation).toHaveBeenCalledTimes(1);
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("preserves a prior blocking package ClawScan verdict when worker retries are exhausted", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("error" in args) return { ok: true, retry: false };
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "packageRelease",
|
|
},
|
|
release: {
|
|
_id: "packageReleases:1",
|
|
llmAnalysis: {
|
|
status: "error",
|
|
verdict: "malicious",
|
|
checkedAt: 123,
|
|
},
|
|
},
|
|
}));
|
|
|
|
const result = await failCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: "Codex worker failed",
|
|
},
|
|
);
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
expect(runMutation).toHaveBeenCalledTimes(1);
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("preserves a prior clean package ClawScan verdict when worker retries are exhausted", async () => {
|
|
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
|
|
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("error" in args) return { ok: true, retry: false };
|
|
return { ok: true };
|
|
});
|
|
const runQuery = vi.fn(async () => ({
|
|
job: {
|
|
_id: "securityScanJobs:1",
|
|
targetKind: "packageRelease",
|
|
},
|
|
release: {
|
|
_id: "packageReleases:1",
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "benign",
|
|
checkedAt: 123,
|
|
},
|
|
},
|
|
}));
|
|
|
|
const result = await failCodexScanJobHandler(
|
|
{ runMutation, runQuery },
|
|
{
|
|
token: "worker-secret",
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
error: "Codex worker failed",
|
|
},
|
|
);
|
|
|
|
expect(result).toEqual({ ok: true, retry: false });
|
|
expect(runMutation).toHaveBeenCalledTimes(1);
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
jobId: "securityScanJobs:1",
|
|
leaseToken: "lease-token",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("dry-runs queued vt-update jobs without deleting", async () => {
|
|
const job = makeScanJob({ _id: "securityScanJobs:dry-run" });
|
|
const { ctx, deleteDoc, take } = makeCancelCtx(
|
|
[job],
|
|
new Map<string, unknown>([["skillVersions:dry-run", makeTarget("clean")]]),
|
|
);
|
|
|
|
const result = await cancelQueuedVtUpdateJobsInternalHandler(ctx, {
|
|
dryRun: true,
|
|
createdBefore: 1000,
|
|
});
|
|
|
|
expect(take).toHaveBeenCalledWith(1000);
|
|
expect(result).toMatchObject({
|
|
dryRun: true,
|
|
scanned: 1,
|
|
matched: 1,
|
|
wouldDelete: 1,
|
|
deleted: 0,
|
|
oldestScannedCreatedAt: 50,
|
|
newestScannedCreatedAt: 50,
|
|
oldestScannedNextRunAt: 100,
|
|
newestScannedNextRunAt: 100,
|
|
skippedByReason: {},
|
|
sampleMatchedJobIds: ["securityScanJobs:dry-run"],
|
|
sampleDeletedJobIds: [],
|
|
});
|
|
expect(deleteDoc).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("deletes all queued vt-update jobs while preserving other sources and running jobs", async () => {
|
|
const jobs = [
|
|
makeScanJob({ _id: "securityScanJobs:clean" }),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:package",
|
|
targetKind: "packageRelease",
|
|
skillVersionId: undefined,
|
|
packageReleaseId: "packageReleases:package",
|
|
}),
|
|
makeScanJob({
|
|
_id: "securityScanJobs:malicious-signal",
|
|
hasMaliciousSignal: true,
|
|
}),
|
|
makeScanJob({ _id: "securityScanJobs:vt-mismatch" }),
|
|
makeScanJob({ _id: "securityScanJobs:no-llm" }),
|
|
makeScanJob({ _id: "securityScanJobs:publish", source: "publish" }),
|
|
makeScanJob({ _id: "securityScanJobs:manual", source: "manual" }),
|
|
makeScanJob({ _id: "securityScanJobs:backfill", source: "backfill" }),
|
|
makeScanJob({ _id: "securityScanJobs:running", status: "running" }),
|
|
];
|
|
const { ctx, deleted, get } = makeCancelCtx(
|
|
jobs,
|
|
new Map<string, unknown>([
|
|
["skillVersions:clean", makeTarget("clean")],
|
|
["packageReleases:package", makeTarget("clean")],
|
|
["skillVersions:malicious-signal", makeTarget("clean")],
|
|
["skillVersions:vt-mismatch", makeTarget("clean")],
|
|
["skillVersions:no-llm", makeTarget()],
|
|
["skillVersions:running", makeTarget("clean")],
|
|
]),
|
|
);
|
|
|
|
const result = await cancelQueuedVtUpdateJobsInternalHandler(ctx, {
|
|
dryRun: false,
|
|
createdBefore: 1000,
|
|
scanLimit: 25,
|
|
deleteLimit: 10,
|
|
});
|
|
|
|
expect(deleted).toEqual([
|
|
"securityScanJobs:clean",
|
|
"securityScanJobs:package",
|
|
"securityScanJobs:vt-mismatch",
|
|
]);
|
|
expect(get).toHaveBeenCalled();
|
|
expect(result).toMatchObject({
|
|
dryRun: false,
|
|
scanned: 9,
|
|
matched: 3,
|
|
wouldDelete: 3,
|
|
deleted: 3,
|
|
skippedByReason: {
|
|
"not-vt-update": 3,
|
|
"not-queued-vt-update": 1,
|
|
"malicious-signal": 1,
|
|
"missing-llm-analysis": 1,
|
|
},
|
|
sampleMatchedJobIds: [
|
|
"securityScanJobs:clean",
|
|
"securityScanJobs:package",
|
|
"securityScanJobs:vt-mismatch",
|
|
],
|
|
sampleDeletedJobIds: [
|
|
"securityScanJobs:clean",
|
|
"securityScanJobs:package",
|
|
"securityScanJobs:vt-mismatch",
|
|
],
|
|
});
|
|
});
|
|
|
|
it("counts matched jobs beyond the per-run delete limit without deleting them", async () => {
|
|
const jobs = [
|
|
makeScanJob({ _id: "securityScanJobs:first" }),
|
|
makeScanJob({ _id: "securityScanJobs:second" }),
|
|
];
|
|
const { ctx, deleted } = makeCancelCtx(
|
|
jobs,
|
|
new Map<string, unknown>([
|
|
["skillVersions:first", makeTarget("clean")],
|
|
["skillVersions:second", makeTarget("clean")],
|
|
]),
|
|
);
|
|
|
|
const result = await cancelQueuedVtUpdateJobsInternalHandler(ctx, {
|
|
dryRun: false,
|
|
createdBefore: 1000,
|
|
deleteLimit: 1,
|
|
});
|
|
|
|
expect(deleted).toEqual(["securityScanJobs:first"]);
|
|
expect(result).toMatchObject({
|
|
scanned: 2,
|
|
matched: 2,
|
|
wouldDelete: 1,
|
|
deleted: 1,
|
|
skippedByReason: {
|
|
"delete-limit-reached": 1,
|
|
},
|
|
sampleMatchedJobIds: ["securityScanJobs:first", "securityScanJobs:second"],
|
|
sampleDeletedJobIds: ["securityScanJobs:first"],
|
|
});
|
|
});
|
|
});
|