mirror of
https://github.com/openai/codex.git
synced 2026-10-17 17:08:55 +00:00
Use the model catalog to select TUI cyber refusal guidance (#49857)
## What changed Select cyber refusal notices from the current model's `available_access_programs` for ChatGPT accounts using the OpenAI provider. Show Astra-specific guidance only when an Astra model offers standard cyber access without Daybreak access, and use the limited notice when Daybreak access is available. Otherwise, show application guidance. Remove the separate account eligibility request, background prefetching, and notice cache. ## Testing Update tests to cover catalog-based notice selection, missing model metadata, isolation from other models' access programs, and rendering the application notice for a cyber policy error. GitOrigin-RevId: 88e9522bd6ece2d2b9caa6cff01a4178768babb7
This commit is contained in:
@@ -325,7 +325,6 @@ impl App {
|
||||
self.agents_overview.pending_usage = None;
|
||||
self.agents_overview.usage_disabled = false;
|
||||
self.repaint_agents_overview();
|
||||
self.chat_widget.cyber_policy_notice = Default::default();
|
||||
self.chat_widget.invalidate_security_setup();
|
||||
if let Some(crate::pager_overlay::Overlay::Analytics(view)) = &mut self.overlay {
|
||||
view.refresh();
|
||||
@@ -367,11 +366,6 @@ impl App {
|
||||
self.app_event_tx.clone(),
|
||||
self.chat_widget.security_setup_request_id,
|
||||
);
|
||||
crate::daybreak::prefetch_notice(
|
||||
&self.config,
|
||||
app_server_client,
|
||||
self.chat_widget.cyber_policy_notice.clone(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,7 +313,6 @@ impl App {
|
||||
!self.app_server_target.uses_remote_workspace()
|
||||
&& app_server.app_server_platform_os() == Some("windows");
|
||||
self.chat_widget.windows_sandbox_host = WindowsSandboxHost::Unknown;
|
||||
self.chat_widget.cyber_policy_notice = Default::default();
|
||||
self.chat_widget.requires_openai_auth = bootstrap.requires_openai_auth;
|
||||
self.chat_widget.remote_connection =
|
||||
crate::status::remote_connection::remote_connection_status_value(
|
||||
@@ -495,11 +494,6 @@ impl App {
|
||||
self.app_event_tx.clone(),
|
||||
self.chat_widget.security_setup_request_id,
|
||||
);
|
||||
crate::daybreak::prefetch_notice(
|
||||
&self.config,
|
||||
app_server,
|
||||
self.chat_widget.cyber_policy_notice.clone(),
|
||||
);
|
||||
}
|
||||
self.feedback_audience = bootstrap.feedback_audience;
|
||||
if displayed.is_some_and(|id| !self.thread_unavailable(id)) {
|
||||
|
||||
@@ -495,7 +495,6 @@ impl App {
|
||||
if !self.chat_widget.realtime_conversation_is_running() {
|
||||
self.retain_realtime_replay_state_before_replace();
|
||||
}
|
||||
chat_widget.cyber_policy_notice = self.chat_widget.cyber_policy_notice.clone();
|
||||
self.commit_animation = None;
|
||||
// Transfer the last-written terminal title to the replacement widget
|
||||
// so it knows what OSC title is currently displayed. Without this, the
|
||||
|
||||
@@ -1064,11 +1064,6 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
app.app_event_tx.clone(),
|
||||
app.chat_widget.security_setup_request_id,
|
||||
);
|
||||
crate::daybreak::prefetch_notice(
|
||||
&app.config,
|
||||
&app_server,
|
||||
app.chat_widget.cyber_policy_notice.clone(),
|
||||
);
|
||||
let reset_hint_request_id = app.chat_widget.start_rate_limit_reset_startup_check();
|
||||
app.refresh_rate_limits(
|
||||
&app_server,
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#[path = "tests/mcp_login_tests.rs"]
|
||||
mod mcp_login_tests;
|
||||
|
||||
#[path = "tests/daybreak_tests.rs"]
|
||||
mod daybreak_tests;
|
||||
#[path = "tests/math_interruption_tests.rs"]
|
||||
mod math_interruption_tests;
|
||||
#[path = "tests/security_setup_tests.rs"]
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
use super::*;
|
||||
use crate::daybreak::Notice;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::ErrorNotification;
|
||||
use codex_login::AuthCredentialsStoreMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn cyber_refusal_reads_eligibility_without_changing_the_model() -> Result<()> {
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
let backend = wiremock::MockServer::start().await;
|
||||
app.config.chatgpt_base_url = backend.uri();
|
||||
// Embedded account discovery reloads the bootstrap URL from disk.
|
||||
std::fs::write(
|
||||
app.config.codex_home.join("config.toml"),
|
||||
format!("chatgpt_base_url = {:?}\n", backend.uri()),
|
||||
)?;
|
||||
app_test_support::mount_workspace_routing(&backend).await;
|
||||
app.config.cli_auth_credentials_store_mode = AuthCredentialsStoreMode::File;
|
||||
write_chatgpt_auth(
|
||||
&app.config.codex_home,
|
||||
ChatGptAuthFixture::new("test-token")
|
||||
.account_id("account")
|
||||
.chatgpt_user_id("user"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("write synthetic auth");
|
||||
let server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
|
||||
let thread_id = ThreadId::new();
|
||||
let mut session = test_thread_session(thread_id, app.config.cwd.to_path_buf());
|
||||
session.model_provider_id = "openai".to_string();
|
||||
app.enqueue_primary_thread_session(session, Vec::new())
|
||||
.await?;
|
||||
for (status, enrolled, model, replace_account, expected) in [
|
||||
(200, true, "gpt-5.6-sol", false, Notice::Limited),
|
||||
(200, false, "gpt-5.6-sol", false, Notice::Apply),
|
||||
(503, false, "gpt-5.6-sol", false, Notice::Limited),
|
||||
(200, true, "gpt-6-astra", false, Notice::Astra),
|
||||
(200, false, "gpt-6-astra", false, Notice::Astra),
|
||||
(503, false, "gpt-6-astra", false, Notice::Astra),
|
||||
(200, true, "gpt-6-astra-wm", false, Notice::Astra),
|
||||
(200, true, "unmapped-model", false, Notice::Limited),
|
||||
(200, false, "gpt-5.6-sol", true, Notice::Limited),
|
||||
] {
|
||||
backend.reset().await;
|
||||
app_test_support::mount_workspace_routing(&backend).await;
|
||||
let codex_home = app.config.codex_home.clone();
|
||||
let response = wiremock::ResponseTemplate::new(status)
|
||||
.set_delay(if model == "gpt-5.6-sol" && enrolled { Duration::from_secs(1) } else { Duration::ZERO })
|
||||
.set_body_json(json!({"programs": [{"program":"cyber", "state": if enrolled { "active" } else { "inactive" }, "grants": if enrolled { json!([{"level":"tac2"}]) } else { json!([]) }}]}));
|
||||
wiremock::Mock::given(wiremock::matchers::path("/accounts/verified_access"))
|
||||
.and(wiremock::matchers::header(
|
||||
"authorization",
|
||||
"Bearer test-token",
|
||||
))
|
||||
.respond_with(move |_: &wiremock::Request| {
|
||||
if replace_account {
|
||||
write_chatgpt_auth(
|
||||
&codex_home,
|
||||
ChatGptAuthFixture::new("test-token").account_id("other-account"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
response.clone()
|
||||
})
|
||||
.expect(1)
|
||||
.mount(&backend)
|
||||
.await;
|
||||
// Account discovery starts before a subsequent model switch to Sol.
|
||||
app.chat_widget.set_model("gpt-6-astra");
|
||||
app.handle_app_server_event(
|
||||
&server,
|
||||
AppServerEvent::ServerNotification(Box::new(ServerNotification::AccountUpdated(
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
plan_type: None,
|
||||
},
|
||||
))),
|
||||
)
|
||||
.await;
|
||||
if model != "gpt-5.6-sol" || !enrolled {
|
||||
wait_for_notice(&app.chat_widget.cyber_policy_notice).await;
|
||||
}
|
||||
app.active_thread_id = Some(thread_id);
|
||||
app.chat_widget.set_model(model);
|
||||
while events.try_recv().is_ok() {}
|
||||
// In the slow Sol case, discovery is still pending: do not block input on it.
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(250),
|
||||
app.handle_app_server_event(
|
||||
&server,
|
||||
AppServerEvent::ServerNotification(Box::new(ServerNotification::Error(
|
||||
ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "server fallback".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::CyberPolicy),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn".into(),
|
||||
},
|
||||
))),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("refusal must not wait for eligibility");
|
||||
let event = app.active_thread_rx.as_mut().unwrap().recv().await.unwrap();
|
||||
app.handle_thread_event_now(event);
|
||||
let cell = std::iter::from_fn(|| events.try_recv().ok())
|
||||
.find_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => Some(cell),
|
||||
_ => None,
|
||||
})
|
||||
.expect("cyber refusal cell");
|
||||
assert_eq!(
|
||||
cell.display_lines(/*width*/ 80),
|
||||
history_cell::new_cyber_policy_error_event(expected).display_lines(/*width*/ 80),
|
||||
"{model}"
|
||||
);
|
||||
assert_eq!(
|
||||
wait_for_notice(&app.chat_widget.cyber_policy_notice)
|
||||
.await
|
||||
.for_model(model),
|
||||
expected
|
||||
);
|
||||
backend.verify().await;
|
||||
assert_eq!(app.chat_widget.current_model(), model);
|
||||
}
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_notice(cache: &crate::daybreak::NoticeCache) -> Notice {
|
||||
tokio::time::timeout(Duration::from_secs(3), async {
|
||||
loop {
|
||||
if let Some(notice) = cache.get() {
|
||||
return *notice;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("eligibility should finish")
|
||||
}
|
||||
@@ -515,7 +515,6 @@ pub(crate) enum ExternalEditorState {
|
||||
pub(crate) struct ChatWidget {
|
||||
pub(crate) empty_state_animation:
|
||||
std::cell::RefCell<crate::empty_state_animation::EmptyStateAnimation>,
|
||||
pub(crate) cyber_policy_notice: crate::daybreak::NoticeCache,
|
||||
app_event_tx: AppEventSender,
|
||||
codex_op_target: CodexOpTarget,
|
||||
bottom_pane: BottomPane,
|
||||
|
||||
@@ -97,7 +97,6 @@ impl ChatWidget {
|
||||
);
|
||||
let mut widget = Self {
|
||||
empty_state_animation: std::cell::RefCell::new(empty_state_animation),
|
||||
cyber_policy_notice: Default::default(),
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
frame_requester: frame_requester.clone(),
|
||||
codex_op_target,
|
||||
|
||||
@@ -123,9 +123,13 @@ async fn resumed_session_hides_unknown_token_usage_until_an_update_arrives() {
|
||||
#[tokio::test]
|
||||
async fn app_server_cyber_policy_error_renders_dedicated_notice() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(Some("gpt-5.6-sol")).await;
|
||||
chat.cyber_policy_notice
|
||||
.set(crate::daybreak::Notice::Apply)
|
||||
.unwrap();
|
||||
chat.has_chatgpt_account = true;
|
||||
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
|
||||
model.model = "gpt-5.6-sol".into();
|
||||
model.available_access_programs = Some(codex_protocol::openai_models::ModelAccessPrograms {
|
||||
cyber: vec![codex_protocol::turn_input::CyberAccessProgram::Standard],
|
||||
});
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![model]));
|
||||
|
||||
handle_error(
|
||||
&mut chat,
|
||||
|
||||
@@ -394,14 +394,10 @@ impl ChatWidget {
|
||||
pub(super) fn on_cyber_policy_error(&mut self) {
|
||||
self.input_queue.submit_pending_steers_after_interrupt = false;
|
||||
self.finalize_turn();
|
||||
let notice = if self.config.model_provider_id == "openai" {
|
||||
self.cyber_policy_notice
|
||||
.get()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.for_model(self.current_model())
|
||||
let notice = if self.has_chatgpt_account && self.config.model_provider_id == "openai" {
|
||||
crate::daybreak::notice_for_model(&self.model_catalog.models, self.current_model())
|
||||
} else {
|
||||
crate::daybreak::Notice::Limited
|
||||
crate::daybreak::Notice::Apply
|
||||
};
|
||||
self.add_to_history(history_cell::new_cyber_policy_error_event(notice));
|
||||
self.request_redraw();
|
||||
|
||||
+18
-135
@@ -1,24 +1,8 @@
|
||||
//! Read-only Daybreak eligibility used to choose cyber refusal copy.
|
||||
//! Account-scoped discovery runs in the background; pending/failed reads use neutral copy.
|
||||
//! Astra takes precedence; the TUI does not configure the app’s Daybreak access program.
|
||||
//! Choose cyber refusal guidance from the connected host's model catalog.
|
||||
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::GetAuthStatusParams;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
use codex_login::CodexAuth;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
pub(crate) type NoticeCache = Arc<OnceCell<Notice>>;
|
||||
use codex_protocol::openai_models::ModelAccessPrograms;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::turn_input::CyberAccessProgram;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum Notice {
|
||||
@@ -28,123 +12,22 @@ pub(crate) enum Notice {
|
||||
Limited,
|
||||
}
|
||||
|
||||
pub(crate) fn prefetch_notice(config: &Config, server: &AppServerSession, cache: NoticeCache) {
|
||||
if config.model_provider_id != "openai"
|
||||
|| server.uses_remote_workspace()
|
||||
|| cache.get().is_some()
|
||||
pub(crate) fn notice_for_model(models: &[ModelPreset], model: &str) -> Notice {
|
||||
let programs = models
|
||||
.iter()
|
||||
.find(|entry| entry.model == model)
|
||||
.and_then(|entry| entry.available_access_programs.as_ref());
|
||||
if matches!(model, "gpt-6-astra" | "gpt-6-astra-wm")
|
||||
&& programs.is_some_and(|programs| {
|
||||
programs.cyber.contains(&CyberAccessProgram::Standard) && programs.daybreak().is_none()
|
||||
})
|
||||
{
|
||||
return;
|
||||
return Notice::Astra;
|
||||
}
|
||||
let config = config.clone();
|
||||
let request_handle = server.request_handle();
|
||||
tokio::spawn(async move {
|
||||
cache
|
||||
.get_or_init(|| read_notice(&config, &request_handle))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn read_notice(config: &Config, request_handle: &AppServerRequestHandle) -> Notice {
|
||||
tokio::time::timeout(Duration::from_secs(3), async {
|
||||
// Allow the server to refresh its token before reading local credentials.
|
||||
let status: GetAuthStatusResponse = request_handle
|
||||
.request_typed(ClientRequest::GetAuthStatus {
|
||||
request_id: RequestId::String(uuid::Uuid::new_v4().to_string()),
|
||||
params: GetAuthStatusParams {
|
||||
include_token: Some(true),
|
||||
refresh_token: Some(false),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.ok()?;
|
||||
let auth = config
|
||||
.auth_config()
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let CodexAuth::Chatgpt(_) = &auth else {
|
||||
return None;
|
||||
};
|
||||
if status.auth_method != Some(AuthMode::Chatgpt)
|
||||
|| status.auth_token.as_deref() != Some(auth.get_token().ok()?.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let client = RouteAwareClientPool::new_without_redirects(
|
||||
config.http_client_factory(),
|
||||
ClientRouteClass::Api,
|
||||
);
|
||||
let url = format!(
|
||||
"{}/accounts/verified_access",
|
||||
config.chatgpt_base_url.trim_end_matches('/')
|
||||
);
|
||||
let response = client
|
||||
.get(url)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
|
||||
.send()
|
||||
.await
|
||||
.ok()?;
|
||||
if !response.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
let access = response.json::<VerifiedAccess>().await.ok()?;
|
||||
let current_auth = config
|
||||
.auth_config()
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()?;
|
||||
if current_auth.get_token().ok()? != auth.get_token().ok()?
|
||||
|| current_auth.get_account_id() != auth.get_account_id()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(access.notice())
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VerifiedAccess {
|
||||
programs: Vec<Program>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "program", rename_all = "snake_case")]
|
||||
enum Program {
|
||||
Cyber {
|
||||
state: String,
|
||||
grants: Vec<serde::de::IgnoredAny>,
|
||||
},
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
impl VerifiedAccess {
|
||||
fn notice(&self) -> Notice {
|
||||
let absent = self.programs.iter().all(|program| match program {
|
||||
Program::Cyber { state, grants } => state == "inactive" && grants.is_empty(),
|
||||
Program::Other => true,
|
||||
});
|
||||
if absent {
|
||||
Notice::Apply
|
||||
} else {
|
||||
Notice::Limited
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Notice {
|
||||
pub(crate) fn for_model(self, model: &str) -> Self {
|
||||
match model {
|
||||
// Same identifiers as the app's isDaybreakUnavailableModel.
|
||||
"gpt-6-astra" | "gpt-6-astra-wm" => Self::Astra,
|
||||
"gpt-5.6-sol" => self,
|
||||
// Other model/access-program mappings are not established for the TUI.
|
||||
_ => Self::Limited,
|
||||
}
|
||||
if programs.and_then(ModelAccessPrograms::daybreak).is_some() {
|
||||
Notice::Limited
|
||||
} else {
|
||||
Notice::Apply
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
use super::*;
|
||||
use codex_protocol::openai_models::ModelAccessPrograms;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn refusal_copy_uses_verified_access() {
|
||||
for (programs, expected) in [
|
||||
(json!([]), Notice::Apply),
|
||||
(
|
||||
json!([{"program":"cyber","state":"inactive","grants":[]}]),
|
||||
Notice::Apply,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"unavailable","grants":[]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"active","grants":[{"level":"tac2"}]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"active","grants":[]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"inactive","grants":[{"level":"tac2"}]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"unknown","grants":[]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
fn refusal_guidance_uses_the_model_catalog() {
|
||||
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
|
||||
model.available_access_programs = Some(ModelAccessPrograms {
|
||||
cyber: vec![CyberAccessProgram::Standard],
|
||||
});
|
||||
for (name, expected) in [
|
||||
("gpt-5.6-sol", Notice::Apply),
|
||||
("gpt-6-astra", Notice::Astra),
|
||||
("gpt-6-astra-wm", Notice::Astra),
|
||||
] {
|
||||
let access: VerifiedAccess = serde_json::from_value(json!({"programs": programs})).unwrap();
|
||||
assert_eq!(access.notice(), expected);
|
||||
model.model = name.into();
|
||||
assert_eq!(
|
||||
notice_for_model(std::slice::from_ref(&model), name),
|
||||
expected
|
||||
);
|
||||
}
|
||||
model
|
||||
.available_access_programs
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.cyber
|
||||
.push(CyberAccessProgram::DaybreakBlue);
|
||||
assert_eq!(
|
||||
notice_for_model(std::slice::from_ref(&model), &model.model),
|
||||
Notice::Limited
|
||||
);
|
||||
|
||||
let mut other = model.clone();
|
||||
model.model = "gpt-5.6-sol".into();
|
||||
other.model = "another-model".into();
|
||||
model.available_access_programs.as_mut().unwrap().cyber = vec![CyberAccessProgram::Standard];
|
||||
for programs in [None, other.available_access_programs.clone()] {
|
||||
other.available_access_programs = programs;
|
||||
assert_eq!(
|
||||
notice_for_model(&[model.clone(), other.clone()], &model.model),
|
||||
Notice::Apply
|
||||
);
|
||||
}
|
||||
assert_eq!(notice_for_model(&[], "gpt-6-astra"), Notice::Apply);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user