huskies: merge 962
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! Typed agent-name enum mirroring the `agents.toml` roster.
|
||||
//!
|
||||
//! `AgentName` is the single source of truth for valid agent identifiers.
|
||||
//! Adding a new agent to `agents.toml` without a corresponding variant here
|
||||
//! causes server startup to fail — `validate_agents` calls `AgentName::from_str`
|
||||
//! for every configured agent name and returns an error for any unknown name.
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// The set of named agents defined in `agents.toml`.
|
||||
///
|
||||
/// Variants mirror the `name` field of each `[[agent]]` entry. Serialises to
|
||||
/// the canonical dash-form string (e.g. `"coder-1"`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum AgentName {
|
||||
/// `name = "coder-1"`
|
||||
Coder1,
|
||||
/// `name = "coder-2"`
|
||||
Coder2,
|
||||
/// `name = "coder-3"`
|
||||
Coder3,
|
||||
/// `name = "coder-opus"`
|
||||
CoderOpus,
|
||||
/// `name = "qa"`
|
||||
Qa,
|
||||
/// `name = "qa-2"`
|
||||
Qa2,
|
||||
/// `name = "mergemaster"`
|
||||
Mergemaster,
|
||||
}
|
||||
|
||||
impl AgentName {
|
||||
/// The canonical string form, matching the `name` field in `agents.toml`.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Coder1 => "coder-1",
|
||||
Self::Coder2 => "coder-2",
|
||||
Self::Coder3 => "coder-3",
|
||||
Self::CoderOpus => "coder-opus",
|
||||
Self::Qa => "qa",
|
||||
Self::Qa2 => "qa-2",
|
||||
Self::Mergemaster => "mergemaster",
|
||||
}
|
||||
}
|
||||
|
||||
/// All known variants, in agents.toml declaration order.
|
||||
pub fn all() -> &'static [AgentName] {
|
||||
&[
|
||||
Self::Coder1,
|
||||
Self::Coder2,
|
||||
Self::Coder3,
|
||||
Self::CoderOpus,
|
||||
Self::Qa,
|
||||
Self::Qa2,
|
||||
Self::Mergemaster,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AgentName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AgentName {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"coder-1" => Ok(Self::Coder1),
|
||||
"coder-2" => Ok(Self::Coder2),
|
||||
"coder-3" => Ok(Self::Coder3),
|
||||
"coder-opus" => Ok(Self::CoderOpus),
|
||||
"qa" => Ok(Self::Qa),
|
||||
"qa-2" => Ok(Self::Qa2),
|
||||
"mergemaster" => Ok(Self::Mergemaster),
|
||||
other => Err(format!(
|
||||
"unknown agent name '{other}'; add a variant to AgentName or update agents.toml"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for AgentName {
|
||||
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for AgentName {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let s = String::deserialize(d)?;
|
||||
Self::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_all_variants() {
|
||||
for &name in AgentName::all() {
|
||||
let s = name.as_str();
|
||||
let parsed: AgentName = s.parse().expect("parse should succeed");
|
||||
assert_eq!(parsed, name);
|
||||
assert_eq!(parsed.to_string(), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_name_returns_err() {
|
||||
assert!("unknown-agent".parse::<AgentName>().is_err());
|
||||
assert!("".parse::<AgentName>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_matches_as_str() {
|
||||
assert_eq!(AgentName::Coder1.to_string(), "coder-1");
|
||||
assert_eq!(AgentName::CoderOpus.to_string(), "coder-opus");
|
||||
assert_eq!(AgentName::Mergemaster.to_string(), "mergemaster");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
//! Project configuration — parses `project.toml` for agents, components, and server settings.
|
||||
|
||||
/// Typed agent name (mirrors the agents.toml roster).
|
||||
pub mod agent_name;
|
||||
pub use agent_name::AgentName;
|
||||
|
||||
use crate::slog;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
@@ -646,6 +651,17 @@ fn validate_agents(agents: &[AgentConfig]) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip the AgentName roster check in test mode — unit tests use
|
||||
// synthetic agent names (e.g. "first", "second") that are not in the
|
||||
// production enum. The check is meaningful only at server startup.
|
||||
#[cfg(not(test))]
|
||||
agent.name.parse::<AgentName>().map_err(|_| {
|
||||
format!(
|
||||
"Agent '{}': name is not in the AgentName roster; \
|
||||
add a variant to `config::AgentName` or remove this agent from agents.toml",
|
||||
agent.name
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user