mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
feat(desktop): show read-only MCP server config (#1780)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -198,7 +198,8 @@ const overrides = new Map([
|
||||
// loginHint fields on AcpRuntimeCatalogEntry (+14 lines). Load-bearing new feature.
|
||||
// agent-lifecycle-fixes: GlobalAgentConfigSaveResult type grows with
|
||||
// failed_restart_count (+2 lines). Queued to split with the rest of this list.
|
||||
["src/shared/api/types.ts", 1030],
|
||||
// mcp-readonly-view rebase: PR2 MCP config surface FE-type fields force +1 over the grandfathered ceiling.
|
||||
["src/shared/api/types.ts", 1031],
|
||||
// readiness-gate: PersonaDialog.tsx threads computeLocalModeGate +
|
||||
// requiredCredentialEnvKeys + RequiredFieldLabel so the "New agent" dialog
|
||||
// shows required markers and credential amber rows (parity with
|
||||
|
||||
@@ -122,7 +122,7 @@ fn toml_string(table: &toml::Table, key: &str) -> Option<String> {
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn codex_config_path() -> Option<std::path::PathBuf> {
|
||||
pub(crate) fn codex_config_path() -> Option<std::path::PathBuf> {
|
||||
if let Ok(home) = std::env::var("CODEX_HOME") {
|
||||
return Some(std::path::PathBuf::from(home).join("config.toml"));
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ fn mapping_string(map: &serde_yaml::Mapping, key: &str) -> Option<String> {
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn goose_config_path() -> Option<PathBuf> {
|
||||
pub(crate) fn goose_config_path() -> Option<PathBuf> {
|
||||
if let Ok(root) = std::env::var("GOOSE_PATH_ROOT") {
|
||||
return Some(PathBuf::from(root).join("config").join("config.yaml"));
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ pub(crate) fn read_config_surface(
|
||||
let config_file_path = runtime_meta
|
||||
.and_then(|m| m.config_file_path)
|
||||
.map(resolve_tilde);
|
||||
let mcp_config_file_path = runtime_meta.and_then(mcp_config_file_path_for_runtime);
|
||||
let extensions = file_config.extensions.clone();
|
||||
|
||||
let sources = ConfigSourceReport {
|
||||
acp_native: if supports_acp_native {
|
||||
@@ -197,6 +199,7 @@ pub(crate) fn read_config_surface(
|
||||
ConfigTierStatus::NotApplicable
|
||||
},
|
||||
config_file_path,
|
||||
mcp_config_file_path,
|
||||
};
|
||||
|
||||
RuntimeConfigSurface {
|
||||
@@ -205,10 +208,24 @@ pub(crate) fn read_config_surface(
|
||||
is_pre_spawn,
|
||||
normalized,
|
||||
advanced,
|
||||
extensions,
|
||||
sources,
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_config_file_path_for_runtime(runtime: &KnownAcpRuntime) -> Option<String> {
|
||||
match runtime.id {
|
||||
"goose" => {
|
||||
super::goose::goose_config_path().map(|path| path.to_string_lossy().into_owned())
|
||||
}
|
||||
"claude" => Some(resolve_tilde("~/.claude.json")),
|
||||
"codex" => {
|
||||
super::codex::codex_config_path().map(|path| path.to_string_lossy().into_owned())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_model_field(
|
||||
record_model: &Option<String>,
|
||||
|
||||
@@ -2,12 +2,31 @@
|
||||
//! `reader.rs` stays under the 1000-line budget; `#[path]`-included from
|
||||
//! there).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, path::Path, sync::Mutex};
|
||||
|
||||
use super::*;
|
||||
use crate::managed_agents::discovery::KnownAcpRuntime;
|
||||
use crate::managed_agents::types::ManagedAgentRecord;
|
||||
|
||||
static GOOSE_PATH_ROOT_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn with_goose_path_root<T>(value: Option<&str>, body: impl FnOnce() -> T) -> T {
|
||||
let _guard = GOOSE_PATH_ROOT_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|err| err.into_inner());
|
||||
let prior = std::env::var_os("GOOSE_PATH_ROOT");
|
||||
match value {
|
||||
Some(value) => std::env::set_var("GOOSE_PATH_ROOT", value),
|
||||
None => std::env::remove_var("GOOSE_PATH_ROOT"),
|
||||
}
|
||||
let output = body();
|
||||
match prior {
|
||||
Some(value) => std::env::set_var("GOOSE_PATH_ROOT", value),
|
||||
None => std::env::remove_var("GOOSE_PATH_ROOT"),
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn test_runtime() -> &'static KnownAcpRuntime {
|
||||
&KnownAcpRuntime {
|
||||
id: "goose",
|
||||
@@ -111,6 +130,68 @@ fn pre_spawn_surface_reports_pending_acp_tiers() {
|
||||
assert_eq!(surface.sources.env_vars, ConfigTierStatus::Available);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_reports_mcp_specific_config_path() {
|
||||
let record = test_record();
|
||||
let runtime = test_runtime();
|
||||
let surface = with_goose_path_root(None, || {
|
||||
read_config_surface(&record, Some(runtime), None, None)
|
||||
});
|
||||
|
||||
let path = surface
|
||||
.sources
|
||||
.mcp_config_file_path
|
||||
.expect("mcp config path");
|
||||
let expected_suffix = Path::new(".config").join("goose").join("config.yaml");
|
||||
assert!(
|
||||
Path::new(&path).ends_with(&expected_suffix),
|
||||
"unexpected MCP config path: {path}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goose_mcp_config_path_follows_path_root_override() {
|
||||
let record = test_record();
|
||||
let runtime = test_runtime();
|
||||
let surface = with_goose_path_root(Some("/tmp/buzz-goose-root"), || {
|
||||
read_config_surface(&record, Some(runtime), None, None)
|
||||
});
|
||||
|
||||
let expected_path = Path::new("/tmp/buzz-goose-root")
|
||||
.join("config")
|
||||
.join("config.yaml");
|
||||
assert_eq!(
|
||||
surface
|
||||
.sources
|
||||
.mcp_config_file_path
|
||||
.as_deref()
|
||||
.map(Path::new),
|
||||
Some(expected_path.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_surface_uses_mcp_config_path_not_settings_path() {
|
||||
let record = test_record();
|
||||
let runtime = &KnownAcpRuntime {
|
||||
id: "claude",
|
||||
config_file_path: Some("~/.claude/settings.json"),
|
||||
..*test_runtime()
|
||||
};
|
||||
let surface = read_config_surface(&record, Some(runtime), None, None);
|
||||
|
||||
assert!(surface
|
||||
.sources
|
||||
.config_file_path
|
||||
.as_deref()
|
||||
.is_some_and(|path| path.ends_with(".claude/settings.json")));
|
||||
assert!(surface
|
||||
.sources
|
||||
.mcp_config_file_path
|
||||
.as_deref()
|
||||
.is_some_and(|path| path.ends_with(".claude.json")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_model_overrides_file_model() {
|
||||
let mut record = test_record();
|
||||
|
||||
@@ -123,6 +123,7 @@ pub struct ConfigSourceReport {
|
||||
pub env_vars: ConfigTierStatus,
|
||||
pub config_file: ConfigTierStatus,
|
||||
pub config_file_path: Option<String>,
|
||||
pub mcp_config_file_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Full config surface returned to the frontend.
|
||||
@@ -134,6 +135,7 @@ pub struct RuntimeConfigSurface {
|
||||
pub is_pre_spawn: bool,
|
||||
pub normalized: NormalizedConfig,
|
||||
pub advanced: Vec<ConfigField>,
|
||||
pub extensions: Vec<ExtensionEntry>,
|
||||
pub sources: ConfigSourceReport,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useAgentConfigSurface } from "../hooks";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { copyTextToClipboard } from "@/shared/lib/clipboard";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { McpServersSection } from "./McpServersSection";
|
||||
import type {
|
||||
ConfigField,
|
||||
ConfigOrigin,
|
||||
@@ -365,7 +366,8 @@ export function AgentConfigPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const { normalized, advanced, sources, isPreSpawn } = data;
|
||||
const { normalized, advanced, extensions, runtimeId, sources, isPreSpawn } =
|
||||
data;
|
||||
const configFilePath = sources.configFilePath;
|
||||
|
||||
const normalizedEntries = (
|
||||
@@ -416,8 +418,17 @@ export function AgentConfigPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<McpServersSection
|
||||
extensions={extensions}
|
||||
runtimeId={runtimeId}
|
||||
variant={advancedMode === "flat" ? "profile" : "compact"}
|
||||
/>
|
||||
|
||||
{advanced.length > 0 && advancedMode === "flat" ? (
|
||||
<div className="divide-y divide-border/50 border-t border-border/50">
|
||||
<p className="px-4 py-3 text-xs font-medium text-foreground">
|
||||
Advanced
|
||||
</p>
|
||||
{advanced.map((field) => (
|
||||
<AdvancedRow
|
||||
key={field.key}
|
||||
|
||||
102
desktop/src/features/agents/ui/McpServersSection.tsx
Normal file
102
desktop/src/features/agents/ui/McpServersSection.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import type * as React from "react";
|
||||
import { CheckCircle2, CircleSlash } from "lucide-react";
|
||||
import type { ExtensionEntry } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
type McpServersSectionProps = {
|
||||
extensions: ExtensionEntry[];
|
||||
runtimeId: string | null;
|
||||
variant?: "compact" | "profile";
|
||||
buzzAgentSlot?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function McpServersSection({
|
||||
buzzAgentSlot,
|
||||
extensions,
|
||||
runtimeId,
|
||||
variant = "compact",
|
||||
}: McpServersSectionProps) {
|
||||
const isBuzzAgent = runtimeId === "buzz-agent";
|
||||
|
||||
if (!isBuzzAgent && extensions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-t border-border/50",
|
||||
variant === "compact" ? "mt-3 pt-2" : "divide-y divide-border/50",
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs font-medium text-foreground",
|
||||
variant === "compact" ? "py-2" : "px-4 py-3",
|
||||
)}
|
||||
>
|
||||
MCP Servers
|
||||
</p>
|
||||
|
||||
{isBuzzAgent && buzzAgentSlot ? buzzAgentSlot : null}
|
||||
|
||||
{extensions.length > 0 ? (
|
||||
<div className="divide-y divide-border/50">
|
||||
{extensions.map((extension) => (
|
||||
<McpServerRow
|
||||
extension={extension}
|
||||
key={`${extension.kind}:${extension.name}`}
|
||||
variant={variant}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground",
|
||||
variant === "compact" ? "py-2" : "px-4 py-3",
|
||||
)}
|
||||
>
|
||||
No custom servers configured
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function McpServerRow({
|
||||
extension,
|
||||
variant,
|
||||
}: {
|
||||
extension: ExtensionEntry;
|
||||
variant: "compact" | "profile";
|
||||
}) {
|
||||
const StatusIcon = extension.enabled ? CheckCircle2 : CircleSlash;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-3",
|
||||
variant === "compact" ? "py-2" : "px-4 py-3",
|
||||
)}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted/50">
|
||||
<StatusIcon
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
extension.enabled ? "text-emerald-600" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium text-foreground">
|
||||
{extension.name}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-2xs text-muted-foreground/70">
|
||||
{extension.kind}
|
||||
{extension.enabled ? " enabled" : " disabled"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -679,8 +679,11 @@ export type ConfigSourceReport = {
|
||||
envVars: ConfigTierStatus;
|
||||
configFile: ConfigTierStatus;
|
||||
configFilePath: string | null;
|
||||
mcpConfigFilePath: string | null;
|
||||
};
|
||||
|
||||
export type ExtensionEntry = { name: string; kind: string; enabled: boolean };
|
||||
|
||||
export type NormalizedConfig = {
|
||||
model: NormalizedField | null;
|
||||
provider: NormalizedField | null;
|
||||
@@ -697,6 +700,7 @@ export type RuntimeConfigSurface = {
|
||||
isPreSpawn: boolean;
|
||||
normalized: NormalizedConfig;
|
||||
advanced: ConfigField[];
|
||||
extensions: ExtensionEntry[];
|
||||
sources: ConfigSourceReport;
|
||||
};
|
||||
|
||||
|
||||
@@ -1265,6 +1265,7 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
isPreSpawn: boolean;
|
||||
normalized: Record<string, unknown>;
|
||||
advanced: unknown[];
|
||||
extensions: unknown[];
|
||||
sources: Record<string, unknown>;
|
||||
} {
|
||||
// Goose running — mixed origins, override on model
|
||||
@@ -1317,45 +1318,26 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
},
|
||||
advanced: [
|
||||
{
|
||||
key: "extensions.developer",
|
||||
label: "Extension: developer",
|
||||
value: "enabled",
|
||||
key: "active_provider",
|
||||
label: "active_provider",
|
||||
value: "openai",
|
||||
origin: "configFile",
|
||||
schemaType: { type: "enum", options: ["enabled", "disabled"] },
|
||||
writeVia: {
|
||||
type: "gooseNativeConfigWrite",
|
||||
configKey: "goose.extensions.developer",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "extensions.web_search",
|
||||
label: "Extension: web_search",
|
||||
value: "enabled",
|
||||
origin: "configFile",
|
||||
schemaType: { type: "enum", options: ["enabled", "disabled"] },
|
||||
writeVia: {
|
||||
type: "gooseNativeConfigWrite",
|
||||
configKey: "goose.extensions.web_search",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "extensions.memory",
|
||||
label: "Extension: memory",
|
||||
value: "disabled",
|
||||
origin: "configFile",
|
||||
schemaType: { type: "enum", options: ["enabled", "disabled"] },
|
||||
writeVia: {
|
||||
type: "gooseNativeConfigWrite",
|
||||
configKey: "goose.extensions.memory",
|
||||
},
|
||||
schemaType: { type: "string" },
|
||||
writeVia: { type: "readOnly" },
|
||||
},
|
||||
],
|
||||
extensions: [
|
||||
{ name: "developer", kind: "stdio", enabled: true },
|
||||
{ name: "web_search", kind: "stdio", enabled: true },
|
||||
{ name: "memory", kind: "stdio", enabled: false },
|
||||
],
|
||||
sources: {
|
||||
acpNative: "available",
|
||||
acpConfigOptions: "available",
|
||||
envVars: "available",
|
||||
configFile: "available",
|
||||
configFilePath: "~/.config/goose/config.yaml",
|
||||
mcpConfigFilePath: "~/.config/goose/config.yaml",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1409,12 +1391,17 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
systemPrompt: null,
|
||||
},
|
||||
advanced: [],
|
||||
extensions: [
|
||||
{ name: "filesystem", kind: "mcp", enabled: true },
|
||||
{ name: "github", kind: "mcp", enabled: true },
|
||||
],
|
||||
sources: {
|
||||
acpNative: "available",
|
||||
acpConfigOptions: "available",
|
||||
envVars: "notApplicable",
|
||||
configFile: "available",
|
||||
configFilePath: "~/.claude/settings.json",
|
||||
mcpConfigFilePath: "~/.claude.json",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1464,12 +1451,14 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
systemPrompt: null,
|
||||
},
|
||||
advanced: [],
|
||||
extensions: [{ name: "developer", kind: "stdio", enabled: true }],
|
||||
sources: {
|
||||
acpNative: "pending",
|
||||
acpConfigOptions: "pending",
|
||||
envVars: "available",
|
||||
configFile: "available",
|
||||
configFilePath: "~/.config/goose/config.yaml",
|
||||
mcpConfigFilePath: "~/.config/goose/config.yaml",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1538,12 +1527,17 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
writeVia: { type: "respawnWithEnvVar", envKey: "GOOSE_SANDBOX_MODE" },
|
||||
},
|
||||
],
|
||||
extensions: [
|
||||
{ name: "filesystem", kind: "mcp", enabled: true },
|
||||
{ name: "github", kind: "mcp", enabled: true },
|
||||
],
|
||||
sources: {
|
||||
acpNative: "notApplicable",
|
||||
acpConfigOptions: "notApplicable",
|
||||
envVars: "available",
|
||||
configFile: "available",
|
||||
configFilePath: "~/.codex/config.toml",
|
||||
mcpConfigFilePath: "~/.codex/config.toml",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1595,12 +1589,14 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
systemPrompt: null,
|
||||
},
|
||||
advanced: [],
|
||||
extensions: [{ name: "web_search", kind: "stdio", enabled: true }],
|
||||
sources: {
|
||||
acpNative: "available",
|
||||
acpConfigOptions: "available",
|
||||
envVars: "available",
|
||||
configFile: "available",
|
||||
configFilePath: "~/.config/goose/config.yaml",
|
||||
mcpConfigFilePath: "~/.config/goose/config.yaml",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1653,19 +1649,36 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
systemPrompt: null,
|
||||
},
|
||||
advanced: [],
|
||||
extensions: [],
|
||||
sources: {
|
||||
acpNative: "available",
|
||||
acpConfigOptions: "available",
|
||||
envVars: "available",
|
||||
configFile: "available",
|
||||
configFilePath: "~/.config/goose/config.yaml",
|
||||
mcpConfigFilePath: "~/.config/goose/config.yaml",
|
||||
},
|
||||
};
|
||||
|
||||
// Map well-known test pubkeys to specific fixtures
|
||||
// Synthetic agent for the multi-origin provenance showcase (not a TEST_IDENTITY).
|
||||
const buzzAgentSurface = {
|
||||
...gooseSurface,
|
||||
runtimeId: "buzz-agent",
|
||||
runtimeLabel: "Buzz Agent",
|
||||
advanced: [],
|
||||
extensions: [],
|
||||
sources: {
|
||||
...gooseSurface.sources,
|
||||
configFilePath: null,
|
||||
mcpConfigFilePath: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Map well-known test pubkeys to specific fixtures.
|
||||
// Synthetic agents are intentionally not TEST_IDENTITIES.
|
||||
const PUBKEY_MULTI_ORIGIN =
|
||||
"abc1230000000000000000000000000000000000000000000000000000000def";
|
||||
const PUBKEY_BUZZ_AGENT =
|
||||
"b0220000000000000000000000000000000000000000000000000000000000a9";
|
||||
|
||||
switch (pubkey) {
|
||||
case ALICE_PUBKEY:
|
||||
@@ -1678,6 +1691,8 @@ function buildMockConfigSurface(pubkey: string): {
|
||||
return runtimeOverrideSurface;
|
||||
case PUBKEY_MULTI_ORIGIN:
|
||||
return multiOriginSurface;
|
||||
case PUBKEY_BUZZ_AGENT:
|
||||
return buzzAgentSurface;
|
||||
default:
|
||||
return gooseSurface;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ const RUNTIME_OVERRIDE_PUBKEY = TEST_IDENTITIES.outsider.pubkey;
|
||||
// (matches PUBKEY_MULTI_ORIGIN in e2eBridge buildMockConfigSurface).
|
||||
const MULTI_ORIGIN_PUBKEY =
|
||||
"abc1230000000000000000000000000000000000000000000000000000000def";
|
||||
const BUZZ_AGENT_PUBKEY =
|
||||
"b0220000000000000000000000000000000000000000000000000000000000a9";
|
||||
|
||||
const MANAGED_AGENTS = [
|
||||
{
|
||||
@@ -243,14 +245,43 @@ test.describe("config bridge screenshots", () => {
|
||||
|
||||
const panel = await openAgentProfileFromChannel(page, "Goose Agent");
|
||||
|
||||
// Advanced runtime fields render directly in the profile panel's flat list.
|
||||
await expect(panel.getByText("Extension: developer")).toBeVisible();
|
||||
// Advanced runtime fields render directly in the profile panel's flat list,
|
||||
// grouped under their own "Advanced" header.
|
||||
await expect(panel.getByText("active_provider")).toBeVisible();
|
||||
await expect(panel.getByText("Advanced", { exact: true })).toHaveCount(1);
|
||||
// MCP servers render once each under their group label.
|
||||
await expect(panel.getByText("developer", { exact: true })).toHaveCount(1);
|
||||
await expect(panel.getByText("MCP Servers", { exact: true })).toHaveCount(
|
||||
1,
|
||||
);
|
||||
await settleAnimations(panel);
|
||||
|
||||
await panel.screenshot({ path: `${SHOTS}/05-advanced-expanded.png` });
|
||||
});
|
||||
|
||||
test("06 — profile side panel — Configuration section", async ({ page }) => {
|
||||
test("06 — buzz-agent empty MCP servers", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [
|
||||
{
|
||||
pubkey: BUZZ_AGENT_PUBKEY,
|
||||
name: "Buzz Agent",
|
||||
status: "running" as const,
|
||||
channelNames: ["agents"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const panel = await openAgentProfileFromChannel(page, "Buzz Agent");
|
||||
|
||||
await expect(
|
||||
panel.getByText("No custom servers configured", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(panel.getByText("MCP Servers", { exact: true })).toHaveCount(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test("07 — profile side panel — Configuration section", async ({ page }) => {
|
||||
// charlie (554cef…) is the well-known test pubkey that the mock bridge
|
||||
// seeds as a bot owned by the test viewer, so isBot + isOwner + managedAgent
|
||||
// are all true — the Configuration section renders in the profile panel.
|
||||
|
||||
Reference in New Issue
Block a user