mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
feat: forced tool tips (#8752)
Force an announcement tooltip in the CLI. This query the gh repo on this [file](https://raw.githubusercontent.com/openai/codex/main/announcement_tip.toml) which contains announcements in TOML looking like this: ``` # Example announcement tips for Codex TUI. # Each [[announcements]] entry is evaluated in order; the last matching one is shown. # Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive. # version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions. # target_app specify which app should display the announcement (cli, vsce, ...). [[announcements]] content = "Welcome to Codex! Check out the new onboarding flow." from_date = "2024-10-01" to_date = "2024-10-15" version_regex = "^0\\.0\\.0$" target_app = "cli" ``` To make this efficient, the announcement is queried on a best effort basis at the launch of the CLI (no refresh made after this). This is done in an async way and we display the announcement (with 100% probability) iff the announcement is available, the cache is correctly warmed and there is a matching announcement (matching is recomputed for each new session).
This commit is contained in:
16
announcement_tip.toml
Normal file
16
announcement_tip.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
to_date = "2026-01-10"
|
||||
@@ -816,11 +816,11 @@ pub(crate) fn padded_emoji(emoji: &str) -> String {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TooltipHistoryCell {
|
||||
tip: &'static str,
|
||||
tip: String,
|
||||
}
|
||||
|
||||
impl TooltipHistoryCell {
|
||||
fn new(tip: &'static str) -> Self {
|
||||
fn new(tip: String) -> Self {
|
||||
Self { tip }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ pub use markdown_render::render_markdown_text;
|
||||
pub use public_widgets::composer_input::ComposerAction;
|
||||
pub use public_widgets::composer_input::ComposerInput;
|
||||
use std::io::Write as _;
|
||||
|
||||
// (tests access modules directly within the crate)
|
||||
|
||||
pub async fn run_main(
|
||||
@@ -344,6 +343,8 @@ async fn run_ratatui_app(
|
||||
) -> color_eyre::Result<AppExitInfo> {
|
||||
color_eyre::install()?;
|
||||
|
||||
tooltips::announcement::prewarm();
|
||||
|
||||
// Forward panic reports through tracing so they appear in the UI status
|
||||
// line, but do not swallow the default/color-eyre panic handler.
|
||||
// Chain to the previous hook so users still get a rich panic report
|
||||
|
||||
@@ -2,15 +2,10 @@ use codex_core::features::FEATURES;
|
||||
use lazy_static::lazy_static;
|
||||
use rand::Rng;
|
||||
|
||||
const ANNOUNCEMENT_TIP_URL: &str =
|
||||
"https://raw.githubusercontent.com/openai/codex/main/announcement_tip.toml";
|
||||
const RAW_TOOLTIPS: &str = include_str!("../tooltips.txt");
|
||||
|
||||
fn beta_tooltips() -> Vec<&'static str> {
|
||||
FEATURES
|
||||
.iter()
|
||||
.filter_map(|spec| spec.stage.beta_announcement())
|
||||
.collect()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref TOOLTIPS: Vec<&'static str> = RAW_TOOLTIPS
|
||||
.lines()
|
||||
@@ -25,9 +20,20 @@ lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn random_tooltip() -> Option<&'static str> {
|
||||
fn beta_tooltips() -> Vec<&'static str> {
|
||||
FEATURES
|
||||
.iter()
|
||||
.filter_map(|spec| spec.stage.beta_announcement())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Pick a random tooltip to show to the user when starting Codex.
|
||||
pub(crate) fn random_tooltip() -> Option<String> {
|
||||
if let Some(announcement) = announcement::fetch_announcement_tip() {
|
||||
return Some(announcement);
|
||||
}
|
||||
let mut rng = rand::rng();
|
||||
pick_tooltip(&mut rng)
|
||||
pick_tooltip(&mut rng).map(str::to_string)
|
||||
}
|
||||
|
||||
fn pick_tooltip<R: Rng + ?Sized>(rng: &mut R) -> Option<&'static str> {
|
||||
@@ -40,9 +46,149 @@ fn pick_tooltip<R: Rng + ?Sized>(rng: &mut R) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod announcement {
|
||||
use crate::tooltips::ANNOUNCEMENT_TIP_URL;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::Utc;
|
||||
use regex_lite::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::sync::OnceLock;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
static ANNOUNCEMENT_TIP: OnceLock<Option<String>> = OnceLock::new();
|
||||
|
||||
/// Prewarm the cache of the announcement tip.
|
||||
pub(crate) fn prewarm() {
|
||||
let _ = thread::spawn(|| ANNOUNCEMENT_TIP.get_or_init(init_announcement_tip_in_thread));
|
||||
}
|
||||
|
||||
/// Fetch the announcement tip, return None if the prewarm is not done yet.
|
||||
pub(crate) fn fetch_announcement_tip() -> Option<String> {
|
||||
ANNOUNCEMENT_TIP
|
||||
.get()
|
||||
.cloned()
|
||||
.flatten()
|
||||
.and_then(|raw| parse_announcement_tip_toml(&raw))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnnouncementTipRaw {
|
||||
content: String,
|
||||
from_date: Option<String>,
|
||||
to_date: Option<String>,
|
||||
version_regex: Option<String>,
|
||||
target_app: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnnouncementTipDocument {
|
||||
announcements: Vec<AnnouncementTipRaw>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AnnouncementTip {
|
||||
content: String,
|
||||
from_date: Option<NaiveDate>,
|
||||
to_date: Option<NaiveDate>,
|
||||
version_regex: Option<Regex>,
|
||||
target_app: String,
|
||||
}
|
||||
|
||||
fn init_announcement_tip_in_thread() -> Option<String> {
|
||||
thread::spawn(blocking_init_announcement_tip)
|
||||
.join()
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn blocking_init_announcement_tip() -> Option<String> {
|
||||
let response = reqwest::blocking::Client::new()
|
||||
.get(ANNOUNCEMENT_TIP_URL)
|
||||
.timeout(Duration::from_millis(2000))
|
||||
.send()
|
||||
.ok()?;
|
||||
response.error_for_status().ok()?.text().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_announcement_tip_toml(text: &str) -> Option<String> {
|
||||
let announcements = toml::from_str::<AnnouncementTipDocument>(text)
|
||||
.map(|doc| doc.announcements)
|
||||
.or_else(|_| toml::from_str::<Vec<AnnouncementTipRaw>>(text))
|
||||
.ok()?;
|
||||
|
||||
let mut latest_match = None;
|
||||
let today = Utc::now().date_naive();
|
||||
for raw in announcements {
|
||||
let Some(tip) = AnnouncementTip::from_raw(raw) else {
|
||||
continue;
|
||||
};
|
||||
if tip.version_matches(CODEX_CLI_VERSION)
|
||||
&& tip.date_matches(today)
|
||||
&& tip.target_app == "cli"
|
||||
{
|
||||
latest_match = Some(tip.content);
|
||||
}
|
||||
}
|
||||
latest_match
|
||||
}
|
||||
|
||||
impl AnnouncementTip {
|
||||
fn from_raw(raw: AnnouncementTipRaw) -> Option<Self> {
|
||||
let content = raw.content.trim();
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let from_date = match raw.from_date {
|
||||
Some(date) => Some(NaiveDate::parse_from_str(&date, "%Y-%m-%d").ok()?),
|
||||
None => None,
|
||||
};
|
||||
let to_date = match raw.to_date {
|
||||
Some(date) => Some(NaiveDate::parse_from_str(&date, "%Y-%m-%d").ok()?),
|
||||
None => None,
|
||||
};
|
||||
let version_regex = match raw.version_regex {
|
||||
Some(pattern) => Some(Regex::new(&pattern).ok()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
content: content.to_string(),
|
||||
from_date,
|
||||
to_date,
|
||||
version_regex,
|
||||
target_app: raw.target_app.unwrap_or("cli".to_string()).to_lowercase(),
|
||||
})
|
||||
}
|
||||
|
||||
fn version_matches(&self, version: &str) -> bool {
|
||||
self.version_regex
|
||||
.as_ref()
|
||||
.is_none_or(|regex| regex.is_match(version))
|
||||
}
|
||||
|
||||
fn date_matches(&self, today: NaiveDate) -> bool {
|
||||
if let Some(from) = self.from_date
|
||||
&& today < from
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(to) = self.to_date
|
||||
&& today >= to
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tooltips::announcement::parse_announcement_tip_toml;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::StdRng;
|
||||
|
||||
@@ -62,4 +208,104 @@ mod tests {
|
||||
let mut rng = StdRng::seed_from_u64(7);
|
||||
assert_eq!(expected, pick_tooltip(&mut rng));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_picks_last_matching() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
Some("latest match".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
Some("latest match".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_picks_no_match() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
to_date = "2000-01-05"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = "invalid_version_name"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match either "
|
||||
target_app = "vsce"
|
||||
"#;
|
||||
|
||||
assert_eq!(None, parse_announcement_tip_toml(toml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_bad_deserialization() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = 123
|
||||
from_date = "2000-01-01"
|
||||
"#;
|
||||
|
||||
assert_eq!(None, parse_announcement_tip_toml(toml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_parse_comments() {
|
||||
let toml = r#"
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
Some("This is a test announcement".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,11 +817,11 @@ pub(crate) fn padded_emoji(emoji: &str) -> String {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TooltipHistoryCell {
|
||||
tip: &'static str,
|
||||
tip: String,
|
||||
}
|
||||
|
||||
impl TooltipHistoryCell {
|
||||
fn new(tip: &'static str) -> Self {
|
||||
fn new(tip: String) -> Self {
|
||||
Self { tip }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,8 @@ async fn run_ratatui_app(
|
||||
) -> color_eyre::Result<AppExitInfo> {
|
||||
color_eyre::install()?;
|
||||
|
||||
tooltips::announcement::prewarm();
|
||||
|
||||
// Forward panic reports through tracing so they appear in the UI status
|
||||
// line, but do not swallow the default/color-eyre panic handler.
|
||||
// Chain to the previous hook so users still get a rich panic report
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use codex_core::features::FEATURES;
|
||||
use lazy_static::lazy_static;
|
||||
use rand::Rng;
|
||||
|
||||
const ANNOUNCEMENT_TIP_URL: &str =
|
||||
"https://raw.githubusercontent.com/openai/codex/main/announcement_tip.toml";
|
||||
const RAW_TOOLTIPS: &str = include_str!("../tooltips.txt");
|
||||
|
||||
lazy_static! {
|
||||
@@ -9,24 +12,183 @@ lazy_static! {
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.collect();
|
||||
static ref ALL_TOOLTIPS: Vec<&'static str> = {
|
||||
let mut tips = Vec::new();
|
||||
tips.extend(TOOLTIPS.iter().copied());
|
||||
tips.extend(beta_tooltips());
|
||||
tips
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn random_tooltip() -> Option<&'static str> {
|
||||
fn beta_tooltips() -> Vec<&'static str> {
|
||||
FEATURES
|
||||
.iter()
|
||||
.filter_map(|spec| spec.stage.beta_announcement())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Pick a random tooltip to show to the user when starting Codex.
|
||||
pub(crate) fn random_tooltip() -> Option<String> {
|
||||
if let Some(announcement) = announcement::fetch_announcement_tip() {
|
||||
return Some(announcement);
|
||||
}
|
||||
let mut rng = rand::rng();
|
||||
pick_tooltip(&mut rng)
|
||||
pick_tooltip(&mut rng).map(str::to_string)
|
||||
}
|
||||
|
||||
fn pick_tooltip<R: Rng + ?Sized>(rng: &mut R) -> Option<&'static str> {
|
||||
if TOOLTIPS.is_empty() {
|
||||
if ALL_TOOLTIPS.is_empty() {
|
||||
None
|
||||
} else {
|
||||
TOOLTIPS.get(rng.random_range(0..TOOLTIPS.len())).copied()
|
||||
ALL_TOOLTIPS
|
||||
.get(rng.random_range(0..ALL_TOOLTIPS.len()))
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod announcement {
|
||||
use crate::tooltips::ANNOUNCEMENT_TIP_URL;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::Utc;
|
||||
use regex_lite::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::sync::OnceLock;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
static ANNOUNCEMENT_TIP: OnceLock<Option<String>> = OnceLock::new();
|
||||
|
||||
/// Prewarm the cache of the announcement tip.
|
||||
pub(crate) fn prewarm() {
|
||||
let _ = thread::spawn(|| ANNOUNCEMENT_TIP.get_or_init(init_announcement_tip_in_thread));
|
||||
}
|
||||
|
||||
/// Fetch the announcement tip, return None if the prewarm is not done yet.
|
||||
pub(crate) fn fetch_announcement_tip() -> Option<String> {
|
||||
ANNOUNCEMENT_TIP
|
||||
.get()
|
||||
.cloned()
|
||||
.flatten()
|
||||
.and_then(|raw| parse_announcement_tip_toml(&raw))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnnouncementTipRaw {
|
||||
content: String,
|
||||
from_date: Option<String>,
|
||||
to_date: Option<String>,
|
||||
version_regex: Option<String>,
|
||||
target_app: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnnouncementTipDocument {
|
||||
announcements: Vec<AnnouncementTipRaw>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AnnouncementTip {
|
||||
content: String,
|
||||
from_date: Option<NaiveDate>,
|
||||
to_date: Option<NaiveDate>,
|
||||
version_regex: Option<Regex>,
|
||||
target_app: String,
|
||||
}
|
||||
|
||||
fn init_announcement_tip_in_thread() -> Option<String> {
|
||||
thread::spawn(blocking_init_announcement_tip)
|
||||
.join()
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn blocking_init_announcement_tip() -> Option<String> {
|
||||
let response = reqwest::blocking::Client::new()
|
||||
.get(ANNOUNCEMENT_TIP_URL)
|
||||
.timeout(Duration::from_millis(2000))
|
||||
.send()
|
||||
.ok()?;
|
||||
response.error_for_status().ok()?.text().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_announcement_tip_toml(text: &str) -> Option<String> {
|
||||
let announcements = toml::from_str::<AnnouncementTipDocument>(text)
|
||||
.map(|doc| doc.announcements)
|
||||
.or_else(|_| toml::from_str::<Vec<AnnouncementTipRaw>>(text))
|
||||
.ok()?;
|
||||
|
||||
let mut latest_match = None;
|
||||
let today = Utc::now().date_naive();
|
||||
for raw in announcements {
|
||||
let Some(tip) = AnnouncementTip::from_raw(raw) else {
|
||||
continue;
|
||||
};
|
||||
if tip.version_matches(CODEX_CLI_VERSION)
|
||||
&& tip.date_matches(today)
|
||||
&& tip.target_app == "cli"
|
||||
{
|
||||
latest_match = Some(tip.content);
|
||||
}
|
||||
}
|
||||
latest_match
|
||||
}
|
||||
|
||||
impl AnnouncementTip {
|
||||
fn from_raw(raw: AnnouncementTipRaw) -> Option<Self> {
|
||||
let content = raw.content.trim();
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let from_date = match raw.from_date {
|
||||
Some(date) => Some(NaiveDate::parse_from_str(&date, "%Y-%m-%d").ok()?),
|
||||
None => None,
|
||||
};
|
||||
let to_date = match raw.to_date {
|
||||
Some(date) => Some(NaiveDate::parse_from_str(&date, "%Y-%m-%d").ok()?),
|
||||
None => None,
|
||||
};
|
||||
let version_regex = match raw.version_regex {
|
||||
Some(pattern) => Some(Regex::new(&pattern).ok()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
content: content.to_string(),
|
||||
from_date,
|
||||
to_date,
|
||||
version_regex,
|
||||
target_app: raw.target_app.unwrap_or("cli".to_string()).to_lowercase(),
|
||||
})
|
||||
}
|
||||
|
||||
fn version_matches(&self, version: &str) -> bool {
|
||||
self.version_regex
|
||||
.as_ref()
|
||||
.is_none_or(|regex| regex.is_match(version))
|
||||
}
|
||||
|
||||
fn date_matches(&self, today: NaiveDate) -> bool {
|
||||
if let Some(from) = self.from_date
|
||||
&& today < from
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(to) = self.to_date
|
||||
&& today >= to
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tooltips::announcement::parse_announcement_tip_toml;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::StdRng;
|
||||
|
||||
@@ -46,4 +208,104 @@ mod tests {
|
||||
let mut rng = StdRng::seed_from_u64(7);
|
||||
assert_eq!(expected, pick_tooltip(&mut rng));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_picks_last_matching() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
Some("latest match".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
Some("latest match".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_picks_no_match() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
to_date = "2000-01-05"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = "invalid_version_name"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match either "
|
||||
target_app = "vsce"
|
||||
"#;
|
||||
|
||||
assert_eq!(None, parse_announcement_tip_toml(toml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_bad_deserialization() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = 123
|
||||
from_date = "2000-01-01"
|
||||
"#;
|
||||
|
||||
assert_eq!(None, parse_announcement_tip_toml(toml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_parse_comments() {
|
||||
let toml = r#"
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
Some("This is a test announcement".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user