mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
review
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use clap::Args;
|
||||
use codex_status::CodexStatusReport;
|
||||
use codex_status::CodexStatus;
|
||||
use codex_status::StatusClient;
|
||||
use std::io::Write;
|
||||
use std::io::{self};
|
||||
@@ -13,46 +13,27 @@ pub struct StatusCommand {
|
||||
|
||||
pub async fn run_status(cmd: StatusCommand) -> anyhow::Result<()> {
|
||||
let client = StatusClient::new()?;
|
||||
let report = client.fetch_codex_status().await?;
|
||||
let codex_status = client.fetch_codex_status().await?;
|
||||
|
||||
if cmd.json {
|
||||
let json = serde_json::to_string_pretty(&report)?;
|
||||
let json = serde_json::to_string_pretty(&codex_status)?;
|
||||
println!("{json}");
|
||||
} else {
|
||||
write_human(&report, &mut io::stdout())?;
|
||||
write_human(&codex_status, &mut io::stdout())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_human<W: Write>(report: &CodexStatusReport, writer: &mut W) -> anyhow::Result<()> {
|
||||
writeln!(
|
||||
writer,
|
||||
"overall: {} ({})",
|
||||
report.overall_description, report.overall_indicator
|
||||
)?;
|
||||
writeln!(writer, "updated_at: {}", report.updated_at)?;
|
||||
fn write_human<W: Write>(status: &CodexStatus, writer: &mut W) -> anyhow::Result<()> {
|
||||
writeln!(writer, "Codex status: {}", status.status)?;
|
||||
writeln!(writer, "operational: {}", status.is_operational)?;
|
||||
writeln!(writer, "component_id: {}", status.component_id)?;
|
||||
|
||||
writeln!(writer, "codex components:")?;
|
||||
if report.components.is_empty() {
|
||||
writeln!(writer, " none")?;
|
||||
if let Some(affected) = &status.raw_affected {
|
||||
writeln!(writer, "affected status: {}", affected.status)?;
|
||||
} else {
|
||||
for component in &report.components {
|
||||
writeln!(writer, " {}: {}", component.name, component.status)?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(writer, "codex incidents:")?;
|
||||
if report.incidents.is_empty() {
|
||||
writeln!(writer, " none")?;
|
||||
} else {
|
||||
for incident in &report.incidents {
|
||||
writeln!(
|
||||
writer,
|
||||
" {}: status={} impact={} updated={}",
|
||||
incident.name, incident.status, incident.impact, incident.updated_at
|
||||
)?;
|
||||
}
|
||||
writeln!(writer, "affected status: none")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -61,56 +42,50 @@ fn write_human<W: Write>(report: &CodexStatusReport, writer: &mut W) -> anyhow::
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_status::ComponentStatus;
|
||||
use codex_status::IncidentStatus;
|
||||
use codex_status::AffectedComponent;
|
||||
use codex_status::ComponentHealth;
|
||||
|
||||
#[test]
|
||||
fn human_output_handles_empty_lists() -> anyhow::Result<()> {
|
||||
let report = CodexStatusReport {
|
||||
overall_description: "All Systems Operational".to_string(),
|
||||
overall_indicator: "none".to_string(),
|
||||
updated_at: "2025-11-07T21:55:20Z".to_string(),
|
||||
components: Vec::new(),
|
||||
incidents: Vec::new(),
|
||||
fn human_output_handles_absent_affected_entry() -> anyhow::Result<()> {
|
||||
let status = CodexStatus {
|
||||
component_id: "cmp-1".to_string(),
|
||||
name: "Codex".to_string(),
|
||||
status: ComponentHealth::Operational,
|
||||
is_operational: true,
|
||||
raw_affected: None,
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
write_human(&report, &mut buffer)?;
|
||||
write_human(&status, &mut buffer)?;
|
||||
|
||||
let output = String::from_utf8(buffer).expect("utf8");
|
||||
assert!(output.contains("overall: All Systems Operational (none)"));
|
||||
assert!(output.contains("updated_at: 2025-11-07T21:55:20Z"));
|
||||
assert!(output.contains("codex components:\n none"));
|
||||
assert!(output.contains("codex incidents:\n none"));
|
||||
assert!(output.contains("Codex status: operational"));
|
||||
assert!(output.contains("operational: true"));
|
||||
assert!(output.contains("component_id: cmp-1"));
|
||||
assert!(output.contains("affected status: none"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_output_lists_components_and_incidents() -> anyhow::Result<()> {
|
||||
let report = CodexStatusReport {
|
||||
overall_description: "Degraded".to_string(),
|
||||
overall_indicator: "minor".to_string(),
|
||||
updated_at: "2025-11-07T21:55:20Z".to_string(),
|
||||
components: vec![ComponentStatus {
|
||||
name: "Codex".to_string(),
|
||||
status: "degraded_performance".to_string(),
|
||||
}],
|
||||
incidents: vec![IncidentStatus {
|
||||
name: "Codex degraded".to_string(),
|
||||
status: "investigating".to_string(),
|
||||
impact: "minor".to_string(),
|
||||
updated_at: "2025-11-07T21:45:00Z".to_string(),
|
||||
}],
|
||||
fn human_output_shows_affected_status() -> anyhow::Result<()> {
|
||||
let status = CodexStatus {
|
||||
component_id: "cmp-1".to_string(),
|
||||
name: "Codex".to_string(),
|
||||
status: ComponentHealth::DegradedPerformance,
|
||||
is_operational: false,
|
||||
raw_affected: Some(AffectedComponent {
|
||||
component_id: "cmp-1".to_string(),
|
||||
status: ComponentHealth::DegradedPerformance,
|
||||
}),
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
write_human(&report, &mut buffer)?;
|
||||
write_human(&status, &mut buffer)?;
|
||||
|
||||
let output = String::from_utf8(buffer).expect("utf8");
|
||||
assert!(output.contains("codex components:\n Codex: degraded_performance"));
|
||||
assert!(output.contains(
|
||||
"codex incidents:\n Codex degraded: status=investigating impact=minor updated=2025-11-07T21:45:00Z"
|
||||
));
|
||||
assert!(output.contains("Codex status: degraded_performance"));
|
||||
assert!(output.contains("operational: false"));
|
||||
assert!(output.contains("affected status: degraded_performance"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -1,47 +1,103 @@
|
||||
use anyhow::Context;
|
||||
use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::{header::CONTENT_TYPE, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const STATUS_SUMMARY_URL: &str = "https://status.openai.com/api/v2/summary.json";
|
||||
pub const DEFAULT_STATUS_WIDGET_URL: &str = "https://status.openai.com/proxy/status.openai.com";
|
||||
pub const STATUS_WIDGET_ENV_VAR: &str = "STATUS_WIDGET_URL";
|
||||
pub const CODEX_COMPONENT_NAME: &str = "Codex";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct CodexStatusReport {
|
||||
pub overall_description: String,
|
||||
pub overall_indicator: String,
|
||||
pub updated_at: String,
|
||||
pub components: Vec<ComponentStatus>,
|
||||
pub incidents: Vec<IncidentStatus>,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ComponentHealth {
|
||||
Operational,
|
||||
DegradedPerformance,
|
||||
PartialOutage,
|
||||
MajorOutage,
|
||||
UnderMaintenance,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ComponentHealth {
|
||||
fn operational() -> Self {
|
||||
Self::Operational
|
||||
}
|
||||
|
||||
pub fn is_operational(self) -> bool {
|
||||
self == Self::Operational
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl fmt::Display for ComponentHealth {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value = Value::from(self)
|
||||
.as_str()
|
||||
.ok_or(fmt::Error)?;
|
||||
|
||||
f.write_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AffectedComponent {
|
||||
pub component_id: String,
|
||||
#[serde(default = "ComponentHealth::operational")]
|
||||
pub status: ComponentHealth,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Component {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub status_page_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Summary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub components: Vec<Component>,
|
||||
#[serde(default)]
|
||||
pub affected_components: Vec<AffectedComponent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct StatusPayload {
|
||||
pub summary: Summary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ComponentStatus {
|
||||
pub struct CodexStatus {
|
||||
pub component_id: String,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct IncidentStatus {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub impact: String,
|
||||
pub updated_at: String,
|
||||
pub status: ComponentHealth,
|
||||
pub is_operational: bool,
|
||||
pub raw_affected: Option<AffectedComponent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatusClient {
|
||||
client: reqwest::Client,
|
||||
summary_url: Url,
|
||||
widget_url: Url,
|
||||
}
|
||||
|
||||
impl StatusClient {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
Self::with_summary_url(Url::parse(STATUS_SUMMARY_URL)?)
|
||||
pub fn new() -> Result<Self> {
|
||||
let widget_url = env::var(STATUS_WIDGET_ENV_VAR)
|
||||
.unwrap_or_else(|_| DEFAULT_STATUS_WIDGET_URL.to_string());
|
||||
Self::with_widget_url(Url::parse(&widget_url)?)
|
||||
}
|
||||
|
||||
pub fn with_summary_url(summary_url: Url) -> anyhow::Result<Self> {
|
||||
let user_agent = format!("codex-status/{}", env!("CARGO_PKG_VERSION"));
|
||||
pub fn with_widget_url(widget_url: Url) -> Result<Self> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let user_agent = format!("codex-status/{version}");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
@@ -49,125 +105,83 @@ impl StatusClient {
|
||||
.build()
|
||||
.context("building HTTP client")?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
summary_url,
|
||||
})
|
||||
Ok(Self { client, widget_url })
|
||||
}
|
||||
|
||||
pub async fn fetch_codex_status(&self) -> anyhow::Result<CodexStatusReport> {
|
||||
pub async fn fetch_status_payload(&self) -> Result<StatusPayload> {
|
||||
let response = self
|
||||
.client
|
||||
.get(self.summary_url.clone())
|
||||
.get(self.widget_url.clone())
|
||||
.send()
|
||||
.await
|
||||
.context("requesting status summary")?;
|
||||
|
||||
let summary: StatusSummary = response
|
||||
.context("requesting status widget")?
|
||||
.error_for_status()
|
||||
.context("status summary returned error")?
|
||||
.json()
|
||||
.context("status widget returned error")?;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
if !content_type.contains("json") {
|
||||
let snippet = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect::<String>();
|
||||
|
||||
let url = &self.widget_url;
|
||||
bail!(
|
||||
"Expected JSON from {url}, got Content-Type={content_type}. Body starts with: {snippet:?}"
|
||||
);
|
||||
}
|
||||
|
||||
response
|
||||
.json::<StatusPayload>()
|
||||
.await
|
||||
.context("parsing status summary JSON")?;
|
||||
.context("parsing status widget JSON")
|
||||
}
|
||||
|
||||
Ok(CodexStatusReport::from_summary(summary))
|
||||
pub async fn fetch_codex_status(&self) -> Result<CodexStatus> {
|
||||
let payload = self.fetch_status_payload().await?;
|
||||
derive_component_status(&payload, CODEX_COMPONENT_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexStatusReport {
|
||||
fn from_summary(summary: StatusSummary) -> Self {
|
||||
let components = summary
|
||||
.components
|
||||
.into_iter()
|
||||
.filter(|component| is_codex_name(&component.name))
|
||||
.map(ComponentStatus::from)
|
||||
.collect();
|
||||
pub fn derive_component_status(payload: &StatusPayload, component_name: &str) -> Result<CodexStatus> {
|
||||
let component = payload
|
||||
.summary
|
||||
.components
|
||||
.iter()
|
||||
.find(|component| component.name == component_name)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("Component {component_name:?} not found in status summary"))?;
|
||||
|
||||
let incidents = summary
|
||||
.incidents
|
||||
.into_iter()
|
||||
.filter(|incident| is_codex_name(&incident.name))
|
||||
.map(IncidentStatus::from)
|
||||
.collect();
|
||||
let affected = payload
|
||||
.summary
|
||||
.affected_components
|
||||
.iter()
|
||||
.find(|affected| affected.component_id == component.id)
|
||||
.cloned();
|
||||
|
||||
CodexStatusReport {
|
||||
overall_description: summary.status.description,
|
||||
overall_indicator: summary.status.indicator,
|
||||
updated_at: summary.page.updated_at,
|
||||
components,
|
||||
incidents,
|
||||
}
|
||||
}
|
||||
}
|
||||
let status = affected
|
||||
.as_ref()
|
||||
.map(|affected| affected.status)
|
||||
.unwrap_or(ComponentHealth::Operational);
|
||||
|
||||
fn is_codex_name(name: &str) -> bool {
|
||||
name.to_ascii_lowercase().contains("codex")
|
||||
}
|
||||
let is_operational = status.is_operational();
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct StatusSummary {
|
||||
#[serde(default)]
|
||||
page: Page,
|
||||
#[serde(default)]
|
||||
status: OverallStatus,
|
||||
#[serde(default)]
|
||||
components: Vec<Component>,
|
||||
#[serde(default)]
|
||||
incidents: Vec<Incident>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct Page {
|
||||
#[serde(default)]
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct OverallStatus {
|
||||
#[serde(default)]
|
||||
indicator: String,
|
||||
#[serde(default)]
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Component {
|
||||
name: String,
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Incident {
|
||||
name: String,
|
||||
status: String,
|
||||
#[serde(default = "default_impact")]
|
||||
impact: String,
|
||||
#[serde(default)]
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
fn default_impact() -> String {
|
||||
"unknown".to_string()
|
||||
}
|
||||
|
||||
impl From<Component> for ComponentStatus {
|
||||
fn from(value: Component) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
status: value.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Incident> for IncidentStatus {
|
||||
fn from(value: Incident) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
status: value.status,
|
||||
impact: value.impact,
|
||||
updated_at: value.updated_at,
|
||||
}
|
||||
}
|
||||
Ok(CodexStatus {
|
||||
component_id: component.id,
|
||||
name: component.name,
|
||||
status,
|
||||
is_operational,
|
||||
raw_affected: affected,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -177,54 +191,93 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn filters_non_codex_components_and_incidents() {
|
||||
let summary = serde_json::from_value::<StatusSummary>(json!({
|
||||
"page": {"updated_at": "2025-11-07T21:55:20Z"},
|
||||
"status": {"description": "All Systems Operational", "indicator": "none"},
|
||||
"components": [
|
||||
{"name": "Codex", "status": "operational"},
|
||||
{"name": "Chat Completions", "status": "operational"}
|
||||
],
|
||||
"incidents": [
|
||||
{"name": "Codex degraded performance", "status": "investigating", "impact": "minor", "updated_at": "2025-11-07T21:50:00Z"},
|
||||
{"name": "Chat downtime", "status": "resolved", "impact": "critical", "updated_at": "2025-11-07T21:00:00Z"}
|
||||
]
|
||||
fn defaults_to_operational_when_not_affected() {
|
||||
let payload = serde_json::from_value::<StatusPayload>(json!({
|
||||
"summary": {
|
||||
"id": "sum-1",
|
||||
"name": "OpenAI",
|
||||
"components": [
|
||||
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"},
|
||||
{"id": "cmp-2", "name": "Chat", "status_page_id": "page-1"}
|
||||
]
|
||||
}
|
||||
}))
|
||||
.expect("valid summary");
|
||||
.expect("valid payload");
|
||||
|
||||
let report = CodexStatusReport::from_summary(summary);
|
||||
let status = derive_component_status(&payload, "Codex").expect("codex component exists");
|
||||
|
||||
assert_eq!(report.overall_description, "All Systems Operational");
|
||||
assert_eq!(report.overall_indicator, "none");
|
||||
assert_eq!(report.updated_at, "2025-11-07T21:55:20Z");
|
||||
assert_eq!(report.components.len(), 1);
|
||||
assert_eq!(report.components[0].name, "Codex");
|
||||
assert_eq!(report.components[0].status, "operational");
|
||||
assert_eq!(report.incidents.len(), 1);
|
||||
assert_eq!(report.incidents[0].name, "Codex degraded performance");
|
||||
assert_eq!(report.incidents[0].status, "investigating");
|
||||
assert_eq!(report.incidents[0].impact, "minor");
|
||||
assert_eq!(report.incidents[0].updated_at, "2025-11-07T21:50:00Z");
|
||||
assert_eq!(status.status, ComponentHealth::Operational);
|
||||
assert!(status.is_operational);
|
||||
assert!(status.raw_affected.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_missing_fields_with_defaults() {
|
||||
let summary =
|
||||
serde_json::from_value::<StatusSummary>(json!({})).expect("valid empty summary");
|
||||
fn uses_affected_component_status() {
|
||||
let payload = serde_json::from_value::<StatusPayload>(json!({
|
||||
"summary": {
|
||||
"id": "sum-1",
|
||||
"name": "OpenAI",
|
||||
"components": [
|
||||
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}
|
||||
],
|
||||
"affected_components": [
|
||||
{"component_id": "cmp-1", "status": "major_outage"}
|
||||
]
|
||||
}
|
||||
}))
|
||||
.expect("valid payload");
|
||||
|
||||
let report = CodexStatusReport::from_summary(summary);
|
||||
let status = derive_component_status(&payload, "Codex").expect("codex component exists");
|
||||
|
||||
assert_eq!(report.overall_description, "");
|
||||
assert_eq!(report.overall_indicator, "");
|
||||
assert_eq!(report.updated_at, "");
|
||||
assert!(report.components.is_empty());
|
||||
assert!(report.incidents.is_empty());
|
||||
assert_eq!(status.status, ComponentHealth::MajorOutage);
|
||||
assert!(!status.is_operational);
|
||||
assert_eq!(
|
||||
status
|
||||
.raw_affected
|
||||
.as_ref()
|
||||
.map(|affected| affected.status),
|
||||
Some(ComponentHealth::MajorOutage)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_codex_name_matches_case_insensitive() {
|
||||
assert!(is_codex_name("Codex"));
|
||||
assert!(is_codex_name("my-codex-component"));
|
||||
assert!(!is_codex_name("Chat"));
|
||||
fn unknown_status_is_preserved_as_unknown() {
|
||||
let payload = serde_json::from_value::<StatusPayload>(json!({
|
||||
"summary": {
|
||||
"id": "sum-1",
|
||||
"name": "OpenAI",
|
||||
"components": [
|
||||
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}
|
||||
],
|
||||
"affected_components": [
|
||||
{"component_id": "cmp-1", "status": "custom_status"}
|
||||
]
|
||||
}
|
||||
}))
|
||||
.expect("valid payload");
|
||||
|
||||
let status = derive_component_status(&payload, "Codex").expect("codex component exists");
|
||||
|
||||
assert_eq!(status.status, ComponentHealth::Unknown);
|
||||
assert!(!status.is_operational);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_component_returns_error() {
|
||||
let payload = serde_json::from_value::<StatusPayload>(json!({
|
||||
"summary": {
|
||||
"id": "sum-1",
|
||||
"name": "OpenAI",
|
||||
"components": [],
|
||||
"affected_components": []
|
||||
}
|
||||
}))
|
||||
.expect("valid payload");
|
||||
|
||||
let error = derive_component_status(&payload, "Codex").expect_err("missing component should error");
|
||||
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("Component \"Codex\" not found in status summary"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user