mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 22:13:59 +01:00
fix(e2e): configure supported rolling agents with warm policy
The warm environment was overridden by each profile’s explicit per-turn agent policy. Use the production managed warm capability to select supported profiles, set their agent policy to warm, and retain per-turn coverage for every qualified native profile. The effective-policy regression fails on the old catalog; 39 affected catalog and controller checks pass. Shared-package type declarations need a fresh build for the broad local typecheck. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8f06bd4e17
commit
5dfe1d4b29
4 files changed
+42
-9
No files matched your search
@@ -191,6 +191,9 @@ Postgres. Each controller has an independent compiled build and cache. The copie
|
||||
app retains the checkout's `cli/` and `packages/` dependency links, including the
|
||||
development Runner vendor entry. Shared workspace dependencies mean this is
|
||||
not released-container or Railway evidence.
|
||||
The warm lane selects providers through the production managed warm-session
|
||||
capability and explicitly sets the agent policy to warm. The per-turn lane
|
||||
retains every registered provider's per-turn policy.
|
||||
The read-only recovery observer captures the live predecessor's identity just
|
||||
before handoff, then requires a new controller generation, authenticated Runner
|
||||
attachment, and new canonical events for the original execution. No fixture
|
||||
|
||||
@@ -33,6 +33,24 @@ import {
|
||||
} from "./selectors.js";
|
||||
|
||||
describe("runner E2E catalog", () => {
|
||||
it("uses effective warm agent policy only for supported rolling warm providers", () => {
|
||||
const warm = runnerMatrix.filter(cell => cell.suite.id === "rolling-web-deployment-warm");
|
||||
const perTurn = runnerMatrix.filter(cell => cell.suite.id === "rolling-web-deployment");
|
||||
expect(warm.some(cell => cell.profile.id === "runner-codex")).toBe(true);
|
||||
expect(warm.some(cell => ["runner-opencode", "runner-acpx-claude", "runner-acpx-grok"].includes(cell.profile.id))).toBe(false);
|
||||
expect(perTurn.length).toBeGreaterThan(warm.length);
|
||||
for (const cell of [...warm, ...perTurn]) {
|
||||
const secretRefs = Object.fromEntries([cell.profile.credential, "DAYTONA_API_KEY"].map(key => [key,
|
||||
{ type: "secret_ref" as const, secretId: "22222222-2222-4222-8222-222222222222", version: "latest" as const }]));
|
||||
const input = { executionId: cell.id, secretRefs, daytonaImage: `fixture@sha256:${"a".repeat(64)}` };
|
||||
const agent = cell.profile.buildAgent({ ...input, environmentId: "env-1", environmentFixtureId: "daytona", workspacePath: "/workspace" });
|
||||
const environment = cell.environment.buildEnvironment(input);
|
||||
const expected = cell.suite.id === "rolling-web-deployment-warm" ? "warm" : "per_turn";
|
||||
expect(agent.adapterConfig).toMatchObject({ lifecycleMode: expected });
|
||||
expect(environment.config).toMatchObject({ runnerLifecycleMode: expected, reuseLease: expected === "warm" });
|
||||
expect(cell.suite.expectedMatrixSize).toBe(cell.suite.profiles.length * cell.suite.tasks.length);
|
||||
}
|
||||
});
|
||||
it("keeps verified artifact transfer a single explicit Daytona run", () => {
|
||||
const id = "api-artifact-transfer.runner-codex.daytona.verified-artifact-copy";
|
||||
const cells = selectRunnerExecutions(parseRunnerSelectors(["--id", id]));
|
||||
@@ -312,11 +330,11 @@ describe("runner E2E catalog", () => {
|
||||
expect(localIntegrityTasks).toHaveLength(2);
|
||||
expect(openRouterBreadthTasks).toHaveLength(3);
|
||||
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
|
||||
1, 9, 9, 9, 9, 4, 2, 1, 3, 2, 2, 4, 4, 4, 4, 4, 4, 8, 10, 6, 63, 12, 6, 4, 2, 4, 2, 30, 3, 16, 16, 20, 2, 1, 6, 8, 46, 2, 23, 15, 52, 6, 6, 20, 26, 52, 5, 28, 18, 2, 6, 6, 13, 10, 4, 48, 16, 10, 2, 1, 1, 4, 116, 2, 1, 2,
|
||||
1, 9, 2, 9, 9, 4, 2, 1, 3, 2, 2, 4, 4, 4, 4, 4, 4, 8, 10, 6, 63, 12, 6, 4, 2, 4, 2, 30, 3, 16, 16, 20, 2, 1, 6, 8, 46, 2, 23, 15, 52, 6, 6, 20, 26, 52, 5, 28, 18, 2, 6, 6, 13, 10, 4, 48, 16, 10, 2, 1, 1, 4, 116, 2, 1, 2,
|
||||
|
||||
]);
|
||||
expect(validateRunnerCatalog()).toHaveLength(837);
|
||||
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(837);
|
||||
expect(validateRunnerCatalog()).toHaveLength(830);
|
||||
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(830);
|
||||
expect(
|
||||
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
|
||||
).toHaveLength(48);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ROLLING_WEB_DEPLOYMENT_SUITE, ROLLING_WEB_DEPLOYMENT_WARM_SUITE } from "./controller-rolling-deployment.js";
|
||||
import { supportsManagedNativeWarmSession } from "../../server/src/services/native-runtime/native-warm-capability.js";
|
||||
import { FEEDBACK_PERSISTENCE_SUITE, feedbackPersistenceTask, feedbackPersistenceDigest } from "./feedback-persistence.js";
|
||||
import { followupStopTask, followupStopDigest } from "./followup-stop-resume.js";
|
||||
import { finalizationRecoveryTask, finalizationRecoveryDigest } from "./finalization-recovery.js";
|
||||
@@ -1252,15 +1253,24 @@ const rollingDeploymentProfiles = [...runnerProfiles, ...extendedHarnessProfiles
|
||||
return { ...agent, adapterConfig };
|
||||
} };
|
||||
});
|
||||
const rollingWarmDeploymentProfiles = rollingDeploymentProfiles.filter(profile => {
|
||||
const backend = profile.provider === "codex" ? "codex_app_server" : profile.provider === "opencode" ? "opencode_server" : "acpx_runtime";
|
||||
return supportsManagedNativeWarmSession({ backend, acpxAgent: "acpxAgent" in profile ? profile.acpxAgent : undefined },
|
||||
profile.credential === "COPILOT_GITHUB_TOKEN" ? { provider: "github", method: "api_key" } : undefined);
|
||||
}).map(profile => ({ ...profile, buildAgent(input: AgentFixtureBuildInput) {
|
||||
const agent = profile.buildAgent(input);
|
||||
return { ...agent, adapterConfig: { ...(agent.adapterConfig as Record<string, unknown>), lifecycleMode: "warm" } };
|
||||
} }));
|
||||
|
||||
const rollingDeploymentSuites: RunnerSuiteFixture[] = [
|
||||
{ id: ROLLING_WEB_DEPLOYMENT_SUITE, environment: runnerEnvironments.find(environment => environment.id === "daytona")! },
|
||||
{ id: ROLLING_WEB_DEPLOYMENT_WARM_SUITE, environment: daytonaControllerOutageEnvironment },
|
||||
].map(({ id, environment }) => ({
|
||||
...remoteWorkspaceStorageSuite, id, label: `Rolling control-plane web deployment (${environment.configurationKey ? "warm" : "per-turn"})`,
|
||||
description: "Overlap independent ephemeral web servers while a remote execution stays active; keep ordinary reads, saves, files and a later native continuation working.",
|
||||
profiles: rollingDeploymentProfiles, environments: [environment],
|
||||
expectedMatrixSize: rollingDeploymentProfiles.length * remoteWorkspaceStorageSuite.tasks.length,
|
||||
definitionMetadata: { ...remoteWorkspaceStorageSuite.definitionMetadata, version: 4, controllerDowntimeMs: 0,
|
||||
profiles: id === ROLLING_WEB_DEPLOYMENT_WARM_SUITE ? rollingWarmDeploymentProfiles : rollingDeploymentProfiles, environments: [environment],
|
||||
expectedMatrixSize: (id === ROLLING_WEB_DEPLOYMENT_WARM_SUITE ? rollingWarmDeploymentProfiles : rollingDeploymentProfiles).length * remoteWorkspaceStorageSuite.tasks.length,
|
||||
definitionMetadata: { ...remoteWorkspaceStorageSuite.definitionMetadata, version: 5, controllerDowntimeMs: 0,
|
||||
activeWorkBoundary: "foreground-180-second-cli-reliability-soak-and-live-owner-recheck",
|
||||
workspaceFileSource: environment.configurationKey ? "live" : "saved-exact-snapshot-after-per-turn-retirement",
|
||||
lifecycleCoverage: environment.configurationKey ? "warm-reusable" : "per-turn-disposable", providerCoverage: "registered-native-daytona-profiles",
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("rolling controller qualification", () => {
|
||||
const [outage] = selectRunnerExecutions(parseRunnerSelectors(["--suite", "remote-workspace-storage"]));
|
||||
expect(controllerOutageForExecution(outage!, {})).toEqual({ signal: "SIGKILL", holdMs: 300_000 });
|
||||
});
|
||||
it("requires both lifecycle modes for every production-qualified native sandbox profile", () => {
|
||||
it("covers every qualified native profile per turn and each supported warm profile", () => {
|
||||
const executions = ["rolling-web-deployment", "rolling-web-deployment-warm"]
|
||||
.flatMap(suite => selectRunnerExecutions(parseRunnerSelectors(["--suite", suite])));
|
||||
const native = [...runnerProfiles, ...extendedHarnessProfiles, kimiRunnerProfile]
|
||||
@@ -74,12 +74,14 @@ describe("rolling controller qualification", () => {
|
||||
&& (!profile.qualificationCandidate || QUALIFIED_ACPX_PROFILES[profile.qualificationCandidate].qualificationStatus !== "pending"))
|
||||
.map(kind));
|
||||
expect(new Set(native.map(kind))).toEqual(registeredKinds);
|
||||
const warmIds = new Set(runnerSuites.find(suite => suite.id === "rolling-web-deployment-warm")!.profiles.map(profile => profile.id));
|
||||
for (const profile of native) {
|
||||
const cells = executions.filter(execution => execution.profile.id === profile.id);
|
||||
expect(cells).toHaveLength(2);
|
||||
expect(cells.filter(cell => cell.suite.id === "rolling-web-deployment")).toHaveLength(1);
|
||||
expect(cells).toHaveLength(warmIds.has(profile.id) ? 2 : 1);
|
||||
expect(cells.every(cell => !cell.profile.qualificationCandidate)).toBe(true);
|
||||
expect(new Set(cells.map(cell => cell.environment.configurationKey ?? "per_turn")))
|
||||
.toEqual(new Set(["per_turn", "warm-controller-outage-v1"]));
|
||||
.toEqual(new Set(warmIds.has(profile.id) ? ["per_turn", "warm-controller-outage-v1"] : ["per_turn"]));
|
||||
}
|
||||
expect(executions.every(execution => execution.suite.definitionMetadata?.retainedControllerContinuation === true)).toBe(true);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user