mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
test(paperclip-runner): stabilize the assigned-skills ACPX runtime host test (#13453)
Release sealed skill homes before test cleanup and reuse one prepared sandbox across the assigned-skills test opens. Preserve the current skill-refresh and prompt assertions without increasing test timeouts. Co-Authored-By: Devin Foley <devinfoley@users.noreply.github.com> Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 file changed
+57
-2
@@ -20,7 +20,10 @@ import type {
|
||||
VerifiedAcpxInstallation,
|
||||
} from "./installation-integrity.js";
|
||||
import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
|
||||
import { prepareAcpxRuntimeSandbox } from "./runtime-sandbox.js";
|
||||
import {
|
||||
prepareAcpxRuntimeSandbox,
|
||||
type AcpxRuntimeSandbox,
|
||||
} from "./runtime-sandbox.js";
|
||||
import {
|
||||
AcpxRuntimeHost,
|
||||
type AcpxRuntimeHostDependencies,
|
||||
@@ -30,6 +33,15 @@ import {
|
||||
} from "./runtime-host.js";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
// A published skill snapshot is sealed read-only by `protectStagedTree`
|
||||
// (runtime-context-materializer.ts:125, :133). Only
|
||||
// `releaseMaterializedNativeRuntimeSkills` restores write permission before
|
||||
// removal. `hostFixture` records every sandbox's skills home here as soon as
|
||||
// the sandbox exists, before the materialize step that seals it and before
|
||||
// any later step in the same open() call can fail or stall past this file's
|
||||
// per-test timeout. `afterEach` releases every recorded home first, so a
|
||||
// forced-open directory removal never has to unlink inside a sealed tree.
|
||||
const materializedSkillsHomes: string[] = [];
|
||||
const admissionControllers: AbortController[] = [];
|
||||
const pendingAdmissionOpenings = new Set<Promise<void>>();
|
||||
const pendingAdmissionCleanups = new Set<Promise<void>>();
|
||||
@@ -167,6 +179,14 @@ afterEach(async () => {
|
||||
}
|
||||
await Promise.all([...pendingAdmissionOpenings]);
|
||||
await Promise.all([...pendingAdmissionCleanups]);
|
||||
// Release every sealed skills tree before the plain `rm` below. `rm` does
|
||||
// not restore write permission, so a tree still sealed at this point would
|
||||
// otherwise fail with EACCES and hide the real test failure.
|
||||
await Promise.all(
|
||||
materializedSkillsHomes
|
||||
.splice(0)
|
||||
.map((skillsHome) => releaseMaterializedNativeRuntimeSkills(skillsHome)),
|
||||
);
|
||||
await Promise.all(
|
||||
temporaryDirectories
|
||||
.splice(0)
|
||||
@@ -281,7 +301,12 @@ describe("ACPX runtime host", () => {
|
||||
const providerStartTurn = vi.fn(() => runtimeTurn());
|
||||
let assigned = true;
|
||||
let expectedReference = "ASSIGNED_SKILL_MARKER";
|
||||
// Prepare the real sandbox once. Each reopen still exercises the host's
|
||||
// real skill refresh, including changed references and removed assignments,
|
||||
// without repeating unrelated durable directory and file writes. Sandbox
|
||||
// preparation itself also has dedicated runtime-sandbox coverage.
|
||||
const dependencies = fixture.dependencies({
|
||||
reuseSandbox: true,
|
||||
openRuntime: async (options) => {
|
||||
skillsHome = join(options.launchEnvironment.CLAUDE_CONFIG_DIR!, "skills");
|
||||
expect(await readdir(skillsHome)).toEqual(assigned ? ["assigned"] : []);
|
||||
@@ -1392,6 +1417,16 @@ describe("ACPX runtime host", () => {
|
||||
agentRuntimePackageJsonPath: null,
|
||||
openCommand,
|
||||
}),
|
||||
// This test builds its own dependency object instead of
|
||||
// `fixture.dependencies()`, so it must record the skills home
|
||||
// itself. Command admission starts after the sandbox exists and
|
||||
// Claude's skills are already materialized and sealed, and abort
|
||||
// can land right there — before this test's own cleanup runs.
|
||||
prepareSandbox: async (sandboxInput) => {
|
||||
const sandbox = await prepareAcpxRuntimeSandbox(sandboxInput);
|
||||
materializedSkillsHomes.push(join(sandbox.agentHomeDirectory, "skills"));
|
||||
return sandbox;
|
||||
},
|
||||
openRuntime,
|
||||
retainAdmissionCleanup: trackAdmissionCleanup,
|
||||
reportRetainedCleanupFailure: vi.fn(),
|
||||
@@ -1705,8 +1740,15 @@ async function hostFixture() {
|
||||
input: Pick<AcpxRuntimeHostDependencies, "openRuntime"> &
|
||||
Partial<
|
||||
Pick<AcpxRuntimeHostDependencies, "reportRetainedCleanupFailure">
|
||||
>,
|
||||
> & {
|
||||
// Opt-in only. When true, `prepareSandbox` runs the real
|
||||
// preparation once, then returns that same sandbox for every
|
||||
// later open in the test. Every other test omits this flag, so
|
||||
// the file still proves that a reopen re-prepares the sandbox.
|
||||
reuseSandbox?: boolean;
|
||||
},
|
||||
): AcpxRuntimeHostDependencies {
|
||||
let reusedSandbox: AcpxRuntimeSandbox | null = null;
|
||||
return {
|
||||
verifyInstallation: async (profile) =>
|
||||
({
|
||||
@@ -1716,6 +1758,19 @@ async function hostFixture() {
|
||||
openCommand: async () => command,
|
||||
}) satisfies VerifiedAcpxInstallation,
|
||||
openRuntime: input.openRuntime,
|
||||
// Record the skills home the instant the sandbox exists, ahead of
|
||||
// the materialize call that seals it. A test that overrides
|
||||
// `prepareSandbox` for a non-Claude agent replaces this wrapper, but
|
||||
// those agents never materialize skills, so nothing is lost.
|
||||
prepareSandbox: async (sandboxInput) => {
|
||||
if (input.reuseSandbox && reusedSandbox) return reusedSandbox;
|
||||
const sandbox = await prepareAcpxRuntimeSandbox(sandboxInput);
|
||||
if (sandboxInput.agent === "claude") {
|
||||
materializedSkillsHomes.push(join(sandbox.agentHomeDirectory, "skills"));
|
||||
}
|
||||
if (input.reuseSandbox) reusedSandbox = sandbox;
|
||||
return sandbox;
|
||||
},
|
||||
retainAdmissionCleanup: trackAdmissionCleanup,
|
||||
reportRetainedCleanupFailure:
|
||||
input.reportRetainedCleanupFailure ?? vi.fn(),
|
||||
|
||||
Reference in new issue
Block a user