Files
PaperClipAI/tests/runner-e2e/stock-harness-digest.test.ts
DottaandPaperclip 2f0c485dec fix(skills): ship the completion helper with the installed skill (#15554)
## Thinking Path

> - Paperclip manages work for AI agents.
> - Legacy agents use the Paperclip skill to save task status and
comments.
> - The skill names a script relative to the task workspace.
> - That script exists only in the Paperclip source repository.
> - Agents in other workspaces can hit a missing command or search for
it.
> - This PR ships the helper inside the skill and uses the installed
skill path.
> - The repository command remains available through a forwarding
wrapper.

## Linked Issues or Issue Description

Fixes #9527. Refs #15548 for the preceding runtime checkout guidance.
Related: #6052 addresses LF line endings for the repository helper; this
change addresses helper delivery and path resolution.

## What Changed

- Bundle the existing issue update helper with the Paperclip skill.
Preserve its HTTP checks, echoed-status check and two-attempt limit.
- Resolve the command from the installed skill directory. Use a verified
PATCH when that path is unavailable, without searching the filesystem.
- Keep the repository command as a wrapper that works from any
directory.
- Test shell execution and exact status/comment payloads through both
provider skill-home layouts, including paths with spaces.
- Add helper sources and existing verification tests to stock-harness
admission. Record an absent historical helper explicitly. Add the
missing declaration for the admission fingerprint export.

## Verification

- Complete directly affected source suites: 30 tests pass. They cover
skill delivery, preserved multiline comments and links, authentication
headers, empty responses, mismatched status, transient retries and
definitive rejections.
- Product E2E typecheck passes. Support suites: 1,835 Vitest tests pass,
one is skipped; 128 Node tests pass.
- Full local build and workspace typecheck pass.
- Full local repository tests are not claimed as passed. Embedded
PostgreSQL was unavailable in this worktree during the preceding task;
Linux CI will run the repository gates.
- The authorized matched Codex/Claude comparison is pending. It uses the
existing assigned-skill case and original oracle, one initial attempt
per profile and variant.
- CI and a fresh Greptile review are pending. Keep this PR in draft
until readiness gates complete.

## Risks

- Correct path resolution depends on the harness supplying the installed
skill path. The instructions use verified PATCH when that path is
unavailable.
- The helper still requires Bash, curl and jq. Its existing retry and
response-verification behavior is unchanged.
- Tests use the shared skill-directory symlink mechanism and an HTTP
fixture. Real provider completion behavior still requires the bounded
live comparison.
- This fix does not redesign native completion, legacy recovery or
ambiguous transport handling.

## Model Used

OpenAI Codex, GPT-6 family. The exact model build and context window are
not exposed in this session. Used code editing, shell tools and test
execution.

## 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-08 07:21:30 -05:00

66 lines
4.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sourceFingerprint } from "./stock-harness-checks.mjs";
import { stockHarnessSourceDigest, stockHarnessSkillSources } from "./stock-harness.js";
vi.mock("node:fs", async importOriginal => ({ ...await importOriginal<typeof import("node:fs")>(), readFileSync: vi.fn() }));
describe("stock harness instruction revision", () => {
it("invalidates the prerequisite fingerprint when checkout observation changes", () => {
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
const original = sourceFingerprint();
vi.mocked(readFileSync).mockImplementation(file => Buffer.from(
String(file).endsWith("tests/runner-e2e/checkout-activity.ts") ? "changed accounting" : "unchanged"));
const changed = sourceFingerprint();
expect(original.sourceErrors).toEqual([]);
expect(changed.sourceErrors).toEqual([]);
expect(changed.fingerprint).not.toBe(original.fingerprint);
});
it.each(["scripts/paperclip-issue-update.sh", "skills/paperclip/scripts/paperclip-issue-update.sh", "tests/runner-e2e/checkout-activity.ts", "server/src/onboarding-assets/default/AGENTS.md", "packages/adapter-utils/src/server-utils.ts", "packages/shared/src/connection-intent-guidance.ts", "skills/paperclip/SKILL.md", "skills/paperclip/references/issue-documents.md", "packages/paperclip-runner/generated/capability/capabilities.yaml", "packages/paperclip-runner/spec/capability/capabilities.yaml", "tests/runner-e2e/stock-harness-manifest.ts", "packages/adapter-utils/src/acpx-engine/execute.ts", "packages/adapter-utils/src/acpx-engine/ephemeral-session-environment.ts", "tests/runner-e2e/stock-harness-instruction-variant.mjs", "tests/runner-e2e/automatic-retry.ts", "tests/runner-e2e/catalog.ts"])(
"changes when the evaluated %s changes", source => {
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
const original = stockHarnessSourceDigest();
vi.mocked(readFileSync).mockImplementation(file => Buffer.from(String(file).endsWith(source) ? "changed instructions" : "unchanged"));
expect(stockHarnessSourceDigest()).not.toBe(original);
});
it.each(["scripts/paperclip-issue-update.sh", "skills/paperclip/scripts/paperclip-issue-update.sh"])(
"invalidates the prerequisite fingerprint when %s changes", source => {
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
const original = sourceFingerprint();
vi.mocked(readFileSync).mockImplementation(file => Buffer.from(
String(file) === resolve(import.meta.dirname, "../..", source) ? "changed helper" : "unchanged"));
expect(sourceFingerprint().fingerprint).not.toBe(original.fingerprint);
});
it("records a missing historical bundled helper without accepting unreadable helper files", () => {
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
const original = sourceFingerprint();
const digest = stockHarnessSourceDigest();
vi.mocked(readFileSync).mockImplementation(file => {
if (String(file).endsWith("skills/paperclip/scripts/paperclip-issue-update.sh"))
throw Object.assign(new Error("absent"), { code: "ENOENT" });
return Buffer.from("unchanged");
});
expect(sourceFingerprint().sourceErrors).toEqual([]);
expect(sourceFingerprint().fingerprint).not.toBe(original.fingerprint);
expect(stockHarnessSourceDigest()).not.toBe(digest);
expect(stockHarnessSkillSources()[3]).toMatchObject({ present: false, sha256: null });
vi.mocked(readFileSync).mockImplementation(() => { throw Object.assign(new Error("unreadable"), { code: "EACCES" }); });
expect(stockHarnessSourceDigest).toThrow("unreadable");
expect(sourceFingerprint().sourceErrors).toContain("skills/paperclip/scripts/paperclip-issue-update.sh");
});
it("records an absent historical recipe without introducing its content or hiding other read errors", () => {
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
const present = stockHarnessSourceDigest();
vi.mocked(readFileSync).mockImplementation(file => {
if (String(file).endsWith("references/issue-documents.md")) throw Object.assign(new Error("absent"), { code: "ENOENT" });
return Buffer.from("unchanged");
});
expect(stockHarnessSkillSources()[1]).toEqual({ path: "skills/paperclip/references/issue-documents.md", present: false, sha256: null });
expect(stockHarnessSourceDigest()).not.toBe(present);
vi.mocked(readFileSync).mockImplementation(() => { throw Object.assign(new Error("unreadable"), { code: "EACCES" }); });
expect(stockHarnessSourceDigest).toThrow("unreadable");
});
});