Files
PaperClipAI/tests/runner-e2e/feedback-persistence.test.ts
T
DottaandPaperclip 40c1ab36cd feat(evals): register explicit Daytona feedback persistence (#17986)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Product E2E evals check real server, browser, runner, and provider
behavior.
> - Feedback persistence already checks saved complaints and suggestions
with exact bodies and run attribution.
> - Its catalog registers only local cells, so a Daytona campaign cannot
select the same workflow.
> - This PR registers the existing workflow for the two native Daytona
profiles and tests their gateway bindings.
> - Remote cells stay explicit-only and unqualified until a bounded live
run passes.

## Linked Issues or Issue Description

Refs #17980 and #17975. No duplicate open PR or issue was found in the
Daytona feedback search.

**What existing behavior does this improve?**

The `feedback-persistence` Product E2E suite and its explicit execution
selectors.

**Current behavior**

At master `d54f776ca0`, the suite has only local Claude and Codex cells.
Selecting the matching Daytona ID fails before admission. The gateway
manifest already supports these native remote profiles.

**Proposed behavior**

Register four cells: local and Daytona for each native profile. Reuse
the existing saved-body and completed-tool oracle. Require an immutable
image and Daytona credentials. Keep one attempt, one expected work run,
five-minute local and inherited 15-minute remote case timeouts, and the
existing 60-minute provider creation TTL.

**Reason and benefit**

A bounded remote qualification can check both a simple task and real
feedback persistence. Local evidence remains separate from remote
acceptance.

**Breaking changes**

Explicit `--suite feedback-persistence` now lists four cells instead of
two. `--all` still excludes the suite. Daily and weekly manifests still
select their existing exact local IDs.

## What Changed

- Register the two native Daytona feedback cells and increment the suite
definition version.
- Address the remote cold-start review: inherit the base task's
15-minute Daytona deadline and retain the five-minute local limit. The
regression failed before the fix and now passes.
- Check immutable-image requirements, credentials, lifetime, and shared
gateway connections in existing contract tests.
- Fix the config type in the TTL catalog test added by #17980. Keep its
runtime lifetime assertions.
- Document remote selection and its unqualified status in the existing
eval guides.

## Verification

- Before the catalog change, the registration regression failed: one
failed and 16 passed.
- Focused catalog, feedback, operations, ledger, manifest, and summary
tests: 114 passed across six files.
- Read-only discovery lists exactly the two Daytona feedback IDs.
- The Node test phase passes separately: 137 tests. The retained Vitest
database failure prevents the unit launcher from reaching that phase.
- Full Product support suite: 4,523 passed, three skipped, and one
embedded PostgreSQL bootstrap failure. The same database failure was
reproduced on a clean baseline before this PR.
- Fresh Linux Product typecheck passed at the review-fix commit:
`GOMEMLIMIT=2GiB GOGC=50 GOMAXPROCS=2 node
node_modules/typescript/bin/tsc -p tests/runner-e2e/tsconfig.json`. The
default run and the ineffective JavaScript-heap attempt exited 137 and
remain retained. The compiler launches a native child process; its
memory controls let the unchanged semantic check complete. Host
typecheck still has 15 stale installed DB/Runner export errors; the two
TTL test typing errors are fixed. All 58 latest-head checks are
acceptable (success, neutral, or skipped). Both fresh reviews are 5/5,
with no unresolved review threads.
- The updated credential-free Linux controller discovers both planned
remote IDs and launches Chromium 151.0.7922.34. Runtime build inputs are
unchanged from the prior controller. The Daytona image content ID is
still
`b55f03dc9c633db300b70206a691ff45a8e779e56a3c546f9358e3282d3597c9`.
- No model requests, sandbox allocation, or paid schedule changes.

## Risks

- Selecting the whole explicit suite now includes remote cells and
requires Daytona configuration. Use exact IDs for a bounded paid
campaign.
- Registration and contract tests do not prove live remote behavior or
token accounting. Remote qualification remains pending.
- The shared dependency checkout limits the local host typecheck. The
fresh Linux semantic check passed. Earlier compiler OOM failures and the
database bootstrap failure remain retained.

## Model Used

- OpenAI GPT-6 through Codex, with tool use and code execution. The
exact serving model ID and context window are not exposed in this
session.

## 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>
2026-10-28 00:54:04 -05:00

82 lines
5.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { expectedFixtureFeedback, feedbackPersistenceDigest, gradeFeedbackPersistence } from "./feedback-persistence.js";
import { runnerMatrix, runnerTasks, suiteDefinitionHash } from "./catalog.js";
import { parseRunnerSelectors, selectRunnerExecutions } from "./selectors.js";
function fixture() {
const nonce = "fixture-nonce", bodies = expectedFixtureFeedback(nonce);
return { nonce, companyId: "company", agentId: "agent", taskId: "task", runIds: ["run"],
evidence: { version: 1, companyId: "company", summary: { collection: "complete", invocationCollection: "complete" },
entries: (["complaint", "suggestion"] as const).map(kind => ({ id: kind, kind, body: bodies[kind],
agent: { id: "agent" }, task: { id: "task" }, runId: "run" })),
invocations: (["complaint", "suggestion"] as const).map(kind => ({ callId: `${kind}-call`, kind, runId: "run", outcome: "completed" })),
},
};
}
describe("feedback persistence oracle", () => {
it("changes the suite hash when the inherited prompt or matchers change", () => {
const base = runnerTasks.find(task => task.id === "message-marker")!;
const suite = runnerMatrix.find(execution => execution.suite.id === "feedback-persistence")!.suite;
const baseline = suiteDefinitionHash(suite);
expect(suite.definitionMetadata?.sourceDigest).toBe(feedbackPersistenceDigest(base));
for (const changed of [
{ ...base, buildPrompt: () => "A different completion contract" },
{ ...base, buildMatchers: () => [] },
]) {
expect(suiteDefinitionHash({ ...suite, definitionMetadata: {
...suite.definitionMetadata, sourceDigest: feedbackPersistenceDigest(changed),
} })).not.toBe(baseline);
}
});
it("accepts independently retained bodies with same-run real tool receipts", () => {
expect(gradeFeedbackPersistence(fixture()).every(check => check.passed)).toBe(true);
});
it.each([
["missing saved evidence", (f: ReturnType<typeof fixture>) => { f.evidence.entries = []; }],
["prose without tool receipts", (f: ReturnType<typeof fixture>) => { f.evidence.invocations = []; }],
["wrong saved body", (f: ReturnType<typeof fixture>) => { f.evidence.entries[0]!.body = "submitted successfully"; }],
["foreign company", (f: ReturnType<typeof fixture>) => { f.evidence.companyId = "other"; }],
["foreign task", (f: ReturnType<typeof fixture>) => { f.evidence.entries[0]!.task.id = "other"; }],
["foreign agent", (f: ReturnType<typeof fixture>) => { f.evidence.entries[0]!.agent.id = "other"; }],
["foreign run", (f: ReturnType<typeof fixture>) => { f.evidence.entries[0]!.runId = "other"; }],
["unrelated successful receipt", (f: ReturnType<typeof fixture>) => { f.evidence.invocations[0]!.runId = "other"; }],
["failed tool", (f: ReturnType<typeof fixture>) => { f.evidence.invocations[0]!.outcome = "failed"; }],
["partial collection", (f: ReturnType<typeof fixture>) => { f.evidence.summary.collection = "partial"; }],
["partial invocation scan", (f: ReturnType<typeof fixture>) => { f.evidence.summary.invocationCollection = "partial"; }],
["duplicate submission", (f: ReturnType<typeof fixture>) => { f.evidence.invocations.push(f.evidence.invocations[0]!); }],
["duplicate saved row", (f: ReturnType<typeof fixture>) => { f.evidence.entries.push(f.evidence.entries[0]!); }],
] as const)("rejects %s", (_name, mutate) => {
const f = fixture(); mutate(f);
expect(gradeFeedbackPersistence(f).some(check => !check.passed)).toBe(true);
});
it("fails closed on unavailable or malformed retained evidence", () => {
for (const evidence of [undefined, null, {}, { entries: [null], invocations: [null] }]) {
expect(gradeFeedbackPersistence({ ...fixture(), evidence }).some(check => !check.passed)).toBe(true);
}
});
it("registers explicit local and Daytona native cases with no automatic retry", () => {
const selected = selectRunnerExecutions(parseRunnerSelectors(["--suite", "feedback-persistence"]));
expect(selected.map(execution => execution.id)).toEqual([
"feedback-persistence.runner-codex.local.complaint-and-suggestion",
"feedback-persistence.runner-codex.daytona.complaint-and-suggestion",
"feedback-persistence.runner-acpx-claude.local.complaint-and-suggestion",
"feedback-persistence.runner-acpx-claude.daytona.complaint-and-suggestion",
]);
expect(selected.every(execution => execution.task.automaticRetryPolicy === "single_attempt")).toBe(true);
expect(selected.every(execution => execution.task.attemptTimeoutMs.local === 5 * 60_000)).toBe(true);
for (const execution of selected.filter(entry => entry.environment.id === "daytona")) {
expect(execution.requiredCredentials).toEqual(expect.arrayContaining([execution.profile.credential, "DAYTONA_API_KEY"]));
expect(execution.requiredCredentials).toHaveLength(2);
expect(execution.task.expectedRunCount).toBe(1);
expect(execution.task.attemptTimeoutMs.daytona).toBe(15 * 60_000);
const secretRefs = { DAYTONA_API_KEY: { type: "secret_ref" as const, secretId: "22222222-2222-4222-8222-222222222222", version: "latest" as const } };
expect(() => execution.environment.buildEnvironment({ executionId: execution.id, secretRefs, daytonaImage: "runner:latest" })).toThrow("immutable");
const payload = execution.environment.buildEnvironment({ executionId: execution.id, secretRefs, daytonaImage: `runner@sha256:${"a".repeat(64)}` });
expect(payload).toMatchObject({ driver: "sandbox", config: { provider: "daytona", reuseLease: false, ttlMinutes: 60 } });
}
expect(runnerMatrix).toEqual(expect.arrayContaining(selected));
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(execution => execution.suite.id === "feedback-persistence")).toBe(false);
});
});