Files
PaperClipAI/tests/runner-e2e/repository-verification.test.ts
T
507a0d7098 test: verify repository delivery after real runner conversion (#17089)
## Thinking Path

> - Paperclip manages agents that perform useful work.
> - Repository setup and Runner selection must preserve that work
context.
> - A successful provider turn cannot prove configuration retention or
usable delivery.
> - The repaired setup chain needs independent product checks with the
recorded models.
> - This PR adds explicit repository and conversion journeys to the
existing Product E2E harness.
> - The observer checks source bytes, Git identity, saved configuration,
and downloaded output.
> - Development and staging qualification remain separate.

## Linked Issues or Issue Description

Refs #17088 and #17047. The complete repository setup repair in #17088
is merged. No competing conversion journey was found in the open PR
search.

**What happened?**

Existing checks do not prove that a real legacy agent retains its
configured context when switched to Paperclip Runner and then delivers
verified repository work. The projectless repair check also uses
different default models from the recorded staging models.

**Expected behavior**

A configured repository task delivers correct, accessible output. A real
legacy-to-native conversion preserves configuration, instructions,
environment, and equivalent permission policy before doing subsequent
work. A projectless task continues through setup without supervision.

**Steps to reproduce**

List the configured-repository, legacy-conversion, and
automatic-repository-configured-models suites. Run one exact ID from the
README. These are explicit paid development checks, excluded from the
default selection.

## What Changed

- Add four configured-repository cells and four real legacy-conversion
cells for Codex gpt-6.1-sol and Claude claude-opus-5-5 on local and
Daytona environments.
- Seed a fresh reference only in an owned disposable Git checkout.
Compare the delivered JSON with its original bytes, hash, and commit.
- Switch the same real legacy agent through the public Runner selection
API. Compare allowlisted configuration and instruction digests before
and after conversion.
- Require one committed native run with the configured provider, model,
and environment, a visible registered download after reload, Done, and
no pending question.
- Add four projectless replay cells with the recorded staging models and
the unchanged strict continuation oracle.
- Use production hire instructions and existing sandbox profiles,
budgets, report pipeline, and cleanup. Record API-key and per-turn
lifecycle differences explicitly.
- Retain the required allowlisted API snapshot after successful
automatic setup. Preserve lifecycle IDs, state, ordering, and receipt
references without raw provider content. Verify both admissions retain
the configured provider and model.

## Verification

- Product E2E support after integrating the setup repair: 3,353 passed,
two existing skips; source prerequisites: 128 passed.
- Product E2E typecheck passes. Discovery lists all twelve new explicit
cells.
- Independent oracle calibration rejects wrong configuration,
instructions, provider, model, source bytes, hash, commit, downloaded
output, delivery, and pending questions.
- The first full support run exposed a catalog count mismatch and an
unprovisioned new profile ID. Both are fixed; the successful run uses
the existing trusted sandbox profile IDs without changing workflow
authority.
- Review found fixture prerequisite defects: optional CLI-only Codex
arguments and platform qualification after Runner selection. Both are
corrected; the disposable Claude baseline also excludes its unsupported
optional CLI turn limit. Seven new setup checks cover probe ordering,
absent qualification, immutable images, local conversion, and uncertain
probe accounting. The final focused cohort passes 86 checks and E2E
typecheck passes.
- The first qualification-regression run also had an incorrect test
assumption about the legacy timeout; the corrected red reaches the
unsupported extraArgs setting. No production conversion restriction was
relaxed.
- A retained four-cell development campaign delivered independently
verified ordinary Codex output and real Codex/Claude conversion results
with retained configuration. All three attempts nevertheless failed the
required evidence contract because api-state.json was missing. The flow
now saves allowlisted task/run lifecycle fields; it does not export
prompts, comments, interactions, configuration contents, or provider
outputs. The projectless cell delivered valid bytes but used a monitor
fallback and failed the unchanged continuation oracle; its driver
correction belongs to #17088. All four attempts remain failures; fresh
exact-candidate acceptance is pending. These API-key/per-turn checks do
not establish subscription authentication, warm reuse, Slack delivery,
or the complete milestone.
- Four fresh local ordinary-repository and conversion cells passed at
`c4e409d819cc0a703736d3f1ed3552064ca690b8`. Independent downloaded
artifacts and saved configuration evidence agree.
- The repaired automatic setup chain passed all six journey checks for
both models, but first failed because the success path omitted
api-state.json. The separate capture repair now passes the fresh
two-cell
[campaign](https://github.com/paperclipai/paperclip/actions/runs/39633381139)
at `7a7c26f6688ec6b0e0454562a43fd48ab1a81c97`. Both required API files
and their run/intent/dispatch bindings were independently checked.
Earlier failures remain failures.
- After merging current master, 114 workspace/admission/continuation
checks across six suites pass. The provider/model and evidence privacy
cohort passes 65 checks, and E2E typecheck passes. The final
model-binding assertion has unit calibration; fresh actual-model
acceptance of that assertion is still pending.
- Exact-head CI/review and complete staging acceptance remain pending.
Local development passes do not establish the 24-cell milestone.

## Risks

The real provider may expose additional conversion or setup defects.
Failures remain evidence and cannot be replaced by catalog registration
or a later retry. The conversion uses the production API and does not
inject a native result. Only synthetic data is seeded in a resolved
fixture-owned directory. Configuration evidence contains digests, not
raw environment values. This PR changes evaluation code, not production
behavior.

## Model Used

OpenAI Codex, GPT-6 family, with repository tools and code execution.
The runtime does not expose its exact deployment ID or context-window
size. Those values are not inferred.

## 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Paperclip <paperclip@paperclip.ing>
2026-10-21 11:51:50 -05:00

63 lines
4.9 KiB
TypeScript

import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { gradeRepositoryProof, repositoryConfigurationDigest, repositoryProofTasks, type RepositoryProofObservation } from "./repository-verification.js";
import { runnerSuites } from "./catalog.js";
function valid(): RepositoryProofObservation {
const sourceBytes = `${"a7".repeat(32)}\n`, commit = "b".repeat(40);
return { conversion: true, originalAdapter: "codex_local", expectedLegacyAdapter: "codex_local", finalAdapter: "paperclip_runner",
configurationBefore: "config", configurationAfter: "config", instructionsBefore: "instructions", instructionsAfter: "instructions",
permissionsPreserved: true, expectedModel: "gpt-6.1-sol", actualModel: "gpt-6.1-sol", expectedProvider: "codex", actualProvider: "codex",
environmentRetained: true, issueDone: true, runCount: 1, committedRuns: 1, sourceBytes, currentSourceBytes: sourceBytes,
expectedCommit: commit, downloaded: { reference: sourceBytes.trimEnd(), sourceSha256: createHash("sha256").update(sourceBytes).digest("hex"), commit },
downloadVerified: true, visibleDelivery: true, pendingInteractions: 0 };
}
const passes = (value: RepositoryProofObservation) => gradeRepositoryProof(value).every(check => check.passed);
describe("configured repository and conversion qualification", () => {
it("accepts independently verified actual conversion and ordinary delivery", () => {
expect(passes(valid())).toBe(true);
expect(passes({ ...valid(), conversion: false, originalAdapter: "paperclip_runner" })).toBe(true);
});
it.each([
{ originalAdapter: "paperclip_runner" }, { finalAdapter: "codex_local" },
{ configurationAfter: "changed environment reference" }, { instructionsAfter: "lost instructions" },
{ permissionsPreserved: false }, { environmentRetained: false }, { actualModel: "gpt-5.6-sol" },
{ actualProvider: "acpx:claude" }, { runCount: 2 }, { committedRuns: 0 }, { issueDone: false },
{ currentSourceBytes: null }, { currentSourceBytes: "recreated" }, { expectedCommit: "" },
{ downloadVerified: false }, { visibleDelivery: false }, { pendingInteractions: 1 },
])("rejects missing or contrary evidence %j", change => expect(passes({ ...valid(), ...change })).toBe(false));
it.each([
null, "a badge is green", [], {}, { reference: "wrong", sourceSha256: "wrong", commit: "wrong" },
])("does not accept provider success or malformed downloaded output %j", downloaded => expect(passes({ ...valid(), downloaded })).toBe(false));
it("rejects each incorrect result field and extra fields", () => {
const baseline = valid(), result = baseline.downloaded as Record<string, unknown>;
for (const field of ["reference", "sourceSha256", "commit"]) expect(passes({ ...baseline, downloaded: { ...result, [field]: "wrong" } })).toBe(false);
expect(passes({ ...baseline, downloaded: { ...result, success: true } })).toBe(false);
});
it("hashes retained configuration consistently without returning its contents", () => {
const before = { defaultEnvironmentId: "env", runtimeConfig: { heartbeat: { maxConcurrentRuns: 1 } },
adapterConfig: { model: "model", cwd: "/owned", env: { KEY: { type: "secret_ref", secretId: "sensitive-fixture-reference", version: 1 } }, timeoutSec: 240 } };
const reordered = { ...before, adapterConfig: { ...before.adapterConfig, env: { KEY: { version: 1, secretId: "sensitive-fixture-reference", type: "secret_ref" } } } };
expect(repositoryConfigurationDigest(before)).toBe(repositoryConfigurationDigest(reordered));
expect(repositoryConfigurationDigest(before)).toMatch(/^[a-f0-9]{64}$/);
expect(repositoryConfigurationDigest(before)).not.toContain("sensitive-fixture-reference");
expect(repositoryConfigurationDigest({ ...before, defaultEnvironmentId: "other" })).not.toBe(repositoryConfigurationDigest(before));
expect(repositoryConfigurationDigest({ ...before, adapterConfig: { ...before.adapterConfig, env: {} } })).not.toBe(repositoryConfigurationDigest(before));
});
it("registers explicit development cells with staging models and production instructions", () => {
for (const id of ["configured-repository", "legacy-conversion", "automatic-repository-configured-models"]) {
const suite = runnerSuites.find(suite => suite.id === id)!;
expect(suite.manualOnly).toBe(true);
expect(suite.groups).toEqual([]);
expect(suite.expectedMatrixSize).toBe(4);
expect(suite.profiles.map(profile => profile.model)).toEqual(["gpt-6.1-sol", "claude-opus-5-5"]);
expect(suite.definitionMetadata?.stagingQualification).toBe(false);
expect(suite.tasks).toHaveLength(1);
expect(suite.tasks[0]?.flow).toBe(id === "automatic-repository-configured-models" ? "automatic_repository" : "repository_verification");
expect(suite.tasks[0]?.automaticRetryPolicy).toBe("single_attempt");
}
expect(repositoryProofTasks.every(task => task.expectedRunCount === 1)).toBe(true);
});
});