Compare commits

...

5 Commits

Author SHA1 Message Date
rreichel3-oai
3d54804a8d Make experimental popup tests wrap-tolerant
In codex-rs/tui/src/chatwidget/tests.rs and codex-rs/tui_app_server/src/chatwidget/tests.rs, normalize whitespace and soft hyphen wraps before checking experimental popup descriptions so Bazel's wrapped layout stays covered after adding another experimental row.
2026-03-19 17:28:39 -04:00
rreichel3-oai
3c4e07849d Fix exec-server filesystem default construction
In codex-rs/exec-server/src/server/filesystem.rs, construct a default Environment before calling get_filesystem so the default filesystem implementation compiles in CI.
2026-03-19 16:51:17 -04:00
rreichel3-oai
87c4b64da9 Mark enhanced execpolicy suggestions as experimental
In codex-rs/core/src/features.rs, move enhanced_exec_policy_suggestions from UnderDevelopment to Experimental with menu metadata so it appears in experimental feature surfaces.

In codex-rs/core/src/features_tests.rs, add explicit coverage for the new experimental stage, display name, description, and default-off behavior.
2026-03-19 16:26:49 -04:00
rreichel3-oai
e808f90724 Gate enhanced execpolicy suggestions behind feature config
In codex-rs/core/src/features.rs and codex-rs/core/config.schema.json, add the enhanced_exec_policy_suggestions feature flag as a disabled-by-default under-development config.

In codex-rs/core/src/exec_policy.rs, codex-rs/core/src/tools/handlers/shell.rs, and codex-rs/core/src/unified_exec/process_manager.rs, route auto-generated execpolicy suggestion behavior through the new feature gate while preserving legacy suggestions when it is off.

In codex-rs/core/src/exec_policy_tests.rs, cover both the legacy default-off behavior and the enhanced suggestion path explicitly.
2026-03-19 16:26:49 -04:00
rreichel3-oai
bb728b0693 Tighten generated execpolicy prefix suggestions
In codex-rs/core/src/exec_policy.rs, narrow auto-generated execpolicy amendments by truncating a command at the first flag-like token, while falling back to the whole command segment when that would leave fewer than two tokens. For parseable multi-command shell scripts, derive the suggestion from the first parsed segment.

In codex-rs/core/src/exec_policy_tests.rs, add and update coverage for single-command flag truncation, early-flag fallback, and multi-command first-segment suggestion behavior.
2026-03-19 16:26:49 -04:00
10 changed files with 331 additions and 62 deletions

View File

@@ -380,6 +380,9 @@
"enable_request_compression": {
"type": "boolean"
},
"enhanced_exec_policy_suggestions": {
"type": "boolean"
},
"exec_permission_approvals": {
"type": "boolean"
},
@@ -1986,6 +1989,9 @@
"enable_request_compression": {
"type": "boolean"
},
"enhanced_exec_policy_suggestions": {
"type": "boolean"
},
"exec_permission_approvals": {
"type": "boolean"
},

View File

