Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Advance work queue enqueue metadata protocol to v2
Add a deterministic 1-to-2 codemod, preserve strict historical message shapes and age-zero Work, and align runtime schemas, audit parsing, documentation, and regression tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
pelikhan and Copilot committed Oct 3, 2026
commit 0c52d839317a17536ffec7b867227b2bfac7981b
13 changes: 7 additions & 6 deletions actions/setup/js/finish_work_queue_claim.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { applyTransactions } from "./work_queue_replay.cjs";
import { main, readFinishIntent, reconcileWorkerClaim, renderSummary } from "./finish_work_queue_claim.cjs";
import { main as writeSnapshot } from "./write_work_queue_snapshot.cjs";
import { serializeTransactionLog } from "./work_queue_replay.cjs";
import { CURRENT_VERSION } from "./work_queue_codemods.cjs";

const worker = { work_id: "w", claim_id: "claim-a" };
const initialTransactions = [
{ version: 1, kind: "Work", work: "w", claim: null, attempt: null },
{ version: 1, kind: "Claim", work: "w", claim: "claim-a", attempt: null },
{ version: CURRENT_VERSION, kind: "Work", work: "w", claim: null, attempt: null },
{ version: CURRENT_VERSION, kind: "Claim", work: "w", claim: "claim-a", attempt: null },
];

let tempDirectory;
Expand Down Expand Up @@ -73,7 +74,7 @@ describe("work queue claim reconciliation", () => {
context,
});
expect(result).toEqual({ authorized: false, status: "cancelled" });
expect(fake.transactions).toContainEqual({ version: 1, kind: "ClaimCancellation", work: "w", claim: "claim-a", attempt: null });
expect(fake.transactions).toContainEqual({ version: CURRENT_VERSION, kind: "ClaimCancellation", work: "w", claim: "claim-a", attempt: null });
});

