Files
PaperClipAI/tests/runner-e2e/prepare-report-lock.test.ts
T
DottaandPaperclip 287b04d72f fix(evals): keep report dependency proof with lockfile refresh (#18063)
<!-- entire-trail-link-start -->
https://entire.com/gh/paperclipai/-/trails/192/changes/paperclip/192
<!-- entire-trail-link-end -->

## Thinking Path

> - Paperclip manages AI agents and runs paid Product E2E evaluations.
> - Paid work needs a report that can install its trusted dependencies.
> - The report uses a reviewed offline lock correction.
> - New UI dependencies can make that correction stale while its lock
hash still matches.
> - The lockfile bot must refresh the correction with its lock changes.
> - This PR also checks manifest consistency offline before model
admission.

## Linked Issues or Issue Description

**What happened?**

Current master declares new CodeMirror and YAML UI dependencies. Its
committed lock omits those UI importer entries. The existing report
correction accepts that lock, but a frozen install then fails with
`ERR_PNPM_OUTDATED_LOCKFILE`. This can waste completed paid work before
the report fails.

Refs #18059. That bot PR supplies the matching lock. This PR keeps the
root lock bot-owned and supplies its exact offline report correction.
The correction changes 21 importer lines and leaves all package records
unchanged.

**Expected behavior**

The existing bot includes matching report proof in its dependency PR.
Paid authorization rejects stale manifests before model work. Report
installation still accepts only reviewed hashes and applies no registry
resolution.

**Steps to reproduce**

1. Run a frozen pnpm 9.15.4 install on master at
`7f10cc058568e3c1ac133cf24f3b4000cbdfca0e`.
2. Observe the missing UI importer entries.
3. Apply the reviewed correction in this PR. The frozen install passes.

## What Changed

- Add an explicit refresh mode that derives the offline patch and hashes
from a known committed lock and the bot's resolved lock.
- Keep the existing unknown-lock, patch hash and output hash guards.
- Include the proof files in the existing lockfile bot PR. New bot PRs
use complete descriptions; existing descriptions stay intact.
- Validate the frozen lock against current manifests offline before paid
admission, using the existing pinned pnpm version.
- Refresh the current correction and document this process. The root
lockfile is not committed here.

## Verification

- 24 focused report-lock and workflow-security tests pass. They cover
offline replay, tampered input/patch/output rejection, real Git refresh,
unknown-base refusal and unchanged proof.
- A full frozen install passes after the reviewed correction, with
lifecycle scripts disabled.
- A fresh source archive validates offline with an empty package store.
- A deliberate manifest change fails that same offline check with
`ERR_PNPM_OUTDATED_LOCKFILE`.
- Both workflow files parse as YAML. All 95 shell steps pass Bash syntax
checks.
- `git diff --check` passes. These checks make no model requests.
- Full repository checks and fresh reviews must pass on the final PR
head before merge.

## Risks

- Dependency changes must still pass review. Refresh refuses an unknown
committed baseline.
- The bot now commits two additional proof files. Its write permissions
and target branch stay unchanged. Existing PR descriptions are
preserved.
- The paid authorization job adds an offline manifest check. It runs no
package lifecycle scripts.
- No budget, provider credential, environment permission, schedule or
paid enable gate changes.

## Model Used

- OpenAI Codex, GPT-6, with reasoning and code execution tools. The
exact serving variant 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
- [ ] 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>
2026-10-28 15:30:58 -05:00

103 lines
6.7 KiB
TypeScript

import { mkdtemp, mkdir, readFile, writeFile, copyFile, realpath, rm } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { expect, it } from 'vitest';
import { parse } from 'yaml';
import { prepareReportLock, refreshReportLock } from './prepare-report-lock.mjs';
it.each(['valid', 'unknown-lock', 'changed-patch', 'wrong-output'])(
'installs only the reviewed dependency correction: %s', async variant => {
const root = await mkdtemp(path.join(tmpdir(), 'eval-report-lock-test-'));
const here = import.meta.dirname, relative = path.join(root, 'tests/runner-e2e');
try {
await mkdir(relative, { recursive: true });
for (const file of ['report-lock.json', 'report-lock.patch']) await copyFile(path.join(here, file), path.join(relative, file));
const committed = execFileSync('git', ['show', 'HEAD:pnpm-lock.yaml'], { cwd: path.resolve(here, '../..') });
const policy = JSON.parse(await readFile(path.join(relative, 'report-lock.json'), 'utf8'));
const committedHash = createHash('sha256').update(committed).digest('hex');
expect([policy.inputSha256, policy.outputSha256]).toContain(committedHash);
const lock = path.join(root, 'pnpm-lock.yaml');
await writeFile(lock, committed);
if (variant === 'valid') {
expect(await prepareReportLock(root)).toBe(committedHash === policy.outputSha256 ? 'already-reviewed' : 'reviewed-correction');
expect(createHash('sha256').update(await readFile(lock)).digest('hex')).toBe(policy.outputSha256);
await writeFile(lock, committed);
}
if (committedHash === policy.outputSha256) {
execFileSync('git', ['apply', '--reverse', path.join(relative, 'report-lock.patch')], { cwd: root });
}
const input = await readFile(lock);
expect(createHash('sha256').update(input).digest('hex')).toBe(policy.inputSha256);
await writeFile(lock, variant === 'unknown-lock' ? 'unknown\n' : input);
if (variant === 'changed-patch') await writeFile(path.join(relative, 'report-lock.patch'), 'changed');
if (variant === 'wrong-output') { policy.outputSha256 = '0'.repeat(64); await writeFile(path.join(relative, 'report-lock.json'), JSON.stringify(policy)); }
if (variant === 'valid') {
expect(await prepareReportLock(root)).toBe('reviewed-correction');
expect(createHash('sha256').update(await readFile(lock)).digest('hex')).toBe(policy.outputSha256);
expect(await prepareReportLock(root)).toBe('already-reviewed');
const reviewed = parse(await readFile(lock, 'utf8'));
const ui = JSON.parse(await readFile(path.resolve(here, '../../ui/package.json'), 'utf8'));
for (const [name, specifier] of Object.entries(ui.dependencies)) {
const entry = reviewed.importers.ui.dependencies[name];
expect(entry.specifier).toBe(specifier);
if (entry.version.startsWith('link:')) continue;
expect(reviewed.packages, name).toHaveProperty(`${name}@${entry.version.split('(')[0]}`);
expect(reviewed.snapshots, name).toHaveProperty(`${name}@${entry.version}`);
}
} else {
await expect(prepareReportLock(root)).rejects.toThrow();
expect(await readFile(lock)).toEqual(variant === 'unknown-lock' ? Buffer.from('unknown\n') : input);
}
} finally { await rm(root, { recursive: true, force: true }); }
},
);
it.each(['input', 'output', 'unknown'])('refreshes dependency proof only from a %s committed lock', async variant => {
const root = await realpath(await mkdtemp(path.join(tmpdir(), 'eval-report-lock-refresh-')));
const relative = path.join(root, 'tests/runner-e2e');
const original = "lockfileVersion: '9.0'\nimporters: {}\npackages:\n old@1.0.0: {resolution: {integrity: fixture-old}}\n";
const resolved = "lockfileVersion: '9.0'\nimporters:\n ui: {}\npackages:\n added@2.0.0: {resolution: {integrity: fixture-new}}\n";
const hash = (value: string) => createHash('sha256').update(value).digest('hex');
try {
await mkdir(relative, { recursive: true });
await writeFile(path.join(root, 'pnpm-lock.yaml'), original);
const initial = { inputSha256: variant === 'input' ? hash(original) : '0'.repeat(64),
outputSha256: variant === 'output' ? hash(original) : '1'.repeat(64), patchSha256: hash('historical') };
await writeFile(path.join(relative, 'report-lock.json'), JSON.stringify(initial));
await writeFile(path.join(relative, 'report-lock.patch'), 'historical');
execFileSync('git', ['init', '-q'], { cwd: root });
execFileSync('git', ['add', '.'], { cwd: root });
execFileSync('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', 'fixture'], { cwd: root });
await writeFile(path.join(root, 'pnpm-lock.yaml'), resolved);
if (variant === 'unknown') {
await expect(refreshReportLock(root)).rejects.toThrow('not reviewed');
expect(await readFile(path.join(relative, 'report-lock.patch'), 'utf8')).toBe('historical');
expect(JSON.parse(await readFile(path.join(relative, 'report-lock.json'), 'utf8'))).toEqual(initial);
expect(await readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8')).toBe(resolved);
return;
}
if (variant === 'output') {
const cli = path.join(relative, 'prepare-report-lock.mjs');
await copyFile(path.join(import.meta.dirname, 'prepare-report-lock.mjs'), cli);
expect(execFileSync(process.execPath, [cli, '--refresh'], { cwd: root, encoding: 'utf8' }).trim()).toBe('refreshed');
} else {
expect(await refreshReportLock(root)).toBe('refreshed');
}
const policy = JSON.parse(await readFile(path.join(relative, 'report-lock.json'), 'utf8'));
const patch = await readFile(path.join(relative, 'report-lock.patch'), 'utf8');
expect(policy).toMatchObject({ inputSha256: hash(original), outputSha256: hash(resolved), patchSha256: hash(patch) });
await writeFile(path.join(root, 'pnpm-lock.yaml'), original);
expect(await prepareReportLock(root)).toBe('reviewed-correction');
expect(await readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8')).toBe(resolved);
expect(parse(await readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8')).packages).toEqual(parse(resolved).packages);
expect(await prepareReportLock(root)).toBe('already-reviewed');
execFileSync('git', ['add', '.'], { cwd: root });
execFileSync('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', 'refresh'], { cwd: root });
expect(await refreshReportLock(root)).toBe('unchanged');
expect(await readFile(path.join(relative, 'report-lock.patch'), 'utf8')).toBe(patch);
} finally { await rm(root, { recursive: true, force: true }); }
});