huskies: merge 962

This commit is contained in:
dave
2026-05-13 11:58:50 +00:00
parent 658e02c9b2
commit 184c214c34
19 changed files with 204 additions and 44 deletions
+125
View File
@@ -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");
}
}
+16
View File
@@ -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(())
}