mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Honor API-key feature gates in TUI Daybreak selection (#52228)
## Why API-key accounts could retain a saved Daybreak preference without `ApiKeyCyberAccessPrograms` enabled. Eligible API-key turns also omitted the `standard` access program when Daybreak was off. ## What changed - Require both `CliDaybreak` and `ApiKeyCyberAccessPrograms` for known API-key accounts, and refresh Daybreak state when either flag changes. - Use account eligibility consistently for normal turns, continuations, and background tasks so eligible API-key accounts can select the catalog's `standard` program when Daybreak is off. - Preserve resumed preferences while account state is unknown and update `/daybreak` help to reflect API-key eligibility. ## Testing Extend regression coverage for feature-flag combinations, API-key continuations, resumed preferences, and `/daybreak` help. GitOrigin-RevId: 966f4789c9d685ff122da196a13da50c60a4b393
This commit is contained in:
@@ -19,7 +19,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
if enabled
|
||||
&& (!self.chat_widget.daybreak_turn_eligible(enabled)
|
||||
&& (!self.chat_widget.daybreak_account_eligible()
|
||||
|| !crate::daybreak::available(&self.chat_widget.model_catalog().models))
|
||||
{
|
||||
self.chat_widget.add_error_message(
|
||||
|
||||
@@ -117,7 +117,7 @@ impl App {
|
||||
let enabled = self.chat_widget.daybreak_enabled
|
||||
&& !self.chat_widget.side_conversation_active()
|
||||
&& !self.side_threads.contains_key(&review.thread_id);
|
||||
let eligible_account = self.chat_widget.daybreak_turn_eligible(enabled);
|
||||
let eligible_account = self.chat_widget.daybreak_account_eligible();
|
||||
let cyber_access_program = match crate::daybreak::program_for_turn(
|
||||
&self.chat_widget.model_catalog().models,
|
||||
self.chat_widget.current_model(),
|
||||
|
||||
@@ -88,7 +88,7 @@ impl App {
|
||||
let daybreak_enabled = self.chat_widget.daybreak_enabled
|
||||
&& !self.chat_widget.side_conversation_active()
|
||||
&& !self.side_threads.contains_key(&thread_id);
|
||||
let eligible_account = self.chat_widget.daybreak_turn_eligible(daybreak_enabled);
|
||||
let eligible_account = self.chat_widget.daybreak_account_eligible();
|
||||
if let Err(message) = crate::daybreak::program_for_turn(
|
||||
&self.chat_widget.model_catalog().models,
|
||||
&model,
|
||||
|
||||
@@ -40,16 +40,21 @@ fn error_notification(
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_continuation_requires_current_review_and_submits_once() -> Result<()> {
|
||||
for (reject, preserve, daybreak, rollout_enabled) in [
|
||||
(false, false, false, true),
|
||||
(false, true, true, true),
|
||||
(true, false, false, true),
|
||||
(false, false, false, false),
|
||||
(false, true, true, false),
|
||||
for (reject, preserve, daybreak, rollout_enabled, has_chatgpt_account) in [
|
||||
(false, false, false, true, true),
|
||||
(false, true, true, true, true),
|
||||
(true, false, false, true, true),
|
||||
(false, false, false, false, true),
|
||||
(false, true, true, false, true),
|
||||
// Continuations submit directly rather than via the normal turn-start path.
|
||||
(false, false, false, true, false),
|
||||
(false, true, true, true, false),
|
||||
] {
|
||||
let (mut app, mut rx, _) = make_test_app_with_channels().await;
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, rollout_enabled);
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, !has_chatgpt_account);
|
||||
let (mut server, requests, proxy) = start_recording_app_server(
|
||||
&app.config,
|
||||
/*blocked_thread_list*/ None,
|
||||
@@ -70,8 +75,10 @@ async fn misalignment_continuation_requires_current_review_and_submits_once() ->
|
||||
app.chat_widget.handle_thread_session(session);
|
||||
if !reject {
|
||||
app.chat_widget.update_account_state(
|
||||
/*status_account_display*/ None, /*plan_type*/ None,
|
||||
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
|
||||
(!has_chatgpt_account).then_some(crate::status::StatusAccountDisplay::ApiKey),
|
||||
/*plan_type*/ None,
|
||||
has_chatgpt_account,
|
||||
/*has_codex_backend_auth*/ has_chatgpt_account,
|
||||
);
|
||||
app.chat_widget.open_model_popup();
|
||||
let request_id = std::iter::from_fn(|| rx.try_recv().ok())
|
||||
|
||||
@@ -196,17 +196,32 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ false,
|
||||
);
|
||||
assert!(
|
||||
!app.chat_widget
|
||||
.set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, /*enabled*/ false,)
|
||||
);
|
||||
app.submit_thread_op(&mut server, thread_id, turn.clone())
|
||||
.await?;
|
||||
app.chat_widget.set_daybreak_enabled(/*enabled*/ false);
|
||||
app.submit_thread_op(&mut server, thread_id, turn).await?;
|
||||
assert!(!app.chat_widget.daybreak_enabled);
|
||||
for (cli_daybreak, api_key_cyber_access_programs, enabled) in [
|
||||
(false, false, true),
|
||||
(false, true, true),
|
||||
(true, false, true),
|
||||
(true, true, true),
|
||||
(true, true, false),
|
||||
] {
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, cli_daybreak);
|
||||
app.chat_widget.set_feature_enabled(
|
||||
Feature::ApiKeyCyberAccessPrograms,
|
||||
api_key_cyber_access_programs,
|
||||
);
|
||||
app.chat_widget.set_daybreak_enabled(enabled);
|
||||
app.submit_thread_op(&mut server, thread_id, turn.clone())
|
||||
.await?;
|
||||
}
|
||||
let turns = recorded_params(&requests, "turn/start");
|
||||
assert_eq!(turns[5]["cyberAccessProgram"], "daybreakBlue");
|
||||
assert!(turns[6]["cyberAccessProgram"].is_null());
|
||||
assert_eq!(
|
||||
turns[5..]
|
||||
.iter()
|
||||
.map(|turn| turn["cyberAccessProgram"].as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![None, None, None, Some("daybreakBlue"), Some("standard")]
|
||||
);
|
||||
while events.try_recv().is_ok() {}
|
||||
|
||||
let missing_thread_id = ThreadId::new();
|
||||
|
||||
@@ -828,7 +828,7 @@ impl App {
|
||||
let enabled = self.chat_widget.daybreak_enabled
|
||||
&& !self.chat_widget.side_conversation_active()
|
||||
&& !self.side_threads.contains_key(&thread_id);
|
||||
let eligible_account = self.chat_widget.daybreak_turn_eligible(enabled);
|
||||
let eligible_account = self.chat_widget.daybreak_account_eligible();
|
||||
let cyber_access_program = match crate::daybreak::program_for_turn(
|
||||
&self.chat_widget.model_catalog().models,
|
||||
model,
|
||||
|
||||
@@ -6,14 +6,29 @@ use crate::chatwidget::rate_limits::RATE_LIMIT_SWITCH_PROMPT_VIEW_ID;
|
||||
|
||||
impl ChatWidget {
|
||||
pub(crate) fn set_daybreak_enabled(&mut self, enabled: bool) {
|
||||
self.daybreak_enabled = enabled && self.config.features.enabled(Feature::CliDaybreak);
|
||||
self.daybreak_enabled = enabled && self.daybreak_feature_enabled();
|
||||
self.bottom_pane
|
||||
.set_daybreak_command_description(self.daybreak_command_description());
|
||||
}
|
||||
|
||||
/// UI eligibility only; the catalog and server still determine model/program access.
|
||||
pub(crate) fn daybreak_account_eligible(&self) -> bool {
|
||||
fn daybreak_feature_enabled(&self) -> bool {
|
||||
// Preserve a resumed preference while account state is still unknown. Once an API-key
|
||||
// account is known, both flags must be enabled before using that preference.
|
||||
self.config.features.enabled(Feature::CliDaybreak)
|
||||
&& (self.has_chatgpt_account
|
||||
|| !matches!(
|
||||
self.status_account_display,
|
||||
Some(StatusAccountDisplay::ApiKey)
|
||||
)
|
||||
|| self
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::ApiKeyCyberAccessPrograms))
|
||||
}
|
||||
|
||||
/// UI and turn-selection eligibility; the catalog and server still determine program access.
|
||||
pub(crate) fn daybreak_account_eligible(&self) -> bool {
|
||||
self.daybreak_feature_enabled()
|
||||
&& self.config.model_provider_id == "openai"
|
||||
&& (self.has_chatgpt_account
|
||||
|| matches!(
|
||||
@@ -22,11 +37,6 @@ impl ChatWidget {
|
||||
))
|
||||
}
|
||||
|
||||
/// API-key turns need no explicit program while Daybreak is off.
|
||||
pub(crate) fn daybreak_turn_eligible(&self, enabled: bool) -> bool {
|
||||
self.daybreak_account_eligible() && (self.has_chatgpt_account || enabled)
|
||||
}
|
||||
|
||||
pub(super) fn daybreak_command_description(&self) -> Option<&'static str> {
|
||||
if self.daybreak_enabled {
|
||||
Some("Disable broader access for cybersecurity work")
|
||||
@@ -112,7 +122,10 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
let enabled = self.config.features.enabled(feature);
|
||||
if feature == Feature::CliDaybreak {
|
||||
if matches!(
|
||||
feature,
|
||||
Feature::CliDaybreak | Feature::ApiKeyCyberAccessPrograms
|
||||
) {
|
||||
self.set_daybreak_enabled(self.daybreak_enabled);
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: tui/src/chatwidget/tests/slash_commands.rs
|
||||
expression: "render_bottom_popup(&chat, 80).lines().next().unwrap()"
|
||||
---
|
||||
› /daybreak Learn about broader access for cybersecurity work
|
||||
› /daybreak Enable broader access for cybersecurity work
|
||||
|
||||
@@ -53,10 +53,30 @@ async fn resumed_initial_messages_render_history() {
|
||||
@""
|
||||
);
|
||||
|
||||
// A saved On preference cannot bypass the API-key gate.
|
||||
chat.status_account_display = Some(StatusAccountDisplay::ApiKey);
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
configured.thread_id = ThreadId::new();
|
||||
chat.handle_thread_session(configured.clone());
|
||||
assert!(!chat.daybreak_enabled);
|
||||
insta::assert_snapshot!(
|
||||
drain_insert_history(&mut rx).into_iter().flatten()
|
||||
.map(|line| line.to_string())
|
||||
.filter(|line| line.contains("Daybreak"))
|
||||
.collect::<Vec<_>>().join("\n"),
|
||||
@""
|
||||
);
|
||||
|
||||
// Session hydration can precede the account update on ChatGPT sign-in.
|
||||
chat.status_account_display = None;
|
||||
configured.thread_id = ThreadId::new();
|
||||
chat.handle_thread_session(configured);
|
||||
assert!(chat.daybreak_enabled);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None, /*plan_type*/ None,
|
||||
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
|
||||
);
|
||||
assert!(chat.daybreak_enabled);
|
||||
replay_user_message_text(
|
||||
&mut chat,
|
||||
"user-1",
|
||||
|
||||
@@ -2272,7 +2272,10 @@ async fn slash_daybreak_offers_an_application_when_unavailable() {
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![model]));
|
||||
chat.has_chatgpt_account = false;
|
||||
chat.status_account_display = Some(StatusAccountDisplay::ApiKey);
|
||||
chat.set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, /*enabled*/ true);
|
||||
chat.set_daybreak_enabled(/*enabled*/ false);
|
||||
chat.bottom_pane
|
||||
.set_composer_text(String::new(), Vec::new(), Vec::new());
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/daybreak".to_string(), Vec::new(), Vec::new());
|
||||
assert_chatwidget_snapshot!(
|
||||
@@ -2282,6 +2285,20 @@ async fn slash_daybreak_offers_an_application_when_unavailable() {
|
||||
.next()
|
||||
.unwrap()
|
||||
);
|
||||
chat.set_daybreak_enabled(/*enabled*/ true);
|
||||
chat.set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, /*enabled*/ false);
|
||||
chat.bottom_pane
|
||||
.set_composer_text(String::new(), Vec::new(), Vec::new());
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/daybreak".to_string(), Vec::new(), Vec::new());
|
||||
insta::assert_snapshot!(
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
.lines()
|
||||
.filter(|line| line.contains("broader access"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
@""
|
||||
);
|
||||
chat.has_chatgpt_account = true;
|
||||
chat.status_account_display = None;
|
||||
for (enabled, name) in [
|
||||
|
||||
Reference in New Issue
Block a user