Full-depth rename per the approved plan; the 'claw' product vocabulary (claws, /claws routes, clawId, Claw Chat) stays — it is now the brand. - Display brand: Clawmates (manifest, titles, hero, login/rail logo 'clawmates'); default host app.clawmates.work; registry ghcr.io/clawmates - Crates tc-* -> cm-* (16 crates + all imports); binaries clawmates-server/broker/bundler; images clawmates/*; env prefix CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config clawmates.toml; helm chart deploy/helm/clawmates with clawmates-* resources; db names clawmates*; sockets /run/clawmates; cookie cm_session; kind cluster clawmates-test; seccomp node profile clawmates-agent-profile.json - All 9 Playwright brand assertions updated in lockstep; historical spec document left untouched as the only remaining 'TeamClaw' - Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared test server clawmates-test-pg, kind cluster recreated with image + profile, compose images rebuilt under clawmates/* Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and the clean-room install rehearsal serving the clawmates login page from a signed bundle of the rebuilt images. Co-Authored-By: Claude Fable 5 <[email protected]>
176 lines
5.9 KiB
Rust
176 lines
5.9 KiB
Rust
// figment::Jail::expect_with dictates closures returning figment's own
|
|
// (large) error type; the lint has nothing actionable here.
|
|
#![allow(clippy::result_large_err)]
|
|
|
|
use cm_config::{AppConfig, AuthMode, ConfigError, DeployTarget, LlmProviderKind};
|
|
|
|
const AIR_GAPPED_TOML: &str = r#"
|
|
deploy_target = "air_gapped"
|
|
listen_addr = "0.0.0.0:8080"
|
|
|
|
[database]
|
|
url = "postgres://clawmates:pw@db:5432/clawmates"
|
|
|
|
[llm]
|
|
provider = "openai_compat"
|
|
base_url = "http://local-llm:8000/v1"
|
|
model = "qwen2.5-72b-instruct"
|
|
|
|
[auth]
|
|
mode = "local"
|
|
"#;
|
|
|
|
const CLOUD_TOML: &str = r#"
|
|
deploy_target = "cloud"
|
|
listen_addr = "0.0.0.0:8080"
|
|
|
|
[database]
|
|
url = "postgres://clawmates:pw@db:5432/clawmates"
|
|
max_connections = 32
|
|
|
|
[llm]
|
|
provider = "anthropic"
|
|
model = "claude-sonnet-4-5"
|
|
|
|
[auth]
|
|
mode = "oidc"
|
|
issuer_url = "https://idp.example.com"
|
|
client_id = "clawmates"
|
|
"#;
|
|
|
|
#[test]
|
|
fn loads_air_gapped_config_from_toml() {
|
|
figment::Jail::expect_with(|jail| {
|
|
jail.create_file("clawmates.toml", AIR_GAPPED_TOML)?;
|
|
let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
|
|
assert_eq!(cfg.deploy_target, DeployTarget::AirGapped);
|
|
assert_eq!(cfg.listen_addr.port(), 8080);
|
|
assert_eq!(
|
|
cfg.database.url,
|
|
"postgres://clawmates:pw@db:5432/clawmates"
|
|
);
|
|
assert_eq!(cfg.llm.provider, LlmProviderKind::OpenAiCompat);
|
|
assert_eq!(
|
|
cfg.llm.base_url.as_deref(),
|
|
Some("http://local-llm:8000/v1")
|
|
);
|
|
assert_eq!(cfg.auth.mode, AuthMode::Local);
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn loads_cloud_config_with_oidc() {
|
|
figment::Jail::expect_with(|jail| {
|
|
jail.create_file("clawmates.toml", CLOUD_TOML)?;
|
|
let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
|
|
assert_eq!(cfg.deploy_target, DeployTarget::Cloud);
|
|
assert_eq!(cfg.database.max_connections, 32);
|
|
assert_eq!(cfg.llm.provider, LlmProviderKind::Anthropic);
|
|
assert_eq!(cfg.auth.mode, AuthMode::Oidc);
|
|
assert_eq!(
|
|
cfg.auth.issuer_url.as_deref(),
|
|
Some("https://idp.example.com")
|
|
);
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn env_overrides_toml_values() {
|
|
figment::Jail::expect_with(|jail| {
|
|
jail.create_file("clawmates.toml", AIR_GAPPED_TOML)?;
|
|
jail.set_env("CLAWMATES_DATABASE__URL", "postgres://other:pw@x:5432/y");
|
|
jail.set_env("CLAWMATES_LLM__MODEL", "llama-3.1-8b-instruct");
|
|
let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
|
|
assert_eq!(cfg.database.url, "postgres://other:pw@x:5432/y");
|
|
assert_eq!(cfg.llm.model, "llama-3.1-8b-instruct");
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn database_max_connections_defaults_to_ten() {
|
|
figment::Jail::expect_with(|jail| {
|
|
jail.create_file("clawmates.toml", AIR_GAPPED_TOML)?;
|
|
let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
|
|
assert_eq!(cfg.database.max_connections, 10);
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn openai_compat_without_base_url_is_rejected() {
|
|
figment::Jail::expect_with(|jail| {
|
|
let toml = AIR_GAPPED_TOML.replace("base_url = \"http://local-llm:8000/v1\"\n", "");
|
|
jail.create_file("clawmates.toml", &toml)?;
|
|
let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
|
|
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("base_url")));
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn scripted_provider_requires_scenario_path() {
|
|
figment::Jail::expect_with(|jail| {
|
|
let toml = AIR_GAPPED_TOML
|
|
.replace("provider = \"openai_compat\"", "provider = \"scripted\"")
|
|
.replace("base_url = \"http://local-llm:8000/v1\"\n", "");
|
|
jail.create_file("clawmates.toml", &toml)?;
|
|
let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
|
|
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("scenario_path")));
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn oidc_mode_requires_issuer_and_client_id() {
|
|
figment::Jail::expect_with(|jail| {
|
|
let toml = CLOUD_TOML.replace("issuer_url = \"https://idp.example.com\"\n", "");
|
|
jail.create_file("clawmates.toml", &toml)?;
|
|
let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
|
|
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("issuer_url")));
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn missing_file_is_a_clear_error() {
|
|
figment::Jail::expect_with(|jail| {
|
|
let err = AppConfig::load_from(&jail.directory().join("absent.toml")).unwrap_err();
|
|
assert!(matches!(err, ConfigError::Load(_)));
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn s3_backend_requires_endpoint_and_bucket() {
|
|
figment::Jail::expect_with(|jail| {
|
|
let toml =
|
|
format!("{AIR_GAPPED_TOML}\n[storage]\ndata_dir = \"./data\"\nbackend = \"s3\"\n");
|
|
jail.create_file("clawmates.toml", &toml)?;
|
|
let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
|
|
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("s3_endpoint")));
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn clerk_mode_requires_the_instance_issuer() {
|
|
figment::Jail::expect_with(|jail| {
|
|
let toml = AIR_GAPPED_TOML.replace("mode = \"local\"", "mode = \"clerk\"");
|
|
jail.create_file("clawmates.toml", &toml)?;
|
|
let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
|
|
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("clerk")));
|
|
|
|
let toml = toml.replace(
|
|
"mode = \"clerk\"",
|
|
"mode = \"clerk\"\nissuer_url = \"https://acme.clerk.accounts.dev\"",
|
|
);
|
|
jail.create_file("clawmates.toml", &toml)?;
|
|
let config = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
|
|
assert_eq!(config.auth.mode, AuthMode::Clerk);
|
|
Ok(())
|
|
});
|
|
}
|