mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 21:01:54 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Runner protocol qualification records real provider behavior in TestOps. > - Controlled attempts keep raw records and accounting in private storage. > - A live case passed, but a later public-render command rejected its controlled context. > - The missing public report caused the final result to be stored as broken. > - This pull request creates the closed TestOps result from existing sanitized records inside the private report job. > - The benefit is correct result reporting while the privacy guard stays effective. ## Linked Issues or Issue Description Refs #18985 and #19011. The related-PR search found no separate repair for this failure. **What happened?** [The live canary](https://github.com/paperclipai/paperclip/actions/runs/41916513378) passed all four provider checks. Its controller verified cleanup and closed its request accounting. The private canonical report recorded one passed case. The next command used `--public-viewer` while the controlled manifest remained set. The eval program rejected it with `controlled campaign evidence must remain private`. The closed exporter then had no public payload and correctly stored a broken result. **Expected behavior** Report the original case grade from sanitized structured records. Keep raw controlled records private. Keep the original checks, source bindings, and missing-evidence rules. **Steps to reproduce** Run one controlled protocol case with TestOps reporting enabled. Let the private canonical report finish. The following public-render step fails before the closed result can read the grade. **Paperclip version or commit** Observed at `4740d251b7032e714d33039ba66a56948238c38f`. This repair is based on `c6cb7e2a2d42743c0c3c4ead2cbad4a299f9a2e7`. **Deployment mode** GitHub-hosted Linux, controlled ACPX Claude protocol qualification, first workflow attempt, zero automatic retries. ## What Changed - Add a narrow `export-runs` path to the existing TestOps helper. Bind the sanitized score and artifact to the exact attempt, case, model, provider, driver, and public-projection schema. Reuse the existing strict grade and check validation. - Keep sanitization inside the private report job. Use structured records for controlled TestOps reporting. Keep the public viewer for legacy publication. - Add record identity, privacy, grade, command, and workflow routing controls. Wire the existing helper and workflow-security files into the required Node registry. Document the reporting path. ## Verification - 43 focused campaign/sanitizer, TestOps and workflow security tests passed. The command regression writes a raw attempt, calls the production sanitizer, and runs `export-runs` on its output. It also repeats that chain after a failed-check mutation. Both new-control files now run through required `test:typescript:prep`, not only the optional publisher script. - The actual required prep ran locally with provider credentials excluded. The eval-kernel and Rust builds passed. Its Node registry executed the new producer-to-export and security controls by name: 403 passed, 2 failed, 11 existing skips. The two original package-contract failures cannot resolve this isolated clone's Codex ACP and Claude ACP package links. This is not a green local full-prep claim. The first attempt also stopped at a missing Node-types link, before an ignored clone-local link to the locked cached version restored that prerequisite. - 72 release workflow tests passed with the existing locked pnpm 9.15.4 wrapper and local loopback permission. The first sandbox run failed because loopback fixtures could not listen; the default local tool path also failed two install fixtures. No test assertions were changed to resolve these failures. - The actual repaired workflow shell ran locally against the retained canary campaign and sanitized records. It emitted one closed passed result with all four original checks. No provider request or upload occurred. - Missing or failed checks still reject a pass. Private-only markers stay absent from the closed output. Wrong model, provider, driver, attempt, case, and schemas reject. - The unmodified, pinned eval program still rejects a controlled public-viewer command. - Actionlint and `git diff --check` passed. [Full final-head CI](https://github.com/paperclipai/paperclip/actions/runs/41927878886) passed on its first attempt at `0d0c090b1b`. Required verify/e2e gates, all four Runner lanes, typecheck, build, backend and browser shards passed. Package suite: 4951 passed, 10 existing skips across 297 passed files and one existing file skip. - The required static log contains passing lines for both the production sanitizer-to-export command regression and the closed-projection security control. The prior optional-script registration did not run those tests in required CI; the one script-entry fix now enforces them. Provider/runtime and workflow bytes are unchanged by the verification-only follow-ups. - Greptile: fresh 5/5 on `0d0c090b1b`, with no actionable new issues. The separate Paperclip Review requested a producer-to-consumer regression. That regression and its required-CI wiring are complete. The addressed thread is resolved; no open review threads remain. - No fresh deployed reporting pass is claimed. The failed workflow and its stored broken result remain unchanged. ## Risks The exporter reads structured records inside the private job. It copies only the closed result fields and requires the existing sanitized marker and identity checks. The final reporting job still receives only those files and verifies the saved result bindings. The Python privacy guard, provider path, case assertions, budgets, deadlines, and retries are unchanged. ## Model Used OpenAI Codex, exact model `gpt-6.1-sol`, max reasoning effort. The inherited runtime reports a 475,000-token context window. Capabilities used: reasoning, code changes, and repository, shell, and GitHub tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
283 lines
19 KiB
JavaScript
283 lines
19 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { spawnSync } from "node:child_process";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { contract, selectCases, inspectSelection, filterCatalog, projectResults, verifyPublication, standingPlanCases, protocolTestOpsFailure,
|
|
captureManualJobRunContext, parseOfficialJobRunContext, payloadFromSanitizedRun } from "./runner-protocol-testops.mjs";
|
|
import { testopsClient } from "../../../tests/runner-e2e/chat-feature-testops-api.mjs";
|
|
import { sanitizeProtocolEvalRuns } from "./runner-protocol-eval-campaign.mjs";
|
|
|
|
const context = { runId: "123", workflowSha: "a".repeat(40), sourceSha: "a".repeat(40), evalsSha: "b".repeat(40),
|
|
allureJobRunId: "7", jobId: 9, launchId: 11, accountingMode: "legacy" };
|
|
const planned = contract.cases.map(row => ({ id: row.testCaseId, selector: row.fullName }));
|
|
const selection = rows => selectCases(rows, context);
|
|
|
|
function sanitizedRun() {
|
|
const expected = contract.cases[0];
|
|
const cell = { rosterId: contract.rosterId, caseId: expected.caseId, model: contract.model, provider: contract.provider,
|
|
driver: contract.driver, finalAttemptId: "attempt-1", disposition: "passed", passed: true };
|
|
const score = { schema: "paperclip-runner/eval-score/v1", attemptId: cell.finalAttemptId, caseId: cell.caseId,
|
|
disposition: cell.disposition, passed: true, checks: Object.entries(expected.checks).map(([id, kind]) => ({ id, kind, passed: true })) };
|
|
const artifact = { schema: "paperclip-runner/eval-session-artifact/v1", attemptId: cell.finalAttemptId,
|
|
requestedModel: contract.model, provider: contract.provider, driver: contract.driver,
|
|
publication: { schema: "paperclip.runner-protocol-eval.public-chat/v1" }, privateExtra: "PRIVATE-SECRET" };
|
|
const campaign = { schema: "paperclip.runner-protocol-eval.campaign/v1", source: { paperclip: { sha: context.sourceSha },
|
|
evals: { sha: context.evalsSha }, workflowRunUrl: "https://github.com/paperclipai/paperclip/actions/runs/123" }, results: [cell] };
|
|
return { cell, score, artifact, campaign };
|
|
}
|
|
|
|
test("binds sanitized structured records without reading or forwarding private report content", () => {
|
|
const { cell, score, artifact, campaign } = sanitizedRun();
|
|
const input = { selection: selection(planned.slice(0, 1)), campaign,
|
|
payloads: new Map([[cell.finalAttemptId, payloadFromSanitizedRun(score, artifact, cell)]]) };
|
|
assert.equal(projectResults(input)[0].result.status, "passed");
|
|
assert.ok(!JSON.stringify(input.payloads.get(cell.finalAttemptId)).includes("PRIVATE-SECRET"));
|
|
for (const mutate of [value => { delete value.publication; }, value => { value.requestedModel = "other-model"; },
|
|
value => { value.provider = "other-provider"; }, value => { value.driver = "other-driver"; },
|
|
value => { value.attemptId = "other-attempt"; }, value => { value.schema = "raw-private-record"; }]) {
|
|
const changed = structuredClone(artifact); mutate(changed);
|
|
assert.throws(() => payloadFromSanitizedRun(score, changed, cell), /sanitized_payload_binding/);
|
|
}
|
|
for (const mutate of [value => { value.attemptId = "other-attempt"; }, value => { value.caseId = "other-case"; },
|
|
value => { value.passed = "true"; }, value => { value.schema = "other-score"; }]) {
|
|
const changed = structuredClone(score); mutate(changed);
|
|
assert.throws(() => payloadFromSanitizedRun(changed, artifact, cell), /sanitized_payload_binding/);
|
|
}
|
|
for (const change of [checks => checks.pop(), checks => { checks[0].passed = false; }]) {
|
|
const changed = structuredClone(input); change(changed.payloads.get(cell.finalAttemptId).checks);
|
|
assert.throws(() => projectResults(changed), /unsupported_pass/);
|
|
}
|
|
});
|
|
|
|
test("exports the production sanitizer output as closed TestOps files under a controlled context", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "runner-testops-structured-"));
|
|
try {
|
|
const { cell, score, artifact, campaign } = sanitizedRun();
|
|
const run = join(root, "raw-runs", cell.finalAttemptId);
|
|
await mkdir(run, { recursive: true });
|
|
delete artifact.publication;
|
|
artifact.providerSessionId = "PRIVATE-SECRET";
|
|
for (const [name, data] of [["selection.json", selectCases(planned.slice(0, 1), { ...context, accountingMode: "controlled" })], ["campaign.json", campaign],
|
|
[`raw-runs/${cell.finalAttemptId}/score.json`, score], [`raw-runs/${cell.finalAttemptId}/artifact.json`, artifact],
|
|
[`raw-runs/${cell.finalAttemptId}/case.json`, { id: cell.caseId, checks: score.checks }],
|
|
[`raw-runs/${cell.finalAttemptId}/config.json`, { schema: "paperclip-runner/eval-config/v1", id: "config",
|
|
model: contract.model, provider: contract.provider, driver: contract.driver }]]) {
|
|
await writeFile(join(root, name), JSON.stringify(data));
|
|
}
|
|
const sanitize = () => sanitizeProtocolEvalRuns({ runsRoot: join(root, "raw-runs"), publicRunsRoot: join(root, "runs") });
|
|
await sanitize();
|
|
const execute = output => spawnSync(process.execPath, [fileURLToPath(new URL("./runner-protocol-testops.mjs", import.meta.url)),
|
|
"export-runs", join(root, "selection.json"), join(root, "campaign.json"), join(root, "runs"), join(root, output)], {
|
|
encoding: "utf8", env: { PATH: process.env.PATH, PAPERCLIP_RUNNER_EVAL_CONTROL_MANIFEST: "private-manifest" } });
|
|
const positive = execute("positive");
|
|
assert.equal(positive.status, 0, positive.stderr);
|
|
assert.deepEqual(JSON.parse(positive.stdout), { reported: 1, passed: 1 });
|
|
const files = await readdir(join(root, "positive"));
|
|
assert.equal(files.length, 2);
|
|
const values = await Promise.all(files.map(file => readFile(join(root, "positive", file), "utf8")));
|
|
assert.ok(!values.join("").includes("PRIVATE-SECRET"));
|
|
assert.equal(JSON.parse(values.find(value => JSON.parse(value).status)).status, "passed");
|
|
score.checks[0].passed = false;
|
|
await writeFile(join(run, "score.json"), JSON.stringify(score));
|
|
await sanitize();
|
|
const failed = execute("failed");
|
|
assert.equal(failed.status, 1);
|
|
assert.match(failed.stderr, /runner_testops_unsupported_pass/);
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
function catalog() {
|
|
const roster = { rosterId: contract.rosterId, model: contract.model, provider: contract.provider,
|
|
driver: contract.driver, cases: contract.cases.map(row => row.caseId) };
|
|
return { schema: "paperclip.runner-protocol-eval.catalog/v1", source: { paperclipSha: context.sourceSha, evalsSha: context.evalsSha },
|
|
rosters: [roster], cells: roster.cases.map(caseId => ({ rosterId: roster.rosterId, caseId })), selection: { kind: "subset" } };
|
|
}
|
|
|
|
test("keeps all 41 plan cases and never promotes a targeted subset to full coverage", () => {
|
|
assert.equal(contract.cases.length, 41);
|
|
assert.equal(selection(planned).fullPlan, true);
|
|
const subset = selection(planned.slice(0, 2));
|
|
assert.equal(subset.fullPlan, false);
|
|
const filtered = filterCatalog(catalog(), subset);
|
|
assert.equal(filtered.cells.length, 2);
|
|
assert.equal(filtered.rosters[0].cases.length, 2);
|
|
assert.equal(filtered.selection.fullPlan, false);
|
|
assert.equal(filtered.matrices.flatMap(row => row.include).length, 2);
|
|
assert.equal(filterCatalog(catalog(), selection(planned)).cells.length, 41);
|
|
});
|
|
|
|
test("rejects foreign, duplicate, missing and mismatched selections before provider access", () => {
|
|
for (const rows of [[], [planned[0], planned[0]], [{ ...planned[0], id: 999 }], [{ ...planned[0], selector: planned[1].selector }]]) {
|
|
assert.throws(() => selection(rows), /runner_testops_/);
|
|
}
|
|
const selected = selection(planned);
|
|
for (const mutate of [value => { value.source.paperclipSha = "c".repeat(40); },
|
|
value => { value.rosters[0].model = "different"; }, value => { value.rosters[0].cases.pop(); }]) {
|
|
const value = catalog(); mutate(value);
|
|
assert.throws(() => filterCatalog(value, selected), /runner_testops_/);
|
|
}
|
|
assert.throws(() => filterCatalog(catalog(), { ...selected, schema: "other" }), /selection_identity/);
|
|
assert.throws(() => selectCases(planned, { ...context, accountingMode: "implicit" }), /accounting_mode/);
|
|
assert.throws(() => projectResults({ selection: { ...selected, private: "PRIVATE-SECRET" } }), /selection_identity/);
|
|
});
|
|
|
|
test("binds the selected TestOps job to this exact workflow run and standing plan", async () => {
|
|
const env = { ALLURE_JOB_RUN_ID: "7", RUNNER_TESTOPS_REQUESTED_JOB_RUN_ID: "7", GITHUB_RUN_ID: "123", GITHUB_SHA: context.workflowSha,
|
|
PAPERCLIP_PROTOCOL_EVAL_SOURCE_SHA: context.sourceSha, PAPERCLIP_PROTOCOL_EVALS_SHA: context.evalsSha,
|
|
PAPERCLIP_PROTOCOL_ACCOUNTING_MODE: "legacy" };
|
|
const run = { id: 7, job: { id: 9, type: "github", url: "https://github.com/paperclipai/paperclip/actions/workflows/runner-protocol-live-evals.yml" },
|
|
url: "https://github.com/paperclipai/paperclip/actions/runs/123", stage: "in_progress", launchId: 11 };
|
|
const job = { ...run.job };
|
|
const values = new Map([["/api/jobrun/7", run], ["/api/job/9", job], ["/api/launch/11", { id: 11, projectId: 1, closed: false }],
|
|
["/api/testplan/3", { id: 3, projectId: 1, name: contract.planName, testCasesCount: 41 }],
|
|
["/api/jobrun/7/plan?expected=true", planned.slice(0, 1)]]);
|
|
const api = async route => { assert.ok(values.has(route)); return values.get(route); };
|
|
for (const alias of [run.job.url, `https://github.com/paperclipai/paperclip/actions/workflows/${contract.workflowId}`,
|
|
"https://github.com/paperclipai/paperclip/blob/master/.github/workflows/runner-protocol-live-evals.yml"]) {
|
|
run.job.url = alias; job.url = alias;
|
|
assert.equal((await inspectSelection(api, env)).cases.length, 1);
|
|
}
|
|
run.job.url = "https://github.com/paperclipai/paperclip/actions/workflows/other.yml";
|
|
await assert.rejects(inspectSelection(api, env), /jobrun_binding/);
|
|
run.job.url = "https://github.com/paperclipai/paperclip/actions/workflows/runner-protocol-live-evals.yml";
|
|
job.url = run.job.url;
|
|
delete run.job.url;
|
|
assert.equal((await inspectSelection(api, env)).cases.length, 1);
|
|
const manual = { version: "1.0", tests: planned.map(row => ({ ...row, id: String(row.id) })) };
|
|
const manualEnv = await captureManualJobRunContext({ ...env, ALLURE_JOB_RUN_ID: "", RUNNER_TESTOPS_REQUESTED_JOB_RUN_ID: "",
|
|
ALLURE_ENDPOINT: "https://paperclip.testops.cloud", ALLURE_PROJECT_ID: "1" },
|
|
async () => ({ stdout: "ALLURE_JOB_RUN_ID=7\nALLURE_LAUNCH_ID=11\n" }));
|
|
const admittedManual = await inspectSelection(api, manualEnv, manual);
|
|
assert.equal(admittedManual.fullPlan, false);
|
|
assert.equal(admittedManual.cases.length, 1);
|
|
assert.equal(admittedManual.cases[0].testCaseId, 194);
|
|
await assert.rejects(inspectSelection(api, { ...manualEnv, RUNNER_TESTOPS_EXPECTED_LAUNCH_ID: "12" }, manual), /jobrun_binding/);
|
|
const controlled = await inspectSelection(api, { ...env, PAPERCLIP_PROTOCOL_ACCOUNTING_MODE: "controlled" });
|
|
assert.equal(controlled.accountingMode, "controlled");
|
|
assert.equal(projectResults({ selection: controlled })[0].evidence.accountingMode, "controlled");
|
|
assert.match(projectResults({ selection: controlled })[0].result.description, /accounting mode: controlled/);
|
|
await assert.rejects(inspectSelection(api, { ...env, PAPERCLIP_PROTOCOL_ACCOUNTING_MODE: undefined }), /accounting_mode/);
|
|
await assert.rejects(inspectSelection(api, env, manual), /selection_mode/);
|
|
assert.throws(() => standingPlanCases({ ...manual, tests: manual.tests.slice(0, 1) }), /standing_plan_coverage/);
|
|
assert.throws(() => standingPlanCases({ ...manual, version: "other" }), /standing_plan/);
|
|
run.url = "https://github.com/paperclipai/paperclip/actions/runs/122";
|
|
await assert.rejects(inspectSelection(api, env), /jobrun_binding/);
|
|
});
|
|
|
|
test("captures only official numeric manual job context without evaluating or exposing its environment", async () => {
|
|
const env = { RUNNER_TESTOPS_REQUESTED_JOB_RUN_ID: "", ALLURE_JOB_RUN_ID: "",
|
|
ALLURE_ENDPOINT: "https://paperclip.testops.cloud", ALLURE_PROJECT_ID: "1" };
|
|
const captured = await captureManualJobRunContext(env, async (command, args, options) => {
|
|
assert.equal(command, "allurectl");
|
|
assert.deepEqual(args, ["job-run", "env"]);
|
|
assert.equal(options.shell, false);
|
|
assert.equal(options.timeout, 30_000);
|
|
assert.equal(options.maxBuffer, 16_384);
|
|
return { stdout: "ALLURE_TOKEN=PRIVATE-SECRET\nALLURE_JOB_RUN_ID=2550\r\nALLURE_LAUNCH_ID=2588\n" };
|
|
});
|
|
assert.equal(captured.ALLURE_JOB_RUN_ID, "2550");
|
|
assert.equal(captured.RUNNER_TESTOPS_EXPECTED_LAUNCH_ID, "2588");
|
|
assert.ok(!JSON.stringify(captured).includes("PRIVATE-SECRET"));
|
|
for (const text of ["", "ALLURE_JOB_RUN_ID=7\n", "ALLURE_JOB_RUN_ID=7\nALLURE_LAUNCH_ID=0\n",
|
|
"ALLURE_JOB_RUN_ID=7\nALLURE_LAUNCH_ID=11\nALLURE_JOB_RUN_ID=7\n",
|
|
"ALLURE_JOB_RUN_ID=$(private)\nALLURE_LAUNCH_ID=11\n", "x".repeat(16_385)]) {
|
|
assert.throws(() => parseOfficialJobRunContext(text), /bootstrap_context/);
|
|
}
|
|
await assert.rejects(captureManualJobRunContext({ ...env, ALLURE_JOB_RUN_ID: "9" },
|
|
async () => ({ stdout: "ALLURE_JOB_RUN_ID=7\nALLURE_LAUNCH_ID=11\n" })), /bootstrap_context/);
|
|
await assert.rejects(captureManualJobRunContext({ ...env, RUNNER_TESTOPS_REQUESTED_JOB_RUN_ID: "7" },
|
|
async () => { throw new Error("must not execute"); }), /bootstrap_context/);
|
|
await assert.rejects(captureManualJobRunContext(env, async () => { throw new Error("PRIVATE-SECRET"); }), /bootstrap_context/);
|
|
});
|
|
|
|
test("reports every unexecuted selected case as broken when the report is unavailable", () => {
|
|
const rows = projectResults({ selection: selection(planned) });
|
|
assert.equal(rows.length, 41);
|
|
assert.ok(rows.every(row => row.result.status === "broken" && row.evidence.recorded === false));
|
|
assert.equal(new Set(rows.map(row => row.result.fullName)).size, 41);
|
|
});
|
|
|
|
test("only current-run successful behavioral evidence becomes a pass", () => {
|
|
const selected = selection(planned.slice(0, 1));
|
|
const expected = selected.cases[0];
|
|
const cell = { rosterId: contract.rosterId, caseId: expected.caseId, model: contract.model, provider: contract.provider,
|
|
driver: contract.driver, finalAttemptId: "attempt-1", disposition: "passed", passed: true };
|
|
const campaign = { schema: "paperclip.runner-protocol-eval.campaign/v1", source: { paperclip: { sha: context.sourceSha },
|
|
evals: { sha: context.evalsSha }, workflowRunUrl: "https://github.com/paperclipai/paperclip/actions/runs/123" }, results: [cell] };
|
|
const payload = { attemptId: "attempt-1", caseId: expected.caseId, disposition: "passed", passed: true,
|
|
publication: { schema: "paperclip.runner-protocol-eval.public-chat/v1" },
|
|
checks: Object.entries(expected.checks).map(([id, kind]) => ({ id, kind, passed: true })) };
|
|
const payloads = new Map([["attempt-1", payload]]);
|
|
assert.equal(projectResults({ selection: selected, campaign, payloads })[0].result.status, "passed");
|
|
cell.model = "other-model";
|
|
assert.throws(() => projectResults({ selection: selected, campaign, payloads }), /campaign_binding/);
|
|
cell.model = contract.model;
|
|
assert.throws(() => projectResults({ selection: selected, campaign }), /unsupported_pass/);
|
|
payload.checks[0].passed = false;
|
|
assert.throws(() => projectResults({ selection: selected, campaign, payloads }), /unsupported_pass/);
|
|
payload.checks[0].passed = true;
|
|
payload.passed = false;
|
|
assert.throws(() => projectResults({ selection: selected, campaign, payloads }), /payload_binding/);
|
|
payload.passed = true;
|
|
cell.usage = { inputTokens: 10, unexpected: "PRIVATE-SECRET" };
|
|
const projected = projectResults({ selection: selected, campaign, payloads })[0];
|
|
assert.equal(projected.evidence.schema, "paperclip.runner-protocol-testops-evidence/v1");
|
|
assert.deepEqual(projected.evidence.usage, { inputTokens: 10 });
|
|
assert.ok(!JSON.stringify(projected).includes("PRIVATE-SECRET"));
|
|
payload.checks[0].id = "PRIVATE-SECRET";
|
|
assert.throws(() => projectResults({ selection: selected, campaign, payloads }), /check_binding/);
|
|
payload.checks[0].id = Object.keys(expected.checks)[0];
|
|
cell.usage.inputTokens = "PRIVATE-SECRET";
|
|
assert.throws(() => projectResults({ selection: selected, campaign, payloads }), /usage_schema/);
|
|
cell.usage.inputTokens = 10;
|
|
campaign.source.evals.sha = "c".repeat(40);
|
|
assert.throws(() => projectResults({ selection: selected, campaign, payloads }), /campaign_binding/);
|
|
});
|
|
|
|
test("retains closed API failure codes and status without raw messages or fabricated codes", async () => {
|
|
await assert.rejects(testopsClient("private-token", async () => new Response("private-body", { status: 401 })), error => {
|
|
assert.deepEqual(protocolTestOpsFailure(error), { code: "testops_auth_http", httpStatus: 401 });
|
|
return true;
|
|
});
|
|
assert.deepEqual(protocolTestOpsFailure(new Error("runner_testops_job_binding")), { code: "runner_testops_job_binding", httpStatus: null });
|
|
const unknown = protocolTestOpsFailure(Object.assign(new Error("PRIVATE-SECRET"), { code: "testops_auth_http", status: 401 }));
|
|
assert.deepEqual(unknown, { code: "runner_testops_execution_failed", httpStatus: null });
|
|
assert.ok(!JSON.stringify(unknown).includes("PRIVATE-SECRET"));
|
|
});
|
|
|
|
test("preserves real behavior failures and infrastructure failures", () => {
|
|
const selected = selection(planned.slice(0, 1));
|
|
for (const [disposition, status] of [["behavior_failure", "failed"], ["infrastructure_failure", "broken"]]) {
|
|
const cell = { rosterId: contract.rosterId, caseId: selected.cases[0].caseId, model: contract.model, provider: contract.provider,
|
|
driver: contract.driver, finalAttemptId: "attempt-1", disposition, passed: false };
|
|
const campaign = { schema: "paperclip.runner-protocol-eval.campaign/v1", source: { paperclip: { sha: context.sourceSha },
|
|
evals: { sha: context.evalsSha }, workflowRunUrl: "https://github.com/paperclipai/paperclip/actions/runs/123" }, results: [cell] };
|
|
const payload = { attemptId: "attempt-1", caseId: cell.caseId, disposition, passed: false,
|
|
publication: { schema: "paperclip.runner-protocol-eval.public-chat/v1" },
|
|
checks: Object.entries(selected.cases[0].checks).map(([id, kind]) => ({ id, kind, passed: false })) };
|
|
assert.equal(projectResults({ selection: selected, campaign, payloads: new Map([["attempt-1", payload]]) })[0].result.status, status);
|
|
}
|
|
});
|
|
|
|
test("waits for ingestion but rejects wrong current-run verdicts and bindings", async () => {
|
|
const selected = selection(planned.slice(0, 1)), expected = selected.cases[0];
|
|
const receipt = { selection: selected, results: [{ testCaseId: expected.testCaseId, fullName: expected.fullName, status: "passed" }] };
|
|
const detail = { id: 33, fullName: expected.fullName, projectId: 1, launchId: selected.launchId,
|
|
jobRun: { id: Number(selected.allureJobRunId) }, status: "passed" };
|
|
let reads = 0;
|
|
const api = async route => route.startsWith("/api/testresult?")
|
|
? ++reads < 2 ? { totalElements: 0, totalPages: 0, content: [] }
|
|
: { totalElements: 1, totalPages: 1, content: [{ id: 33, testCaseId: expected.testCaseId }] }
|
|
: detail;
|
|
assert.equal((await verifyPublication(api, receipt, { attempts: 2, pause: async () => {} })).verified, 1);
|
|
detail.status = "failed";
|
|
await assert.rejects(verifyPublication(api, receipt), /result_mismatch/);
|
|
detail.status = "passed"; detail.jobRun.id++;
|
|
await assert.rejects(verifyPublication(api, receipt), /result_mismatch/);
|
|
await assert.rejects(verifyPublication(async () => ({ totalElements: 0, totalPages: 0, content: [] }), receipt,
|
|
{ attempts: 2, pause: async () => {} }), /result_missing/);
|
|
});
|