Files
PaperClipAI/server/src/__tests__/native-procedure-measurement.test.ts
DottaandPaperclip caf120105c test: prepare neutral native connection guidance evals (#15407)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents need to discover connections, obtain consent, and continue
from saved decisions.
> - We want to reduce repeated instructions only when measured behavior
supports the change.
> - The existing decline tasks tell the model not to retry. One
provider-decline check can pass without an explanation or an observed
service counter.
> - This PR adds neutral tasks and stricter saved-evidence checks before
any connection instruction reduction.
> - Production instructions remain unchanged. The new cells are
configured, not live-qualified.

## Linked Issues or Issue Description

Refs #15218. Refs #15389.

**What existing behavior does this improve?**

The Product E2E connection workflow evaluation and its instruction
measurement provenance.

**Current behavior**

Some decline prompts supply the policy they intend to test. The
provider-decline workflow does not require a saved post-decision
explanation. Its old no-call check can use a missing fixture counter as
zero. OpenCode has no connection cases in the original Everyday matrix.

**Proposed behavior**

Add an explicit-only suite with five connection stories on native Codex,
ACPX Claude, and OpenCode. Require an explanation attributed by exact
run ID after a saved decline. Observe the provider fixture counter.
Preserve the original cases and grades.

## What Changed

- Add fifteen configured cells with one attempt, twelve-minute
deadlines, and verified 1,000-cent company and agent budget stops.
- Remove procedure hints from the three new decline prompts. Keep a
user-permitted explanation fallback and the existing positive controls.
- Require saved decline state, one decision, unchanged connections,
observed zero service calls where applicable, and a post-decision
explanation from a successful run on the same task.
- Add negative grader calibration and test the actual fixture budget
payloads. Exclude the suite from default and generic selection.
- Extend the existing full-catalog measurement source manifest with
connection descriptions and schemas. Add an audit of fixed text, tool
descriptions, returned instructions, and unqualified behavior.
- Rebase on master `a6306ba606eb87c89b9ef0344e9fe8e0025580f9` and
preserve its new Cursor suites. No production, credential, workflow, or
lockfile change.

## Verification

- Before rebase: Product E2E support passed 1,424 TypeScript tests and
128 Node checks. Six catalog measurement tests, repository
typecheck/build, Product E2E typecheck, and exact fifteen-cell discovery
passed.
- The full pre-rebase repository test run was stopped when master
advanced. Its partial result is not a pass.
- After rebase and the review correction: repository build/typecheck,
Product E2E typecheck, 1,799 TypeScript support tests (one skipped), 128
Node checks, six measurement tests, and exact fifteen-cell discovery
pass. The duplicate local full-suite run was stopped incomplete after
about 20 minutes once complete CI passed; no local full-suite pass is
claimed.
- Review found that the initial grader read `runId` instead of public
`createdByRunId`. A regression calibration reproduced both rejection of
valid public comments and acceptance of the wrong alias. The fix uses
the actual field and binds the evidence type to the shared
`IssueComment` contract. A subsequent type-only import path correction
passes Product E2E typecheck.
- Final source `0de306b9664bfbdebb6709ddb54c95152740d1ad` passes
[complete
CI](https://github.com/paperclipai/paperclip/actions/runs/37560250545):
51 successful checks and two intentional Storybook skips, plus separate
Snyk success. Fresh Greptile review is 5/5 with the single review thread
resolved and no new findings. The PR is clean and mergeable.
- Local commands: `pnpm build`, `pnpm -r typecheck`, `pnpm
test:e2e:runner:unit`, `pnpm test:e2e:runner:typecheck`, and `pnpm
test:e2e:runner -- --list --suite native-connection-guidance`. The
measurement uses
`PAPERCLIP_NATIVE_PROCEDURE_MEASUREMENT=/tmp/connection-measurement.json
pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/native-procedure-measurement.test.ts`. Validation
used pinned pnpm 9.15.4.
- No paid provider campaign was started. There is no baseline/candidate
behavior result for these new cells.
- The audit records 654 UTF-8 bytes of fixed connection guidance. A
clean capture at `a04b8c6a452315625014888335d45670a2094fb6` confirms 41
supplied tools, 53,341 normalized bytes at start/resume, 50,949 at
compact continuation, and a 48,195-byte authenticated OpenCode MCP
catalog. These are byte counts, not tokens, bills, vendor-private prompt
sizes, or savings from this PR.

## Risks

- This is eval preparation. Passing support tests do not establish live
model behavior or qualify an instruction reduction.
- The explanation oracle checks attributed saved output. It does not
prove cognition or arbitrary prose truthfulness. One saved interaction
also does not prove the absence of repeated idempotent tool calls.
- Successful new authentication and tool refresh, existing-connection
agent grants, independent work while waiting, explicit retry after
decline, and blocking when mandatory work remains still need separate
coverage.
- Notion setup decline does not execute a real Notion service. Positive
service approval uses an already installed deterministic service; it
does not qualify new connection creation.
- The original historical failures remain unchanged. Future comparisons
must freeze source, fixture, model, input, and grading controls and
retain every actual attempt.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools, and code
execution. The exact serving model ID and context-window size were not
exposed in this session; they are not inferred. No model provider was
invoked by the eval suite in this PR preparation.

## 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-06 21:28:16 -05:00

165 lines
13 KiB
TypeScript

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { afterAll, describe, expect, it } from "vitest";
import type { Db } from "@paperclipai/db";
import { PaperclipRunnerToolAuthority } from "../services/native-runtime/paperclip-runner-tool-authority.js";
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../../../packages/paperclip-runner/src/contracts/native-execution.js";
import type { PersistedNativeSession } from "../../../packages/paperclip-runner/src/contracts/native-session-backend.js";
import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, canonicalNativeRuntimeContextDigest, nativeRuntimePromptDigest } from "../../../packages/paperclip-runner/src/contracts/runtime-context.js";
import { createRunnerdNativeSessionBackend } from "../../../packages/paperclip-runner/src/backends/codex-native-backend.js";
import { resolveQualifiedAcpxProfile } from "../../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js";
import { FakeCodexTransport, WORKSPACE } from "../../../packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test-support.js";
import { startOpenCodeMcpBridge } from "../../../packages/paperclip-runner/src/drivers/opencode/mcp-bridge.js";
// Production catalog selection, scripted provider transport: this measures what
// Paperclip delivers, not the vendor's private prompt, tokenizer or billing.
const root = new URL("../../../", import.meta.url);
const sha = (value: string | Buffer) => createHash("sha256").update(value).digest("hex");
const identity = { companyId: "company", agentId: "agent", issueId: "issue", runId: "run", sessionId: "session" };
const tools = new PaperclipRunnerToolAuthority({} as Db, {
...identity, apiToolsEnabled: true, workspaceRoot: WORKSPACE,
}).definitions();
const { qualificationModel: _model, reportedModelId: _reportedModel, permissionPolicy: _policy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-sonnet-5");
const providers: NativeExecutionInput["provider"][] = [
{ kind: "codex", model: "gpt-5.6-sol", approvalPolicy: "never" },
{ kind: "acpx", agent: "claude", model: "claude-sonnet-5", permissionMode: "approve-all", profile },
{ kind: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", permissionMode: "allow" },
];
const receipts: unknown[] = [];
let mcpReceipt: unknown;
function measure(value: unknown) {
const json = JSON.stringify(value);
const normalized = json.replaceAll(WORKSPACE, "/workspace");
return {
utf8Bytes: Buffer.byteLength(json), characters: json.length, sha256: sha(json),
normalizedComparison: { utf8Bytes: Buffer.byteLength(normalized), characters: normalized.length, sha256: sha(normalized) },
};
}
function execution(provider: NativeExecutionInput["provider"]): NativeExecutionInput {
const entry = readFileSync(new URL("server/src/onboarding-assets/default/AGENTS.md", root), "utf8");
writeFileSync(`${WORKSPACE}/AGENTS.md`, entry);
const context = {
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, rootPath: WORKSPACE, digest: sha(entry), manifestDigest: sha(entry), fileCount: 1, totalBytes: Buffer.byteLength(entry) } },
skills: [], mcp: { assignmentSetId: "none", digest: "0".repeat(64), bindingId: null },
};
return parseNativeExecutionInput({
schema: "paperclip.native-execution-input.v5", binding: { companyId: identity.companyId, agentId: identity.agentId, issueId: identity.issueId, runId: identity.runId, executionWorkspaceId: "workspace" },
task: { identifier: "TASK-1", title: "Review delegated work", description: "Review delegated work", prompt: "Review delegated work.", workMode: "standard" },
workspace: { cwd: WORKSPACE, repoUrl: null, repoRef: null, branchName: null },
session: { normalizedSessionId: "session", driverKind: provider.kind === "acpx" ? "acpx_runtime" : provider.kind === "opencode" ? "opencode_server" : "codex_app_server", protocolVersion: 1 },
provider, executionMode: "default", planningContext: null,
completionContract: { id: "contract", sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.completion-contract.v1", contract: { revision: "1", objective: "Review delegated work", criteria: [{ id: "objective", requirement: "Review complete" }] } },
interactionResponses: [], credentialBindings: [],
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
});
}
function transport(provider: NativeExecutionInput["provider"]) {
return new FakeCodexTransport("thread-1", "provider-session-1", provider.kind === "acpx" ? {
kind: "acpx", normalizedSessionId: "session", acpxRecordId: "fixture-record", backendSessionId: "fixture-backend",
agentSessionId: "fixture-agent", profileDigest: `sha256:${"a".repeat(64)}`, workspaceDigest: `sha256:${"b".repeat(64)}`,
requestedModel: provider.model, effectiveModel: provider.model, permissionMode: "approve-all",
providerLifetimeFenceCandidates: [60001, 60002, 60003],
} : undefined);
}
describe("production native procedure instruction measurement", () => {
it("retains delivered serialized bytes separately from path-normalized comparison", () => {
const value = { cwd: WORKSPACE, text: "λ" };
expect(measure(value).utf8Bytes).toBe(Buffer.byteLength(JSON.stringify(value)));
expect(measure(value).normalizedComparison.utf8Bytes).toBe(Buffer.byteLength(JSON.stringify({ ...value, cwd: "/workspace" })));
expect(measure(value).sha256).not.toBe(measure(value).normalizedComparison.sha256);
});
it("uses the real standard-mode authority, including optional and connection tools", () => {
expect(tools.map(tool => tool.name)).toEqual(expect.arrayContaining([
"hire_agent", "list_agents", "create_task", "set_dependencies", "connections_search", "connection_request", "register_deliverable",
]));
expect(new Set(tools.map(tool => tool.name)).size).toBe(tools.length);
const withoutApi = new PaperclipRunnerToolAuthority({} as Db, { ...identity, apiToolsEnabled: false }).definitions();
expect(withoutApi.some(tool => tool.name === "hire_agent")).toBe(false);
});
for (const provider of providers) it(`captures the full ${provider.kind} start/resume/continuation payload`, async () => {
const input = execution(provider);
let checkpoint: PersistedNativeSession | undefined;
for (const phase of ["start", "resume", "continuation"] as const) {
const captured = transport(provider);
captured.readResponse = { thread: { id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE, turns: [] } };
const current = phase === "continuation" ? parseNativeExecutionInput({ ...input, continuationPrompt: "Review the teammate's saved result." }) : input;
const backend = createRunnerdNativeSessionBackend(current, { transportFactory: () => captured, dynamicTools: tools, environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE } });
const recovery = checkpoint ? await backend.recoverSession!(checkpoint, { signal: new AbortController().signal }) : null;
if (recovery) expect(recovery.recovered).toBe(true);
const session = recovery?.session ?? await backend.openSession({ identity, workingDirectory: WORKSPACE });
try {
checkpoint ??= await session.snapshot();
const envelope = buildNativeModelEnvelope(current, phase === "continuation" ? { resumedSession: true } : undefined);
if ("requestedSkills" in envelope && backend.preparedTaskConstraints) envelope.constraints = [...backend.preparedTaskConstraints];
await session.startTurn({ message: { role: "user", text: JSON.stringify(envelope) } });
const setup = captured.calls.find(call => call.method === (phase === "start" ? "thread/start" : "thread/resume"))!.params;
const turn = captured.calls.find(call => call.method === "turn/start")!.params;
const instructions = setup.developerInstructions ?? setup.baseInstructions;
const delivered = setup.dynamicTools as Record<string, unknown>[];
expect(delivered.filter(tool => !["paperclip_finish", "paperclip_block"].includes(String(tool.name)))).toEqual(tools);
expect(delivered).toHaveLength(tools.length + 2);
expect(instructions).toContain(PAPERCLIP_EXECUTION_PROMPT);
expect(JSON.parse((turn.input as {text: string}[])[0]!.text)).toEqual(envelope);
receipts.push({ provider: provider.kind, phase, instructions: measure(instructions), tools: measure(delivered), input: measure(turn.input),
fullProjection: measure({ instructions, tools: delivered, input: turn.input }), deliveredToolCount: delivered.length });
} finally { await session.close({ reason: "measurement" }); }
}
});
it("preserves the full catalog through the actual OpenCode MCP tools/list bridge", async () => {
const bridge = await startOpenCodeMcpBridge({ tools, handler: async () => { throw new Error("No tool execution in measurement"); } });
try {
const response = await fetch(bridge.url, { method: "POST", headers: { authorization: `Bearer ${bridge.secret}`, "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }) });
expect(response.ok).toBe(true);
const result = await response.json() as { result: { tools: Record<string, unknown>[] } };
expect(result.result.tools.filter(tool => !["paperclip_finish", "paperclip_block"].includes(String(tool.name)))).toEqual(
tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
);
mcpReceipt = { boundary: "authenticated loopback MCP tools/list; no OpenCode/provider process", serverAnnotationsForwarded: false, tools: measure(result.result.tools), count: result.result.tools.length };
} finally { await bridge.close(); }
});
});
afterAll(() => {
const output = process.env.PAPERCLIP_NATIVE_PROCEDURE_MEASUREMENT;
if (!output) return;
if (receipts.length !== 9 || !mcpReceipt) throw new Error("Refusing incomplete instruction measurement");
const paths = [
"server/src/onboarding-assets/default/AGENTS.md",
"packages/paperclip-runner/src/contracts/runtime-context.ts",
"packages/paperclip-runner/src/protocol-actions/hire-agent.ts",
"packages/paperclip-runner/src/protocol-actions/create-task.ts",
"packages/paperclip-runner/src/protocol-actions/set-dependencies.ts",
"packages/paperclip-runner/src/catalog/semantic-action-catalog.ts",
"packages/paperclip-runner/generated/semantic-action-catalog.json",
"packages/paperclip-runner/generated/capability/semantic-tool-contracts.json",
"packages/paperclip-runner/protocol/fixtures/evals/native-execution-seeded.json",
"packages/paperclip-runner/protocol/manifest.json",
"packages/paperclip-runner/src/backends/codex-native-backend.ts",
"packages/paperclip-runner/src/drivers/opencode/mcp-bridge.ts",
"server/src/services/native-runtime/paperclip-runner-tool-authority.ts",
"server/src/services/connection-tool-definitions.ts",
"packages/shared/src/connection-intent-guidance.ts",
"packages/shared/src/validators/connection-intent.ts",
"server/src/services/native-runtime/native-session-resume.ts",
"server/src/__tests__/native-procedure-measurement.test.ts",
];
writeFileSync(output, `${JSON.stringify({ schema: "paperclip.native-procedure-measurement.v2",
sourceSha: execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim(),
sourceDirty: execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8" }).trim().length > 0,
measuredInputsDirty: execFileSync("git", ["status", "--porcelain", "--", ...paths], { cwd: root, encoding: "utf8" }).trim().length > 0,
sourceHashes: Object.fromEntries(paths.map(path => [path, sha(readFileSync(new URL(path, root)))])),
boundary: "scripted runnerd RPC with production standard-mode authority, API tools enabled, local workspace, no assigned external apps",
byteAccounting: "utf8Bytes counts the unmodified JSON serialization of each extracted component/projection; normalizedComparison substitutes only the fixture workspace path. Neither is the entire transport request.",
providerCalls: 0, tokenCount: null, upstreamLoadingOrTruncation: "unverified", vendorStockPrompt: "unavailable",
toolDescriptions: tools.map(tool => ({ name: tool.name, characters: String(tool.description).length, utf8Bytes: Buffer.byteLength(String(tool.description)), inputSchema: measure(tool.inputSchema) })),
receipts, mcpReceipt,
}, null, 2)}\n`);
});