#![cfg(feature = "client")]
use chrono::{TimeZone, Utc};
use vta_sdk::client::{AclEntryResponse, AclListResponse, ContextResponse, RenameKeyResponse};
use vta_sdk::protocols::acl_management::entry::AclEntry;
use vta_sdk::protocols::acl_management::list::ListAclResultBody;
#[track_caller]
fn agent_to_client<S, C>(surface: &str, agent_body: &S) -> C
where
S: serde::Serialize,
C: serde::de::DeserializeOwned,
{
let wire = serde_json::to_string(agent_body).expect("agent body must serialize");
match serde_json::from_str::<C>(&wire) {
Ok(decoded) => decoded,
Err(e) => panic!(
"{surface} is BROKEN against a current agent.\n\
\n agent emitted: {wire}\n client decode: {e}\n\
\n\
The client's decode type disagrees with the body the agent \
serializes. Fix the type in vta-sdk/src/client/types.rs — best by \
re-exporting the agent's body (#1035) rather than mirroring it. Do \
not change this test.",
surface = surface,
),
}
}
fn ts() -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 8, 19, 9, 0, 0).unwrap()
}
fn agent_acl_entry() -> AclEntry {
let mut entry = AclEntry::new(
"did:key:z6MkSubject".into(),
"admin".into(),
vec!["personal".into()],
);
entry.allowed_keys = Some(vec![]);
entry.label = Some("laptop".into());
entry.created_at = Some(ts());
entry.created_by = Some("did:key:z6MkAdmin".into());
entry.updated_at = Some(ts());
entry.updated_by = Some("did:key:z6MkAdmin".into());
entry.expires_at = Some(ts());
entry
}
#[test]
fn acl_entry_decodes() {
let got: AclEntryResponse =
agent_to_client("pnm acl grant / show / update", &agent_acl_entry());
assert_eq!(got.did, "did:key:z6MkSubject", "subject renames to did");
assert_eq!(
got.allowed_contexts,
vec!["personal".to_string()],
"scopes renames to allowed_contexts"
);
assert_eq!(
got.allowed_keys,
Some(vec![]),
"Some(empty) is the narrowest grant and must not decode as None"
);
assert_eq!(got.label.as_deref(), Some("laptop"));
assert_ne!(got.created_at, 0, "RFC 3339 must convert to epoch seconds");
assert!(got.expires_at.is_some());
}
#[test]
fn acl_list_decodes_including_its_elements() {
let agent = ListAclResultBody {
entries: vec![agent_acl_entry()],
truncated: false,
cursor: None,
redacted_fields: vec![],
};
let got: AclListResponse = agent_to_client("pnm acl list", &agent);
assert_eq!(got.entries.len(), 1, "the element must survive the decode");
assert_eq!(got.entries[0].did, "did:key:z6MkSubject");
assert_ne!(got.entries[0].created_at, 0);
}
#[test]
fn a_legacy_agent_snake_case_response_still_decodes() {
let legacy = serde_json::json!({
"id": "personal",
"name": "Personal",
"did": null,
"description": null,
"base_path": "m/26'/2'/0'",
"created_at": "2026-08-19T09:00:00Z",
"updated_at": "2026-08-19T09:00:00Z"
});
let got: ContextResponse = serde_json::from_value(legacy)
.expect("a pre-#1000 agent's snake_case response must still decode");
assert_eq!(got.base_path, "m/26'/2'/0'");
assert_eq!(got.created_at, ts());
let legacy_key = serde_json::json!({ "key_id": "key-1", "updated_at": "2026-08-19T09:00:00Z" });
let got: RenameKeyResponse =
serde_json::from_value(legacy_key).expect("legacy key body must still decode");
assert_eq!(got.key_id, "key-1");
}
#[test]
fn same_type_sites_cannot_drift() {
fn assert_same_type(
_: vta_sdk::protocols::context_management::create::CreateContextResultBody,
) {
}
let _: fn(ContextResponse) = assert_same_type;
}