use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ToolsetRuntimeError {
#[error("toolset.unknown_action: action '{action}' is not in the capability→tool matrix")]
UnknownToolsetAction {
action: String,
},
#[error(
"toolset.capability_not_declared: action '{action}' requires capability \
'{capability}' which this toolset did not declare"
)]
CapabilityNotDeclared {
action: String,
capability: String,
},
#[error(
"toolset.tool_not_allowed: tool '{tool}' is not in this toolset's allowed_tools list \
(action '{action}')"
)]
ToolNotAllowed {
tool: String,
action: String,
},
#[error("toolset.not_installed: toolset '{name}' is not installed")]
ToolsetNotInstalled {
name: String,
},
#[error("toolset.io_error: {0}")]
Io(String),
#[error(
"toolset.content_digest_mismatch: TOOLSET.md for '{name}' has been modified since install \
(dispatch-time content digest check failed; reinstall the toolset to resolve)"
)]
ContentDigestMismatch {
name: String,
},
#[error(
"toolset.first_invoke_approval_required: first-invoke gate requires operator approval \
(approval_nonce={approval_nonce}; run `stellar-agent approve --id {approval_nonce}` \
then re-invoke)"
)]
FirstInvokeApprovalRequired {
approval_nonce: String,
toolset_name: String,
capability: String,
},
#[error("toolset.grant_store_error: {detail}")]
GrantStoreError {
detail: String,
},
#[error(
"toolset.invalid_authoritative_amount: authoritative_amount_stroops must be > 0 \
(got {amount_stroops})"
)]
InvalidAuthoritativeAmount {
amount_stroops: i64,
},
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test-only; panics acceptable in unit tests"
)]
mod tests {
use super::*;
#[test]
fn all_variants_have_distinct_display() {
let variants = [
ToolsetRuntimeError::UnknownToolsetAction {
action: "test-action".to_owned(),
},
ToolsetRuntimeError::CapabilityNotDeclared {
action: "test-action".to_owned(),
capability: "read-balance".to_owned(),
},
ToolsetRuntimeError::ToolNotAllowed {
tool: "stellar_balances".to_owned(),
action: "test-action".to_owned(),
},
ToolsetRuntimeError::ToolsetNotInstalled {
name: "test-toolset".to_owned(),
},
ToolsetRuntimeError::Io("disk error".to_owned()),
ToolsetRuntimeError::ContentDigestMismatch {
name: "test-toolset".to_owned(),
},
ToolsetRuntimeError::FirstInvokeApprovalRequired {
approval_nonce: "AbCdEfGhIjKlMnOpQrStUv".to_owned(),
toolset_name: "test-toolset".to_owned(),
capability: "sign-payment".to_owned(),
},
ToolsetRuntimeError::GrantStoreError {
detail: "test error".to_owned(),
},
ToolsetRuntimeError::InvalidAuthoritativeAmount { amount_stroops: 0 },
];
let displays: Vec<String> = variants.iter().map(|v| v.to_string()).collect();
let unique: std::collections::HashSet<&str> = displays.iter().map(String::as_str).collect();
assert_eq!(
unique.len(),
variants.len(),
"variant Display strings must all be distinct (closed-set parity): {displays:?}"
);
}
#[test]
fn error_code_prefixes_present() {
let e = ToolsetRuntimeError::UnknownToolsetAction {
action: "x".to_owned(),
};
assert!(e.to_string().contains("toolset.unknown_action"));
let e = ToolsetRuntimeError::CapabilityNotDeclared {
action: "x".to_owned(),
capability: "y".to_owned(),
};
assert!(e.to_string().contains("toolset.capability_not_declared"));
let e = ToolsetRuntimeError::ToolNotAllowed {
tool: "t".to_owned(),
action: "x".to_owned(),
};
assert!(e.to_string().contains("toolset.tool_not_allowed"));
let e = ToolsetRuntimeError::ToolsetNotInstalled {
name: "s".to_owned(),
};
assert!(e.to_string().contains("toolset.not_installed"));
let e = ToolsetRuntimeError::Io("err".to_owned());
assert!(e.to_string().contains("toolset.io_error"));
let e = ToolsetRuntimeError::ContentDigestMismatch {
name: "my-distinct-toolset".to_owned(),
};
assert!(e.to_string().contains("toolset.content_digest_mismatch"));
assert!(
e.to_string().contains("my-distinct-toolset"),
"toolset name must appear in message"
);
let e = ToolsetRuntimeError::FirstInvokeApprovalRequired {
approval_nonce: "AbCdEfGhIjKlMnOpQrStUv".to_owned(),
toolset_name: "s".to_owned(),
capability: "sign-payment".to_owned(),
};
assert!(
e.to_string()
.contains("toolset.first_invoke_approval_required")
);
assert!(
e.to_string().contains("AbCdEfGhIjKlMnOpQrStUv"),
"approval_nonce must appear in the error message for agent recovery"
);
let e = ToolsetRuntimeError::GrantStoreError {
detail: "test error".to_owned(),
};
assert!(e.to_string().contains("toolset.grant_store_error"));
let e = ToolsetRuntimeError::InvalidAuthoritativeAmount { amount_stroops: -1 };
assert!(
e.to_string()
.contains("toolset.invalid_authoritative_amount")
);
assert!(
e.to_string().contains("-1"),
"rejected amount must appear in error message"
);
}
}