mirror of
https://github.com/openai/codex.git
synced 2026-04-19 20:24:50 +00:00
Compare commits
2 Commits
codex-debu
...
codex/app-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc8c0955bc | ||
|
|
f050203aa5 |
@@ -468,6 +468,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -9447,6 +9447,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, };
|
||||
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, turnId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, };
|
||||
|
||||
@@ -1741,6 +1741,8 @@ pub struct FeedbackUploadParams {
|
||||
pub reason: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub thread_id: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub turn_id: Option<String>,
|
||||
pub include_logs: bool,
|
||||
#[ts(optional = nullable)]
|
||||
pub extra_log_files: Option<Vec<PathBuf>>,
|
||||
|
||||
@@ -6887,6 +6887,7 @@ impl CodexMessageProcessor {
|
||||
classification,
|
||||
reason,
|
||||
thread_id,
|
||||
turn_id,
|
||||
include_logs,
|
||||
extra_log_files,
|
||||
} = params;
|
||||
@@ -6929,6 +6930,7 @@ impl CodexMessageProcessor {
|
||||
snapshot.upload_feedback(
|
||||
&classification,
|
||||
reason.as_deref(),
|
||||
turn_id.as_deref(),
|
||||
include_logs,
|
||||
&attachment_paths,
|
||||
Some(session_source),
|
||||
|
||||
@@ -262,6 +262,12 @@ impl CodexAuth {
|
||||
self.get_current_token_data().and_then(|t| t.id_token.email)
|
||||
}
|
||||
|
||||
/// Returns `None` if `is_chatgpt_auth()` is false.
|
||||
pub fn get_chatgpt_user_id(&self) -> Option<String> {
|
||||
self.get_current_token_data()
|
||||
.and_then(|t| t.id_token.chatgpt_user_id)
|
||||
}
|
||||
|
||||
/// Account-facing plan classification derived from the current token.
|
||||
/// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…)
|
||||
/// mapped from the ID token's internal plan value. Prefer this when you
|
||||
|
||||
@@ -1282,6 +1282,9 @@ impl Session {
|
||||
let account_id = auth.and_then(CodexAuth::get_account_id);
|
||||
let account_email = auth.and_then(CodexAuth::get_account_email);
|
||||
let originator = crate::default_client::originator().value;
|
||||
let chatgpt_user_id = auth.and_then(CodexAuth::get_chatgpt_user_id).filter(|_| {
|
||||
crate::default_client::should_emit_chatgpt_user_id_metrics(originator.as_str())
|
||||
});
|
||||
let terminal_type = terminal::user_agent();
|
||||
let session_model = session_configuration.collaboration_mode.model().to_string();
|
||||
let mut otel_manager = OtelManager::new(
|
||||
@@ -1296,6 +1299,9 @@ impl Session {
|
||||
terminal_type.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
);
|
||||
if let Some(chatgpt_user_id) = chatgpt_user_id.as_deref() {
|
||||
otel_manager = otel_manager.with_chatgpt_user_id(chatgpt_user_id);
|
||||
}
|
||||
if let Some(service_name) = session_configuration.metrics_service_name.as_deref() {
|
||||
otel_manager = otel_manager.with_metrics_service_name(service_name);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ use std::sync::RwLock;
|
||||
pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
|
||||
pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
|
||||
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
||||
pub const CODEX_INTERNAL_EMIT_CHATGPT_USER_ID_METRICS_ENV_VAR: &str =
|
||||
"CODEX_INTERNAL_EMIT_CHATGPT_USER_ID_METRICS";
|
||||
pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -118,6 +120,15 @@ pub fn is_first_party_chat_originator(originator_value: &str) -> bool {
|
||||
originator_value == "codex_atlas" || originator_value == "codex_chatgpt_desktop"
|
||||
}
|
||||
|
||||
pub fn is_app_originator_allowed_for_chatgpt_user_id_metrics(originator_value: &str) -> bool {
|
||||
originator_value == "codex_vscode" || originator_value == "Codex Desktop"
|
||||
}
|
||||
|
||||
pub fn should_emit_chatgpt_user_id_metrics(originator_value: &str) -> bool {
|
||||
std::env::var_os(CODEX_INTERNAL_EMIT_CHATGPT_USER_ID_METRICS_ENV_VAR).is_some()
|
||||
&& is_app_originator_allowed_for_chatgpt_user_id_metrics(originator_value)
|
||||
}
|
||||
|
||||
pub fn get_codex_user_agent() -> String {
|
||||
let build_version = env!("CARGO_PKG_VERSION");
|
||||
let os_info = os_info::get();
|
||||
@@ -249,6 +260,30 @@ mod tests {
|
||||
assert_eq!(is_first_party_chat_originator("codex_vscode"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_originator_allowlist_for_chatgpt_user_id_metrics_is_narrow() {
|
||||
assert_eq!(
|
||||
is_app_originator_allowed_for_chatgpt_user_id_metrics("codex_vscode"),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_app_originator_allowed_for_chatgpt_user_id_metrics("Codex Desktop"),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_app_originator_allowed_for_chatgpt_user_id_metrics(DEFAULT_ORIGINATOR),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
is_app_originator_allowed_for_chatgpt_user_id_metrics("Codex Something Else"),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
is_app_originator_allowed_for_chatgpt_user_id_metrics("codex_chatgpt_desktop"),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_client_sets_default_headers() {
|
||||
skip_if_no_network!();
|
||||
|
||||
@@ -218,16 +218,58 @@ impl CodexLogSnapshot {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn upload_tags(
|
||||
&self,
|
||||
classification: &str,
|
||||
reason: Option<&str>,
|
||||
turn_id: Option<&str>,
|
||||
session_source: Option<&SessionSource>,
|
||||
) -> BTreeMap<String, String> {
|
||||
let cli_version = env!("CARGO_PKG_VERSION");
|
||||
let mut tags = BTreeMap::from([
|
||||
(String::from("thread_id"), self.thread_id.to_string()),
|
||||
(String::from("classification"), classification.to_string()),
|
||||
(String::from("cli_version"), cli_version.to_string()),
|
||||
]);
|
||||
if let Some(turn_id) = turn_id {
|
||||
tags.insert(String::from("turn_id"), turn_id.to_string());
|
||||
}
|
||||
if let Some(source) = session_source {
|
||||
tags.insert(String::from("session_source"), source.to_string());
|
||||
}
|
||||
if let Some(r) = reason {
|
||||
tags.insert(String::from("reason"), r.to_string());
|
||||
}
|
||||
|
||||
let reserved = [
|
||||
"thread_id",
|
||||
"turn_id",
|
||||
"classification",
|
||||
"cli_version",
|
||||
"session_source",
|
||||
"reason",
|
||||
];
|
||||
for (key, value) in &self.tags {
|
||||
if reserved.contains(&key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Entry::Vacant(entry) = tags.entry(key.clone()) {
|
||||
entry.insert(value.clone());
|
||||
}
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
/// Upload feedback to Sentry with optional attachments.
|
||||
pub fn upload_feedback(
|
||||
&self,
|
||||
classification: &str,
|
||||
reason: Option<&str>,
|
||||
turn_id: Option<&str>,
|
||||
include_logs: bool,
|
||||
extra_log_files: &[PathBuf],
|
||||
session_source: Option<SessionSource>,
|
||||
) -> Result<()> {
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -249,34 +291,7 @@ impl CodexLogSnapshot {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let cli_version = env!("CARGO_PKG_VERSION");
|
||||
let mut tags = BTreeMap::from([
|
||||
(String::from("thread_id"), self.thread_id.to_string()),
|
||||
(String::from("classification"), classification.to_string()),
|
||||
(String::from("cli_version"), cli_version.to_string()),
|
||||
]);
|
||||
if let Some(source) = session_source.as_ref() {
|
||||
tags.insert(String::from("session_source"), source.to_string());
|
||||
}
|
||||
if let Some(r) = reason {
|
||||
tags.insert(String::from("reason"), r.to_string());
|
||||
}
|
||||
|
||||
let reserved = [
|
||||
"thread_id",
|
||||
"classification",
|
||||
"cli_version",
|
||||
"session_source",
|
||||
"reason",
|
||||
];
|
||||
for (key, value) in &self.tags {
|
||||
if reserved.contains(&key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Entry::Vacant(entry) = tags.entry(key.clone()) {
|
||||
entry.insert(value.clone());
|
||||
}
|
||||
}
|
||||
let tags = self.upload_tags(classification, reason, turn_id, session_source.as_ref());
|
||||
|
||||
let level = match classification {
|
||||
"bug" | "bad_result" | "safety_check" => Level::Error,
|
||||
@@ -459,4 +474,28 @@ mod tests {
|
||||
pretty_assertions::assert_eq!(snap.tags.get("model").map(String::as_str), Some("gpt-5"));
|
||||
pretty_assertions::assert_eq!(snap.tags.get("cached").map(String::as_str), Some("true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_tags_include_turn_id_and_preserve_reserved_fields() {
|
||||
let snap = CodexLogSnapshot {
|
||||
bytes: Vec::new(),
|
||||
tags: BTreeMap::from([
|
||||
(String::from("custom"), String::from("tag")),
|
||||
(String::from("turn_id"), String::from("stale-turn")),
|
||||
]),
|
||||
thread_id: String::from("thread-1"),
|
||||
};
|
||||
|
||||
let tags = snap.upload_tags(
|
||||
"bug",
|
||||
Some("broken"),
|
||||
Some("turn-123"),
|
||||
Some(&SessionSource::Cli),
|
||||
);
|
||||
|
||||
pretty_assertions::assert_eq!(tags.get("thread_id").map(String::as_str), Some("thread-1"));
|
||||
pretty_assertions::assert_eq!(tags.get("turn_id").map(String::as_str), Some("turn-123"));
|
||||
pretty_assertions::assert_eq!(tags.get("custom").map(String::as_str), Some("tag"));
|
||||
pretty_assertions::assert_eq!(tags.get("reason").map(String::as_str), Some("broken"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ pub struct OtelEventMetadata {
|
||||
pub(crate) auth_mode: Option<String>,
|
||||
pub(crate) account_id: Option<String>,
|
||||
pub(crate) account_email: Option<String>,
|
||||
pub(crate) chatgpt_user_id: Option<String>,
|
||||
pub(crate) originator: String,
|
||||
pub(crate) service_name: Option<String>,
|
||||
pub(crate) session_source: String,
|
||||
@@ -73,6 +74,11 @@ impl OtelManager {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_chatgpt_user_id(mut self, chatgpt_user_id: &str) -> Self {
|
||||
self.metadata.chatgpt_user_id = Some(sanitize_metric_tag_value(chatgpt_user_id));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_metrics(mut self, metrics: MetricsClient) -> Self {
|
||||
self.metrics = Some(metrics);
|
||||
self.metrics_use_metadata_tags = true;
|
||||
@@ -203,7 +209,7 @@ impl OtelManager {
|
||||
if !self.metrics_use_metadata_tags {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut tags = Vec::with_capacity(7);
|
||||
let mut tags = Vec::with_capacity(9);
|
||||
Self::push_metadata_tag(&mut tags, "auth_mode", self.metadata.auth_mode.as_deref())?;
|
||||
Self::push_metadata_tag(
|
||||
&mut tags,
|
||||
@@ -221,6 +227,16 @@ impl OtelManager {
|
||||
self.metadata.service_name.as_deref(),
|
||||
)?;
|
||||
Self::push_metadata_tag(&mut tags, "model", Some(self.metadata.model.as_str()))?;
|
||||
Self::push_metadata_tag(
|
||||
&mut tags,
|
||||
"enduser.id",
|
||||
self.metadata.chatgpt_user_id.as_deref(),
|
||||
)?;
|
||||
Self::push_metadata_tag(
|
||||
&mut tags,
|
||||
"user_id",
|
||||
self.metadata.chatgpt_user_id.as_deref(),
|
||||
)?;
|
||||
Self::push_metadata_tag(&mut tags, "app.version", Some(self.metadata.app_version))?;
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ impl OtelManager {
|
||||
auth_mode: auth_mode.map(|m| m.to_string()),
|
||||
account_id,
|
||||
account_email,
|
||||
chatgpt_user_id: None,
|
||||
originator: sanitize_metric_tag_value(originator.as_str()),
|
||||
service_name: None,
|
||||
session_source: session_source.to_string(),
|
||||
|
||||
@@ -153,3 +153,45 @@ fn manager_attaches_optional_service_name_tag() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_attaches_chatgpt_user_id_tags_when_present() -> Result<()> {
|
||||
let (metrics, exporter) = build_metrics_with_defaults(&[])?;
|
||||
let manager = OtelManager::new(
|
||||
ThreadId::new(),
|
||||
"gpt-5.1",
|
||||
"gpt-5.1",
|
||||
Some("account-id".to_string()),
|
||||
None,
|
||||
Some(TelemetryAuthMode::Chatgpt),
|
||||
"codex_vscode".to_string(),
|
||||
true,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.with_chatgpt_user_id("user-123")
|
||||
.with_metrics(metrics);
|
||||
|
||||
manager.counter("codex.session_started", 1, &[]);
|
||||
manager.shutdown_metrics()?;
|
||||
|
||||
let resource_metrics = latest_metrics(&exporter);
|
||||
let metric =
|
||||
find_metric(&resource_metrics, "codex.session_started").expect("counter metric missing");
|
||||
let attrs = match metric.data() {
|
||||
AggregatedMetrics::U64(data) => match data {
|
||||
MetricData::Sum(sum) => {
|
||||
let points: Vec<_> = sum.data_points().collect();
|
||||
assert_eq!(points.len(), 1);
|
||||
attributes_to_map(points[0].attributes())
|
||||
}
|
||||
_ => panic!("unexpected counter aggregation"),
|
||||
},
|
||||
_ => panic!("unexpected counter data type"),
|
||||
};
|
||||
|
||||
assert_eq!(attrs.get("enduser.id"), Some(&"user-123".to_string()));
|
||||
assert_eq!(attrs.get("user_id"), Some(&"user-123".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1560,18 +1560,27 @@ impl App {
|
||||
let auth_mode = auth_ref
|
||||
.map(CodexAuth::auth_mode)
|
||||
.map(TelemetryAuthMode::from);
|
||||
let otel_manager = OtelManager::new(
|
||||
let originator = codex_core::default_client::originator().value;
|
||||
let chatgpt_user_id = auth_ref
|
||||
.and_then(CodexAuth::get_chatgpt_user_id)
|
||||
.filter(|_| {
|
||||
codex_core::default_client::should_emit_chatgpt_user_id_metrics(originator.as_str())
|
||||
});
|
||||
let mut otel_manager = OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model.as_str(),
|
||||
model.as_str(),
|
||||
auth_ref.and_then(CodexAuth::get_account_id),
|
||||
auth_ref.and_then(CodexAuth::get_account_email),
|
||||
auth_mode,
|
||||
codex_core::default_client::originator().value,
|
||||
originator,
|
||||
config.otel.log_user_prompt,
|
||||
codex_core::terminal::user_agent(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
if let Some(chatgpt_user_id) = chatgpt_user_id.as_deref() {
|
||||
otel_manager = otel_manager.with_chatgpt_user_id(chatgpt_user_id);
|
||||
}
|
||||
if config
|
||||
.tui_status_line
|
||||
.as_ref()
|
||||
|
||||
@@ -47,6 +47,7 @@ pub(crate) enum FeedbackAudience {
|
||||
pub(crate) struct FeedbackNoteView {
|
||||
category: FeedbackCategory,
|
||||
snapshot: codex_feedback::CodexLogSnapshot,
|
||||
turn_id: Option<String>,
|
||||
rollout_path: Option<PathBuf>,
|
||||
app_event_tx: AppEventSender,
|
||||
include_logs: bool,
|
||||
@@ -62,6 +63,7 @@ impl FeedbackNoteView {
|
||||
pub(crate) fn new(
|
||||
category: FeedbackCategory,
|
||||
snapshot: codex_feedback::CodexLogSnapshot,
|
||||
turn_id: Option<String>,
|
||||
rollout_path: Option<PathBuf>,
|
||||
app_event_tx: AppEventSender,
|
||||
include_logs: bool,
|
||||
@@ -70,6 +72,7 @@ impl FeedbackNoteView {
|
||||
Self {
|
||||
category,
|
||||
snapshot,
|
||||
turn_id,
|
||||
rollout_path,
|
||||
app_event_tx,
|
||||
include_logs,
|
||||
@@ -99,6 +102,7 @@ impl FeedbackNoteView {
|
||||
let result = self.snapshot.upload_feedback(
|
||||
classification,
|
||||
reason_opt,
|
||||
self.turn_id.as_deref(),
|
||||
self.include_logs,
|
||||
&log_file_paths,
|
||||
Some(SessionSource::Cli),
|
||||
@@ -593,6 +597,7 @@ mod tests {
|
||||
category,
|
||||
snapshot,
|
||||
None,
|
||||
None,
|
||||
tx,
|
||||
true,
|
||||
FeedbackAudience::External,
|
||||
|
||||
@@ -127,6 +127,7 @@ use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnDiffEvent;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UndoCompletedEvent;
|
||||
use codex_protocol::protocol::UndoStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
@@ -599,6 +600,7 @@ pub(crate) struct ChatWidget {
|
||||
// Set when commentary output completes; once stream queues go idle we restore the status row.
|
||||
pending_status_indicator_restore: bool,
|
||||
thread_id: Option<ThreadId>,
|
||||
last_turn_id: Option<String>,
|
||||
thread_name: Option<String>,
|
||||
forked_from: Option<ThreadId>,
|
||||
frame_requester: FrameRequester,
|
||||
@@ -1253,6 +1255,7 @@ impl ChatWidget {
|
||||
let view = crate::bottom_pane::FeedbackNoteView::new(
|
||||
category,
|
||||
snapshot,
|
||||
self.last_turn_id.clone(),
|
||||
rollout,
|
||||
self.app_event_tx.clone(),
|
||||
include_logs,
|
||||
@@ -2865,6 +2868,7 @@ impl ChatWidget {
|
||||
retry_status_header: None,
|
||||
pending_status_indicator_restore: false,
|
||||
thread_id: None,
|
||||
last_turn_id: None,
|
||||
thread_name: None,
|
||||
forked_from: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -3044,6 +3048,7 @@ impl ChatWidget {
|
||||
retry_status_header: None,
|
||||
pending_status_indicator_restore: false,
|
||||
thread_id: None,
|
||||
last_turn_id: None,
|
||||
thread_name: None,
|
||||
forked_from: None,
|
||||
saw_plan_update_this_turn: false,
|
||||
@@ -3212,6 +3217,7 @@ impl ChatWidget {
|
||||
retry_status_header: None,
|
||||
pending_status_indicator_restore: false,
|
||||
thread_id: None,
|
||||
last_turn_id: None,
|
||||
thread_name: None,
|
||||
forked_from: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -4360,7 +4366,8 @@ impl ChatWidget {
|
||||
self.on_agent_reasoning_final();
|
||||
}
|
||||
EventMsg::AgentReasoningSectionBreak(_) => self.on_reasoning_section_break(),
|
||||
EventMsg::TurnStarted(_) => {
|
||||
EventMsg::TurnStarted(TurnStartedEvent { turn_id, .. }) => {
|
||||
self.last_turn_id = Some(turn_id);
|
||||
if !is_resume_initial_replay {
|
||||
self.on_task_started();
|
||||
}
|
||||
|
||||
@@ -1702,6 +1702,7 @@ async fn make_chatwidget_manual(
|
||||
retry_status_header: None,
|
||||
pending_status_indicator_restore: false,
|
||||
thread_id: None,
|
||||
last_turn_id: None,
|
||||
thread_name: None,
|
||||
forked_from: None,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
@@ -7934,6 +7935,23 @@ async fn replayed_turn_started_does_not_mark_task_running() {
|
||||
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
assert!(chat.bottom_pane.status_widget().is_none());
|
||||
assert_eq!(chat.last_turn_id.as_deref(), Some("turn-1"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_turn_started_records_last_turn_id_for_feedback() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-feedback".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-feedback".to_string(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
|
||||
assert_eq!(chat.last_turn_id.as_deref(), Some("turn-feedback"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user