fix: retain supported ACPX readiness checks after integration
Update stop lifecycle fixtures to require verified teardown before acknowledgement. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
ebfdbfb0b1
commit
ea52c83823
|
|
@ -279,6 +279,8 @@ describe("server adapter registry", () => {
|
|||
|
||||
it.each([
|
||||
["claude", "claude-sonnet-5"],
|
||||
["codex", "gpt-5.6-sol"],
|
||||
["pi", "openrouter/deepseek/deepseek-v4-flash-0731"],
|
||||
] as const)("does not claim runtime readiness from the remote ACPX %s platform alone", async (acpxAgent, model) => {
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment({
|
||||
companyId: "company-1",
|
||||
|
|
@ -315,20 +317,23 @@ describe("server adapter registry", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("accepts the qualified ACPX Pi profile", async () => {
|
||||
it.each([
|
||||
["codex", "gpt-5.6-sol"],
|
||||
["pi", "openrouter/deepseek/deepseek-v4-flash-0731"],
|
||||
])("accepts qualified ACPX %s without claiming installation readiness", async (acpxAgent, model) => {
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "paperclip_runner",
|
||||
config: {
|
||||
provider: "acpx",
|
||||
acpxAgent: "pi",
|
||||
model: "openrouter/deepseek/deepseek-v4-flash-0731",
|
||||
acpxAgent,
|
||||
model,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "pass",
|
||||
checks: [{ code: "acpx_profile_qualified", level: "info" }],
|
||||
status: "warn",
|
||||
checks: [{ code: "acpx_runtime_unverified", level: "warn" }],
|
||||
});
|
||||
});
|
||||
it("wraps built-in npm runtime installs with the sandbox-aware install helper", () => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { agents, companies, createDb, heartbeatRuns, startEmbeddedPostgresTestDatabase, type Db } from "@paperclipai/db";
|
||||
import * as processes from "../services/hot-restart.js";
|
||||
import * as adapters from "../adapters/index.js";
|
||||
import * as orchestration from "../services/environment-run-orchestrator.js";
|
||||
import * as compatibility from "../services/legacy-sandbox-workspace.js";
|
||||
import * as cancellation from "@paperclipai/adapter-utils/adapter-run-cancellation";
|
||||
import * as gitCredentials from "../services/git-credentials.js";
|
||||
import { bindAdapterRunStop, hasAdapterRunCancellation } from "@paperclipai/adapter-utils/adapter-run-cancellation";
|
||||
import * as executionTargets from "@paperclipai/adapter-utils/execution-target";
|
||||
import { heartbeatService, persistHeartbeatRunProcessMetadata } from "../services/heartbeat.js";
|
||||
|
|
@ -22,6 +22,12 @@ describe("heartbeat process identity persistence", () => {
|
|||
await db.insert(agents).values({ id: agentId, companyId, name: "Runner", role: "engineer", status: "idle", adapterType: "codex_local" });
|
||||
}, 60_000);
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
beforeEach(() => {
|
||||
// These tests exercise process/cancellation ownership, not Git transport.
|
||||
// Their synthetic targets deliberately have no remote command runner.
|
||||
vi.spyOn(executionTargets, "prepareGitHubExecutionEnvironment").mockImplementation(async (input) => input.env);
|
||||
vi.spyOn(gitCredentials, "resolveManagedGitHubIdentitySelection").mockResolvedValue({ configured: true });
|
||||
});
|
||||
afterAll(async () => { await database?.cleanup(); });
|
||||
async function running() {
|
||||
const [run] = await db.insert(heartbeatRuns).values({ companyId, agentId, status: "running", invocationSource: "on_demand", startedAt: new Date(),
|
||||
|
|
@ -103,7 +109,7 @@ describe("heartbeat process identity persistence", () => {
|
|||
} finally { release(); await heartbeat.drainActiveRunExecutions(); }
|
||||
}, 30_000);
|
||||
|
||||
it.each([false, true])("cancels the remote adapter and waits for teardown (scope lookup raced: %s)", async (scopeLookupRaced) => {
|
||||
it("cancels the remote adapter and waits for teardown before acknowledging Stop", async () => {
|
||||
const originalOrchestrator = orchestration.environmentRunOrchestrator;
|
||||
vi.spyOn(orchestration, "environmentRunOrchestrator").mockImplementation((...args) => {
|
||||
const actual = originalOrchestrator(...args);
|
||||
|
|
@ -125,14 +131,17 @@ describe("heartbeat process identity persistence", () => {
|
|||
execute: async (input) => {
|
||||
expect(hasAdapterRunCancellation(input.runId)).toBe(true);
|
||||
const cleanup = await bindAdapterRunStop(input.runId, async () => {
|
||||
expect((await heartbeat.getRun(input.runId))?.status).toBe("cancelled");
|
||||
const run = await heartbeat.getRun(input.runId);
|
||||
expect(run?.status).toBe("running");
|
||||
expect(run?.resultJson?.executionCancellation).toMatchObject({ state: "requested" });
|
||||
stopped();
|
||||
});
|
||||
ready();
|
||||
await interrupted;
|
||||
await teardown;
|
||||
await cleanup();
|
||||
return { exitCode: 143, signal: "SIGTERM", timedOut: false };
|
||||
return { exitCode: 143, signal: "SIGTERM", timedOut: false,
|
||||
resultJson: { executionCancellation: { state: "acknowledged" } } };
|
||||
},
|
||||
} as ReturnType<typeof adapters.getServerAdapter>);
|
||||
let pending: Promise<unknown> | undefined;
|
||||
|
|
@ -140,9 +149,6 @@ describe("heartbeat process identity persistence", () => {
|
|||
const queued = await heartbeat.invoke(agentId, "on_demand", {}, "manual");
|
||||
expect(queued).not.toBeNull();
|
||||
await started;
|
||||
// Model the initial lookup occurring before registration. The final
|
||||
// lookup after persisting cancellation must still notify the scope.
|
||||
if (scopeLookupRaced) vi.spyOn(cancellation, "hasAdapterRunCancellation").mockReturnValueOnce(false);
|
||||
let acknowledged = false;
|
||||
pending = heartbeat.cancelRun(queued!.id).then((result) => { acknowledged = true; return result; });
|
||||
await interrupted;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ it("dispatch retains raw trust before workspace and broker setup", () => {
|
|||
"utf8",
|
||||
);
|
||||
const start = heartbeat.indexOf(
|
||||
"const retainedTrust = await resolveAndRetainRunTrustPreset",
|
||||
"const retainedTrust = await ",
|
||||
);
|
||||
const end = heartbeat.indexOf(
|
||||
"const config = parseObject(agent.adapterConfig);",
|
||||
|
|
@ -61,6 +61,7 @@ it("dispatch retains raw trust before workspace and broker setup", () => {
|
|||
);
|
||||
const dispatch = heartbeat.slice(start, end);
|
||||
expect(start).toBeGreaterThan(0);
|
||||
expect(dispatch).toContain("resolveAndRetainRunTrustPreset(db,");
|
||||
expect(dispatch).toContain(
|
||||
"executionWorkspacePolicy: projectContext.executionWorkspacePolicy",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -405,7 +405,6 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
|||
}
|
||||
if (profile.provider === "acpx") {
|
||||
try {
|
||||
if (profile.acpxAgent !== "claude") throw new Error("Select Codex to use the native Codex runner.");
|
||||
const target = context.executionTarget;
|
||||
if (target?.kind === "remote") {
|
||||
const probe = await runAdapterExecutionTargetShellCommand(
|
||||
|
|
@ -414,8 +413,8 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
|||
);
|
||||
if (probe.timedOut || probe.exitCode !== 0) throw new Error("Could not verify the remote ACPX runner platform.");
|
||||
const [os, arch] = probe.stdout.trim().split(/\s+/);
|
||||
if (!((os === "Linux" && arch === "x86_64") || (os === "Darwin" && ["arm64", "x86_64"].includes(arch ?? "")))) {
|
||||
throw new Error("ACPX Claude requires Linux x64 or macOS ARM64/x64.");
|
||||
if (!((os === "Linux" && arch === "x86_64") || (profile.acpxAgent === "claude" && os === "Darwin" && ["arm64", "x86_64"].includes(arch ?? "")))) {
|
||||
throw new Error(`ACPX ${profile.acpxAgent} requires Linux x64${profile.acpxAgent === "claude" ? " or macOS ARM64/x64" : ""}.`);
|
||||
}
|
||||
return {
|
||||
adapterType: "paperclip_runner", status: "warn" as const, testedAt: new Date().toISOString(),
|
||||
|
|
@ -423,6 +422,13 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
|||
message: "The remote platform is supported. Runtime package integrity and readiness must still be verified by the remote runner before launch." }],
|
||||
};
|
||||
}
|
||||
if (profile.acpxAgent !== "claude") {
|
||||
return {
|
||||
adapterType: "paperclip_runner", status: "warn" as const, testedAt: new Date().toISOString(),
|
||||
checks: [{ code: "acpx_runtime_unverified", level: "warn" as const,
|
||||
message: `The ACPX ${profile.acpxAgent} profile is qualified. Runtime package integrity, platform support, and readiness must still be verified by the runner before launch.` }],
|
||||
};
|
||||
}
|
||||
const { probeAcpxClaudeInstallation } = await import("@paperclipai/paperclip-runner/live");
|
||||
await probeAcpxClaudeInstallation(profile.model);
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in New Issue