fix: qualify Codex sandbox probe and diagnose retained native sessions
This commit is contained in:
parent
3f55e74a22
commit
bd59c07a21
|
|
@ -632,7 +632,9 @@ impl AcpxCommandExecutor {
|
|||
event_type: "run.terminal".to_owned(),
|
||||
priority: EventPriority::P0,
|
||||
payload: json!({
|
||||
"schema": "paperclip.prp.terminal.v1",
|
||||
"status": "failed",
|
||||
"turnTerminalState": "failed",
|
||||
"runTerminalState": "failed",
|
||||
"reportedWorkDisposition": "unknown",
|
||||
"provider": "acpx",
|
||||
|
|
@ -2229,6 +2231,8 @@ mod tests {
|
|||
assert_eq!(events[0].event_type, "turn.failed");
|
||||
assert_eq!(events[0].payload["providerShutdownFailed"], true);
|
||||
assert_eq!(events[1].event_type, "run.terminal");
|
||||
assert_eq!(events[1].payload["schema"], "paperclip.prp.terminal.v1");
|
||||
assert_eq!(events[1].payload["turnTerminalState"], "failed");
|
||||
let cleanup_error = recovered
|
||||
.shutdown()
|
||||
.expect_err("cleanup must not succeed while the original lifetime remains active");
|
||||
|
|
|
|||
|
|
@ -642,6 +642,14 @@ fn response_error_classification(error: &ResponseError) -> &'static str {
|
|||
_ => {}
|
||||
}
|
||||
match error.message.as_str() {
|
||||
"ACPX recovery identity conflicts with the immutable session configuration" => {
|
||||
"recovery_configuration_mismatch"
|
||||
}
|
||||
"ACPX recovery identity does not match the persisted runtime record" => {
|
||||
"recovery_identity_mismatch"
|
||||
}
|
||||
"ACPX provider lifetime lease is unavailable" => "provider_lifetime_unavailable",
|
||||
"Managed Codex credential home already has an active lease" => "provider_lifetime_owned",
|
||||
"ACPX session handshake exceeded its admission deadline" => "session_handshake_timeout",
|
||||
"ACPX provider lifetime guardian exited before ownership transfer" => {
|
||||
"provider_guardian_exit"
|
||||
|
|
@ -734,6 +742,39 @@ mod tests {
|
|||
)),
|
||||
"session_handshake_timeout"
|
||||
);
|
||||
for (message, classification) in [
|
||||
(
|
||||
"ACPX recovery identity conflicts with the immutable session configuration",
|
||||
"recovery_configuration_mismatch",
|
||||
),
|
||||
(
|
||||
"ACPX recovery identity does not match the persisted runtime record",
|
||||
"recovery_identity_mismatch",
|
||||
),
|
||||
(
|
||||
"ACPX provider lifetime lease is unavailable",
|
||||
"provider_lifetime_unavailable",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
response_error_classification(&error("acpx_sidecar_command_failed", message)),
|
||||
classification
|
||||
);
|
||||
assert_eq!(
|
||||
response_error_classification(&error(
|
||||
"acpx_sidecar_command_failed",
|
||||
&format!("{message}: private-provider-detail")
|
||||
)),
|
||||
"unclassified"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
response_error_classification(&error(
|
||||
"acpx_sidecar_command_failed",
|
||||
"Managed Codex credential home already has an active lease"
|
||||
)),
|
||||
"provider_lifetime_owned"
|
||||
);
|
||||
let admission_failures = [
|
||||
(
|
||||
"ACPX_RUNTIME_ADMISSION_VERIFICATION_TIMEOUT",
|
||||
|
|
|
|||
|
|
@ -405,6 +405,57 @@ fn executes_a_qualified_acpx_profile_through_the_native_selector() {
|
|||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumes_an_idle_acpx_session_in_a_cold_replacement_runner() {
|
||||
let directory = temporary_directory("acpx-cold-idle-recovery");
|
||||
let config = acpx_config(&directory, "turns-reserved-result-terminal");
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
prepare_payload(&directory, "codex"),
|
||||
))
|
||||
.unwrap();
|
||||
let original = executor
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(&command(
|
||||
3,
|
||||
"turn.start",
|
||||
json!({"text":"Acknowledge.", "turnId":"provider-turn-first"}),
|
||||
))
|
||||
.unwrap();
|
||||
let events = executor.poll_events().unwrap();
|
||||
executor.acknowledge_events(events.len()).unwrap();
|
||||
executor
|
||||
.execute(&command(4, "runner.suspend", json!({})))
|
||||
.unwrap();
|
||||
executor.shutdown().unwrap();
|
||||
drop(executor);
|
||||
|
||||
let mut replacement_config = config.clone();
|
||||
replacement_config.run_id = "run-2".to_owned();
|
||||
replacement_config.turn_id = "turn-2".to_owned();
|
||||
let mut replacement =
|
||||
NativeProviderCommandExecutor::with_runner_config(&directory, &replacement_config);
|
||||
let mut payload = prepare_payload(&directory, "codex");
|
||||
payload["provider"]["runId"] = json!("run-2");
|
||||
let resumed = replacement
|
||||
.execute(&command(1, "run.attach", payload))
|
||||
.unwrap();
|
||||
assert_eq!(resumed.result["status"], "resumed");
|
||||
assert_eq!(
|
||||
resumed.result["providerSessionId"],
|
||||
original.result["providerSessionId"]
|
||||
);
|
||||
// Admission itself must preserve the provider identity before any new
|
||||
// model turn. The fixture's scripted terminal events belong to run-1.
|
||||
replacement.shutdown().unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_native_acpx_semantic_events_on_the_durable_controller_turn() {
|
||||
let directory = temporary_directory("acpx-durable-turn-correlation");
|
||||
|
|
|
|||
|
|
@ -245,7 +245,9 @@ export async function runChatFlow(input: {
|
|||
enableClassicTaskInterface: false,
|
||||
});
|
||||
expect(await api.get(chatPath)).toBeNull();
|
||||
await page.goto(route);
|
||||
// Cold Vite startup can keep unrelated assets loading after the chat is
|
||||
// interactive. The composer assertion below verifies actual UI readiness.
|
||||
await page.goto(route, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
await expect(page.getByTestId("task-chat-composer-input")).toBeVisible();
|
||||
expect(await api.get(chatPath)).toBeNull();
|
||||
expect(await allRuns()).toHaveLength(0);
|
||||
|
|
@ -261,7 +263,7 @@ export async function runChatFlow(input: {
|
|||
const initialId = issue!.id;
|
||||
const before = runs.filter((run) => !isResetRun(run))[0]!;
|
||||
await noTasks();
|
||||
await page.reload();
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
if (caseId === "continuity-restart") {
|
||||
await turn(
|
||||
"What phrase did I just ask you to remember? Reply with the phrase only.",
|
||||
|
|
@ -272,7 +274,7 @@ export async function runChatFlow(input: {
|
|||
).toContain(secret);
|
||||
const count = runs.length;
|
||||
await input.restart();
|
||||
await page.reload();
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
await idle(2);
|
||||
expect(runs).toHaveLength(count);
|
||||
await turn(
|
||||
|
|
@ -365,7 +367,7 @@ export async function runChatFlow(input: {
|
|||
).toEqual(
|
||||
oldComments.filter((c) => c.createdByRunId === cancelledId),
|
||||
);
|
||||
await page.reload();
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
await expect(
|
||||
page.getByText("New session", { exact: true }),
|
||||
).toHaveCount(1);
|
||||
|
|
@ -677,7 +679,7 @@ export async function runChatFlow(input: {
|
|||
expect(projects[0]!.workspaces.filter((w) => w.repoUrl)).toHaveLength(
|
||||
0,
|
||||
);
|
||||
await page.reload();
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
await expect(
|
||||
page.getByRole("article", { name: /Project created:/ }),
|
||||
).toHaveCount(1);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ export async function prepareCodexCiSandbox(repositoryRoot: string, temporaryRoo
|
|||
execFileSync("sudo", ["-n", "apparmor_parser", "-r", profilePath], { timeout: 15_000, stdio: "pipe" });
|
||||
const probeHome = path.join(temporaryRoot, "codex-sandbox-probe");
|
||||
await mkdir(probeHome, { mode: 0o700 });
|
||||
execFileSync(binary, ["sandbox", "-C", temporaryRoot, "--", "/bin/true"], {
|
||||
execFileSync(binary, [
|
||||
"sandbox", "--permission-profile", "paperclip-e2e-probe",
|
||||
"-c", 'permissions.paperclip-e2e-probe.filesystem={":root"="read"}',
|
||||
"-c", "permissions.paperclip-e2e-probe.network.enabled=false",
|
||||
"-C", temporaryRoot, "--", "/bin/true",
|
||||
], {
|
||||
cwd: temporaryRoot,
|
||||
env: { PATH: process.env.PATH, CODEX_HOME: probeHome },
|
||||
timeout: 15_000,
|
||||
|
|
|
|||
Loading…
Reference in New Issue