it("publishes and verifies Completion before authorizing safe outputs", async () => {
Expand All @@ -92,7 +93,7 @@ describe("work queue claim reconciliation", () => {

expect(result).toEqual({ authorized: true, status: "completed" });
expect(fake.transactions).toContainEqual({
version: 1,
version: CURRENT_VERSION,
kind: "Completion",
work: "w",
claim: "claim-a",
Expand All @@ -111,11 +112,11 @@ describe("work queue claim reconciliation", () => {
});

expect(result).toEqual({ authorized: false, status: "cancelled" });
expect(fake.transactions).toContainEqual({ version: 1, kind: "ClaimCancellation", work: "w", claim: "claim-a", attempt: null });
expect(fake.transactions).toContainEqual({ version: CURRENT_VERSION, kind: "ClaimCancellation", work: "w", claim: "claim-a", attempt: null });
});

it("does not publish or authorize a superseded claim", async () => {
const fake = setup([...initialTransactions, { version: 1, kind: "Claim", work: "w", claim: "claim-0", attempt: null }]);
const fake = setup([...initialTransactions, { version: CURRENT_VERSION, kind: "Claim", work: "w", claim: "claim-0", attempt: null }]);
const publish = vi.fn(fake.applyAndPublish);
const result = await reconcileWorkerClaim({
worker,
Expand Down
15 changes: 13 additions & 2 deletions actions/setup/js/work_queue_codemods.cjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// @ts-check
"use strict";

const CURRENT_VERSION = 1;
const CURRENT_VERSION = 2;

// Keep each successive protocol transformation as data so historical upgrades
// remain inspectable and deterministic. An absent version is the original log.
const CODEMODS = Object.freeze([Object.freeze({ from: 0, to: 1, set: Object.freeze({ version: 1 }) })]);
const CODEMODS = Object.freeze([Object.freeze({ from: 0, to: 1, set: Object.freeze({ version: 1 }) }), Object.freeze({ from: 1, to: 2, set: Object.freeze({ version: 2 }) })]);

const HISTORICAL_FIELDS = Object.freeze(["kind", "work", "claim", "attempt"]);

function upgradeTransaction(message) {
if (!message || typeof message !== "object" || Array.isArray(message)) {
Expand All @@ -15,6 +17,15 @@ function upgradeTransaction(message) {
if (!Number.isSafeInteger(version) || version < 0 || version > CURRENT_VERSION) {
throw new TypeError("unsupported work queue transaction version");
}
// Historical versions are closed shapes. Do not legitimize v2 metadata by
// relabeling an invalid historical record, or invent an enqueue timestamp.
if (version < CURRENT_VERSION) {
const expectedFields = Object.hasOwn(message, "version") ? ["version", ...HISTORICAL_FIELDS] : HISTORICAL_FIELDS;
const fields = Object.keys(message);
if (fields.length !== expectedFields.length || fields.some(field => !expectedFields.includes(field))) {
throw new TypeError("historical transaction must contain exactly version (if present), kind, work, claim, and attempt");
}
}
let upgraded = message;
for (let next = version; next < CURRENT_VERSION; next++) {
const codemod = CODEMODS.find(item => item.from === next);
Expand Down
22 changes: 20 additions & 2 deletions actions/setup/js/work_queue_mcp_server.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import path from "path";
import { createWorkQueueFinishTool, createWorkQueueStateTool, loadWorkQueueSnapshot, readWorkQueueState } from "./work_queue_mcp_server.cjs";
import { createWorkTransaction, serializeTransactionLog } from "./work_queue_replay.cjs";

const work = id => ({ version: 1, kind: "Work", work: id, claim: null, attempt: null });
const claim = (workId, id) => ({ version: 1, kind: "Claim", work: workId, claim: id, attempt: null });
const work = id => ({ version: 2, kind: "Work", work: id, claim: null, attempt: null });
const claim = (workId, id) => ({ version: 2, kind: "Claim", work: workId, claim: id, attempt: null });

const tempFiles = [];

Expand Down Expand Up @@ -77,6 +77,24 @@ describe("work queue MCP snapshot", () => {
expect(readWorkQueueState(snapshot, { work: "a-new" }).next_work).toBe("z-old");
});

it("loads v1 Work at age zero without changing the snapshot envelope", () => {
const snapshotPath = writeSnapshot({
version: 2,
sha: "historical-head",
transactionLog: `${JSON.stringify(createWorkTransaction("new", 100))}\n${JSON.stringify({ ...work("legacy"), version: 1 })}\n`,
worker: null,
});
const snapshot = loadWorkQueueSnapshot(snapshotPath);
expect(JSON.parse(fs.readFileSync(snapshotPath, "utf8")).version).toBe(2);
expect(snapshot.projection.transactions).toContainEqual(work("legacy"));
const result = readWorkQueueState(snapshot);
expect(result.next_work).toBe("legacy");
expect(result.works.map(item => [item.id, item.enqueued])).toEqual([
["legacy", 0],
["new", 100],
]);
});

it("rejects snapshots with an unsupported shape or invalid transaction log", () => {
expect(() => loadWorkQueueSnapshot(writeSnapshot({ version: 1, sha: null, transactionLog: "" }))).toThrow(/invalid shape/);
expect(() => loadWorkQueueSnapshot(writeSnapshot({ version: 2, sha: null, transactionLog: "{}\n", worker: null }))).toThrow(/invalid transaction log/);
Expand Down
62 changes: 49 additions & 13 deletions actions/setup/js/work_queue_replay.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { describe, expect, it, vi } from "vitest";
import { createHash } from "node:crypto";
import selectionFixtures from "../../../specs/work-queue/selection-fixtures.json";
import { applyTransactions, claimOldestAvailableWork, compactTransactions, createWorkTransaction, oldestAvailableWork, parseTransactionLog, replayTransactions, serializeTransactionLog, validateTransaction } from "./work_queue_replay.cjs";
import { CODEMODS, CURRENT_VERSION, upgradeTransaction } from "./work_queue_codemods.cjs";

const work = id => ({ version: 1, kind: "Work", work: id, claim: null, attempt: null });
const claim = (workId, id) => ({ version: 1, kind: "Claim", work: workId, claim: id, attempt: null });
const cancelClaim = (workId, id) => ({ version: 1, kind: "ClaimCancellation", work: workId, claim: id, attempt: null });
const complete = (workId, claimId, attempt) => ({ version: 1, kind: "Completion", work: workId, claim: claimId, attempt });
const cancelWork = id => ({ version: 1, kind: "WorkCancellation", work: id, claim: null, attempt: null });
const work = id => ({ version: CURRENT_VERSION, kind: "Work", work: id, claim: null, attempt: null });
const claim = (workId, id) => ({ version: CURRENT_VERSION, kind: "Claim", work: workId, claim: id, attempt: null });
const cancelClaim = (workId, id) => ({ version: CURRENT_VERSION, kind: "ClaimCancellation", work: workId, claim: id, attempt: null });
const complete = (workId, claimId, attempt) => ({ version: CURRENT_VERSION, kind: "Completion", work: workId, claim: claimId, attempt });
const cancelWork = id => ({ version: CURRENT_VERSION, kind: "WorkCancellation", work: id, claim: null, attempt: null });

function permutations(items) {
if (items.length < 2) return [items];
Expand Down Expand Up @@ -180,20 +181,55 @@ describe("work queue replay", () => {
expect(parseTransactionLog("")).toEqual([]);
});

it("upgrades unversioned and version-zero records to the current protocol", () => {
const oldWork = { kind: "Work", work: "w", claim: null, attempt: null };
const oldClaim = { version: 0, kind: "Claim", work: "w", claim: "c", attempt: null };
const upgraded = parseTransactionLog(`${JSON.stringify(oldWork)}\n${JSON.stringify(oldClaim)}\n`);
expect(upgraded).toEqual([work("w"), claim("w", "c")]);
expect(serializeTransactionLog(upgraded)).toBe(serializeTransactionLog([work("w"), claim("w", "c")]));
expect(() => validateTransaction(oldWork)).toThrow("exactly version");
it("upgrades all historical kinds deterministically to v2 without inventing age", () => {
expect(CURRENT_VERSION).toBe(2);
expect(CODEMODS).toEqual([
{ from: 0, to: 1, set: { version: 1 } },
{ from: 1, to: 2, set: { version: 2 } },
]);
const facts = [work("w"), claim("w", "a"), claim("w", "b"), cancelClaim("w", "a"), complete("w", "b", "run"), work("other"), cancelWork("other")];
for (const version of [undefined, 0, 1]) {
const historical = facts.map(({ version: _, ...fact }) => (version === undefined ? fact : { version, ...fact }));
const before = JSON.stringify(historical);
const upgraded = parseTransactionLog(`${historical.map(fact => JSON.stringify(fact)).join("\n")}\n`);
expect(upgraded).toEqual(facts);
expect(serializeTransactionLog(upgraded)).toBe(serializeTransactionLog(facts));
expect(upgradeTransaction(historical[0])).toEqual(work("w"));
expect(upgradeTransaction(upgradeTransaction(historical[0]))).toEqual(work("w"));
expect(JSON.stringify(historical)).toBe(before);
expect(upgraded.filter(fact => fact.kind === "Work").every(fact => !Object.hasOwn(fact, "enqueued"))).toBe(true);
}
const legacyWork = { version: 1, kind: "Work", work: "legacy", claim: null, attempt: null };
const upgraded = parseTransactionLog(`${JSON.stringify(createWorkTransaction("new", 1))}\n${JSON.stringify(legacyWork)}\n`);
expect(replayTransactions(upgraded).available).toEqual(["legacy", "new"]);
expect(applyTransactions(upgraded, [createWorkTransaction("legacy", 100)]).transactions).toEqual(upgraded);
expect(() => validateTransaction(legacyWork)).toThrow("unsupported work queue transaction version");
expect(() => validateTransaction({ kind: "Work", work: "w", claim: null, attempt: null })).toThrow("exactly version");
});

it("rejects extra metadata on closed historical versions before upgrading", () => {
for (const version of [undefined, 0, 1]) {
for (const fact of [work("w"), claim("w", "c")]) {
const { version: _, ...fields } = fact;
const historical = version === undefined ? fields : { version, ...fields };
for (const extra of [{ enqueued: 0 }, { enqueued: 123 }, { run_id: "run" }, { extra: true }]) {
expect(() => upgradeTransaction({ ...historical, ...extra })).toThrow("historical transaction must contain exactly");
expect(() => parseTransactionLog(`${JSON.stringify({ ...historical, ...extra })}\n`)).toThrow("historical transaction must contain exactly");
}
const missingField = { ...historical };
delete missingField.attempt;
expect(() => upgradeTransaction(missingField)).toThrow("historical transaction must contain exactly");
}
}
});

it("rejects unknown or malformed message versions and never accepts partial upgrades", () => {
for (const version of [-1, 1.5, "1", 2, null]) {
for (const version of [-1, 1.5, "1", 3, null, true, {}, Number.MAX_SAFE_INTEGER + 1]) {
expect(() => parseTransactionLog(`${JSON.stringify(work("w"))}\n${JSON.stringify({ ...claim("w", "c"), version })}\n`)).toThrow("unsupported work queue transaction version");
}
expect(() => parseTransactionLog(`${JSON.stringify({ kind: "Work", work: "w", claim: null, attempt: null, extra: true })}\n`)).toThrow("exactly version");
expect(() => parseTransactionLog(`${JSON.stringify({ version: 1, kind: "Work", work: "w", claim: "invalid", attempt: null })}\n`)).toThrow("must not include");
expect(() => serializeTransactionLog([{ ...work("w"), version: 1 }])).toThrow("unsupported work queue transaction version");
});

it("rejects malformed JSONL records and blank lines", () => {
Expand Down
56 changes: 45 additions & 11 deletions actions/setup/js/work_queue_store.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import { describe, expect, it, vi } from "vitest";
import { applyAndPublishWorkQueueTransactions, WORK_QUEUE_BRANCH, WORK_QUEUE_LOG_PATH, readWorkQueueLog } from "./work_queue_store.cjs";
import { claimOldestAvailableWork, createWorkTransaction, parseTransactionLog, replayTransactions, serializeTransactionLog } from "./work_queue_replay.cjs";
import { CURRENT_VERSION } from "./work_queue_codemods.cjs";

const work = id => ({ version: 1, kind: "Work", work: id, claim: null, attempt: null });
const claim = (workId, id) => ({ version: 1, kind: "Claim", work: workId, claim: id, attempt: null });
const work = id => ({ version: CURRENT_VERSION, kind: "Work", work: id, claim: null, attempt: null });
const claim = (workId, id) => ({ version: CURRENT_VERSION, kind: "Claim", work: workId, claim: id, attempt: null });

function createFakeGitHub() {
const blobs = new Map();
Expand Down Expand Up @@ -201,6 +202,32 @@ describe("work queue Git store", () => {
expect(fake.blobs.size).toBe(0);
});

it("upgrades a v1 log through checked publication while preserving historical Work age", async () => {
const fake = createFakeGitHub();
fake.state.sha = "v1-head";
fake.state.transactions = [work("old")];
fake.state.rawContents = `${JSON.stringify({ ...work("old"), version: 1 })}\n`;

const readOnly = await readWorkQueueLog({ githubClient: fake.githubClient, owner: "owner", repo: "repo", publishUpgrades: false });
expect(readOnly.sha).toBe("v1-head");
expect(readOnly.transactions).toEqual([work("old")]);
expect(fake.blobs.size).toBe(0);

const result = await applyAndPublishWorkQueueTransactions({
githubClient: fake.githubClient,
owner: "owner",
repo: "repo",
intents: [createWorkTransaction("old", 200), createWorkTransaction("new", 100)],
});
expect(result.persisted).toBe(true);
expect(fake.state.updateCalls).toBe(1);
expect(result.transactions).toEqual([createWorkTransaction("new", 100), work("old")]);
expect(replayTransactions(result.transactions).available).toEqual(["old", "new"]);
expect([...fake.blobs.values()]).toEqual([serializeTransactionLog(result.transactions)]);
expect(await readWorkQueueLog({ githubClient: fake.githubClient, owner: "owner", repo: "repo" })).toMatchObject({ transactions: result.transactions });
expect(fake.state.updateCalls).toBe(1);
});

it("retries a concurrent upgrade against the new head", async () => {
const fake = createFakeGitHub();
fake.state.sha = "legacy-head";
Expand All @@ -215,13 +242,20 @@ describe("work queue Git store", () => {
});

it("never publishes an invalid or unsupported mixed-version log", async () => {
const fake = createFakeGitHub();
fake.state.sha = "invalid-head";
fake.state.transactions = [work("w")];
fake.state.rawContents = `${JSON.stringify({ kind: "Work", work: "w", claim: null, attempt: null })}\n${JSON.stringify({ ...claim("w", "c"), version: 99 })}\n`;
await expect(readWorkQueueLog({ githubClient: fake.githubClient, owner: "owner", repo: "repo" })).rejects.toThrow("Failed to read work queue log");
expect(fake.state.updateCalls).toBe(0);
expect(fake.blobs.size).toBe(0);
for (const invalid of [
{ ...claim("w", "c"), version: 99 },
{ kind: "Work", work: "invalid", claim: null, attempt: null, enqueued: 1 },
{ version: 0, kind: "Work", work: "invalid", claim: null, attempt: null, enqueued: 1 },
{ version: 1, kind: "Work", work: "invalid", claim: null, attempt: null, enqueued: 1 },
]) {
const fake = createFakeGitHub();
fake.state.sha = "invalid-head";
fake.state.transactions = [work("w")];
fake.state.rawContents = `${JSON.stringify({ kind: "Work", work: "w", claim: null, attempt: null })}\n${JSON.stringify(invalid)}\n`;
await expect(readWorkQueueLog({ githubClient: fake.githubClient, owner: "owner", repo: "repo" })).rejects.toThrow("Failed to read work queue log");
expect(fake.state.updateCalls).toBe(0);
expect(fake.blobs.size).toBe(0);
}
});

it("retains live legacy storage for read-only snapshots and checked writes", async () => {
Expand All @@ -241,12 +275,12 @@ describe("work queue Git store", () => {
githubClient: fake.githubClient,
owner: "owner",
repo: "repo",
intents: [{ version: 1, kind: "Completion", work: "w", claim: "c", attempt: "a" }],
intents: [{ version: CURRENT_VERSION, kind: "Completion", work: "w", claim: "c", attempt: "a" }],
sleepFn: async () => {},
});
expect(result.persisted).toBe(true);
expect(fake.state.transactions).toContainEqual(work("remote-work"));
expect(fake.state.transactions).toContainEqual({ version: 1, kind: "Completion", work: "w", claim: "c", attempt: "a" });
expect(fake.state.transactions).toContainEqual({ version: CURRENT_VERSION, kind: "Completion", work: "w", claim: "c", attempt: "a" });
expect(fake.state.updateCalls).toBe(2);
expect(fake.state.branch).toBe("dispatch-coordinator");
});
Expand Down
12 changes: 6 additions & 6 deletions actions/setup/js/work_queue_summary.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import path from "path";
import { main, renderSummary } from "./work_queue_summary.cjs";
import { replayTransactions, serializeTransactionLog } from "./work_queue_replay.cjs";

const work = id => ({ version: 1, kind: "Work", work: id, claim: null, attempt: null });
const claim = (id, claimId) => ({ version: 1, kind: "Claim", work: id, claim: claimId, attempt: null });
const work = id => ({ version: 2, kind: "Work", work: id, claim: null, attempt: null });
const claim = (id, claimId) => ({ version: 2, kind: "Claim", work: id, claim: claimId, attempt: null });
const initial = [work("done"), claim("done", "claim-a"), work("retry"), claim("retry", "claim-b"), work("cancel"), work("waiting")];
const latest = [
...initial,
{ version: 1, kind: "Completion", work: "done", claim: "claim-a", attempt: "run-1" },
{ version: 1, kind: "ClaimCancellation", work: "retry", claim: "claim-b", attempt: null },
{ version: 1, kind: "WorkCancellation", work: "cancel", claim: null, attempt: null },
{ version: 2, kind: "Completion", work: "done", claim: "claim-a", attempt: "run-1" },
{ version: 2, kind: "ClaimCancellation", work: "retry", claim: "claim-b", attempt: null },
{ version: 2, kind: "WorkCancellation", work: "cancel", claim: null, attempt: null },
work("new"),
claim("new", "claim-c"),
claim("new", "claim-d"),
Expand Down Expand Up @@ -98,7 +98,7 @@ describe("work queue conclusion summary", () => {

it("never renders untrusted queue identifiers or attempt data", () => {
const id = "</details>\n<script>secret</script>";
const summary = renderSummary(snapshot([]), replayTransactions([work(id), claim(id, "`unsafe|claim`"), { version: 1, kind: "Completion", work: id, claim: "`unsafe|claim`", attempt: "sensitive-attempt" }]));
const summary = renderSummary(snapshot([]), replayTransactions([work(id), claim(id, "`unsafe|claim`"), { version: 2, kind: "Completion", work: id, claim: "`unsafe|claim`", attempt: "sensitive-attempt" }]));
expect(summary).not.toContain(id);
expect(summary).not.toContain("unsafe");
expect(summary).not.toContain("sensitive-attempt");
Expand Down
Loading
Loading