@@ -223,9 +223,33 @@ impl ExecPolicyManager {
self.policy.load_full()
}
#[cfg(test)]
pub(crate) async fn create_exec_approval_requirement_for_command(
&self,
req: ExecApprovalRequest<'_>,
) -> ExecApprovalRequirement {
self.create_exec_approval_requirement_for_command_impl(
req, /*enhanced_exec_policy_suggestions*/ false,
)
.await
}
pub(crate) async fn create_exec_approval_requirement_for_command_with_enhanced_suggestions(
&self,
req: ExecApprovalRequest<'_>,
enhanced_exec_policy_suggestions: bool,
) -> ExecApprovalRequirement {
self.create_exec_approval_requirement_for_command_impl(
req,
enhanced_exec_policy_suggestions,
)
.await
}
async fn create_exec_approval_requirement_for_command_impl(
&self,
req: ExecApprovalRequest<'_>,
enhanced_exec_policy_suggestions: bool,
) -> ExecApprovalRequirement {
let ExecApprovalRequest {
command,
@@ -287,6 +311,8 @@ impl ExecPolicyManager {
if auto_amendment_allowed {
try_derive_execpolicy_amendment_for_prompt_rules(
&evaluation.matched_rules,
&commands,
enhanced_exec_policy_suggestions,
)
} else {
None
@@ -301,7 +327,11 @@ impl ExecPolicyManager {
is_policy_match(rule_match) && rule_match.decision() == Decision::Allow
}),
proposed_execpolicy_amendment: if auto_amendment_allowed {
try_derive_execpolicy_amendment_for_allow_rules(&evaluation.matched_rules)
try_derive_execpolicy_amendment_for_allow_rules(
&evaluation.matched_rules,
&commands,
enhanced_exec_policy_suggestions,
)
} else {
None
},
@@ -640,12 +670,15 @@ fn commands_for_exec_policy(command: &[String]) -> (Vec<Vec<String>>, bool) {
/// - Examples:
/// - execpolicy: empty. Command: `["python"]`. Heuristics prompt -> `Some(vec!["python"])`.
/// - execpolicy: empty. Command: `["bash", "-c", "cd /some/folder && prog1 --option1 arg1 && prog2 --option2 arg2"]`.
/// Parsed commands include `cd /some/folder`, `prog1 --option1 arg1`, and `prog2 --option2 arg2`. If heuristics allow `cd` but prompt
/// on `prog1`, we return `Some(vec!["prog1", "--option1", "arg1"])`.
/// Parsed commands include `cd /some/folder`, `prog1 --option1 arg1`, and `prog2 --option2 arg2`. In enhanced mode for
/// multi-command scripts, we derive the suggestion from the first parsed segment, so this returns
/// `Some(vec!["cd", "/some/folder"])`.
/// - execpolicy: contains a `prompt for prefix ["prog2"]` rule. For the same command as above,
/// we return `None` because an execpolicy prompt still applies even if we amend execpolicy to allow ["prog1", "--option1", "arg1"].
/// we return `None` because an execpolicy prompt still applies even if we amend execpolicy to allow ["cd", "/some/folder"].
fn try_derive_execpolicy_amendment_for_prompt_rules(
matched_rules: &[RuleMatch],
commands: &[Vec<String>],
enhanced_exec_policy_suggestions: bool,
) -> Option<ExecPolicyAmendment> {
if matched_rules
.iter()
@@ -654,13 +687,23 @@ fn try_derive_execpolicy_amendment_for_prompt_rules(
return None;
}
if enhanced_exec_policy_suggestions && commands.len() > 1 {
return auto_derived_execpolicy_amendment_for_mode(
&commands[0],
/*enhanced_exec_policy_suggestions*/ true,
);
}
matched_rules
.iter()
.find_map(|rule_match| match rule_match {
RuleMatch::HeuristicsRuleMatch {
command,
decision: Decision::Prompt,
} => Some(ExecPolicyAmendment::from(command.clone())),
} => auto_derived_execpolicy_amendment_for_mode(
command,
enhanced_exec_policy_suggestions,
),
_ => None,
})
}
@@ -670,22 +713,65 @@ fn try_derive_execpolicy_amendment_for_prompt_rules(
/// - If any execpolicy rule matches, return None, because we would already be running command outside the sandbox
fn try_derive_execpolicy_amendment_for_allow_rules(
matched_rules: &[RuleMatch],
commands: &[Vec<String>],
enhanced_exec_policy_suggestions: bool,
) -> Option<ExecPolicyAmendment> {
if matched_rules.iter().any(is_policy_match) {
return None;
}
if enhanced_exec_policy_suggestions && commands.len() > 1 {
return auto_derived_execpolicy_amendment_for_mode(
&commands[0],
/*enhanced_exec_policy_suggestions*/ true,
);
}
matched_rules
.iter()
.find_map(|rule_match| match rule_match {
RuleMatch::HeuristicsRuleMatch {
command,
decision: Decision::Allow,
} => Some(ExecPolicyAmendment::from(command.clone())),
} => auto_derived_execpolicy_amendment_for_mode(
command,
enhanced_exec_policy_suggestions,
),
_ => None,
})
}
/// In enhanced mode, keep generated execpolicy suggestions broad enough to
/// cover similar invocations, but stop before the first flag so we do not bake
/// incidental option values into a persisted allow rule.
///
/// If truncating before the first flag would leave fewer than two tokens, fall
/// back to the whole command segment.
fn auto_derived_execpolicy_amendment_for_mode(
command: &[String],
enhanced_exec_policy_suggestions: bool,
) -> Option<ExecPolicyAmendment> {
if command.is_empty() {
return None;
}
if !enhanced_exec_policy_suggestions {
return Some(ExecPolicyAmendment::from(command.to_vec()));
}
let prefix: Vec<String> = command
.iter()
.take_while(|token| !token.starts_with('-'))
.cloned()
.collect();
if prefix.len() >= 2 {
return Some(ExecPolicyAmendment::from(prefix));
}
Some(ExecPolicyAmendment::from(command.to_vec()))
}
fn derive_requested_execpolicy_amendment_from_prefix_rule(
prefix_rule: Option<&Vec<String>>,
matched_rules: &[RuleMatch],

View File

@@ -81,6 +81,17 @@ fn unrestricted_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
FileSystemSandboxPolicy::unrestricted()
}
async fn create_exec_approval_requirement_with_enhanced_suggestions(
manager: &ExecPolicyManager,
req: ExecApprovalRequest<'_>,
) -> ExecApprovalRequirement {
manager
.create_exec_approval_requirement_for_command_with_enhanced_suggestions(
req, /*enhanced_exec_policy_suggestions*/ true,
)
.await
}
async fn test_config() -> (TempDir, Config) {
let home = TempDir::new().expect("create temp dir");
let config = ConfigBuilder::default()
@@ -1032,7 +1043,7 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
}
#[tokio::test]
async fn empty_bash_lc_script_falls_back_to_original_command() {
async fn empty_bash_lc_script_falls_back_to_whole_command_when_truncated_prefix_is_too_short() {
let command = vec!["bash".to_string(), "-lc".to_string(), "".to_string()];
let manager = ExecPolicyManager::default();
@@ -1057,7 +1068,8 @@ async fn empty_bash_lc_script_falls_back_to_original_command() {
}
#[tokio::test]
async fn whitespace_bash_lc_script_falls_back_to_original_command() {
async fn whitespace_bash_lc_script_falls_back_to_whole_command_when_truncated_prefix_is_too_short()
{
let command = vec![
"bash".to_string(),
"-lc".to_string(),
@@ -1118,7 +1130,7 @@ async fn request_rule_uses_prefix_rule() {
}
#[tokio::test]
async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands() {
async fn request_rule_falls_back_to_first_segment_for_multi_command_scripts() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
@@ -1126,25 +1138,27 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands(
];
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
let requirement = create_exec_approval_requirement_with_enhanced_suggestions(
&manager,
ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
})
.await;
},
)
.await;
assert_eq!(
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"rm".to_string(),
"-rf".to_string(),
"/tmp/codex".to_string(),
"cargo".to_string(),
"install".to_string(),
"cargo-insta".to_string(),
])),
}
);
@@ -1165,21 +1179,23 @@ async fn heuristics_apply_when_other_commands_match_policy() {
];
assert_eq!(
ExecPolicyManager::new(policy)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
create_exec_approval_requirement_with_enhanced_suggestions(
&ExecPolicyManager::new(policy),
ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await,
},
)
.await,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"orange".to_string()
]))
"apple".to_string()
])),
}
);
}
@@ -1260,6 +1276,98 @@ async fn proposed_execpolicy_amendment_is_present_for_single_command_without_pol
);
}
#[tokio::test]
async fn proposed_execpolicy_amendment_stops_before_first_flag_for_generated_suggestions() {
let command = vec![
"cargo".to_string(),
"test".to_string(),
"--package".to_string(),
"codex-core".to_string(),
];
let manager = ExecPolicyManager::default();
let requirement = create_exec_approval_requirement_with_enhanced_suggestions(
&manager,
ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
},
)
.await;
assert_eq!(
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"cargo".to_string(),
"test".to_string(),
]))
}
);
}
#[tokio::test]
async fn proposed_execpolicy_amendment_preserves_full_command_when_enhanced_suggestions_disabled() {
let command = vec![
"cargo".to_string(),
"test".to_string(),
"--package".to_string(),
"codex-core".to_string(),
];
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command))
}
);
}
#[tokio::test]
async fn proposed_execpolicy_amendment_falls_back_to_whole_command_if_flag_starts_too_early() {
let command = vec!["curl".to_string(), "-k".to_string(), "xyz.com".to_string()];
let manager = ExecPolicyManager::default();
let requirement = create_exec_approval_requirement_with_enhanced_suggestions(
&manager,
ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
},
)
.await;
assert_eq!(
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command))
}
);
}
#[tokio::test]
async fn proposed_execpolicy_amendment_is_omitted_when_policy_prompts() {
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
@@ -1292,23 +1400,25 @@ async fn proposed_execpolicy_amendment_is_omitted_when_policy_prompts() {
}
#[tokio::test]
async fn proposed_execpolicy_amendment_is_present_for_multi_command_scripts() {
async fn proposed_execpolicy_amendment_is_based_on_first_segment_for_multi_command_scripts() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cargo build && echo ok".to_string(),
];
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
let requirement = create_exec_approval_requirement_with_enhanced_suggestions(
&manager,
ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
},
)
.await;
assert_eq!(
requirement,
@@ -1323,7 +1433,8 @@ async fn proposed_execpolicy_amendment_is_present_for_multi_command_scripts() {
}
#[tokio::test]
async fn proposed_execpolicy_amendment_uses_first_no_match_in_multi_command_scripts() {
async fn proposed_execpolicy_amendment_uses_whole_one_token_first_segment_for_multi_command_scripts()
{
let policy_src = r#"prefix_rule(pattern=["cat"], decision="allow")"#;
let mut parser = PolicyParser::new();
parser
@@ -1338,21 +1449,21 @@ async fn proposed_execpolicy_amendment_uses_first_no_match_in_multi_command_scri
];
assert_eq!(
ExecPolicyManager::new(policy)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
create_exec_approval_requirement_with_enhanced_suggestions(
&ExecPolicyManager::new(policy),
ExecApprovalRequest {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await,
},
)
.await,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"apple".to_string()
])),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec!["cat".to_string()])),
}
);
}

