test: isolate GitHub Skill Sync proof jobs (#3257)

This commit is contained in:
Patrick Erichsen
2026-07-24 21:36:16 -05:00
committed by GitHub
parent 5a3b050751
commit 906428a557
3 changed files with 152 additions and 2 deletions
+117 -1
View File
@@ -61,6 +61,7 @@ const claimCodexScanJobLeasesHandler = (
lane?: "priority" | "shared" | "catalog";
limit?: number;
leaseMs?: number;
targetedJobIds?: string[];
},
Array<ScanJob & { leaseToken: string; workerId: string }>
>
@@ -77,7 +78,13 @@ const hydrateCodexScanJobHandler = (
const claimQueuedJobsInternalHandler = (
claimQueuedJobsInternal as unknown as WrappedHandler<
{ workerId: string; lane?: "priority" | "shared" | "catalog"; limit: number; leaseMs?: number },
{
workerId: string;
lane?: "priority" | "shared" | "catalog";
limit: number;
leaseMs?: number;
targetedJobIds?: string[];
},
Array<ScanJob & { leaseToken: string; workerId: string }>
>
)._handler;
@@ -376,6 +383,7 @@ type ScanJob = {
skillVersionId?: string;
packageReleaseId?: string;
skillScanRequestId?: string;
rolloutGate?: "github-skill-sync";
source: string;
priority: number;
hasMaliciousSignal: boolean;
@@ -3223,6 +3231,36 @@ describe("securityScan", () => {
expect(getUrl).not.toHaveBeenCalled();
});
it("forwards exact Test GitHub Skill Sync job IDs to the lease mutation", async () => {
vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret");
const leases = [
{
...claimedJob,
_id: "securityScanJobs:github-sync",
leaseToken: "lease-github-sync",
},
];
const runMutation = vi.fn(async () => leases);
const result = await claimCodexScanJobLeasesHandler(
{ runMutation, runQuery: vi.fn(), storage: { getUrl: vi.fn() } },
{
token: "worker-secret",
workerId: "worker-1",
limit: 1,
targetedJobIds: ["securityScanJobs:github-sync"],
},
);
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
targetedJobIds: ["securityScanJobs:github-sync"],
}),
);
expect(result).toEqual(leases);
});
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 () => ({
@@ -3751,6 +3789,84 @@ describe("securityScan", () => {
expect(patches.map((entry) => entry.id)).toEqual(claimed.map((job) => job._id));
});
it("claims only requested GitHub Skill Sync jobs in the permanent Test environment", async () => {
vi.stubEnv("CLAWHUB_ENV", "test");
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
vi.stubEnv("CLAWHUB_GITHUB_SKILL_SYNC_ROLLOUT_MODE", "test");
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
const { ctx, patches } = makeClaimCtx([
makeScanJob({
_id: "securityScanJobs:unrelated",
source: "publish",
createdAt: 1,
nextRunAt: 1,
}),
makeScanJob({
_id: "securityScanJobs:github-sync",
source: "publish",
rolloutGate: "github-skill-sync",
createdAt: 2,
nextRunAt: 2,
}),
]);
const claimed = await claimQueuedJobsInternalHandler(ctx, {
workerId: "targeted-test-worker",
limit: 1,
targetedJobIds: ["securityScanJobs:github-sync"],
});
expect(claimed.map((job) => job._id)).toEqual(["securityScanJobs:github-sync"]);
expect(patches.map((entry) => entry.id)).toEqual(["securityScanJobs:github-sync"]);
});
it("does not turn an empty exact claim into a broad queue claim", async () => {
vi.stubEnv("CLAWHUB_ENV", "test");
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "academic-chihuahua-392");
vi.stubEnv("CLAWHUB_GITHUB_SKILL_SYNC_ROLLOUT_MODE", "test");
vi.stubEnv("CONVEX_CLOUD_URL", "https://academic-chihuahua-392.convex.cloud");
const { ctx, patches } = makeClaimCtx([
makeScanJob({
_id: "securityScanJobs:unrelated",
source: "publish",
createdAt: 1,
nextRunAt: 1,
}),
]);
const claimed = await claimQueuedJobsInternalHandler(ctx, {
workerId: "targeted-test-worker",
limit: 1,
targetedJobIds: [],
});
expect(claimed).toEqual([]);
expect(patches).toEqual([]);
});
it("rejects exact GitHub Skill Sync job claims outside the permanent Test rollout", async () => {
vi.stubEnv("CLAWHUB_ENV", "production");
vi.stubEnv("CLAWHUB_DEPLOYMENT_NAME", "wry-manatee-359");
vi.stubEnv("CLAWHUB_GITHUB_SKILL_SYNC_ROLLOUT_MODE", "off");
vi.stubEnv("CONVEX_CLOUD_URL", "https://wry-manatee-359.convex.cloud");
const { ctx } = makeClaimCtx([
makeScanJob({
_id: "securityScanJobs:github-sync",
source: "publish",
rolloutGate: "github-skill-sync",
nextRunAt: 1,
}),
]);
await expect(
claimQueuedJobsInternalHandler(ctx, {
workerId: "targeted-test-worker",
limit: 1,
targetedJobIds: ["securityScanJobs:github-sync"],
}),
).rejects.toThrow("Exact GitHub Skill Sync job claims are Test-only");
});
it("claims bulk rescans after every supported source", async () => {
const { ctx } = makeClaimCtx([
makeScanJob({
+30 -1
View File
@@ -35,6 +35,7 @@ import { requestSecurityScanDispatch } from "./securityScanDispatch";
const DEFAULT_VT_WAIT_MS = 10 * 60 * 1000;
const DEFAULT_LEASE_MS = 60 * 60 * 1000;
const MAX_TARGETED_TEST_GITHUB_SYNC_JOBS = 32;
const MAX_ATTEMPTS = 3;
const DEFAULT_CODEX_SCAN_CLAIM_LIMIT = 64;
const MAX_CODEX_SCAN_CLAIM_LIMIT = 512;
@@ -2842,6 +2843,7 @@ export const claimQueuedJobsInternal = internalMutation({
lane: v.optional(codexScanWorkerLaneValidator),
limit: v.number(),
leaseMs: v.optional(v.number()),
targetedJobIds: v.optional(v.array(v.id("securityScanJobs"))),
},
handler: async (ctx, args) => {
const now = Date.now();
@@ -2943,7 +2945,32 @@ export const claimQueuedJobsInternal = internalMutation({
return eligible;
};
if (args.lane === "catalog") {
const targetedJobIds = args.targetedJobIds;
if (targetedJobIds !== undefined) {
const rollout = getRuntimeRolloutCapabilities();
if (
rollout.environment !== "test" ||
rollout.githubSkillSync.mode !== "test" ||
!rollout.githubSkillSync.runtimeEnabled
) {
throw new ConvexError("Exact GitHub Skill Sync job claims are Test-only");
}
if (targetedJobIds.length > MAX_TARGETED_TEST_GITHUB_SYNC_JOBS) {
throw new ConvexError("Too many exact GitHub Skill Sync jobs requested");
}
const targetedJobs: Doc<"securityScanJobs">[] = [];
for (const jobId of new Set(targetedJobIds)) {
const job = await ctx.db.get(jobId);
if (
job?.status === "queued" &&
job.rolloutGate === "github-skill-sync" &&
job.nextRunAt <= now
) {
targetedJobs.push(job);
}
}
addReadyJobs(targetedJobs);
} else if (args.lane === "catalog") {
addReadyJobs(await takeReadySourceJobs("skills-sh-catalog-test"), false);
} else {
addReadyJobs(await takeReadySourceJobs("manual"));
@@ -3697,6 +3724,7 @@ export const claimCodexScanJobLeases = action({
lane: v.optional(codexScanWorkerLaneValidator),
limit: v.optional(v.number()),
leaseMs: v.optional(v.number()),
targetedJobIds: v.optional(v.array(v.id("securityScanJobs"))),
},
handler: async (ctx, args) => {
assertWorkerToken(args.token);
@@ -3708,6 +3736,7 @@ export const claimCodexScanJobLeases = action({
lane: args.lane ?? "shared",
limit: normalizeLimit(args.limit),
leaseMs: args.leaseMs,
targetedJobIds: args.targetedJobIds,
},
);
},
+5
View File
@@ -335,6 +335,11 @@ returns the same commit-pinned GitHub descriptor used by native GitHub-backed
skills. Catalog pause, kill, publication disable, and exact-attempt rollback
must fail closed without disabling or mutating native scan work.
Permanent-Test proof workers may claim explicit security job IDs only when the
runtime is the permanent Test environment, GitHub Skill Sync is in `test` mode,
and every selected job carries the `github-skill-sync` rollout gate. Production
and broad queue claims must not accept this proof-only targeting path.
Pending verification keeps the skill visible in ClawHub search and detail UI,
but normal install/update returns a structured block: