import assert from "node:assert/strict"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { buildProtocolEvalPointers, createProtocolEvalBundleManifest, emptyProtocolEvalHistory, isPublicProtocolEvalPath, mergeProtocolEvalHistory, protocolEvalHistoryRecord, renderProtocolEvalHistoryIndex, validateProtocolEvalHistoryDestination, validatePublicProtocolEvalReport, writeProtocolEvalPublicationLinks, } from "./publish-runner-protocol-eval-history.mjs"; const roots = []; test("successful publication exposes exact report and history links to Actions", async () => { const root = await mkdtemp(join(tmpdir(), "evalbook-publication-links-")); roots.push(root); const environment = { GITHUB_OUTPUT: join(root, "outputs"), GITHUB_STEP_SUMMARY: join(root, "summary") }; const result = { campaignId: "gha-42-2", reportUrl: "https://reports.example/runner-protocol-evals/campaigns/gha-42-2/index.html", historyUrl: "https://reports.example/runner-protocol-evals/index.html", }; await writeProtocolEvalPublicationLinks(result, environment); assert.equal(await readFile(environment.GITHUB_OUTPUT, "utf8"), `report_url=${result.reportUrl}\nhistory_url=${result.historyUrl}\n`); const summary = await readFile(environment.GITHUB_STEP_SUMMARY, "utf8"); assert.ok(summary.includes(`[Open this run's Evalbook](<${result.reportUrl}>)`)); assert.ok(summary.includes(`[All eval runs](<${result.historyUrl}>)`)); await assert.rejects(writeProtocolEvalPublicationLinks({ ...result, reportUrl: "https://example.test/\nreport_url=bad" }, environment)); assert.throws(() => renderProtocolEvalHistoryIndex(emptyProtocolEvalHistory(), "https://untrusted.example/style.css")); }); test.afterEach(async () => { await Promise.all( roots.splice(0).map((root) => rm(root, { recursive: true, force: true })), ); }); function campaign(overrides = {}) { return { schema: "paperclip.runner-protocol-eval.campaign/v1", campaignId: "gha-42-1", generatedAt: "2026-09-05T00:00:00.000Z", source: { paperclip: { sha: "a".repeat(40), ref: "refs/heads/master" }, evals: { repository: "paperclipai/paperclip-evals", sha: "b".repeat(40) }, }, complete: true, allPassed: true, totals: { selected: 35, passed: 35, behaviorFailures: 0, infrastructureFailures: 0, }, rosters: [ { rosterId: "protocol-live-mini", model: "gpt-5.4-mini", provider: "codex", driver: "codex_app_server", selected: 35, passed: 35, }, ], results: [], ...overrides, }; } async function reportFixture() { const root = await mkdtemp(join(tmpdir(), "runner-protocol-public-")); roots.push(root); await Promise.all([ mkdir(join(root, "tests"), { recursive: true }), mkdir(join(root, "attempts"), { recursive: true }), ]); await Promise.all([ writeFile(join(root, "campaign.json"), JSON.stringify(campaign())), writeFile( join(root, "index.html"), 'testattempt', ), writeFile( join(root, "tests/get-context.html"), 'overview', ), writeFile( join(root, "attempts/attempt-01.html"), 'overview', ), ]); return root; } test("accepts only credential-free HTTPS destinations and the dedicated prefix", () => { assert.deepEqual( validateProtocolEvalHistoryDestination({ bucket: "paperclip-public-reports", prefix: "/runner-protocol-evals/", publicBaseUrl: "https://reports.paperclip.ing/", }), { bucket: "paperclip-public-reports", prefix: "runner-protocol-evals", publicBaseUrl: "https://reports.paperclip.ing", }, ); assert.throws( () => validateProtocolEvalHistoryDestination({ bucket: "bad", prefix: "../evals", publicBaseUrl: "http://example.test", }), /prefix|HTTPS/, ); }); test("keeps publication to the canonical static Evalbook surface", () => { for (const file of [ "index.html", "latest.html", "inventory.html", "tests/get-context.html", "attempts/attempt-01.html", "attempts/attempt-01/index.html", "viewer/assets/index-build.js", "campaign.json", ]) { assert.equal(isPublicProtocolEvalPath(file), true, file); } for (const file of [ "runs/attempt/artifact.json", "provider-trace.log", "../secret", "assets/app.js", ]) { assert.equal(isPublicProtocolEvalPath(file), false, file); } }); test("validates inert linked reports and creates deterministic manifests", async () => { const root = await reportFixture(); const result = await validatePublicProtocolEvalReport(root); assert.equal(result.files.length, 4); const first = await createProtocolEvalBundleManifest(root, "gha-42-1"); const second = await createProtocolEvalBundleManifest(root, "gha-42-1"); assert.deepEqual(second, first); assert.match(first.bundleDigest, /^[0-9a-f]{64}$/); }); test("rejects scripts, remote resources, raw sessions, secret-shaped values, and broken links", async () => { for (const bad of [ "", 'remote', '
{"providerSessionId":"session"}',
`${"sk-" + "a".repeat(24)}`,
'broken',
]) {
const root = await reportFixture();
await writeFile(join(root, "index.html"), bad);
await assert.rejects(validatePublicProtocolEvalReport(root));
}
});
test("retains immutable history and independent latest-green pointers", () => {
const green = protocolEvalHistoryRecord(
campaign(),
"https://reports.example/runner-protocol-evals",
);
const red = protocolEvalHistoryRecord(
campaign({
campaignId: "gha-43-1",
generatedAt: "2026-09-05T01:00:00.000Z",
allPassed: false,
totals: {
selected: 35,
passed: 34,
behaviorFailures: 1,
infrastructureFailures: 0,
},
rosters: [{ ...campaign().rosters[0], passed: 34 }],
}),
"https://reports.example/runner-protocol-evals",
);
let history = mergeProtocolEvalHistory(emptyProtocolEvalHistory(), green);
history = mergeProtocolEvalHistory(history, red);
assert.equal(history.latestCampaignId, "gha-43-1");
assert.equal(history.latestGreenCampaignId, "gha-42-1");
const pointers = buildProtocolEvalPointers(history);
assert.equal(pointers.latest.campaign.campaignId, "gha-43-1");
assert.equal(pointers.latestGreen.campaign.campaignId, "gha-42-1");
const index = renderProtocolEvalHistoryIndex(history, "campaigns/gha-43-1/viewer/assets/index.css");
assert.match(index, /class="evalbook-site"/);
assert.match(index, /href="campaigns\/gha-43-1\/viewer\/assets\/index.css"/);
assert.doesNotMatch(index, /