View File

@@ -118,6 +118,8 @@ pub enum Feature {
UseLegacyLandlock,
/// Allow the model to request approval and propose exec rules.
RequestRule,
/// Use tighter generated execpolicy prefix suggestions for approval prompts.
EnhancedExecPolicySuggestions,
/// Enable Windows sandbox (restricted token) on Windows.
WindowsSandbox,
/// Use the elevated Windows sandbox pipeline (setup + runner).
@@ -677,6 +679,16 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::EnhancedExecPolicySuggestions,
key: "enhanced_exec_policy_suggestions",
stage: Stage::Experimental {
name: "Enhanced exec policy suggestions",
menu_description: "When Codex proposes a reusable exec policy prefix, stop before the first flag-like argument and use the first segment of a compound shell command.",
announcement: "",
},
default_enabled: false,
},
FeatureSpec {
id: Feature::WindowsSandbox,
key: "experimental_windows_sandbox",

View File

@@ -85,6 +85,29 @@ fn guardian_approval_is_experimental_and_user_toggleable() {
assert_eq!(Feature::GuardianApproval.default_enabled(), false);
}
#[test]
fn enhanced_exec_policy_suggestions_is_experimental_and_user_toggleable() {
let spec = Feature::EnhancedExecPolicySuggestions.info();
let stage = spec.stage;
assert!(matches!(stage, Stage::Experimental { .. }));
assert_eq!(
stage.experimental_menu_name(),
Some("Enhanced exec policy suggestions")
);
assert_eq!(
stage.experimental_menu_description(),
Some(
"When Codex proposes a reusable exec policy prefix, stop before the first flag-like argument and use the first segment of a compound shell command."
)
);
assert_eq!(stage.experimental_announcement(), None);
assert_eq!(
Feature::EnhancedExecPolicySuggestions.default_enabled(),
false
);
}
#[test]
fn request_permissions_is_under_development() {
assert_eq!(

View File

@@ -428,18 +428,24 @@ impl ShellHandler {
let exec_approval_requirement = session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &exec_params.command,
approval_policy: turn.approval_policy.value(),
sandbox_policy: turn.sandbox_policy.get(),
file_system_sandbox_policy: &turn.file_system_sandbox_policy,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
codex_protocol::models::SandboxPermissions::UseDefault
} else {
effective_additional_permissions.sandbox_permissions
.create_exec_approval_requirement_for_command_with_enhanced_suggestions(
ExecApprovalRequest {
command: &exec_params.command,
approval_policy: turn.approval_policy.value(),
sandbox_policy: turn.sandbox_policy.get(),
file_system_sandbox_policy: &turn.file_system_sandbox_policy,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved
{
codex_protocol::models::SandboxPermissions::UseDefault
} else {
effective_additional_permissions.sandbox_permissions
},
prefix_rule,
},
prefix_rule,
})
session
.features()
.enabled(Feature::EnhancedExecPolicySuggestions),
)
.await;
let req = ShellRequest {

View File

@@ -15,6 +15,7 @@ use tokio_util::sync::CancellationToken;
use crate::exec_env::create_env;
use crate::exec_policy::ExecApprovalRequest;
use crate::features::Feature;
use crate::protocol::ExecCommandSource;
use crate::sandboxing::ExecRequest;
use crate::tools::context::ExecCommandToolOutput;
@@ -596,18 +597,24 @@ impl UnifiedExecProcessManager {
.session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &request.command,
approval_policy: context.turn.approval_policy.value(),
sandbox_policy: context.turn.sandbox_policy.get(),
file_system_sandbox_policy: &context.turn.file_system_sandbox_policy,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
request.sandbox_permissions
.create_exec_approval_requirement_for_command_with_enhanced_suggestions(
ExecApprovalRequest {
command: &request.command,
approval_policy: context.turn.approval_policy.value(),
sandbox_policy: context.turn.sandbox_policy.get(),
file_system_sandbox_policy: &context.turn.file_system_sandbox_policy,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
request.sandbox_permissions
},
prefix_rule: request.prefix_rule.clone(),
},
prefix_rule: request.prefix_rule.clone(),
})
context
.session
.features()
.enabled(Feature::EnhancedExecPolicySuggestions),
)
.await;
let req = UnifiedExecToolRequest {
command: request.command.clone(),

View File

@@ -36,7 +36,7 @@ pub(crate) struct ExecServerFileSystem {
impl Default for ExecServerFileSystem {
fn default() -> Self {
Self {
file_system: Arc::new(Environment.get_filesystem()),
file_system: Arc::new(Environment::default().get_filesystem()),
}
}
}

View File

@@ -7720,8 +7720,13 @@ async fn experimental_popup_shows_js_repl_node_requirement() {
chat.open_experimental_popup();
let popup = render_bottom_popup(&chat, 120);
let normalized_popup = popup
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace("- ", "-");
assert!(
popup.contains(node_requirement),
normalized_popup.contains(node_requirement),
"expected js_repl feature description to mention the required Node version, got:\n{popup}"
);
}
@@ -7744,7 +7749,11 @@ async fn experimental_popup_includes_guardian_approval() {
chat.open_experimental_popup();
let popup = render_bottom_popup(&chat, 120);
let normalized_popup = popup.split_whitespace().collect::<Vec<_>>().join(" ");
let normalized_popup = popup
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace("- ", "-");
assert!(
popup.contains(guardian_name),
"expected guardian approvals entry in experimental popup, got:\n{popup}"

View File

@@ -8264,8 +8264,13 @@ async fn experimental_popup_shows_js_repl_node_requirement() {
chat.open_experimental_popup();
let popup = render_bottom_popup(&chat, 120);
let normalized_popup = popup
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace("- ", "-");
assert!(
popup.contains(node_requirement),
normalized_popup.contains(node_requirement),
"expected js_repl feature description to mention the required Node version, got:\n{popup}"
);
}
@@ -8288,7 +8293,11 @@ async fn experimental_popup_includes_guardian_approval() {
chat.open_experimental_popup();
let popup = render_bottom_popup(&chat, 120);
let normalized_popup = popup.split_whitespace().collect::<Vec<_>>().join(" ");
let normalized_popup = popup
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace("- ", "-");
assert!(
popup.contains(guardian_name),
"expected guardian approvals entry in experimental popup, got:\n{popup}"