Files
PaperClipAI/tests/runner-e2e/reliability-testops.test.mjs
T
DottaandClaude Opus 5.5 02a2299173 feat(codex): default Codex to gpt-6.1-sol, half the gpt-5.6-sol price (#19623)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Codex agents run through Paperclip Runner or the legacy
`codex_local` harness, and the runner E2E suites run paid Codex cells
> - The shared Codex default and many selections preferred `gpt-5.6-sol`
> - The reviewed OpenAI rate card in
`server/src/services/codex-pricing.ts` (`openai-standard-2026-09-30`)
prices `gpt-6.1-sol` at $2 input / $0.10 cached / $10 output per 1M
tokens, and `gpt-5.6-sol` at $4 / $0.40 / $20
> - So every default run and every paid Codex eval cell cost twice as
much as necessary
> - This pull request moves every place that prefers or selects
`gpt-5.6-sol` to `gpt-6.1-sol`, and keeps historical facts, rate cards
and compatibility fallbacks on `gpt-5.6-sol`
> - The benefit is half the token price for new default Codex agents and
for the Codex E2E and TestOps cells

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The default Codex model for new agents and the Codex model that the
runner E2E suites and TestOps plans select.

**Subsystem affected**
Adapters (`adapter-utils`, `codex-local`), server native runtime
fallback, UI AI connection pools, Paperclip Runner devtools and live
evals, runner E2E catalog and TestOps mappings.

**Current behavior**
New Codex agents without an explicit model store `gpt-5.6-sol`. The
runner E2E `runner-codex` profile, the Reliability `queue-steer` plan
cell, the Product regression TestOps mapping and the ACPX Codex
qualification selection all use `gpt-5.6-sol`.

**Proposed behavior**
They use `gpt-6.1-sol`. Existing agents that store a model keep it.
`gpt-5.6-sol` stays selectable.

**Reason and benefit**
`gpt-6.1-sol` costs half as much per token on the in-repo reviewed rate
card. The current OpenAI pricing page (retrieved 2026-11-06) shows the
same short-context rates and also lists long-context rates of $4 / $0.20
/ $15 for `gpt-6.1-sol` against $8 / $0.80 / $30 for `gpt-5.6-sol`.

**Breaking changes**
None for stored configurations. See Risks.

## What Changed

Changed defaults and selections (all now `gpt-6.1-sol`):

- `PAPERCLIP_RUNNER_DEFAULT_MODELS.codex` in
`packages/adapter-utils/src/paperclip-runner-permissions.ts`. This also
changes `DEFAULT_CODEX_LOCAL_MODEL`,
`resolvePaperclipRunnerModel("codex", …)`, runner config normalization,
`codex_local` → Paperclip Runner conversion, the UI new-agent and
agent-config defaults, the composer default model and the public MCP
effort lookup.
- `codex-local` `models`: the 5.6 entry is now the literal
`gpt-5.6-sol`, because it used to be `DEFAULT_CODEX_LOCAL_MODEL`. The
list order and contents do not change.
- AI connection pool: a new OpenAI member profile uses
`PAPERCLIP_RUNNER_DEFAULT_MODELS.codex` instead of a hard-coded
`gpt-5.6-sol`.
- Paperclip Runner devtools ACPX Codex default and preset, the
`runner-codex-sol` local provider smoke, and the `acpx-codex-sol` live
workflow candidate.
- `tests/acpx-qualification-models.ts` Codex selection (runner E2E and
runner acceptance ACPX Codex profiles).
- Runner E2E: the `runner-codex` profile follows the default, so all
native Codex cells move. This includes the `runner-codex` cells among
the 52 standard `everyday-workflows` cells and the seven Codex
reliability cells. `runner-codex-mini` stays on `gpt-5.4-mini`. The
Reliability plan `queue-steer` `steeringProfile` is now
`['runner-codex', 'gpt-6.1-sol']`. Its executionId
(`everyday-workflows.runner-codex.daytona.queue-steer`), its fullName
and its TestOps case identity do not change. No separate profile is
necessary.
- `product-regression-testops.mjs`: the declared model for every
`runner-codex` case is now `gpt-6.1-sol` (it was already that for
`configured-repository`).
- `tests/runner-e2e/installed-cli.ts`: the credential-free installed
startup proof opens a session on the default model.
- `tests/runner-e2e/scheduled-prices.json`: add a `gpt-6.1-sol` price
(`openai-standard-2026-11-06`, including 30m/12h cache writes and
long-context rates from the OpenAI pricing page). The weekly scheduled
Codex cells need an exact price for their model, or admission fails
closed.
- Storybook: the composer model picker fixtures, the connection pool
stories and host, and the agent settings preview show `gpt-6.1-sol` as
the default.
- Docs: `doc/DEVELOPING.md` (the Codex conversion default) and
`tests/runner-e2e/EVERYDAY-WORKFLOWS.md` (local matrix, reliability
extras, `queue-steer` row).
- Tests that assert the default now expect `gpt-6.1-sol`. New
assertions: the default has the `0.159.0` CLI floor, the compatibility
fallback is floor-free, the `queue-steer` plan cell runs `runner-codex`
on `gpt-6.1-sol`, and all native Codex everyday cells (standard and
reliability) use `gpt-6.1-sol`.

Deliberately kept on `gpt-5.6-sol`:

- Rate cards: `server/src/services/codex-pricing.ts` and
`packages/paperclip-runner/src/evals/model-pricing.ts`. Old receipts
need them. (The runner eval rate card has no `gpt-6.1-sol` entry. An
unpriced candidate is reported as unpriced, not as zero. I did not add a
rate there because that card has its own version and source.)
- The remote Codex CLI compatibility fallback. A new
`CODEX_LOCAL_COMPATIBILITY_FALLBACK_MODEL = "gpt-5.6-sol"` replaces the
default in `codexModelFallbackCandidates`. `gpt-6.1-sol` needs Codex
0.159.0 or newer, so it cannot be the floor-free last resort. Fallback
behavior is unchanged.
- `tests/runner-e2e/provider-usage.ts`: the implicit request
cache-policy attribution stays qualified only for `gpt-5.6-sol`. I did
not verify that policy for `gpt-6.1-sol`. A `gpt-6.1-sol` response that
omits the write-TTL split keeps its cache-write cost unknown, which is
the existing behavior for unqualified models.
- `tests/runner-e2e/scheduled-measurements.json`: historical
`gpt-5.6-sol` measurements. They no longer match the current suite
definitions, so the forecast reports them as historical only. The
forecast tests now build their synthetic cohorts on the current
definition hash.
- Model catalogs that list `gpt-5.6-sol` as available (`codex-local`,
`cursor-local`, `opencode-local`, Speko voice diagnostics, Pi picker
fixture).
- The `gpt-5.6` → `gpt-5.6-sol` alias for old stored configs.
- Dated or evidence docs: `doc/plans/*`, `doc/adapter-model-audit-*`,
`QUALIFICATION-*`, `doc/connections/MEMORY.md`,
`doc/connections/TASK-REVIEWS-VERIFICATION.md`,
`tests/runner-e2e/CHAT-FEATURES.md`, the Slack eval evidence, and the
S49 soak signature text (the soak failure happened on `gpt-5.6-sol`).
- Test fixtures that parse, price, migrate or round-trip an existing
`gpt-5.6-sol` value (Rust runner tests, driver tests, pricing tests,
stored-config tests, stories that show an existing agent).

## Verification

- `npx vitest run` on every server, UI, adapter-utils and codex-local
test file that mentions `gpt-5.6-sol` or the default, plus all of
`packages/adapter-utils` and `packages/adapters/codex-local`: 5,483
passed. Three timeouts under load (`sandbox-managed-runtime`,
`acp.quota`) passed when rerun.
- `packages/paperclip-runner`: `vitest run` for `src/eval`, `src/evals`,
`src/drivers/acpx`, `src/drivers/codex`, `src/cli`, `src/contracts`,
`src/backends`: 2,641 passed.
- `node --test tests/runner-e2e/reliability-testops.test.mjs
tests/runner-e2e/product-regression-testops.test.mjs`: 28 passed.
- `node tests/runner-e2e/run-unit.mjs`: all model, catalog, reliability,
scheduled and TestOps tests pass. Failures remain only in
`copilot-protection-flow`, `denied-target-isolation-flow`,
`pi-controls-flow`, `workflow-target` and `unit-launcher`. These five
also fail on unmodified master in this local environment (filesystem
watch and git environment), and they do not touch models.
- `vitest run --config tests/runner-acceptance/vitest.config.ts`: 19
passed.
- `pnpm test:e2e:runner -- --list --suite everyday-workflows --group
reliability`: the seven `everyday-workflows.runner-codex.daytona.<case>`
cells show `gpt-6.1-sol` with `OPENAI_API_KEY,DAYTONA_API_KEY`.
`--matrix-json` gives 16 reliability cells with the same credentials as
before. `--list --all` shows no `gpt-5.6-sol` cell.
- Typecheck: `adapter-utils`, `adapter-codex-local`, `server`, `ui`,
`paperclip-runner` (TypeScript), and `pnpm test:e2e:runner:typecheck`.
- `pnpm check:token-gates`: clean.
- Not run: paid cells, Rust tests (no Rust change), the browser suites,
and the full `pnpm -r typecheck` / `pnpm build` (CI runs them).

## Risks

- Agents without an explicit model change model on their next save or
normalization, and Paperclip Runner rows with a blank model use
`gpt-6.1-sol` at execution. Stored models, including `gpt-5.6-sol`, are
never rewritten. No migration is necessary.
- `gpt-6.1-sol` needs Codex CLI 0.159.0 or newer with ChatGPT sign-in.
The runner pins `@openai/codex@0.160.0`. On a remote target with an
older image CLI, the existing compatibility check falls back to
`gpt-6-sol` and then `gpt-5.6-sol`, and that probe now runs for
default-model agents too. A legacy `codex_local` agent created with the
new default on a host with an older Codex CLI gets the existing
actionable "upgrade Codex" Test result.
- Qualification: the native Codex profile uses `adapter_constant` model
qualification, so it follows the default. No qualification gate is
dropped. The ACPX Codex qualification selection moves to `gpt-6.1-sol`.
Its next qualification run uses the new model. The provider-usage
cache-policy attribution stays limited to `gpt-5.6-sol`.
- Runner E2E history: the Codex cells change their suite definition
hashes, so earlier `gpt-5.6-sol` results are a separate cohort and do
not count as current.
- The `gpt-6.1-sol` scheduled price uses the file's existing `expiresAt`
(2026-11-19). It needs the same review as the other entries before then.
- No `.github/` change. The Reliability queue-steer cell keeps the
`runner-codex` profile id, so the existing workflow credential and setup
steps still match.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, running in
Claude Code with tool use (shell, file edits, web fetch of the OpenAI
pricing page).

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Related: #19604 (commits the Reliability TestOps identities; this PR
changes only `steeringProfile` in the same file).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:35:34 -06:00

