mirror of
https://github.com/openai/codex.git
synced 2026-04-26 23:55:25 +00:00
## Why Enterprises can already constrain approvals, sandboxing, and web search through `requirements.toml` and MDM, but feature flags were still only configurable as managed defaults. That meant an enterprise could suggest feature values, but it could not actually pin them. This change closes that gap and makes enterprise feature requirements behave like the other constrained settings. The effective feature set now stays consistent with enterprise requirements during config load, when config writes are validated, and when runtime code mutates feature flags later in the session. It also tightens the runtime API for managed features. `ManagedFeatures` now follows the same constraint-oriented shape as `Constrained<T>` instead of exposing panic-prone mutation helpers, and production code can no longer construct it through an unconstrained `From<Features>` path. The PR also hardens the `compact_resume_fork` integration coverage on Windows. After the feature-management changes, `compact_resume_after_second_compaction_preserves_history` was overflowing the libtest/Tokio thread stacks on Windows, so the test now uses an explicit larger-stack harness as a pragmatic mitigation. That may not be the ideal root-cause fix, and it merits a parallel investigation into whether part of the async future chain should be boxed to reduce stack pressure instead. ## What Changed Enterprises can now pin feature values in `requirements.toml` with the requirements-side `features` table: ```toml [features] personality = true unified_exec = false ``` Only canonical feature keys are allowed in the requirements `features` table; omitted keys remain unconstrained. - Added a requirements-side pinned feature map to `ConfigRequirementsToml`, threaded it through source-preserving requirements merge and normalization in `codex-config`, and made the TOML surface use `[features]` (while still accepting legacy `[feature_requirements]` for compatibility). - Exposed `featureRequirements` from `configRequirements/read`, regenerated the JSON/TypeScript schema artifacts, and updated the app-server README. - Wrapped the effective feature set in `ManagedFeatures`, backed by `ConstrainedWithSource<Features>`, and changed its API to mirror `Constrained<T>`: `can_set(...)`, `set(...) -> ConstraintResult<()>`, and result-returning `enable` / `disable` / `set_enabled` helpers. - Removed the legacy-usage and bulk-map passthroughs from `ManagedFeatures`; callers that need those behaviors now mutate a plain `Features` value and reapply it through `set(...)`, so the constrained wrapper remains the enforcement boundary. - Removed the production loophole for constructing unconstrained `ManagedFeatures`. Non-test code now creates it through the configured feature-loading path, and `impl From<Features> for ManagedFeatures` is restricted to `#[cfg(test)]`. - Rejected legacy feature aliases in enterprise feature requirements, and return a load error when a pinned combination cannot survive dependency normalization. - Validated config writes against enterprise feature requirements before persisting changes, including explicit conflicting writes and profile-specific feature states that normalize into invalid combinations. - Updated runtime and TUI feature-toggle paths to use the constrained setter API and to persist or apply the effective post-constraint value rather than the requested value. - Updated the `core_test_support` Bazel target to include the bundled core model-catalog fixtures in its runtime data, so helper code that resolves `core/models.json` through runfiles works in remote Bazel test environments. - Renamed the core config test coverage to emphasize that effective feature values are normalized at runtime, while conflicting persisted config writes are rejected. - Ran `compact_resume_after_second_compaction_preserves_history` inside an explicit 8 MiB test thread and Tokio runtime worker stack, following the existing larger-stack integration-test pattern, to keep the Windows `compact_resume_fork` test slice from aborting while a parallel investigation continues into whether some of the underlying async futures should be boxed. ## Verification - `cargo test -p codex-config` - `cargo test -p codex-core feature_requirements_ -- --nocapture` - `cargo test -p codex-core load_requirements_toml_produces_expected_constraints -- --nocapture` - `cargo test -p codex-core compact_resume_after_second_compaction_preserves_history -- --nocapture` - `cargo test -p codex-core compact_resume_fork -- --nocapture` - Re-ran the built `codex-core` `tests/all` binary with `RUST_MIN_STACK=262144` for `compact_resume_after_second_compaction_preserves_history` to confirm the explicit-stack harness fixes the deterministic low-stack repro. - `cargo test -p codex-core` - This still fails locally in unrelated integration areas that expect the `codex` / `test_stdio_server` binaries or hit existing `search_tool` wiremock mismatches. ## Docs `developers.openai.com/codex` should document the requirements-side `[features]` table for enterprise and MDM-managed configuration, including that it only accepts canonical feature keys and that conflicting config writes are rejected.
594 lines
18 KiB
Rust
594 lines
18 KiB
Rust
#![cfg(not(target_os = "windows"))]
|
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
|
|
|
use std::fs;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use codex_core::features::Feature;
|
|
use codex_core::sandboxing::SandboxPermissions;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use core_test_support::assert_regex_match;
|
|
use core_test_support::responses::ev_assistant_message;
|
|
use core_test_support::responses::ev_completed;
|
|
use core_test_support::responses::ev_custom_tool_call;
|
|
use core_test_support::responses::ev_function_call;
|
|
use core_test_support::responses::ev_response_created;
|
|
use core_test_support::responses::mount_sse_once;
|
|
use core_test_support::responses::mount_sse_sequence;
|
|
use core_test_support::responses::sse;
|
|
use core_test_support::responses::start_mock_server;
|
|
use core_test_support::skip_if_no_network;
|
|
use core_test_support::test_codex::test_codex;
|
|
use regex_lite::Regex;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
|
|
fn tool_names(body: &Value) -> Vec<String> {
|
|
body.get("tools")
|
|
.and_then(Value::as_array)
|
|
.map(|tools| {
|
|
tools
|
|
.iter()
|
|
.filter_map(|tool| {
|
|
tool.get("name")
|
|
.or_else(|| tool.get("type"))
|
|
.and_then(Value::as_str)
|
|
.map(str::to_string)
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn custom_tool_unknown_returns_custom_output_error() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let server = start_mock_server().await;
|
|
let mut builder = test_codex();
|
|
let test = builder.build(&server).await?;
|
|
|
|
let call_id = "custom-unsupported";
|
|
let tool_name = "unsupported_tool";
|
|
|
|
mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_custom_tool_call(call_id, tool_name, "\"payload\""),
|
|
ev_completed("resp-1"),
|
|
]),
|
|
)
|
|
.await;
|
|
let mock = mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-2"),
|
|
]),
|
|
)
|
|
.await;
|
|
|
|
test.submit_turn_with_policies(
|
|
"invoke custom tool",
|
|
AskForApproval::Never,
|
|
SandboxPolicy::DangerFullAccess,
|
|
)
|
|
.await?;
|
|
|
|
let item = mock.single_request().custom_tool_call_output(call_id);
|
|
let output = item
|
|
.get("output")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default();
|
|
let expected = format!("unsupported custom tool call: {tool_name}");
|
|
assert_eq!(output, expected);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn shell_escalated_permissions_rejected_then_ok() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let server = start_mock_server().await;
|
|
let mut builder = test_codex().with_model("gpt-5");
|
|
let test = builder.build(&server).await?;
|
|
|
|
let command = ["/bin/echo", "shell ok"];
|
|
let call_id_blocked = "shell-blocked";
|
|
let call_id_success = "shell-success";
|
|
|
|
let first_args = json!({
|
|
"command": command,
|
|
"timeout_ms": 1_000,
|
|
"sandbox_permissions": SandboxPermissions::RequireEscalated,
|
|
});
|
|
let second_args = json!({
|
|
"command": command,
|
|
"timeout_ms": 1_000,
|
|
});
|
|
|
|
mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_function_call(
|
|
call_id_blocked,
|
|
"shell",
|
|
&serde_json::to_string(&first_args)?,
|
|
),
|
|
ev_completed("resp-1"),
|
|
]),
|
|
)
|
|
.await;
|
|
let second_mock = mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_response_created("resp-2"),
|
|
ev_function_call(
|
|
call_id_success,
|
|
"shell",
|
|
&serde_json::to_string(&second_args)?,
|
|
),
|
|
ev_completed("resp-2"),
|
|
]),
|
|
)
|
|
.await;
|
|
let third_mock = mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-3"),
|
|
]),
|
|
)
|
|
.await;
|
|
|
|
test.submit_turn_with_policies(
|
|
"run the shell command",
|
|
AskForApproval::Never,
|
|
SandboxPolicy::DangerFullAccess,
|
|
)
|
|
.await?;
|
|
|
|
let policy = AskForApproval::Never;
|
|
let expected_message = format!(
|
|
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}"
|
|
);
|
|
|
|
let blocked_output = second_mock
|
|
.single_request()
|
|
.function_call_output_content_and_success(call_id_blocked)
|
|
.and_then(|(content, _)| content)
|
|
.expect("blocked output string");
|
|
assert_eq!(
|
|
blocked_output, expected_message,
|
|
"unexpected rejection message"
|
|
);
|
|
|
|
let success_output = third_mock
|
|
.single_request()
|
|
.function_call_output_content_and_success(call_id_success)
|
|
.and_then(|(content, _)| content)
|
|
.expect("success output string");
|
|
let output_json: Value = serde_json::from_str(&success_output)?;
|
|
assert_eq!(
|
|
output_json["metadata"]["exit_code"].as_i64(),
|
|
Some(0),
|
|
"expected exit code 0 after rerunning without escalation",
|
|
);
|
|
let stdout = output_json["output"].as_str().unwrap_or_default();
|
|
let stdout_pattern = r"(?s)^shell ok\n?$";
|
|
assert_regex_match(stdout_pattern, stdout);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn sandbox_denied_shell_returns_original_output() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let server = start_mock_server().await;
|
|
let mut builder = test_codex().with_model("gpt-5.1-codex");
|
|
let fixture = builder.build(&server).await?;
|
|
|
|
let call_id = "sandbox-denied-shell";
|
|
let target_path = fixture.workspace_path("sandbox-denied.txt");
|
|
let sentinel = "sandbox-denied sentinel output";
|
|
let command = vec![
|
|
"/bin/sh".to_string(),
|
|
"-c".to_string(),
|
|
format!(
|
|
"printf {sentinel:?}; printf {content:?} > {path:?}",
|
|
sentinel = format!("{sentinel}\n"),
|
|
content = "sandbox denied",
|
|
path = &target_path
|
|
),
|
|
];
|
|
let args = json!({
|
|
"command": command,
|
|
"timeout_ms": 1_000,
|
|
});
|
|
|
|
let responses = vec![
|
|
sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_function_call(call_id, "shell", &serde_json::to_string(&args)?),
|
|
ev_completed("resp-1"),
|
|
]),
|
|
sse(vec![
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-2"),
|
|
]),
|
|
];
|
|
let mock = mount_sse_sequence(&server, responses).await;
|
|
|
|
fixture
|
|
.submit_turn_with_policy(
|
|
"run a command that should be denied by the read-only sandbox",
|
|
SandboxPolicy::new_read_only_policy(),
|
|
)
|
|
.await?;
|
|
|
|
let output_text = mock
|
|
.function_call_output_text(call_id)
|
|
.context("shell output present")?;
|
|
let exit_code_line = output_text
|
|
.lines()
|
|
.next()
|
|
.context("exit code line present")?;
|
|
let exit_code = exit_code_line
|
|
.strip_prefix("Exit code: ")
|
|
.context("exit code prefix present")?
|
|
.trim()
|
|
.parse::<i32>()
|
|
.context("exit code is integer")?;
|
|
let body = output_text;
|
|
|
|
let body_lower = body.to_lowercase();
|
|
// Required for multi-OS.
|
|
let has_denial = body_lower.contains("permission denied")
|
|
|| body_lower.contains("operation not permitted")
|
|
|| body_lower.contains("read-only file system");
|
|
assert!(
|
|
has_denial,
|
|
"expected sandbox denial details in tool output: {body}"
|
|
);
|
|
assert!(
|
|
body.contains(sentinel),
|
|
"expected sentinel output from command to reach the model: {body}"
|
|
);
|
|
let target_path_str = target_path
|
|
.to_str()
|
|
.context("target path string representation")?;
|
|
assert!(
|
|
body.contains(target_path_str),
|
|
"expected sandbox error to mention denied path: {body}"
|
|
);
|
|
assert!(
|
|
!body_lower.contains("failed in sandbox"),
|
|
"expected original tool output, found fallback message: {body}"
|
|
);
|
|
assert_ne!(
|
|
exit_code, 0,
|
|
"sandbox denial should surface a non-zero exit code"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn collect_tools(use_unified_exec: bool) -> Result<Vec<String>> {
|
|
let server = start_mock_server().await;
|
|
|
|
let responses = vec![sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-1"),
|
|
])];
|
|
let mock = mount_sse_sequence(&server, responses).await;
|
|
|
|
let mut builder = test_codex().with_config(move |config| {
|
|
if use_unified_exec {
|
|
config
|
|
.features
|
|
.enable(Feature::UnifiedExec)
|
|
.expect("test config should allow feature update");
|
|
} else {
|
|
config
|
|
.features
|
|
.disable(Feature::UnifiedExec)
|
|
.expect("test config should allow feature update");
|
|
}
|
|
});
|
|
let test = builder.build(&server).await?;
|
|
|
|
test.submit_turn_with_policies(
|
|
"list tools",
|
|
AskForApproval::Never,
|
|
SandboxPolicy::DangerFullAccess,
|
|
)
|
|
.await?;
|
|
|
|
let first_body = mock.single_request().body_json();
|
|
Ok(tool_names(&first_body))
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn unified_exec_spec_toggle_end_to_end() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let tools_disabled = collect_tools(false).await?;
|
|
assert!(
|
|
!tools_disabled.iter().any(|name| name == "exec_command"),
|
|
"tools list should not include exec_command when disabled: {tools_disabled:?}"
|
|
);
|
|
assert!(
|
|
!tools_disabled.iter().any(|name| name == "write_stdin"),
|
|
"tools list should not include write_stdin when disabled: {tools_disabled:?}"
|
|
);
|
|
|
|
let tools_enabled = collect_tools(true).await?;
|
|
assert!(
|
|
tools_enabled.iter().any(|name| name == "exec_command"),
|
|
"tools list should include exec_command when enabled: {tools_enabled:?}"
|
|
);
|
|
assert!(
|
|
tools_enabled.iter().any(|name| name == "write_stdin"),
|
|
"tools list should include write_stdin when enabled: {tools_enabled:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn shell_timeout_includes_timeout_prefix_and_metadata() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let server = start_mock_server().await;
|
|
let mut builder = test_codex().with_model("gpt-5");
|
|
let test = builder.build(&server).await?;
|
|
|
|
let call_id = "shell-timeout";
|
|
let timeout_ms = 50u64;
|
|
let args = json!({
|
|
"command": ["/bin/sh", "-c", "yes line | head -n 400; sleep 1"],
|
|
"timeout_ms": timeout_ms,
|
|
});
|
|
|
|
mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_function_call(call_id, "shell", &serde_json::to_string(&args)?),
|
|
ev_completed("resp-1"),
|
|
]),
|
|
)
|
|
.await;
|
|
let second_mock = mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-2"),
|
|
]),
|
|
)
|
|
.await;
|
|
|
|
test.submit_turn_with_policies(
|
|
"run a long command",
|
|
AskForApproval::Never,
|
|
SandboxPolicy::DangerFullAccess,
|
|
)
|
|
.await?;
|
|
|
|
let timeout_item = second_mock.single_request().function_call_output(call_id);
|
|
|
|
let output_str = timeout_item
|
|
.get("output")
|
|
.and_then(Value::as_str)
|
|
.expect("timeout output string");
|
|
|
|
// The exec path can report a timeout in two ways depending on timing:
|
|
// 1) Structured JSON with exit_code 124 and a timeout prefix (preferred), or
|
|
// 2) A plain error string if the child is observed as killed by a signal first.
|
|
if let Ok(output_json) = serde_json::from_str::<Value>(output_str) {
|
|
assert_eq!(
|
|
output_json["metadata"]["exit_code"].as_i64(),
|
|
Some(124),
|
|
"expected timeout exit code 124",
|
|
);
|
|
|
|
let stdout = output_json["output"].as_str().unwrap_or_default();
|
|
assert!(
|
|
stdout.contains("command timed out"),
|
|
"timeout output missing `command timed out`: {stdout}"
|
|
);
|
|
} else {
|
|
// Fallback: accept the signal classification path to deflake the test.
|
|
let signal_pattern = r"(?is)^execution error:.*signal.*$";
|
|
assert_regex_match(signal_pattern, output_str);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn shell_timeout_handles_background_grandchild_stdout() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let server = start_mock_server().await;
|
|
let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| {
|
|
config
|
|
.permissions
|
|
.sandbox_policy
|
|
.set(SandboxPolicy::DangerFullAccess)
|
|
.expect("set sandbox policy");
|
|
});
|
|
let test = builder.build(&server).await?;
|
|
|
|
let call_id = "shell-grandchild-timeout";
|
|
let pid_path = test.cwd.path().join("grandchild_pid.txt");
|
|
let script_path = test.cwd.path().join("spawn_detached.py");
|
|
let script = format!(
|
|
r#"import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# Spawn a detached grandchild that inherits stdout/stderr so the pipe stays open.
|
|
proc = subprocess.Popen(["/bin/sh", "-c", "sleep 60"], start_new_session=True)
|
|
Path({pid_path:?}).write_text(str(proc.pid))
|
|
time.sleep(60)
|
|
"#
|
|
);
|
|
fs::write(&script_path, script)?;
|
|
|
|
let args = json!({
|
|
"command": ["python3", script_path.to_string_lossy()],
|
|
"timeout_ms": 200,
|
|
});
|
|
|
|
mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_function_call(call_id, "shell", &serde_json::to_string(&args)?),
|
|
ev_completed("resp-1"),
|
|
]),
|
|
)
|
|
.await;
|
|
let second_mock = mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-2"),
|
|
]),
|
|
)
|
|
.await;
|
|
|
|
let start = Instant::now();
|
|
let output_str = tokio::time::timeout(Duration::from_secs(10), async {
|
|
test.submit_turn_with_policies(
|
|
"run a command with a detached grandchild",
|
|
AskForApproval::Never,
|
|
SandboxPolicy::DangerFullAccess,
|
|
)
|
|
.await?;
|
|
let timeout_item = second_mock.single_request().function_call_output(call_id);
|
|
timeout_item
|
|
.get("output")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_string)
|
|
.context("timeout output string")
|
|
})
|
|
.await
|
|
.context("exec call should not hang waiting for grandchild pipes to close")??;
|
|
let elapsed = start.elapsed();
|
|
|
|
if let Ok(output_json) = serde_json::from_str::<Value>(&output_str) {
|
|
assert_eq!(
|
|
output_json["metadata"]["exit_code"].as_i64(),
|
|
Some(124),
|
|
"expected timeout exit code 124",
|
|
);
|
|
} else {
|
|
let timeout_pattern = r"(?is)command timed out|timeout";
|
|
assert_regex_match(timeout_pattern, &output_str);
|
|
}
|
|
|
|
assert!(
|
|
elapsed < Duration::from_secs(9),
|
|
"command should return shortly after timeout even with live grandchildren: {elapsed:?}"
|
|
);
|
|
|
|
if let Ok(pid_str) = fs::read_to_string(&pid_path)
|
|
&& let Ok(pid) = pid_str.trim().parse::<libc::pid_t>()
|
|
{
|
|
unsafe { libc::kill(pid, libc::SIGKILL) };
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn shell_spawn_failure_truncates_exec_error() -> Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let server = start_mock_server().await;
|
|
let mut builder = test_codex().with_config(|cfg| {
|
|
cfg.permissions
|
|
.sandbox_policy
|
|
.set(SandboxPolicy::DangerFullAccess)
|
|
.expect("set sandbox policy");
|
|
});
|
|
let test = builder.build(&server).await?;
|
|
|
|
let call_id = "shell-spawn-failure";
|
|
let bogus_component = "missing-bin-".repeat(700);
|
|
let bogus_exe = test
|
|
.cwd
|
|
.path()
|
|
.join(bogus_component)
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
|
|
let args = json!({
|
|
"command": [bogus_exe],
|
|
"timeout_ms": 1_000,
|
|
});
|
|
|
|
mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_function_call(call_id, "shell", &serde_json::to_string(&args)?),
|
|
ev_completed("resp-1"),
|
|
]),
|
|
)
|
|
.await;
|
|
let second_mock = mount_sse_once(
|
|
&server,
|
|
sse(vec![
|
|
ev_assistant_message("msg-1", "done"),
|
|
ev_completed("resp-2"),
|
|
]),
|
|
)
|
|
.await;
|
|
|
|
test.submit_turn_with_policies(
|
|
"spawn a missing binary",
|
|
AskForApproval::Never,
|
|
SandboxPolicy::DangerFullAccess,
|
|
)
|
|
.await?;
|
|
|
|
let failure_item = second_mock.single_request().function_call_output(call_id);
|
|
|
|
let output = failure_item
|
|
.get("output")
|
|
.and_then(Value::as_str)
|
|
.expect("spawn failure output string");
|
|
|
|
let spawn_error_pattern = r#"(?s)^Exit code: -?\d+
|
|
Wall time: [0-9]+(?:\.[0-9]+)? seconds
|
|
Output:
|
|
execution error: .*$"#;
|
|
let spawn_truncated_pattern = r#"(?s)^Exit code: -?\d+
|
|
Wall time: [0-9]+(?:\.[0-9]+)? seconds
|
|
Total output lines: \d+
|
|
Output:
|
|
|
|
execution error: .*$"#;
|
|
let spawn_error_regex = Regex::new(spawn_error_pattern)?;
|
|
let spawn_truncated_regex = Regex::new(spawn_truncated_pattern)?;
|
|
if !spawn_error_regex.is_match(output) && !spawn_truncated_regex.is_match(output) {
|
|
let fallback_pattern = r"(?s)^execution error: .*$";
|
|
assert_regex_match(fallback_pattern, output);
|
|
}
|
|
assert!(output.len() <= 10 * 1024);
|
|
|
|
Ok(())
|
|
}
|