diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs
index 7cb2d8e3b83..37d5dc5a198 100644
--- a/desktop/src-tauri/src/commands/mod.rs
+++ b/desktop/src-tauri/src/commands/mod.rs
@@ -120,6 +120,7 @@ pub use relay_members::*;
pub use relay_reconnect::*;
pub use social::*;
pub use team_snapshot::*;
+pub(crate) use teams::replay_pending_team_membership;
pub use teams::*;
pub use updater::*;
pub use window_chrome::*;
diff --git a/desktop/src-tauri/src/commands/personas/mod.rs b/desktop/src-tauri/src/commands/personas/mod.rs
index 81371e72ed0..f3f2da7c82d 100644
--- a/desktop/src-tauri/src/commands/personas/mod.rs
+++ b/desktop/src-tauri/src/commands/personas/mod.rs
@@ -4,7 +4,7 @@ use crate::{
app_state::AppState,
managed_agents::{
current_instance_id, delete_agent_key, load_managed_agents, load_personas, load_teams,
- save_managed_agents, save_personas, stop_managed_agent_process,
+ save_managed_agents, save_personas, source_team_exists, stop_managed_agent_process,
sync_managed_agent_processes, try_regenerate_nest, validate_persona_activation_change,
validate_persona_deletion, AgentDefinition, ManagedAgentRecord,
},
@@ -133,12 +133,14 @@ pub async fn delete_persona(id: String, app: AppHandle) -> Result<(), String> {
.iter()
.find(|record| record.id == id)
.ok_or_else(|| format!("persona {id} not found"))?;
- let referenced_by_team = load_teams(&app)?.iter().any(|team| {
+ let teams = load_teams(&app)?;
+ let referenced_by_team = teams.iter().any(|team| {
team.persona_ids
.iter()
.any(|persona_id| persona_id == id.as_str())
});
- validate_persona_deletion(persona, referenced_by_team)?;
+ let source_team_exists = source_team_exists(persona, &teams);
+ validate_persona_deletion(persona, referenced_by_team, source_team_exists)?;
// Capture the coordinate before the record might leave the list. Only
// reached for non-builtin, non-team personas (both rejected above),
// so every deleted persona here is one this owner published.
diff --git a/desktop/src-tauri/src/commands/team_snapshot.rs b/desktop/src-tauri/src/commands/team_snapshot.rs
index 26f6450c568..0742b7f087b 100644
--- a/desktop/src-tauri/src/commands/team_snapshot.rs
+++ b/desktop/src-tauri/src/commands/team_snapshot.rs
@@ -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.
@@ -269,6 +273,13 @@ struct MintedMember {
effective_avatar: Option,
}
+/// 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],
@@ -276,6 +287,10 @@ fn build_team_export_snapshot(
memory_level: MemoryLevel,
memory_entries_by_persona: &std::collections::HashMap>,
) -> Result {
+ if team.persona_ids.is_empty() {
+ return Err(EMPTY_TEAM_EXPORT_ERROR.to_string());
+ }
+
let members = team
.persona_ids
.iter()
diff --git a/desktop/src-tauri/src/commands/team_snapshot/tests.rs b/desktop/src-tauri/src/commands/team_snapshot/tests.rs
index b1c93a283ec..09ec4a6c57d 100644
--- a/desktop/src-tauri/src/commands/team_snapshot/tests.rs
+++ b/desktop/src-tauri/src/commands/team_snapshot/tests.rs
@@ -295,6 +295,45 @@ 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,
+ shared: false,
+ catalog_source: None,
+ 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");
diff --git a/desktop/src-tauri/src/commands/teams/mod.rs b/desktop/src-tauri/src/commands/teams/mod.rs
index 208ac3a7117..5d50a2fa329 100644
--- a/desktop/src-tauri/src/commands/teams/mod.rs
+++ b/desktop/src-tauri/src/commands/teams/mod.rs
@@ -1,3 +1,6 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use uuid::Uuid;
@@ -26,14 +29,222 @@ fn trim_optional(value: Option) -> Option {
})
}
+/// A staged team update. The record persists before the two stores change so a
+/// later save or launch can replay the original membership delta.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+struct PendingTeamMembershipUpdate {
+ team_id: String,
+ previous_persona_ids: Vec,
+ current_persona_ids: Vec,
+}
+
+fn pending_team_membership_path(app: &AppHandle) -> Result {
+ Ok(crate::managed_agents::managed_agents_base_dir(app)?.join("pending-team-membership.json"))
+}
+
+fn save_pending_team_membership_at(
+ path: &std::path::Path,
+ pending: Option<&PendingTeamMembershipUpdate>,
+) -> Result<(), String> {
+ let payload = serde_json::to_vec_pretty(&pending)
+ .map_err(|error| format!("failed to serialize pending team update: {error}"))?;
+ crate::managed_agents::storage::atomic_write_json(path, &payload)
+}
+
+fn load_pending_team_membership_at(
+ path: &std::path::Path,
+) -> Result
) : null}
+ {isEmptyTeam ? (
+
+ This team has no agents. Add one to deploy or share it, or
+ delete the team.
+
+ ) : null}
);
})}
diff --git a/desktop/src/features/agents/ui/teamDialogSelection.test.mjs b/desktop/src/features/agents/ui/teamDialogSelection.test.mjs
index 6beb71ae965..6515ec94dc0 100644
--- a/desktop/src/features/agents/ui/teamDialogSelection.test.mjs
+++ b/desktop/src/features/agents/ui/teamDialogSelection.test.mjs
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
+ canSubmitTeamDialog,
copySelectedPersonaIds,
countMissingPersonaIds,
filterAvailablePersonaIds,
@@ -90,3 +91,21 @@ test("orderPersonasByInitiallySelected keeps initially selected personas at top"
],
);
});
+
+// ── canSubmitTeamDialog ───────────────────────────────────────────────────
+//
+// A team with no members must be savable. If it is not, you cannot delete a
+// team, because you must first remove each member.
+
+test("canSubmitTeamDialog allows saving a team with an empty roster", () => {
+ assert.equal(canSubmitTeamDialog({ name: "Hive", isPending: false }), true);
+});
+
+test("canSubmitTeamDialog still requires a non-blank name", () => {
+ assert.equal(canSubmitTeamDialog({ name: "", isPending: false }), false);
+ assert.equal(canSubmitTeamDialog({ name: " ", isPending: false }), false);
+});
+
+test("canSubmitTeamDialog blocks while a save is in flight", () => {
+ assert.equal(canSubmitTeamDialog({ name: "Hive", isPending: true }), false);
+});
diff --git a/desktop/src/features/agents/ui/teamDialogSelection.ts b/desktop/src/features/agents/ui/teamDialogSelection.ts
index 74e9daa62a0..02e0417de74 100644
--- a/desktop/src/features/agents/ui/teamDialogSelection.ts
+++ b/desktop/src/features/agents/ui/teamDialogSelection.ts
@@ -8,6 +8,22 @@ export function copySelectedPersonaIds(personaIds: string[]): string[] {
return [...personaIds];
}
+/**
+ * Tells you if the submit button in the team dialog is enabled.
+ *
+ * A name is necessary. A member is not. A team with no members must be
+ * savable, because you must empty a team before you can delete it.
+ */
+export function canSubmitTeamDialog({
+ name,
+ isPending,
+}: {
+ name: string;
+ isPending: boolean;
+}): boolean {
+ return name.trim().length > 0 && !isPending;
+}
+
export function countMissingPersonaIds(
personaIds: string[],
personas: AgentPersona[],
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index bf16dd3b00a..ebe4fbee220 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -99,6 +99,8 @@ export type MockManagedAgentSeed = {
personaId?: string | null;
/** Harness/runtime id pin; `null` = inherit from persona (native default). */
runtime?: string | null;
+ /** Team binding. Seed it to reproduce the native delete guard in a test. */
+ teamId?: string | null;
status?: RawManagedAgent["status"];
channelNames?: string[];
channelIds?: string[];
@@ -906,6 +908,9 @@ type RawManagedAgent = {
persona_id: string | null;
/** Record-level harness/runtime pin (`null` when inheriting from the persona). */
runtime: string | null;
+ /** Team this instance belongs to (`null` when unbound). The native delete
+ * guard refuses a team while an agent still carries its id. */
+ team_id: string | null;
relay_url: string;
acp_command: string;
agent_command: string;
@@ -1765,6 +1770,7 @@ function cloneManagedAgent(agent: MockManagedAgent): RawManagedAgent {
name: agent.name,
persona_id: agent.persona_id,
runtime: agent.runtime ?? null,
+ team_id: agent.team_id ?? null,
relay_url: agent.relay_url,
acp_command: agent.acp_command,
agent_command: agent.agent_command,
@@ -2326,6 +2332,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent {
// Native serde always emits this key (`null` when unpinned) — the bridge
// must mirror the wire shape, not omit the key.
runtime: seed.runtime ?? null,
+ team_id: seed.teamId ?? null,
relay_url: DEFAULT_RELAY_WS_URL,
acp_command: "buzz-acp",
agent_command: agentCommand,
@@ -8876,21 +8883,55 @@ async function handleUpdateTeam(args: {
team.persona_ids = [...args.input.personaIds];
team.updated_at = new Date().toISOString();
+ // Mirror the native `propagate_membership`: an instance bound to this team
+ // whose persona left the roster loses the binding. Without this, the mock
+ // keeps the binding and no e2e can reach the delete guard below.
+ const now = team.updated_at;
+ for (const agent of mockManagedAgents) {
+ if (agent.team_id !== team.id) continue;
+ if (agent.persona_id && team.persona_ids.includes(agent.persona_id)) {
+ continue;
+ }
+ agent.team_id = null;
+ agent.updated_at = now;
+ }
+
return { ...team, persona_ids: [...team.persona_ids] };
}
+/** Mirrors the native `agents_referencing_team` guard: names of the managed
+ * agents that still carry this team's id. */
+function mockAgentsReferencingTeam(teamId: string): string[] {
+ return mockManagedAgents
+ .filter((agent) => agent.team_id === teamId)
+ .map((agent) => agent.name);
+}
+
async function handleDeleteTeam(args: { id: string }): Promise {
const team = mockTeams.find((candidate) => candidate.id === args.id);
if (team?.is_builtin) {
throw new Error("Built-in teams cannot be deleted.");
}
+ // Mirror `delete_team_with_cascade`: a bound agent blocks the delete. This is
+ // the contract the empty-team feature depends on, so the mock must hold it.
+ const referencing = mockAgentsReferencingTeam(args.id);
+ if (referencing.length > 0) {
+ throw new Error(
+ `Cannot delete team "${args.id}": ${referencing.length} agent(s) still reference it (${referencing.join(", ")}). Delete or reconfigure them first.`,
+ );
+ }
mockTeams = mockTeams.filter((candidate) => candidate.id !== args.id);
}
-async function handleExportTeamToJson(args: { id: string }): Promise {
- const team = mockTeams.find((candidate) => candidate.id === args.id);
+function getMockTeamForExport(id: string): RawTeam {
+ const team = mockTeams.find((candidate) => candidate.id === id);
if (!team) {
- throw new Error(`Team ${args.id} not found.`);
+ throw new Error(`Team ${id} not found.`);
+ }
+ if (team.persona_ids.length === 0) {
+ throw new Error(
+ "This team has no agents. Add at least one agent before you share or export it.",
+ );
}
const missingPersonaIds = team.persona_ids.filter(
@@ -8903,6 +8944,18 @@ async function handleExportTeamToJson(args: { id: string }): Promise {
);
}
+ return team;
+}
+
+async function handleExportTeamToJson(args: { id: string }): Promise {
+ getMockTeamForExport(args.id);
+ return true;
+}
+
+async function handleExportTeamSnapshot(args: {
+ id: string;
+}): Promise {
+ getMockTeamForExport(args.id);
return true;
}
@@ -8971,6 +9024,7 @@ async function handleCreateManagedAgent(
input: {
name: string;
personaId?: string;
+ teamId?: string | null;
relayUrl?: string;
acpCommand?: string;
agentCommand?: string;
@@ -9049,6 +9103,7 @@ async function handleCreateManagedAgent(
persona_id: args.input.personaId ?? null,
// Create never pins a harness id — the record inherits from the persona.
runtime: null,
+ team_id: args.input.teamId ?? null,
relay_url: args.input.relayUrl ?? DEFAULT_RELAY_WS_URL,
acp_command: args.input.acpCommand ?? "buzz-acp",
agent_command: agentCommand,
@@ -13055,9 +13110,16 @@ export function maybeInstallE2eTauriMocks() {
return importResult;
}
case "export_team_snapshot":
- // Mimics the save-to-disk path: report success without a real dialog.
- return true;
+ return handleExportTeamSnapshot(
+ payload as Parameters[0],
+ );
case "encode_team_snapshot_for_send": {
+ const input = payload as {
+ id: string;
+ memoryLevel: "none" | "core" | "everything";
+ format: "json" | "png";
+ };
+ getMockTeamForExport(input.id);
// Return a minimal PNG-shaped payload so the send flow can proceed
// through upload_media_bytes without a real Rust encode step.
const encodeDelayMs = activeConfig?.mock?.encodeDelayMs ?? 0;
diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts
index b81c5889f3a..97f0d1f4bb8 100644
--- a/desktop/tests/e2e/agents.spec.ts
+++ b/desktop/tests/e2e/agents.spec.ts
@@ -2708,3 +2708,104 @@ test("duplicate instances move from the agents gallery into the agent profile",
page.getByTestId(`user-profile-agent-delete-${additionalPubkey}`),
).toHaveCount(0);
});
+
+// You must be able to empty a team and then delete it. Before, the submit
+// button needed one member or more, so neither step was possible.
+//
+// The team here has a bound managed agent, so the test also crosses the native
+// delete guard: the delete must fail while the agent carries the team id, and it
+// must succeed after the save clears the binding.
+test("a team can be emptied and then deleted", async ({ page }) => {
+ await installMockBridge(page, {
+ personas: [
+ {
+ id: "custom:deadlock-a",
+ displayName: "Deadlock A",
+ systemPrompt: "First member of the team under test.",
+ },
+ {
+ id: "custom:deadlock-b",
+ displayName: "Deadlock B",
+ systemPrompt: "Second member of the team under test.",
+ },
+ ],
+ teams: [
+ {
+ id: "team-deadlock",
+ name: "Deadlock Team",
+ personaIds: ["custom:deadlock-a", "custom:deadlock-b"],
+ },
+ ],
+ managedAgents: [
+ {
+ pubkey: "de".repeat(32),
+ name: "Deadlock Instance",
+ personaId: "custom:deadlock-a",
+ teamId: "team-deadlock",
+ status: "stopped",
+ },
+ ],
+ });
+ await gotoApp(page);
+ await page.getByTestId("open-agents-view").click();
+
+ const teamCard = page.getByTestId("team-card-team-deadlock");
+ await expect(teamCard).toBeVisible();
+
+ // The starting state: the agent is bound, so the delete guard refuses.
+ const blocked = await invokeTauriExpectError(page, "delete_team", {
+ id: "team-deadlock",
+ });
+ expect(blocked).toContain("still reference it (Deadlock Instance)");
+
+ // Empty the roster via the edit dialog.
+ await page.getByLabel("Deadlock Team team actions").click();
+ await page.getByRole("menuitem", { name: "Edit" }).click();
+
+ const roster = page.getByRole("listbox", { name: "Agents" });
+ await roster.getByRole("option", { name: /Deadlock A/ }).click();
+ await roster.getByRole("option", { name: /Deadlock B/ }).click();
+
+ // This is the corrected behavior. No member is selected, and the save
+ // button must be enabled.
+ const save = page.getByRole("button", { name: "Save changes" });
+ await expect(save).toBeEnabled();
+ await save.click();
+
+ // The app asks what to do with the agents of the removed members. Keep them.
+ await page.getByRole("button", { name: "Keep agents" }).click();
+
+ // The team is still present, it has no members, and the card shows this.
+ await expect(teamCard).toContainText("This team has no agents");
+ const teams = await invokeTauri>(
+ page,
+ "list_teams",
+ );
+ expect(
+ teams.find((team) => team.id === "team-deadlock")?.persona_ids,
+ ).toEqual([]);
+
+ // The save also cleared the binding. This is what the delete guard reads, and
+ // it is the state that the save must guarantee before it reports success.
+ const agents = await invokeTauri<
+ Array<{ pubkey: string; team_id: string | null }>
+ >(page, "list_managed_agents");
+ expect(
+ agents.find((agent) => agent.pubkey === "de".repeat(32))?.team_id,
+ ).toBe(null);
+
+ // No agent points to the team now. Thus you can delete the team.
+ await page.getByLabel("Deadlock Team team actions").click();
+ await page.getByRole("menuitem", { name: "Delete" }).click();
+ await page
+ .getByRole("button", { name: "Delete", exact: true })
+ .last()
+ .click();
+
+ await expect(teamCard).toHaveCount(0);
+ const remaining = await invokeTauri>(
+ page,
+ "list_teams",
+ );
+ expect(remaining.some((team) => team.id === "team-deadlock")).toBe(false);
+});
diff --git a/desktop/tests/e2e/team-snapshot.spec.ts b/desktop/tests/e2e/team-snapshot.spec.ts
index 6246c7ac8c0..1dbda83c9a0 100644
--- a/desktop/tests/e2e/team-snapshot.spec.ts
+++ b/desktop/tests/e2e/team-snapshot.spec.ts
@@ -23,6 +23,35 @@ async function readCommandLog(page: import("@playwright/test").Page) {
});
}
+async function invokeTauriExpectError(
+ page: import("@playwright/test").Page,
+ command: string,
+ payload?: Record,
+) {
+ return page.evaluate(
+ async ({ targetCommand, targetPayload }) => {
+ const invoke = (
+ window as Window & {
+ __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
+ command: string,
+ payload?: Record,
+ ) => Promise;
+ }
+ ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
+ if (!invoke) {
+ throw new Error("Mock invoke bridge is unavailable.");
+ }
+ try {
+ await invoke(targetCommand, targetPayload);
+ return null;
+ } catch (error) {
+ return error instanceof Error ? error.message : String(error);
+ }
+ },
+ { targetCommand: command, targetPayload: payload },
+ );
+}
+
async function gotoAgentsPage(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByTestId("open-agents-view").click();
@@ -49,6 +78,40 @@ const ANALYST_PERSONA_ID = "test-analyst";
const ANALYST_PUBKEY =
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";
+// The share tests need a team with a member. The default mock team
+// "Engineering" has none, so Share is disabled for it. Use a different name,
+// or a locator matches both teams.
+const SHARE_TEAM_NAME = "Delivery Crew";
+const SHARE_TEAM_SEED = {
+ id: "team-share-001",
+ name: SHARE_TEAM_NAME,
+ description: "Team for the share tests",
+ personaIds: [ANALYST_PERSONA_ID],
+};
+
+const EMPTY_TEAM_EXPORT_ERROR =
+ "This team has no agents. Add at least one agent before you share or export it.";
+
+test("empty teams cannot export or share snapshots through the mock bridge", async ({
+ page,
+}) => {
+ await installMockBridge(page);
+ await gotoAgentsPage(page);
+
+ for (const command of [
+ "export_team_to_json",
+ "export_team_snapshot",
+ "encode_team_snapshot_for_send",
+ ]) {
+ const error = await invokeTauriExpectError(page, command, {
+ id: "team-engineering-001",
+ format: "png",
+ memoryLevel: "none",
+ });
+ expect(error).toBe(EMPTY_TEAM_EXPORT_ERROR);
+ }
+});
+
// ── (a) Confirm-fail + retry ────────────────────────────────────────────────
test("team_snapshot_import_confirm_fail_renders_error_and_retry_succeeds", async ({
@@ -251,16 +314,17 @@ test("team sharing uses the people picker and gates memory before sending", asyn
displayName: "Charlie",
},
],
+ teams: [SHARE_TEAM_SEED],
uploadDescriptors: [MOCK_UPLOAD_DESCRIPTOR],
});
await gotoAgentsPage(page);
- await page.getByLabel("Engineering team actions").click();
+ await page.getByLabel(`${SHARE_TEAM_NAME} team actions`).click();
await page.getByRole("menuitem", { name: "Share" }).click();
const shareDialog = page.getByTestId("team-share-dialog");
await expect(shareDialog).toBeVisible();
await expect(
- shareDialog.getByRole("heading", { name: "Share Engineering" }),
+ shareDialog.getByRole("heading", { name: `Share ${SHARE_TEAM_NAME}` }),
).toBeVisible();
const search = shareDialog.getByTestId("team-share-recipient-search");
@@ -289,9 +353,11 @@ test("team sharing uses the people picker and gates memory before sending", asyn
expect(encodeLevelsBeforeConfirmation).toEqual([]);
await memoryConfirmation.getByTestId("team-share-memory-confirm").click();
- await expect(page.getByText("Sent a copy of Engineering")).toBeVisible({
- timeout: 8_000,
- });
+ await expect(page.getByText(`Sent a copy of ${SHARE_TEAM_NAME}`)).toBeVisible(
+ {
+ timeout: 8_000,
+ },
+ );
const log = await readCommandLog(page);
expect(
@@ -307,7 +373,7 @@ test("team sharing uses the people picker and gates memory before sending", asyn
);
expect(sendEntry).toBeTruthy();
const sendPayload = sendEntry?.payload as { content?: string } | undefined;
- expect(sendPayload?.content).toContain("[Engineering](");
+ expect(sendPayload?.content).toContain(`[${SHARE_TEAM_NAME}](`);
expect(sendPayload?.content).not.toContain(";
});
@@ -332,11 +398,12 @@ test("team share level carries memories onto the link path too", async ({
},
],
agentMemory: createMockAgentMemoryListing(),
+ teams: [SHARE_TEAM_SEED],
uploadDescriptors: [MOCK_UPLOAD_DESCRIPTOR],
});
await gotoAgentsPage(page);
- await page.getByLabel("Engineering team actions").click();
+ await page.getByLabel(`${SHARE_TEAM_NAME} team actions`).click();
await page.getByRole("menuitem", { name: "Share" }).click();
const shareDialog = page.getByTestId("team-share-dialog");
await expect(shareDialog).toBeVisible();
@@ -397,12 +464,13 @@ test("team sharing keeps link copy and export in the shared surface", async ({
personaId: ANALYST_PERSONA_ID,
},
],
+ teams: [SHARE_TEAM_SEED],
uploadDescriptors: [MOCK_UPLOAD_DESCRIPTOR],
uploadDelayMs: 800,
});
await gotoAgentsPage(page);
- await page.getByLabel("Engineering team actions").click();
+ await page.getByLabel(`${SHARE_TEAM_NAME} team actions`).click();
const menu = page.getByRole("menu");
await expect(
menu.getByRole("menuitem", { name: "Export snapshot" }),
@@ -508,24 +576,24 @@ test("team sharing keeps link copy and export in the shared surface", async ({
const composerTeamCard = page.getByTestId("composer-team-snapshot-card");
await expect(composerTeamCard).toBeVisible();
- await expect(composerTeamCard).toContainText("Engineering");
+ await expect(composerTeamCard).toContainText(SHARE_TEAM_NAME);
await expect(composerTeamCard.locator("img")).toHaveCount(0);
await page.getByTestId("send-message").click();
const sentTeamCard = page.getByTestId("agent-snapshot-card").last();
await expect(sentTeamCard).toBeVisible();
- await expect(sentTeamCard).toContainText("Engineering");
+ await expect(sentTeamCard).toContainText(SHARE_TEAM_NAME);
await expect(sentTeamCard).toContainText("Add team");
await expect(sentTeamCard.locator("img")).toHaveCount(0);
await page.getByTestId("open-agents-view").click();
- await page.getByLabel("Engineering team actions").click();
+ await page.getByLabel(`${SHARE_TEAM_NAME} team actions`).click();
await page.getByRole("menuitem", { name: "Share" }).click();
await page.getByTestId("team-share-export").click();
const exportDialog = page.getByTestId("team-snapshot-export-dialog");
await expect(exportDialog).toBeVisible();
await expect(
- exportDialog.getByRole("heading", { name: "Export Engineering" }),
+ exportDialog.getByRole("heading", { name: `Export ${SHARE_TEAM_NAME}` }),
).toBeVisible();
await expect(
exportDialog.getByTestId("team-snapshot-memory-trigger"),