343 lines
25 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { parse } from 'yaml';
import { productRegressionCases, projectProductResults, selectProductRegression, testOpsPlanDetails, testOpsPlans,
validateSelection, workflowUrl } from './product-regression-testops.mjs';
import { productTestOps } from './product-regression-testops-cli.mjs';
import { registeredReliabilityCases, reliabilityDefinitions, reliabilityModel, reliabilityPlanName, reliabilityProfile,
reliabilityRegistration } from './reliability-testops.mjs';
import { admitRegistration, inspectReliability, registerReliability, registrationReceipt, registrationSnippet,
registrationWorkflowRef } from './reliability-testops-registration.mjs';
// Synthetic identities exercise the bound path only. Real IDs come from TestOps.
const synthetic = { planId: 9001, caseIds: Object.fromEntries(reliabilityDefinitions.map((row, index) => [row.name, 9101 + index])) };
const plans = testOpsPlans(synthetic);
const cases = plans.reliability;
const workflowSha = 'a'.repeat(40), targetSha = 'b'.repeat(40);
const env = {
GITHUB_REPOSITORY: 'paperclipai/paperclip', GITHUB_REF: 'refs/heads/master', GITHUB_EVENT_NAME: 'workflow_dispatch',
GITHUB_WORKFLOW_REF: 'paperclipai/paperclip/.github/workflows/runner-full-stack-e2e.yml@refs/heads/master',
GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '1', GITHUB_SHA: workflowSha, SOURCE_SHA: targetSha,
ALLURE_ENDPOINT: 'https://paperclip.testops.cloud', ALLURE_PROJECT_ID: '1',
};
const selection = selected => ({ schema: 'paperclip.product-regression-testops-selection/v1', runId: '123', attempt: '1',
workflowSha, targetSha, jobRunId: '456', launchId: 789, cases: selected });
const result = (row, overrides = {}) => ({
executionId: row.executionId, suiteId: row.suite, caseId: row.name, profileId: row.profile, environmentId: row.environment,
source: { sha: targetSha }, attempt: 1, status: 'passed', cleanup: 'passed', model: row.model,
matcherResults: [{ matcher: { kind: 'json_path', path: 'resume.retry-works-first-use', expected: true }, passed: true }], ...overrides,
});
const campaign = (selected, results) => ({ schema: 'paperclip.runner-e2e.campaign/v2', campaignId: 'gha-123-1',
source: { sha: targetSha }, expected: selected.map(row => row.executionId), results });
test('the plan is six DeepSeek 1017 cells plus native Codex for steering, Daytona only for sandbox signatures', () => {
assert.equal(reliabilityDefinitions.length, 7);
assert.equal(new Set(reliabilityDefinitions.map(row => row.executionId)).size, 7);
assert.ok(reliabilityDefinitions.every(row => row.fullName === `paperclip.Product E2E.everyday-workflows.${row.name}`));
// OpenCode declares steering: false, so only queue-steer uses the steering-capable native Codex cell.
assert.deepEqual(reliabilityDefinitions.filter(row => row.profile !== reliabilityProfile || row.model !== reliabilityModel)
.map(row => [row.name, row.profile, row.model, row.environment]), [['queue-steer', 'runner-codex', 'gpt-6.1-sol', 'daytona']]);
assert.deepEqual(reliabilityDefinitions.filter(row => row.profile === reliabilityProfile && row.environment === 'daytona').map(row => row.name),
['three-turn-revise', 'long-silent-command', 'queued-across-restart']);
assert.equal(testOpsPlanDetails.reliability.size, 7);
assert.equal(testOpsPlanDetails['product-regression'].size, 20);
});
test('the committed registration is either absent or complete, and selection fails closed until it exists', () => {
const committed = registeredReliabilityCases(reliabilityRegistration, productRegressionCases.map(row => row.id));
if (committed === null) {
assert.equal(testOpsPlans().reliability, null);
assert.throws(() => selectProductRegression(undefined, 'all', false, 'reliability'), /product_regression_testops_reliability_unregistered/);
assert.throws(() => selectProductRegression(undefined, 'queue-steer', false, 'reliability'), /reliability_unregistered/);
} else {
assert.equal(committed.length, 7);
assert.equal(selectProductRegression(undefined, 'all', false, 'reliability').length, 7);
}
// Plan 2 keeps its exact standing bootstrap behavior.
assert.equal(selectProductRegression(undefined, 'all').length, 20);
assert.equal(selectProductRegression(undefined, '66')[0].name, 'message-marker');
});
test('a partial, duplicate, foreign or plan-2 registration is rejected rather than selecting a subset', () => {
const reserved = productRegressionCases.map(row => row.id);
const { 'queue-steer': _omitted, ...partial } = synthetic.caseIds;
for (const invalid of [{ ...synthetic, planId: null }, { ...synthetic, planId: 2 }, { ...synthetic, caseIds: partial },
{ ...synthetic, caseIds: { ...synthetic.caseIds, 'queue-steer': synthetic.caseIds['reassign-midrun'] } },
{ ...synthetic, caseIds: { ...synthetic.caseIds, 'queue-steer': 66 } },
{ ...synthetic, caseIds: { ...synthetic.caseIds, 'queue-steer': '9105' } },
{ ...synthetic, caseIds: { ...synthetic.caseIds, unknown: 1 } }]) {
assert.throws(() => registeredReliabilityCases(invalid, reserved), /reliability_registration_invalid/);
}
assert.equal(registeredReliabilityCases({ planId: null, caseIds: {} }, reserved), null);
});
test('a TestOps launch selects one plan by its case identities and never mixes the plans', () => {
const all = { version: '1.0', tests: cases.map(row => ({ id: String(row.id), selector: row.fullName })) };
assert.deepEqual(selectProductRegression(all, 'all', true, 'product-regression', plans).map(row => row.name),
reliabilityDefinitions.map(row => row.name));
const subset = { version: '1.0', tests: [{ id: String(cases[5].id), selector: cases[5].fullName }] };
assert.deepEqual(selectProductRegression(subset, 'all', true, 'product-regression', plans), [cases[5]]);
const plan2 = productRegressionCases[0];
assert.throws(() => selectProductRegression({ version: '1.0', tests: [...subset.tests, { id: String(plan2.id), selector: plan2.fullName }] },
'all', true, 'product-regression', plans), /plan_mixed/);
assert.throws(() => selectProductRegression({ version: '1.0', tests: [{ id: String(cases[0].id), selector: cases[1].fullName }] },
'all', true, 'product-regression', plans), /case_mismatch/);
// Without the reviewed identities, a TestOps reliability launch cannot bind any case.
assert.throws(() => selectProductRegression(subset, 'all', true, 'product-regression', testOpsPlans({ planId: null, caseIds: {} })), /case_mismatch/);
assert.throws(() => selectProductRegression(undefined, 'all', false, 'unknown', plans), /plan_invalid/);
assert.equal(selectProductRegression(undefined, 'long-silent-command', false, 'reliability', plans)[0].environment, 'daytona');
validateSelection(selection(cases), env, plans);
assert.throws(() => validateSelection(selection(cases), env, testOpsPlans({ planId: null, caseIds: {} })));
});
test('projection keeps each cell environment, a seven-case denominator and the original verdicts', () => {
const projected = projectProductResults(campaign(cases, cases.map(row => result(row))), selection(cases), targetSha);
assert.deepEqual({ ...projected.summary, cases: undefined },
{ selected: 7, passed: 7, failed: 0, broken: 0, plan: 'reliability', fullPlan: true, cases: undefined });
const daytona = cases.find(row => row.name === 'queued-across-restart');
const records = projected.records.filter(row => row.file.endsWith('-result.json')).map(row => row.value);
const restart = records.find(row => row.name === 'queued-across-restart');
assert.equal(restart.parameters.find(row => row.name === 'environment').value, 'daytona / GitHub Actions');
assert.ok(restart.labels.some(row => row.name === 'tag' && row.value === 'soak-S55/U9'));
assert.equal(restart.labels.find(row => row.name === 'AS_ID').value, String(daytona.id));
const attachments = projected.records.filter(row => !row.file.endsWith('-result.json')).map(row => row.value);
assert.equal(attachments.find(row => row.executionId === daytona.executionId).environment, 'daytona');
// The environment is part of the binding: a local result cannot stand in for a Daytona cell.
assert.throws(() => projectProductResults(campaign([daytona], [result(daytona, { environmentId: 'local' })]), selection([daytona]), targetSha),
/result_binding/);
for (const [failureClass, status] of [['candidate_failure', 'failed'], ['provider_variance', 'failed'], ['transient_infrastructure', 'broken']]) {
const row = projectProductResults(campaign([daytona], [result(daytona, { status: 'failed', failureClass })]), selection([daytona]), targetSha);
assert.equal(row.summary.cases[0].status, status);
assert.equal(row.summary.fullPlan, false);
}
const missing = projectProductResults(undefined, selection(cases), targetSha);
assert.equal(missing.summary.broken, 7);
assert.throws(() => projectProductResults(campaign([daytona], [result(daytona, { model: 'gpt-5.6-sol' })]), selection([daytona]), targetSha),
/metadata_invalid/);
assert.throws(() => projectProductResults(campaign(cases, []), selection([...cases, productRegressionCases[0]]), targetSha),
/selection_binding/);
});
test('a manual reliability bootstrap fails before opening any TestOps run until registration exists', async t => {
const root = await mkdtemp(path.join(tmpdir(), 'reliability-testops-'));
t.after(() => rm(root, { recursive: true, force: true }));
const runtime = { ...env, RUNNER_TEMP: root, GITHUB_OUTPUT: path.join(root, 'output'), ALLURE_TOKEN: 'synthetic-test-token',
TESTOPS_PLAN: 'reliability', TESTOPS_CASE: 'all', ALLURE_JOB_RUN_ID: '' };
let opened = false;
const exec = async () => { opened = true; return { stdout: '' }; };
const fetchImpl = async () => { opened = true; throw new Error('unexpected network'); };
await assert.rejects(productTestOps('select', path.join(root, 'selection.json'), runtime, exec, fetchImpl, undefined,
testOpsPlans({ planId: null, caseIds: {} })), /reliability_unregistered/);
await assert.rejects(productTestOps('select', path.join(root, 'selection.json'), { ...runtime, TESTOPS_PLAN: 'unknown' }, exec, fetchImpl),
/plan_invalid/);
assert.equal(opened, false);
});
test('a TestOps reliability launch binds, projects and verifies through the standing plan path', async t => {
const root = await mkdtemp(path.join(tmpdir(), 'reliability-testops-launch-'));
t.after(() => rm(root, { recursive: true, force: true }));
const selectionFile = path.join(root, 'selection.json');
const runtime = { ...env, RUNNER_TEMP: root, GITHUB_OUTPUT: path.join(root, 'output'), GITHUB_STEP_SUMMARY: path.join(root, 'summary'),
ALLURE_JOB_RUN_ID: '456', ALLURE_TOKEN: 'synthetic-test-token', TESTOPS_CASE: '', TESTOPS_PLAN: 'product-regression',
REPORT_DIR: path.join(root, 'report'), ALLURE_RESULTS: path.join(root, 'allure'), EXPORT_SUMMARY: path.join(root, 'export.json') };
const uploaded = cases.map((row, index) => ({ id: 600 + index, testCaseId: row.id, fullName: row.fullName, projectId: 1,
launchId: 789, jobRun: { id: 456 }, status: 'passed' }));
const fetchImpl = async url => {
const route = new URL(url).pathname;
const resources = {
'/api/uaa/oauth/token': { access_token: 'synthetic-access-token' },
'/api/jobrun/456': { id: 456, launchId: 789, job: { id: 50, type: 'github' }, url: 'https://github.com/paperclipai/paperclip/actions/runs/123' },
'/api/job/50': { id: 50, type: 'github', url: workflowUrl }, '/api/launch/789': { id: 789, projectId: 1 },
'/api/testresult': { content: uploaded.map(row => ({ id: row.id, testCaseId: row.testCaseId })), totalPages: 1, totalElements: uploaded.length },
...Object.fromEntries(uploaded.map(row => [`/api/testresult/${row.id}`, row])),
};
assert.ok(resources[route], route);
return new Response(JSON.stringify(resources[route]), { status: 200, headers: { 'Content-Type': 'application/json' } });
};
const exec = async (command, args) => {
if (args[1] === 'env') return { stdout: 'ALLURE_JOB_RUN_ID=456\nALLURE_LAUNCH_ID=789\n' };
if (args[1] === 'plan') await writeFile(args.at(-1), JSON.stringify({ version: '1.0', tests: cases.map(row => ({ id: String(row.id), selector: row.fullName })) }));
return { stdout: '' };
};
assert.equal(await productTestOps('select', selectionFile, runtime, exec, fetchImpl, undefined, plans), 0);
const output = await readFile(runtime.GITHUB_OUTPUT, 'utf8');
assert.match(output, new RegExp(`ids=${cases.map(row => row.executionId.replaceAll('.', '\\.')).join(',')}\\n`));
runtime.SELECTION_SHA256 = output.match(/selection_sha256=([a-f0-9]{64})/)[1];
await mkdir(path.join(runtime.REPORT_DIR, 'normalized'), { recursive: true });
await writeFile(path.join(runtime.REPORT_DIR, 'normalized', 'normalized-results.json'), JSON.stringify(campaign(cases, cases.map(row => result(row)))));
assert.equal(await productTestOps('project', selectionFile, runtime, exec, fetchImpl, undefined, plans), 0);
assert.match(await readFile(runtime.GITHUB_STEP_SUMMARY, 'utf8'), /Reliability TestOps results.*\n\n7\/7 passed.*Complete 7-case plan selection/);
assert.equal(await productTestOps('verify', selectionFile, runtime, exec, fetchImpl, async () => {}, plans), 0);
// The committed (unregistered) mapping cannot validate a selection bound with other identities.
if (testOpsPlans().reliability === null) {
await assert.rejects(productTestOps('project', selectionFile, { ...runtime, ALLURE_RESULTS: path.join(root, 'other'),
EXPORT_SUMMARY: path.join(root, 'other.json') }, exec, fetchImpl), /case_mismatch/);
}
});
function fakeTestOps({ existing = [], plan = null, assignments = [], standing = [{ job: { id: 50, type: 'github', url: workflowUrl, parameters: [] }, testCasesCount: 20 }] } = {}) {
const state = { cases: new Map([[66, { id: 66, projectId: 1, fullName: productRegressionCases.find(row => row.id === 66).fullName }],
...existing.map(row => [row.id, { projectId: 1, automated: true, ...row }])]), plan, assignments, calls: [], nextId: 9500 };
const api = async (route, method = 'GET', body) => {
state.calls.push({ route, method, body });
const url = new URL(`https://paperclip.testops.cloud${route}`);
if (method === 'GET' && url.pathname === '/api/testcase') {
const rows = [...state.cases.keys()].sort((a, b) => a - b).map(id => ({ id }));
return { content: rows, number: 0, totalPages: 1, totalElements: rows.length };
}
const caseMatch = /^\/api\/testcase\/(\d+)$/.exec(url.pathname);
if (method === 'GET' && caseMatch) return state.cases.get(Number(caseMatch[1]));
if (method === 'POST' && url.pathname === '/api/testcase') {
const id = state.nextId++; state.cases.set(id, { id, ...body }); return { id };
}
if (method === 'GET' && url.pathname === '/api/testplan') return { content: state.plan ? [{ id: state.plan.id, name: state.plan.name }] : [], totalPages: 1 };
if (method === 'POST' && url.pathname === '/api/testplan') {
state.plan = { id: 77, projectId: 1, name: body.name, treeSelection: body.treeSelection, testCasesCount: body.treeSelection.leafsInclude.length };
// Live TestOps lists a new plan's uncovered cases as one job-less row.
state.assignments = [{ testCasesCount: body.treeSelection.leafsInclude.length }];
return { id: 77 };
}
if (method === 'GET' && url.pathname === '/api/testplan/77') return state.plan;
if (method === 'PATCH' && url.pathname === '/api/testplan/77') {
state.plan = { ...state.plan, treeSelection: body.treeSelection, testCasesCount: body.treeSelection.leafsInclude.length }; return null;
}
if (method === 'POST' && url.pathname === '/api/testplan/77/jobparameter') {
state.assignments = state.assignments.map(row => row.job.id === body.jobId ? { ...row, job: { ...row.job, parameters: body.parameters } } : row);
return null;
}
if (method === 'GET' && url.pathname === '/api/testplan/2/job') return standing;
if (method === 'GET' && url.pathname === '/api/job/50') return { id: 50, type: 'github', url: workflowUrl };
if (method === 'GET' && url.pathname === '/api/testplan/77/job') return state.assignments;
if (method === 'POST' && url.pathname === '/api/testplan/77/assign') {
state.assignments = [{ job: { id: body.jobId, parameters: [] }, testCasesCount: body.selection.leafsInclude.length }]; return null;
}
throw new Error(`unexpected ${method} ${route}`);
};
return { api, state };
}
test('registration is admitted only from the trusted dispatch and reads before it writes', async () => {
const admitted = { ...env, GITHUB_WORKFLOW_REF: registrationWorkflowRef, REGISTRATION_PLAN: 'reliability', REGISTRATION_OPERATION: 'inspect' };
assert.equal(admitRegistration(admitted), 'inspect');
for (const mutation of [{ GITHUB_REF: 'refs/heads/candidate' }, { GITHUB_EVENT_NAME: 'pull_request_target' },
{ GITHUB_WORKFLOW_REF: env.GITHUB_WORKFLOW_REF }, { ALLURE_ENDPOINT: 'https://attacker.invalid' },
{ REGISTRATION_OPERATION: 'delete' }, { REGISTRATION_PLAN: 'product-regression' }]) {
assert.throws(() => admitRegistration({ ...admitted, ...mutation }), /untrusted_context/);
}
const { api, state } = fakeTestOps();
const inspected = await inspectReliability(api);
assert.deepEqual(inspected.caseIds, {});
assert.equal(inspected.jobId, 50);
assert.equal(state.calls.some(row => row.method !== 'GET'), false);
const receipt = registrationReceipt('inspect', inspected, admitted);
assert.equal(receipt.complete, false);
assert.equal(receipt.missing.length, 7);
});
test('registration creates only missing cases, one plan on the standing job, and is idempotent', async () => {
const reused = reliabilityDefinitions[0];
const { api, state } = fakeTestOps({ existing: [{ id: 300, fullName: reused.fullName }] });
const first = await registerReliability(api);
assert.equal(first.caseIds[reused.name], 300);
assert.deepEqual(first.created.cases, reliabilityDefinitions.slice(1).map(row => row.name));
assert.equal(first.created.plan, true);
assert.equal(first.created.assignment, true);
assert.equal(state.plan.name, reliabilityPlanName);
assert.deepEqual(state.plan.treeSelection.leafsInclude, reliabilityDefinitions.map(row => first.caseIds[row.name]));
for (const row of reliabilityDefinitions.slice(1)) {
const created = state.cases.get(first.caseIds[row.name]);
assert.equal(created.fullName, row.fullName);
assert.equal(created.automated, true);
assert.match(created.description, new RegExp(row.executionId.replaceAll('.', '\\.')));
}
const receipt = registrationReceipt('register', first, { GITHUB_RUN_ID: '1', GITHUB_SHA: workflowSha });
assert.equal(receipt.complete, true);
assert.equal(registeredReliabilityCases(receipt.registration, productRegressionCases.map(row => row.id)).length, 7);
assert.match(registrationSnippet(receipt), /planId: 77,\n {2}caseIds: Object\.freeze\(\{\n {4}'interrupt-redirect': 300,/);
const writes = state.calls.filter(row => row.method !== 'GET').length;
const second = await registerReliability(api);
assert.deepEqual(second.caseIds, first.caseIds);
assert.equal(state.calls.filter(row => row.method !== 'GET').length, writes);
});
test('registration copies the standing plan job parameters and repairs a drifted plan selection once', async () => {
const parameters = [{ name: 'all', value: 'false' }];
const { api, state } = fakeTestOps({ standing: [{ job: { id: 50, type: 'github', url: workflowUrl, parameters }, testCasesCount: 20 }] });
const first = await registerReliability(api);
assert.equal(first.created.parameters, true);
assert.deepEqual(state.calls.filter(row => row.route === '/api/testplan/77/jobparameter').map(row => row.body),
[{ jobId: 50, parameters }]);
assert.deepEqual(state.assignments[0].job.parameters, parameters);
const writes = state.calls.filter(row => row.method !== 'GET').length;
assert.equal((await registerReliability(api)).created.parameters, false);
assert.equal(state.calls.filter(row => row.method !== 'GET').length, writes);
// A plan whose selection drifted is patched back to exactly the seven cases.
state.plan = { ...state.plan, treeSelection: { ...state.plan.treeSelection, leafsInclude: [first.caseIds['queue-steer']] }, testCasesCount: 1 };
await registerReliability(api);
assert.deepEqual(state.calls.filter(row => row.method === 'PATCH').map(row => row.route), ['/api/testplan/77']);
assert.deepEqual(state.plan.treeSelection.leafsInclude, reliabilityDefinitions.map(row => first.caseIds[row.name]));
// A parameter write that does not read back fails instead of leaving a plan that dispatches differently.
const stale = fakeTestOps({ standing: [{ job: { id: 50, type: 'github', url: workflowUrl, parameters }, testCasesCount: 20 }] });
const original = stale.api;
const ignoringParameters = async (route, method, body) => route.endsWith('/jobparameter') ? null : original(route, method, body);
await assert.rejects(registerReliability(ignoringParameters), /plan_job/);
});
test('registration refuses a standing plan without exactly one trusted job or a plan bound to another job', async () => {
await assert.rejects(registerReliability(fakeTestOps({ standing: [] }).api), /standing_job/);
await assert.rejects(registerReliability(fakeTestOps({ standing: [{ job: { id: 51, type: 'github', url: 'https://github.com/other/repo' } }] }).api), /standing_job/);
const { api, state } = fakeTestOps();
await registerReliability(api);
state.assignments = [{ job: { id: 99 }, testCasesCount: 7 }];
await assert.rejects(registerReliability(api), /plan_job_conflict/);
});
test('a plan TestOps reports only as a job-less row is unassigned, so registration assigns the standing job', async () => {
const { api, state } = fakeTestOps();
await registerReliability(api);
// Reproduces staging plan 135: created, then the run stopped before assignment.
state.assignments = [{ testCasesCount: 7 }];
const inspected = await inspectReliability(api);
assert.deepEqual(inspected.assignments, []);
assert.equal(registrationReceipt('inspect', inspected, env).complete, false);
const repaired = await registerReliability(api);
assert.equal(repaired.created.assignment, true);
assert.deepEqual(state.assignments.map(row => row.job?.id), [50]);
assert.equal(registrationReceipt('register', repaired, { GITHUB_RUN_ID: '1', GITHUB_SHA: workflowSha }).complete, true);
});
test('the workflows expose the reliability plan without new provider or reporting authority', async () => {
const full = parse(await readFile(new URL('../../.github/workflows/runner-full-stack-e2e.yml', import.meta.url), 'utf8'));
assert.deepEqual(full.on.workflow_dispatch.inputs.testops_plan.options, ['product-regression', 'reliability']);
assert.equal(full.on.workflow_dispatch.inputs.testops_plan.default, 'product-regression');
// Not run per PR or on the bounded cron: only a TestOps launch or manual dispatch selects the plan.
assert.deepEqual(Object.keys(full.on).sort(), ['schedule', 'workflow_dispatch']);
const selectionStep = full.jobs.testops_selection.steps.find(step => step.id === 'selection');
assert.equal(selectionStep.env.TESTOPS_PLAN, "${{ inputs.testops_plan || 'product-regression' }}");
const testEnv = full.jobs.test.steps.flatMap(step => Object.entries(step.env ?? {}));
assert.ok(testEnv.some(([name, value]) => name === 'OPENROUTER_API_KEY' && value === "${{ matrix.credentialName == 'OPENROUTER_API_KEY' && secrets.OPENROUTER_API_KEY || '' }}"));
assert.ok(testEnv.some(([name, value]) => name === 'DAYTONA_API_KEY' && value === "${{ matrix.environmentId == 'daytona' && secrets.DAYTONA_API_KEY || '' }}"));
const opencodeSteps = full.jobs.test.steps.filter(step => /OpenCode executable|remote provider pack/.test(step.name ?? ''));
assert.ok(opencodeSteps.length >= 3);
assert.ok(opencodeSteps.every(step => String(step.if).includes("startsWith(matrix.profileId, 'runner-opencode')")));
const registration = parse(await readFile(new URL('../../.github/workflows/runner-e2e-testops-registration.yml', import.meta.url), 'utf8'));
assert.deepEqual(Object.keys(registration.on), ['workflow_dispatch']);
assert.equal(registration.jobs.register.environment, 'runner-e2e-testops-reporting');
assert.deepEqual(registration.jobs.register.needs, 'authorize');
// The only gate in front of ALLURE_TOKEN: default branch plus the numeric actor allowlist.
const gate = registration.jobs.authorize.steps[0];
assert.equal(gate.env.ALLOWED_ACTOR_IDS, '${{ vars.RUNNER_E2E_ALLOWED_ACTOR_IDS }}');
assert.equal(gate.env.REF, '${{ github.ref }}');
assert.match(gate.run, /if \[ "\$REF" != "refs\/heads\/\$DEFAULT_BRANCH" \]; then/);
assert.match(gate.run, /type == "array" and length > 0 and all\(\.\[\]; type == "number"/);
assert.match(gate.run, /for candidate in "\$triggering_actor_id" "\$ACTOR_ID"; do/);
assert.equal(registration.jobs.authorize.environment, undefined);
assert.equal(JSON.stringify(registration.jobs.authorize).includes('secrets.'), false);
const text = JSON.stringify(registration);
assert.doesNotMatch(text, /secrets\.(OPENAI|OPENROUTER|ANTHROPIC|DAYTONA)/);
assert.equal(registration.jobs.register.steps[0].with.ref, '${{ github.sha }}');
});