mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add explicit AvatarState serde round-trip and creation-invariant tests
Adds focused tests proving the AvatarState back-compat guarantee directly,
the load-bearing safety net flagged in review:
- legacy bare string -> Set(url)
- JSON null -> Unmigrated
- absent field (via #[serde(default)]) -> Unmigrated
- Cleared -> serialize to {"cleared":true} -> deserialize -> Cleared
(forward-only sentinel round-trip; the case guarding a future refactor
from silently breaking the cleared-avatar marker)
- Set/Unmigrated round-trip
Also adds the creation invariant: resolve_created_avatar_url with no input,
no persona, and an unresolvable command yields Unmigrated, never Cleared --
Cleared must be reachable only via an explicit user clear through the update
path. Bumps the types.rs file-size override 1100 -> 1180 for the added tests.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
co-authored by
Taylor Ho
parent
a95c13a01a
commit
622c826734
@@ -44,7 +44,7 @@ const overrides = new Map([
|
||||
// Option<String> avatar field — a load-bearing state-model fix that closes
|
||||
// the clear-vs-backfill ambiguity, not generic debt growth. Approved
|
||||
// override; still queued to split with the rest of this list.
|
||||
["src-tauri/src/managed_agents/types.rs", 1100],
|
||||
["src-tauri/src/managed_agents/types.rs", 1180],
|
||||
["src-tauri/src/managed_agents/personas.rs", 1080],
|
||||
["src-tauri/src/managed_agents/persona_card.rs", 1050],
|
||||
// applyWorkspace reposDir parameter threaded through the Tauri invoke for
|
||||
|
||||
@@ -170,3 +170,17 @@ fn legacy_avatar_empty_when_nothing_resolves() {
|
||||
|
||||
assert!(resolved.is_empty());
|
||||
}
|
||||
|
||||
/// Invariant: no creation/constructor path can ever produce `AvatarState::Cleared`.
|
||||
/// `Cleared` must be reachable ONLY through an explicit user clear via the update
|
||||
/// path — if creation could mint it, the mutual-exclusivity guarantee leaks and a
|
||||
/// brand-new avatarless agent would be wrongly excluded from relay/persona backfill.
|
||||
/// With no input, no persona, and an unknown command (so the command fallback
|
||||
/// yields nothing), the result must be `Unmigrated`, never `Cleared`.
|
||||
#[test]
|
||||
fn created_avatar_never_cleared_without_explicit_clear() {
|
||||
let resolved = resolve_created_avatar_url(None, None, "totally-unknown-command");
|
||||
|
||||
assert_eq!(resolved, AvatarState::Unmigrated);
|
||||
assert_ne!(resolved, AvatarState::Cleared);
|
||||
}
|
||||
|
||||
@@ -834,6 +834,84 @@ mod tests {
|
||||
assert_eq!(record.auth_tag, record2.auth_tag);
|
||||
}
|
||||
|
||||
// ── AvatarState serde round-trip tests ───────────────────────────────
|
||||
// These exercise all four on-disk cases directly on `AvatarState`, which
|
||||
// is the load-bearing back-compat guarantee: every legacy record must land
|
||||
// on a sane state, and the `Cleared` sentinel must round-trip forever.
|
||||
|
||||
/// A legacy bare-string avatar deserializes as an explicit `Set(url)`.
|
||||
#[test]
|
||||
fn avatar_state_legacy_string_deserializes_as_set() {
|
||||
let state: AvatarState =
|
||||
serde_json::from_str(r#""https://x/avatar.png""#).expect("string should deserialize");
|
||||
assert_eq!(state, AvatarState::Set("https://x/avatar.png".to_string()));
|
||||
}
|
||||
|
||||
/// JSON `null` maps to `Unmigrated`, preserving the legacy backfill path.
|
||||
#[test]
|
||||
fn avatar_state_null_deserializes_as_unmigrated() {
|
||||
let state: AvatarState = serde_json::from_str("null").expect("null should deserialize");
|
||||
assert_eq!(state, AvatarState::Unmigrated);
|
||||
}
|
||||
|
||||
/// An absent field (via `#[serde(default)]` on the struct) yields
|
||||
/// `Unmigrated` — same as `null`. We assert this through a record whose
|
||||
/// `avatar_url` key is omitted entirely.
|
||||
#[test]
|
||||
fn avatar_state_absent_field_defaults_to_unmigrated() {
|
||||
let record: ManagedAgentRecord = serde_json::from_str(
|
||||
r#"{
|
||||
"pubkey": "abcd1234",
|
||||
"name": "test-agent",
|
||||
"private_key_nsec": "nsec1fake",
|
||||
"relay_url": "wss://localhost:3000",
|
||||
"acp_command": "buzz-acp",
|
||||
"agent_command": "goose",
|
||||
"agent_args": [],
|
||||
"mcp_command": "",
|
||||
"turn_timeout_seconds": 320,
|
||||
"system_prompt": null,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z",
|
||||
"last_started_at": null,
|
||||
"last_stopped_at": null,
|
||||
"last_exit_code": null,
|
||||
"last_error": null
|
||||
}"#,
|
||||
)
|
||||
.expect("record with absent avatar_url should deserialize");
|
||||
assert_eq!(record.avatar_url, AvatarState::Unmigrated);
|
||||
}
|
||||
|
||||
/// The forward-only case that matters most: `Cleared` serializes to the
|
||||
/// self-describing `{"cleared":true}` sentinel and deserializes back to
|
||||
/// `Cleared`. This is the only thing standing between a future refactor and
|
||||
/// silently breaking the cleared-avatar sentinel.
|
||||
#[test]
|
||||
fn avatar_state_cleared_round_trips_via_sentinel() {
|
||||
let serialized =
|
||||
serde_json::to_string(&AvatarState::Cleared).expect("Cleared should serialize");
|
||||
assert_eq!(serialized, r#"{"cleared":true}"#);
|
||||
|
||||
let back: AvatarState =
|
||||
serde_json::from_str(&serialized).expect("sentinel should deserialize");
|
||||
assert_eq!(back, AvatarState::Cleared);
|
||||
}
|
||||
|
||||
/// `Set` and `Unmigrated` also round-trip cleanly through serialize +
|
||||
/// deserialize, closing the loop on every variant.
|
||||
#[test]
|
||||
fn avatar_state_set_and_unmigrated_round_trip() {
|
||||
for state in [
|
||||
AvatarState::Set("https://x/a.png".to_string()),
|
||||
AvatarState::Unmigrated,
|
||||
] {
|
||||
let serialized = serde_json::to_string(&state).expect("should serialize");
|
||||
let back: AvatarState = serde_json::from_str(&serialized).expect("should round-trip");
|
||||
assert_eq!(back, state);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inbound author gate tests ────────────────────────────────────────
|
||||
|
||||
use super::{validate_respond_to_allowlist, RespondTo};
|
||||
|
||||
Reference in New Issue
Block a user