sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Package: dirigent_zed
Zed editor integration for Dirigent -- detection, agent discovery, binary resolution.
## Quick Facts
- **Type**: Library
- **Main Entry**: src/lib.rs
- **Dependencies**: dirigent_config, dirs, serde, serde_json, thiserror, tracing
- **Status**: Initial implementation
## Purpose
Detects Zed editor installations on the current system, discovers configured
ACP agents from Zed's `settings.json`, and resolves downloaded binary paths
from the Zed data directory.
## Key Types
- `ZedChannel` -- Release channel enum (Stable, Preview, Nightly, Dev)
- `ZedAgent` -- Discovered agent with name, type, binary path, env overrides
- `AgentServerType` -- Registry, Custom, or Extension
- `ZedInstallation` -- Detected installation with channel, paths, and agents
## Module Organization
- **`paths.rs`** -- Platform path resolution for Zed config/data directories
- **`agents.rs`** -- Agent discovery from settings.json, JSONC stripping, binary resolution
- **`detection.rs`** -- High-level installation detection combining paths and agents
## Platform Paths
| Platform | Config Dir | Data Dir |
|----------|-----------|----------|
| Linux | `$XDG_CONFIG_HOME/zed` | `$XDG_DATA_HOME/zed` |
| macOS | `~/.config/zed` | `~/Library/Application Support/Zed` |
| Windows | `%APPDATA%\Zed` | `%LOCALAPPDATA%\Zed` |
## Usage
```rust
let installations = dirigent_zed::detect_installations();
for inst in &installations {
for agent in &inst.agents {
if let Some(ref binary) = agent.binary_path {
println!("{}: {}", agent.name, binary.display());
}
}
}
```
## Testing
```bash
cargo test -p dirigent_zed
```
## Related Packages
- **dirigent_config** -- Dirigent's own path resolution (dependency)
- **dirigent_core** -- Will consume this crate for Zed connector integration (future)
## Research
See `docs/research/zed-integration.md` for detailed platform paths and detection strategies.
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "dirigent_zed"
version = "0.1.0"
edition = "2021"
description = "Zed editor integration for Dirigent — detection, agent discovery, binary resolution"
[dependencies]
dirigent_config = { path = "../dirigent_config" }
dirs = "5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
tracing = "0.1"
[dev-dependencies]
tempfile = "3"
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
//! Detection of Zed editor installations on the current system.
//!
//! Checks for the existence of Zed configuration directories and discovers
//! configured agents within each installation.
use crate::agents::{self, ZedAgent};
use crate::paths::{self, ZedChannel};
use std::path::PathBuf;
/// A detected Zed editor installation with its configuration and agents.
#[derive(Debug, Clone)]
pub struct ZedInstallation {
/// Release channel (Stable, Preview, Nightly, Dev).
pub channel: ZedChannel,
/// Path to the configuration directory (contains `settings.json`).
pub config_dir: PathBuf,
/// Path to the data directory (contains `external_agents/`).
pub data_dir: PathBuf,
/// Agents discovered from settings and resolved binary paths.
pub agents: Vec<ZedAgent>,
}
/// Detect Zed installations on this system.
///
/// Checks for the existence of `settings.json` in the Zed config directory.
/// Currently, Zed uses a single config directory for all channels (unlike
/// some editors that have per-channel directories). We report one installation
/// per detected config directory.
///
/// For each found installation:
/// 1. Discovers agents from `settings.json` (`agent_servers` key)
/// 2. Resolves downloaded binary paths from the data directory
pub fn detect_installations() -> Vec<ZedInstallation> {
let config_dir = match paths::zed_config_dir() {
Some(d) => d,
None => {
tracing::debug!("Could not determine Zed config directory for this platform");
return Vec::new();
}
};
let data_dir = match paths::zed_data_dir() {
Some(d) => d,
None => {
tracing::debug!("Could not determine Zed data directory for this platform");
return Vec::new();
}
};
let settings_path = config_dir.join("settings.json");
if !settings_path.exists() {
tracing::debug!(
path = %settings_path.display(),
"Zed settings.json not found — no installation detected"
);
return Vec::new();
}
tracing::info!(
config = %config_dir.display(),
data = %data_dir.display(),
"Detected Zed installation"
);
let mut found_agents = agents::discover_agents_from_settings(&config_dir);
// Also discover agents from external_agents/ that aren't in settings.
let extra_agents = agents::discover_agents_from_external_dir(&data_dir, &found_agents);
found_agents.extend(extra_agents);
agents::resolve_binary_paths(&mut found_agents, &data_dir);
// Enrich agents with registry metadata (display names, descriptions, args, icons).
let registry = crate::registry::parse_registry(&data_dir);
agents::enrich_agents_from_registry(&mut found_agents, &registry);
// Zed currently uses a single config dir for all channels. We report it
// as Stable by default. If we later learn how to distinguish channels
// (e.g., via a marker file or binary path), we can refine this.
vec![ZedInstallation {
channel: ZedChannel::Stable,
config_dir,
data_dir,
agents: found_agents,
}]
}
/// Detect installations using explicit paths (useful for testing or overrides).
pub fn detect_installation_at(config_dir: PathBuf, data_dir: PathBuf) -> Option<ZedInstallation> {
let settings_path = config_dir.join("settings.json");
if !settings_path.exists() {
return None;
}
let mut found_agents = agents::discover_agents_from_settings(&config_dir);
let extra_agents = agents::discover_agents_from_external_dir(&data_dir, &found_agents);
found_agents.extend(extra_agents);
agents::resolve_binary_paths(&mut found_agents, &data_dir);
let registry = crate::registry::parse_registry(&data_dir);
agents::enrich_agents_from_registry(&mut found_agents, &registry);
Some(ZedInstallation {
channel: ZedChannel::Stable,
config_dir,
data_dir,
agents: found_agents,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_installation_at_missing_settings() {
let dir = tempfile::tempdir().unwrap();
let result = detect_installation_at(dir.path().to_path_buf(), dir.path().to_path_buf());
assert!(result.is_none());
}
#[test]
fn test_detect_installation_at_with_settings() {
let config_dir = tempfile::tempdir().unwrap();
let data_dir = tempfile::tempdir().unwrap();
let settings = r#"{
"agent_servers": {
"claude-acp": { "type": "registry" }
}
}"#;
std::fs::write(config_dir.path().join("settings.json"), settings).unwrap();
let installation =
detect_installation_at(config_dir.path().to_path_buf(), data_dir.path().to_path_buf());
assert!(installation.is_some());
let inst = installation.unwrap();
assert_eq!(inst.channel, ZedChannel::Stable);
assert_eq!(inst.agents.len(), 1);
assert_eq!(inst.agents[0].name, "claude-acp");
}
#[test]
fn test_detect_installation_at_empty_settings() {
let config_dir = tempfile::tempdir().unwrap();
let data_dir = tempfile::tempdir().unwrap();
std::fs::write(config_dir.path().join("settings.json"), "{}").unwrap();
let installation =
detect_installation_at(config_dir.path().to_path_buf(), data_dir.path().to_path_buf());
assert!(installation.is_some());
let inst = installation.unwrap();
assert!(inst.agents.is_empty());
}
}
+31
View File
@@ -0,0 +1,31 @@
//! Zed editor integration for Dirigent.
//!
//! Provides detection of Zed installations, discovery of configured ACP agents,
//! and binary path resolution for agent servers managed by Zed.
//!
//! # Usage
//!
//! ```rust,no_run
//! use dirigent_zed::{detect_installations, ZedAgent, AgentServerType};
//!
//! let installations = detect_installations();
//! for inst in &installations {
//! println!("Zed {} at {}", inst.channel, inst.config_dir.display());
//! for agent in &inst.agents {
//! println!(" Agent: {} ({})", agent.name, agent.agent_type);
//! if let Some(ref path) = agent.binary_path {
//! println!(" Binary: {}", path.display());
//! }
//! }
//! }
//! ```
pub mod agents;
pub mod detection;
pub mod paths;
pub mod registry;
pub use agents::{AgentServerType, ZedAgent};
pub use detection::{detect_installation_at, detect_installations, ZedInstallation};
pub use paths::ZedChannel;
pub use registry::{parse_registry, find_registry_match, RegistryAgentInfo};
+147
View File
@@ -0,0 +1,147 @@
//! Platform path resolution for Zed editor directories.
//!
//! Resolves configuration and data directories for each Zed release channel
//! across Linux, macOS, and Windows.
use std::path::PathBuf;
/// Zed release channel. Each channel has independent config and data directories.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ZedChannel {
Stable,
Preview,
Nightly,
Dev,
}
impl ZedChannel {
/// All known release channels.
pub fn all() -> &'static [ZedChannel] {
&[
ZedChannel::Stable,
ZedChannel::Preview,
ZedChannel::Nightly,
ZedChannel::Dev,
]
}
/// Socket/identifier name used in IPC paths.
pub fn socket_name(&self) -> &'static str {
match self {
ZedChannel::Stable => "stable",
ZedChannel::Preview => "preview",
ZedChannel::Nightly => "nightly",
ZedChannel::Dev => "dev",
}
}
/// Display name for the channel.
pub fn display_name(&self) -> &'static str {
match self {
ZedChannel::Stable => "Stable",
ZedChannel::Preview => "Preview",
ZedChannel::Nightly => "Nightly",
ZedChannel::Dev => "Dev",
}
}
}
impl std::fmt::Display for ZedChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.display_name())
}
}
/// Resolve the Zed configuration directory for this platform.
///
/// | Platform | Path |
/// |----------|------|
/// | Linux | `$XDG_CONFIG_HOME/zed` (default: `~/.config/zed`) |
/// | macOS | `~/.config/zed` |
/// | Windows | `%APPDATA%\Zed` |
pub fn zed_config_dir() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
dirs::home_dir().map(|d| d.join(".config").join("zed"))
}
#[cfg(target_os = "windows")]
{
dirs::config_dir().map(|d| d.join("Zed"))
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
dirs::config_dir().map(|d| d.join("zed"))
}
}
/// Resolve the Zed data directory for this platform.
///
/// | Platform | Path |
/// |----------|------|
/// | Linux | `$XDG_DATA_HOME/zed` (default: `~/.local/share/zed`) |
/// | macOS | `~/Library/Application Support/Zed` |
/// | Windows | `%LOCALAPPDATA%\Zed` |
pub fn zed_data_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
dirs::data_local_dir().map(|d| d.join("Zed"))
}
#[cfg(not(target_os = "windows"))]
{
dirs::data_dir().map(|d| d.join("zed"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_channels() {
let channels = ZedChannel::all();
assert_eq!(channels.len(), 4);
assert!(channels.contains(&ZedChannel::Stable));
assert!(channels.contains(&ZedChannel::Preview));
assert!(channels.contains(&ZedChannel::Nightly));
assert!(channels.contains(&ZedChannel::Dev));
}
#[test]
fn test_socket_names() {
assert_eq!(ZedChannel::Stable.socket_name(), "stable");
assert_eq!(ZedChannel::Preview.socket_name(), "preview");
assert_eq!(ZedChannel::Nightly.socket_name(), "nightly");
assert_eq!(ZedChannel::Dev.socket_name(), "dev");
}
#[test]
fn test_display_names() {
assert_eq!(ZedChannel::Stable.to_string(), "Stable");
assert_eq!(ZedChannel::Dev.to_string(), "Dev");
}
#[test]
fn test_config_dir_is_some() {
// On any supported platform, this should resolve.
let dir = zed_config_dir();
assert!(dir.is_some(), "zed_config_dir should resolve on this platform");
let path = dir.unwrap();
let path_str = path.to_string_lossy();
assert!(
path_str.contains("zed") || path_str.contains("Zed"),
"config dir should contain 'zed': {path_str}"
);
}
#[test]
fn test_data_dir_is_some() {
let dir = zed_data_dir();
assert!(dir.is_some(), "zed_data_dir should resolve on this platform");
let path = dir.unwrap();
let path_str = path.to_string_lossy();
assert!(
path_str.contains("zed") || path_str.contains("Zed"),
"data dir should contain 'zed': {path_str}"
);
}
}
+546
View File
@@ -0,0 +1,546 @@
//! Registry metadata parsing for Zed ACP agents.
//!
//! Reads the local `registry.json` file that Zed caches from the ACP registry CDN.
//! Provides enrichment data (display names, descriptions, command args, icon paths)
//! for discovered agents.
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
/// Metadata about an agent from the ACP registry.
#[derive(Debug, Clone)]
pub struct RegistryAgentInfo {
/// Registry identifier (e.g. "claude-acp", "codex-acp").
pub id: String,
/// Human-friendly display name (e.g. "Claude Agent", "Codex CLI").
pub display_name: String,
/// Short description of the agent.
pub description: String,
/// Icon URL from the CDN.
pub icon_url: Option<String>,
/// Path to the locally cached icon file (SVG), if it exists.
pub icon_local_path: Option<PathBuf>,
/// Version string from the registry.
pub version: String,
/// Command arguments from the distribution config.
///
/// For npx-distributed agents this may include flags like `["--acp"]`.
/// For binary-distributed agents this is the platform-specific `cmd` value.
pub args: Vec<String>,
/// The command to run, extracted from the platform-appropriate distribution.
///
/// For binary distributions this is the `cmd` field (e.g. `"./codex-acp"`).
/// For npx distributions this is the npx package specifier.
pub command: Option<String>,
/// Environment variables from the distribution config.
pub env: HashMap<String, String>,
}
// ---------------------------------------------------------------------------
// Raw serde models for registry.json
// ---------------------------------------------------------------------------
#[derive(Debug, Deserialize)]
struct RegistryFile {
#[serde(default)]
agents: Vec<RawRegistryAgent>,
}
#[derive(Debug, Deserialize)]
struct RawRegistryAgent {
id: String,
name: String,
#[serde(default)]
version: String,
#[serde(default)]
description: String,
#[serde(default)]
icon: Option<String>,
#[serde(default)]
distribution: Option<RawDistribution>,
}
#[derive(Debug, Deserialize)]
struct RawDistribution {
#[serde(default)]
binary: Option<HashMap<String, RawBinaryTarget>>,
#[serde(default)]
npx: Option<RawNpxDistribution>,
#[serde(default)]
uvx: Option<RawUvxDistribution>,
}
#[derive(Debug, Deserialize)]
struct RawBinaryTarget {
#[serde(default)]
cmd: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RawNpxDistribution {
#[serde(default)]
package: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct RawUvxDistribution {
#[serde(default)]
package: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: HashMap<String, String>,
}
/// Determine the current platform key used in the registry's binary distribution map.
///
/// Returns keys like `"windows-x86_64"`, `"linux-aarch64"`, `"darwin-aarch64"`, etc.
fn current_platform_key() -> Option<&'static str> {
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
{
Some("windows-x86_64")
}
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
{
Some("windows-aarch64")
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
Some("darwin-aarch64")
}
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
{
Some("darwin-x86_64")
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
Some("linux-x86_64")
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
{
Some("linux-aarch64")
}
#[cfg(not(any(
all(target_os = "windows", target_arch = "x86_64"),
all(target_os = "windows", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
)))]
{
None
}
}
/// Parse the local Zed registry.json and return a map of agent id -> metadata.
///
/// The registry file lives at `{data_dir}/external_agents/registry/registry.json`.
/// Icons are cached at `{data_dir}/external_agents/registry/icons/{id}.svg`.
///
/// Returns an empty map if the file doesn't exist or can't be parsed.
pub fn parse_registry(data_dir: &Path) -> HashMap<String, RegistryAgentInfo> {
let registry_dir = data_dir.join("external_agents").join("registry");
let registry_path = registry_dir.join("registry.json");
let content = match std::fs::read_to_string(&registry_path) {
Ok(c) => c,
Err(e) => {
tracing::debug!(
path = %registry_path.display(),
error = %e,
"Could not read Zed registry.json"
);
return HashMap::new();
}
};
let registry: RegistryFile = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => {
tracing::warn!(
path = %registry_path.display(),
error = %e,
"Failed to parse Zed registry.json"
);
return HashMap::new();
}
};
let icons_dir = registry_dir.join("icons");
let platform_key = current_platform_key();
let mut map = HashMap::with_capacity(registry.agents.len());
for agent in registry.agents {
let icon_local_path = {
let candidate = icons_dir.join(format!("{}.svg", agent.id));
if candidate.exists() {
Some(candidate)
} else {
None
}
};
let (command, args, env) =
extract_distribution_info(agent.distribution.as_ref(), platform_key);
let info = RegistryAgentInfo {
id: agent.id.clone(),
display_name: agent.name,
description: agent.description,
icon_url: agent.icon,
icon_local_path,
version: agent.version,
args,
command,
env,
};
map.insert(agent.id, info);
}
tracing::debug!(
count = map.len(),
"Parsed Zed registry with {} agents",
map.len()
);
map
}
/// Extract command, args, and env from the distribution config.
///
/// Priority: binary (platform-specific) > npx > uvx.
fn extract_distribution_info(
distribution: Option<&RawDistribution>,
platform_key: Option<&str>,
) -> (Option<String>, Vec<String>, HashMap<String, String>) {
let dist = match distribution {
Some(d) => d,
None => return (None, Vec::new(), HashMap::new()),
};
// Prefer binary distribution for the current platform.
if let Some(ref binary) = dist.binary {
if let Some(key) = platform_key {
if let Some(target) = binary.get(key) {
let cmd = target.cmd.clone();
return (cmd, Vec::new(), HashMap::new());
}
}
}
// Fall back to npx distribution.
if let Some(ref npx) = dist.npx {
return (
npx.package.clone(),
npx.args.clone(),
npx.env.clone(),
);
}
// Fall back to uvx distribution.
if let Some(ref uvx) = dist.uvx {
return (
uvx.package.clone(),
uvx.args.clone(),
uvx.env.clone(),
);
}
(None, Vec::new(), HashMap::new())
}
/// Look up a registry entry by matching an agent name or directory name to a registry id.
///
/// The matching strategy:
/// 1. Exact match on registry id
/// 2. Substring: registry id contained in agent name, or vice versa
/// 3. Core-name match: strip common ACP suffixes and compare base names
/// (e.g. "claude-agent-acp" and "claude-acp" both have core name "claude")
pub fn find_registry_match<'a>(
agent_name: &str,
registry: &'a HashMap<String, RegistryAgentInfo>,
) -> Option<&'a RegistryAgentInfo> {
let name_lower = agent_name.to_lowercase();
// 1. Exact match on registry id.
if let Some(info) = registry.get(&name_lower) {
return Some(info);
}
// 2. Substring match: registry id contained in agent name, or vice versa.
let mut best: Option<&'a RegistryAgentInfo> = None;
let mut best_len = 0;
for (id, info) in registry {
let id_lower = id.to_lowercase();
if name_lower.contains(&id_lower) || id_lower.contains(&name_lower) {
if id_lower.len() > best_len {
best = Some(info);
best_len = id_lower.len();
}
}
}
if best.is_some() {
return best;
}
// 3. Core-name match: strip ACP-related suffixes and compare.
let agent_core = strip_acp_suffixes(&name_lower);
for (id, info) in registry {
let id_lower = id.to_lowercase();
let id_core = strip_acp_suffixes(&id_lower);
if !agent_core.is_empty() && agent_core == id_core {
return Some(info);
}
}
None
}
/// Strip common ACP-related suffixes to extract the core agent name.
///
/// For example:
/// - "claude-agent-acp" -> "claude"
/// - "claude-acp" -> "claude"
/// - "claude-code-acp" -> "claude"
/// - "codex" -> "codex"
pub fn strip_acp_suffixes(name: &str) -> &str {
// Strip known suffixes in order of specificity (longest first).
for suffix in &["-agent-acp", "-code-acp", "-acp", "-cli"] {
if let Some(core) = name.strip_suffix(suffix) {
return core;
}
}
name
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn sample_registry_json() -> &'static str {
r#"{
"version": "1.0.0",
"agents": [
{
"id": "claude-acp",
"name": "Claude Agent",
"version": "0.24.2",
"description": "ACP wrapper for Anthropic's Claude",
"icon": "https://cdn.agentclientprotocol.com/registry/v1/latest/claude-acp.svg",
"distribution": {
"npx": {
"package": "@agentclientprotocol/claude-agent-acp@0.24.2"
}
}
},
{
"id": "codex-acp",
"name": "Codex CLI",
"version": "0.10.0",
"description": "ACP adapter for OpenAI's coding assistant",
"icon": "https://cdn.agentclientprotocol.com/registry/v1/latest/codex-acp.svg",
"distribution": {
"binary": {
"linux-x86_64": {
"archive": "https://example.com/codex.tar.gz",
"cmd": "./codex-acp"
},
"windows-x86_64": {
"archive": "https://example.com/codex.zip",
"cmd": "./codex-acp.exe"
},
"darwin-aarch64": {
"archive": "https://example.com/codex-mac.tar.gz",
"cmd": "./codex-acp"
}
},
"npx": {
"package": "@zed-industries/codex-acp@0.10.0"
}
}
},
{
"id": "auggie",
"name": "Auggie CLI",
"version": "0.21.0",
"description": "Augment Code's powerful software agent",
"distribution": {
"npx": {
"package": "@augmentcode/auggie@0.21.0",
"args": ["--acp"],
"env": { "AUGMENT_DISABLE_AUTO_UPDATE": "1" }
}
}
}
]
}"#
}
#[test]
fn test_parse_registry_basic() {
let dir = tempfile::tempdir().unwrap();
let registry_dir = dir
.path()
.join("external_agents")
.join("registry");
std::fs::create_dir_all(&registry_dir).unwrap();
let mut f =
std::fs::File::create(registry_dir.join("registry.json")).unwrap();
f.write_all(sample_registry_json().as_bytes()).unwrap();
let map = parse_registry(dir.path());
assert_eq!(map.len(), 3);
let claude = map.get("claude-acp").unwrap();
assert_eq!(claude.display_name, "Claude Agent");
assert_eq!(claude.description, "ACP wrapper for Anthropic's Claude");
assert!(claude.icon_url.is_some());
// No local icon file created in test.
assert!(claude.icon_local_path.is_none());
let auggie = map.get("auggie").unwrap();
assert_eq!(auggie.args, vec!["--acp"]);
assert_eq!(
auggie.env.get("AUGMENT_DISABLE_AUTO_UPDATE"),
Some(&"1".to_string())
);
}
#[test]
fn test_parse_registry_with_local_icon() {
let dir = tempfile::tempdir().unwrap();
let registry_dir = dir
.path()
.join("external_agents")
.join("registry");
let icons_dir = registry_dir.join("icons");
std::fs::create_dir_all(&icons_dir).unwrap();
std::fs::File::create(registry_dir.join("registry.json"))
.unwrap()
.write_all(sample_registry_json().as_bytes())
.unwrap();
// Create a fake icon file.
std::fs::write(icons_dir.join("claude-acp.svg"), "<svg/>").unwrap();
let map = parse_registry(dir.path());
let claude = map.get("claude-acp").unwrap();
assert!(claude.icon_local_path.is_some());
assert!(claude
.icon_local_path
.as_ref()
.unwrap()
.to_string_lossy()
.contains("claude-acp.svg"));
}
#[test]
fn test_parse_registry_missing_file() {
let dir = tempfile::tempdir().unwrap();
let map = parse_registry(dir.path());
assert!(map.is_empty());
}
#[test]
fn test_parse_registry_invalid_json() {
let dir = tempfile::tempdir().unwrap();
let registry_dir = dir
.path()
.join("external_agents")
.join("registry");
std::fs::create_dir_all(&registry_dir).unwrap();
std::fs::write(registry_dir.join("registry.json"), "not json").unwrap();
let map = parse_registry(dir.path());
assert!(map.is_empty());
}
#[test]
fn test_find_registry_match_exact() {
let dir = tempfile::tempdir().unwrap();
let registry_dir = dir
.path()
.join("external_agents")
.join("registry");
std::fs::create_dir_all(&registry_dir).unwrap();
std::fs::File::create(registry_dir.join("registry.json"))
.unwrap()
.write_all(sample_registry_json().as_bytes())
.unwrap();
let map = parse_registry(dir.path());
// Exact match.
let info = find_registry_match("claude-acp", &map).unwrap();
assert_eq!(info.display_name, "Claude Agent");
}
#[test]
fn test_find_registry_match_fuzzy() {
let dir = tempfile::tempdir().unwrap();
let registry_dir = dir
.path()
.join("external_agents")
.join("registry");
std::fs::create_dir_all(&registry_dir).unwrap();
std::fs::File::create(registry_dir.join("registry.json"))
.unwrap()
.write_all(sample_registry_json().as_bytes())
.unwrap();
let map = parse_registry(dir.path());
// Agent name "claude" should fuzzy-match "claude-acp".
let info = find_registry_match("claude", &map).unwrap();
assert_eq!(info.display_name, "Claude Agent");
// Directory name "claude-agent-acp" should fuzzy-match "claude-acp".
let info2 = find_registry_match("claude-agent-acp", &map).unwrap();
assert_eq!(info2.display_name, "Claude Agent");
}
#[test]
fn test_find_registry_match_no_match() {
let map = HashMap::new();
assert!(find_registry_match("nonexistent", &map).is_none());
}
#[test]
fn test_binary_distribution_platform_cmd() {
let dir = tempfile::tempdir().unwrap();
let registry_dir = dir
.path()
.join("external_agents")
.join("registry");
std::fs::create_dir_all(&registry_dir).unwrap();
std::fs::File::create(registry_dir.join("registry.json"))
.unwrap()
.write_all(sample_registry_json().as_bytes())
.unwrap();
let map = parse_registry(dir.path());
let codex = map.get("codex-acp").unwrap();
// On any supported platform, the binary distribution should produce a command.
// The exact value depends on the compile target.
if current_platform_key().is_some() {
assert!(
codex.command.is_some(),
"codex-acp should have a command from binary distribution"
);
}
}
}