Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 236 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1602,11 +1602,13 @@ maintenance_merge_similarity_threshold = 1.1
dm_allowed_users: vec![],
},
];
assert!(validate_named_messaging_adapters(&messaging, &bindings).is_ok());
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert_eq!(result.len(), 2);
Comment thread
l33t0 marked this conversation as resolved.
}

#[test]
fn validate_named_adapters_missing_instance() {
fn validate_named_adapters_missing_instance_skipped() {
let messaging = MessagingConfig {
discord: None,
slack: None,
Expand All @@ -1632,7 +1634,9 @@ maintenance_merge_similarity_threshold = 1.1
require_mention: false,
dm_allowed_users: vec![],
}];
assert!(validate_named_messaging_adapters(&messaging, &bindings).is_err());
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert!(result.is_empty(), "unresolvable binding should be skipped");
}

#[test]
Expand Down Expand Up @@ -1695,11 +1699,16 @@ maintenance_merge_similarity_threshold = 1.1
require_mention: false,
dm_allowed_users: vec![],
}];
assert!(validate_named_messaging_adapters(&messaging, &bindings).is_err());
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert!(
result.is_empty(),
"unsupported platform binding should be skipped"
);
}

#[test]
fn validate_binding_without_default_adapter_rejected() {
fn validate_binding_without_default_adapter_skipped() {
let messaging = MessagingConfig {
discord: None,
slack: None,
Expand Down Expand Up @@ -1731,7 +1740,228 @@ maintenance_merge_similarity_threshold = 1.1
require_mention: false,
dm_allowed_users: vec![],
}];
assert!(validate_named_messaging_adapters(&messaging, &bindings).is_err());
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert!(
result.is_empty(),
"binding without default adapter should be skipped"
);
}

#[test]
fn validate_mixed_valid_and_invalid_bindings_filters_correctly() {
let messaging = MessagingConfig {
discord: None,
slack: None,
telegram: Some(TelegramConfig {
enabled: true,
token: "tok".into(),
instances: vec![TelegramInstanceConfig {
name: "support".into(),
enabled: true,
token: "tok2".into(),
dm_allowed_users: vec![],
}],
dm_allowed_users: vec![],
}),
email: None,
webhook: None,
twitch: None,
signal: None,
};
let bindings = vec![
// Valid: default adapter with credentials
Binding {
agent_id: "agent-a".into(),
channel: "telegram".into(),
adapter: None,
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
},
// Invalid: references a non-existent named adapter
Binding {
agent_id: "agent-b".into(),
channel: "telegram".into(),
adapter: Some("ghost".into()),
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
},
// Valid: references an existing named adapter
Binding {
agent_id: "agent-c".into(),
channel: "telegram".into(),
adapter: Some("support".into()),
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
},
// Invalid: no discord config at all
Binding {
agent_id: "agent-d".into(),
channel: "discord".into(),
adapter: None,
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
},
];
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert_eq!(
result.len(),
2,
"only the two valid bindings should survive"
);
assert_eq!(result[0].agent_id, "agent-a");
assert_eq!(result[1].agent_id, "agent-c");
}

#[test]
fn validate_missing_messaging_config_skipped() {
let messaging = MessagingConfig {
discord: None,
slack: None,
telegram: None,
email: None,
webhook: None,
twitch: None,
signal: None,
};
let bindings = vec![Binding {
agent_id: "main".into(),
channel: "telegram".into(),
adapter: None,
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
}];
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert!(
result.is_empty(),
"binding with no messaging config should be skipped"
);
}

#[test]
fn validate_strict_mode_rejects_missing_messaging_config() {
let messaging = MessagingConfig {
discord: None,
slack: None,
telegram: None,
email: None,
webhook: None,
twitch: None,
signal: None,
};
let bindings = vec![Binding {
agent_id: "main".into(),
channel: "telegram".into(),
adapter: None,
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
}];
let result = validate_named_messaging_adapters(&messaging, bindings, true);
assert!(
result.is_err(),
"strict mode should reject unresolvable bindings"
);
}

#[test]
fn validate_disabled_instance_is_filtered_out() {
let messaging = MessagingConfig {
discord: None,
slack: None,
telegram: Some(TelegramConfig {
enabled: true,
token: "tok".into(),
instances: vec![TelegramInstanceConfig {
name: "support".into(),
enabled: false,
token: "tok2".into(),
dm_allowed_users: vec![],
}],
dm_allowed_users: vec![],
}),
email: None,
webhook: None,
twitch: None,
signal: None,
};
let bindings = vec![Binding {
agent_id: "main".into(),
channel: "telegram".into(),
adapter: Some("support".into()),
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
}];
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert!(
result.is_empty(),
"binding to disabled instance should be skipped"
);
}

#[test]
fn validate_disabled_platform_default_is_filtered_out() {
let messaging = MessagingConfig {
discord: None,
slack: None,
telegram: Some(TelegramConfig {
enabled: false,
token: "tok".into(),
instances: vec![],
dm_allowed_users: vec![],
}),
email: None,
webhook: None,
twitch: None,
signal: None,
};
let bindings = vec![Binding {
agent_id: "main".into(),
channel: "telegram".into(),
adapter: None,
guild_id: None,
workspace_id: None,
chat_id: None,
channel_ids: vec![],
require_mention: false,
dm_allowed_users: vec![],
}];
let result = validate_named_messaging_adapters(&messaging, bindings, false)
.expect("bindings should be resolvable");
assert!(
result.is_empty(),
"binding to disabled platform default should be skipped"
);
}

#[test]
Expand Down
15 changes: 12 additions & 3 deletions src/config/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,13 +905,22 @@ impl Config {

let toml_config: TomlConfig =
toml::from_str(content).context("failed to parse config TOML")?;
// Run full conversion to catch semantic errors (env resolution, defaults, etc.)
// Run full conversion with strict binding validation so config
// authoring catches unresolvable bindings as errors.
let instance_dir = Self::default_instance_dir();
Self::from_toml(toml_config, instance_dir)?;
Self::from_toml_inner(toml_config, instance_dir, true)?;
Ok(())
}

pub(super) fn from_toml(toml: TomlConfig, instance_dir: PathBuf) -> Result<Self> {
Self::from_toml_inner(toml, instance_dir, false)
}

fn from_toml_inner(
toml: TomlConfig,
instance_dir: PathBuf,
strict_bindings: bool,
) -> Result<Self> {
// Validate providers before processing
for (provider_id, config) in &toml.llm.providers {
// Validate provider_id
Expand Down Expand Up @@ -2245,7 +2254,7 @@ impl Config {
})
.collect();

validate_named_messaging_adapters(&messaging, &bindings)?;
let bindings = validate_named_messaging_adapters(&messaging, bindings, strict_bindings)?;

let api = ApiConfig {
enabled: toml.api.enabled,
Expand Down
Loading
Loading