Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
15 changes: 15 additions & 0 deletions desktop/src-tauri/src/commands/team_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const PNG_MAGIC: [u8; 4] = [0x89, 0x50, 0x4e, 0x47];
const ZIP_MAGIC_PREFIX: [u8; 2] = [0x50, 0x4b];
const LEGACY_TEAM_ERROR: &str =
"Legacy team files are no longer supported. Export a buzz-team-snapshot v1 .team.json or .team.png instead.";
/// Refusal for an export of a team that has no members. The importer rejects
/// such a snapshot, so the producer must not write one.
pub(crate) const EMPTY_TEAM_EXPORT_ERROR: &str =
"This team has no agents. Add at least one agent before you share or export it.";

/// Decode a canonical team snapshot, rejecting retired flat team JSON and
/// persona-pack ZIP files with a migration-oriented error.
Expand Down Expand Up @@ -263,13 +267,24 @@ struct MintedMember {
effective_avatar: Option<String>,
}

/// Builds the snapshot for an export.
///
/// A team with no members must not become a snapshot. The importer rejects such
/// an artifact, because `validate_team_snapshot` needs one member or more. Both
/// export commands come through this function, so the rejection belongs here.
/// A disabled menu item is not sufficient: the commands stay callable, and a
/// share dialog can submit after the roster becomes empty.
fn build_team_export_snapshot(
team: &TeamRecord,
personas: &[AgentDefinition],
records: &[ManagedAgentRecord],
memory_level: MemoryLevel,
memory_entries_by_persona: &std::collections::HashMap<String, Vec<AgentSnapshotMemoryEntry>>,
) -> Result<TeamSnapshot, String> {
if team.persona_ids.is_empty() {
return Err(EMPTY_TEAM_EXPORT_ERROR.to_string());
}

let members = team
.persona_ids
.iter()
Expand Down
37 changes: 37 additions & 0 deletions desktop/src-tauri/src/commands/team_snapshot/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,43 @@ fn team_export_with_instance_and_memory_level_uses_supplied_entries() {
assert!(snap_no_instance.members[0].memory.entries.is_empty());
}

/// The producer must refuse a team that has no members.
///
/// The importer rejects such a snapshot, so an export of it makes a file that
/// nobody can import. `managed_agents::team_snapshot` pins that rejection in
/// `validate_rejects_zero_members`. A disabled menu item does not stop the
/// export: both export commands stay callable, and they read the team from disk
/// at the time of the call. This test holds the guard in the producer, where
/// both commands pass.
#[test]
fn empty_team_export_is_refused_before_any_bytes() {
let team = TeamRecord {
id: "empty".to_string(),
name: "Emptied Team".to_string(),
description: None,
instructions: None,
persona_ids: vec![],
is_builtin: false,
source_dir: None,
is_symlink: false,
symlink_target: None,
version: None,
created_at: "now".to_string(),
updated_at: "now".to_string(),
};

let err = build_team_export_snapshot(
&team,
&[],
&[],
MemoryLevel::None,
&std::collections::HashMap::new(),
)
.expect_err("an export of a team with no members must fail");

assert_eq!(err, EMPTY_TEAM_EXPORT_ERROR);
}

#[test]
fn team_import_definitions_are_built_for_all_members() {
let mut memory_bearing = member("Alice");
Expand Down
Loading
Loading