Add Devin as a preset ACP harness (#3225)

## Summary

- Add Devin to the built-in preset harness catalog using the official
native ACP invocation: `devin acp`.
- Link setup guidance to Cognition's official Devin CLI documentation.
- Render a bundled, attributed Devin mark on a white canvas through
Buzz's existing runtime-icon system.
- Keep preset capability metadata in the Rust catalog; no duplicate
TypeScript runtime table or React runtime checks.
- Move the existing preset catalog and its focused tests into a Rust
submodule without changing existing preset behavior, keeping the touched
files within the repository's file-size limit.

### Related issue

Follow-up to the generic BYOH harness work in #2773.

### Scope

This is the small preset/data-entry follow-up described in the #2773
discussion. It uses the generic preset readiness contract and does not
add Devin-specific authentication probing, permission bypasses, model
switching, cloud handoff, or cloud Devin capability claims.

The preset supplies:

- ID: `devin`
- Executable: `devin`
- Arguments: `acp`
- Installation guidance: https://docs.devin.ai/cli

### Testing

Local verification was rerun at the final PR head,
`7bb9aa6e862a47a5062b5b8234fdb5ce2aae6c1d`.

- Focused Rust preset tests: 7 passed
- Desktop JavaScript tests: 3,768 passed
- Desktop lint, formatting, file-size, and text guards: passed
- Full Tauri test suite: 1,851 passed, 14 ignored
- Root Rust unit-test groups: passed
- Web production build: passed
- Mobile format, analyze, and test suites: passed
- Full repository `just ci`: passed

The branch also merges cleanly with the current Block `main`. The
upstream fork-triggered CI workflow is awaiting maintainer approval;
DCO, Semgrep OSS, and zizmor are passing.

The bundled SVG was rendered and visually inspected in both its source
dimensions and a 512px preview. The cross-language preset-logo guard
verifies that the Devin mapping exists and the asset is present on disk.

Signed-off-by: Mark Fenner <markfenner57@yahoo.com>
This commit is contained in:
fenner888
2026-07-30 16:58:28 -04:00
committed by GitHub
parent 4d47aa8345
commit 1b3ff96a57
7 changed files with 351 additions and 310 deletions
+1
View File
@@ -9,6 +9,7 @@ license permits redistribution.
| File | Upstream | Commit | License | Source path | Modifications |
|---|---|---|---|---|---|
| `devin.svg` | [Cognition Devin documentation](https://docs.devin.ai/cli) | Retrieved 2026-07-27 | Cognition trademark; nominative use to identify the Devin harness | Official documentation `logo/favicon.svg` | Added the official black mark to a white square canvas so it remains legible in both app themes |
| `hermes.png` | [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) | `6ad632b` | MIT © 2025 Nous Research | `website/static/img/logo.png` | Cropped the baked-in border frame, padded to square, resized to 64×64, quantised to a 16-colour palette |
| `openclaw.svg` | [openclaw/openclaw](https://github.com/openclaw/openclaw) | `b06f40a` | MIT © 2026 OpenClaw Foundation | `ui/public/favicon.svg` | Removed the SMIL animation elements (renders the upstream rest pose statically — verified pixel-identical to the upstream frame at t=0); minified paths |
| `omp.svg` | [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) | `667111575ebba136dadfd6989379e7f67e0d40d9` | MIT © 2025 Mario Zechner; © 2025–2026 Can Bölük | `assets/icon.svg` | None |
+4
View File
@@ -0,0 +1,4 @@
<svg width="425" height="425" viewBox="0 0 425 425" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="425" height="425" fill="white"/>
<path d="M70 159.333V91.3471C70 88.3592 71.594 85.5983 74.1816 84.1044L133.043 50.1205C135.631 48.6265 138.819 48.6265 141.407 50.1205L200.269 84.1044C202.856 85.5983 204.45 88.3592 204.45 91.3471V126.068C204.708 137.606 210.806 148.734 221.531 154.926C232.256 161.117 244.942 160.834 255.063 155.289L285.132 137.929C287.719 136.435 290.907 136.435 293.495 137.929L352.357 171.913C354.944 173.406 356.538 176.167 356.538 179.155V247.123C356.538 250.111 354.944 252.872 352.357 254.366L293.495 288.35C290.907 289.844 287.719 289.844 285.132 288.35L255.306 271.13C245.146 265.456 232.344 265.117 221.534 271.358C210.809 277.55 204.711 288.678 204.453 300.215V334.926C204.453 337.914 202.859 340.675 200.271 342.169L141.41 376.153C138.822 377.647 135.634 377.647 133.046 376.153L74.1845 342.169C71.5969 340.675 70.0028 337.914 70.0028 334.926V266.959C70.0029 263.971 71.5969 261.21 74.1845 259.716L133.046 225.732C135.634 224.238 138.822 224.238 141.41 225.732L171.547 243.132C181.656 248.638 194.306 248.906 205.005 242.729C215.815 236.488 221.922 225.231 222.088 213.595C221.83 202.057 215.732 189.737 205.008 183.545C194.283 177.353 181.597 177.636 171.476 183.181L141.269 200.72C138.67 202.229 135.461 202.228 132.864 200.716L74.1576 166.562C71.5835 165.065 70 162.311 70 159.333Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -10,8 +10,11 @@ use crate::managed_agents::{
HarnessSource,
};
mod presets;
mod runtime_metadata;
use presets::{preset_catalog_entry, PRESET_HARNESSES};
pub(crate) use presets::{preset_harness_definitions, preset_harness_ids};
pub(crate) use runtime_metadata::KnownAcpRuntime;
const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
@@ -1436,225 +1439,6 @@ pub(crate) fn discover_acp_runtime_availability(runtime_id: &str) -> Option<AcpA
.map(|partial| partial.entry.availability)
}
// ── Tier-2 preset harnesses ────────────────────────────────────────────────
//
// Static data for well-known ACP harnesses that have bundled logos and
// verified command/args. PATH-probed at discovery time (Detected badge);
// not editable or deletable by users. Logos are bundled assets referenced
// by id in the frontend `RUNTIME_LOGOS` map.
struct PresetHarness {
id: &'static str,
label: &'static str,
command: &'static str,
args: &'static [&'static str],
install_instructions_url: &'static str,
install_hint: &'static str,
/// Vendor CLI the ACP command wraps, when the preset is an adapter
/// (e.g. Amp's `amp-acp` wraps the separately-installed `amp` CLI).
/// Consulted only when the adapter is absent, so `AdapterMissing`
/// replaces the misleading `NotInstalled` when the CLI is present but
/// the adapter is not. Deliberately NOT fed through the builtins'
/// full `classify_runtime` predicate: that would flip
/// adapter-present/CLI-absent from today's `Available` to `CliMissing`
/// (unselectable), and presets carry a single flat `install_hint`, so
/// the `CliMissing` copy would tell the user to install the adapter
/// they already have. `None` when the command IS the vendor CLI.
underlying_cli: Option<&'static str>,
}
/// Build the catalog entry for one preset harness through an injectable
/// resolver — the seam the preset loop consumes and tests bind.
///
/// Availability consumes only the adapter-missing arm of the builtin
/// predicate: adapter presence alone decides `Available` (exactly today's
/// behavior — an `amp-acp` without `amp` stays selectable), and
/// `underlying_cli` is consulted only when the adapter is absent, to
/// distinguish `AdapterMissing` (vendor CLI present) from `NotInstalled`
/// (neither found). See the `underlying_cli` field doc for why the full
/// `classify_runtime` predicate is deliberately not used here.
fn preset_catalog_entry(
def: &PresetHarness,
resolve: impl Fn(&str) -> Option<PathBuf>,
) -> AcpRuntimeCatalogEntry {
let (availability, command, binary_path) = match resolve(def.command) {
Some(path) => (
AcpAvailabilityStatus::Available,
Some(def.command.to_string()),
Some(path.display().to_string()),
),
None => {
let underlying_cli_found = def
.underlying_cli
.map(|cli| resolve(cli).is_some())
.unwrap_or(false);
if underlying_cli_found {
(AcpAvailabilityStatus::AdapterMissing, None, None)
} else {
(AcpAvailabilityStatus::NotInstalled, None, None)
}
}
};
let underlying_cli_path = def
.underlying_cli
.and_then(resolve)
.map(|p| p.display().to_string());
let default_args = normalize_agent_args(
def.command,
def.args.iter().map(|s| s.to_string()).collect(),
);
AcpRuntimeCatalogEntry {
id: def.id.to_string(),
label: def.label.to_string(),
// No remote URL — all preset icons are bundled assets.
avatar_url: String::new(),
availability,
command,
binary_path,
default_args,
mcp_command: None,
model_env_var: None,
provider_env_var: None,
thinking_env_var: None,
install_hint: def.install_hint.to_string(),
install_instructions_url: def.install_instructions_url.to_string(),
can_auto_install: false,
// Kept false even for adapter presets: presets carry one flat
// install_hint (the adapter's), so the requiresExternalCli
// "CLI is missing" wording would pair the wrong noun with it.
// The builtin path, with per-availability hints, is the only
// consumer of the true case.
requires_external_cli: false,
underlying_cli_path,
node_required: false,
auth_status: AuthStatus::NotApplicable,
login_hint: None,
source: HarnessSource::Preset,
// Preset entries have static, non-editable env; definition_env is empty.
definition_env: Default::default(),
}
}
const PRESET_HARNESSES: &[PresetHarness] = &[
PresetHarness {
id: "cursor",
label: "Cursor",
command: "cursor-agent",
args: &["acp"],
install_instructions_url: "https://cursor.com/downloads",
install_hint: "Buzz talks to Cursor through the cursor-agent CLI's ACP mode.",
underlying_cli: None,
},
PresetHarness {
id: "omp",
label: "Oh My Pi",
command: "omp",
args: &["acp"],
install_instructions_url: "https://github.com/can1357/oh-my-pi",
install_hint: "Buzz talks to Oh My Pi through its CLI's ACP mode (omp acp).",
underlying_cli: None,
},
PresetHarness {
id: "grok",
label: "Grok Build",
command: "grok",
args: &["agent", "--always-approve", "stdio"],
install_instructions_url: "https://build.x.ai/docs",
install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.",
underlying_cli: None,
},
PresetHarness {
id: "opencode",
label: "OpenCode",
command: "opencode",
args: &["acp"],
install_instructions_url: "https://opencode.ai/docs",
install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).",
underlying_cli: None,
},
PresetHarness {
id: "kimi",
label: "Kimi Code",
command: "kimi",
args: &["acp"],
install_instructions_url: "https://kimi.ai/download",
install_hint: "Buzz talks to Kimi Code through its CLI's ACP mode (kimi acp).",
underlying_cli: None,
},
PresetHarness {
id: "amp",
label: "Amp",
command: "amp-acp",
args: &[],
install_instructions_url: "https://github.com/tao12345666333/amp-acp",
install_hint: "Buzz talks to the Amp CLI through the amp-acp adapter. Follow the setup guide to install the adapter so the amp-acp command is on your PATH.",
underlying_cli: Some("amp"),
},
PresetHarness {
id: "hermes",
label: "Hermes Agent",
command: "hermes-acp",
args: &[],
install_instructions_url: "https://hermes-agent.nousresearch.com",
install_hint: "Buzz talks to Hermes Agent through its hermes-acp command.",
underlying_cli: None,
},
PresetHarness {
id: "openclaw",
label: "OpenClaw",
command: "openclaw",
args: &["acp"],
install_instructions_url: "https://docs.openclaw.ai/start/getting-started",
install_hint: "Buzz talks to OpenClaw through its ACP mode (openclaw acp), which relies on the OpenClaw Gateway daemon. Follow the setup guide to install both.\n\n\
⚠️ Execution-locus note: `openclaw acp` runs tools inside the \
OpenClaw Gateway daemon, not in the Desktop process. \
Desktop-injected BUZZ_* env vars are visible to the `openclaw` \
harness process itself, but do NOT automatically reach the \
Gateway's execution environment. If your tools or agent logic \
needs BUZZ_* credentials at execution time, set them on the \
Gateway's own environment separately.",
underlying_cli: None,
},
];
/// Return the static preset harness definitions as `HarnessDefinition` values.
///
/// Used by `warm_harness_registry_from_dir` to seed the loaded-harness registry
/// at startup before the frontend triggers a full discovery run.
pub(crate) fn preset_harness_definitions(
) -> Vec<crate::managed_agents::custom_harnesses::HarnessDefinition> {
PRESET_HARNESSES
.iter()
.map(
|p| crate::managed_agents::custom_harnesses::HarnessDefinition {
id: p.id.to_string(),
label: p.label.to_string(),
command: p.command.to_string(),
args: p.args.iter().map(|s| s.to_string()).collect(),
env: std::collections::BTreeMap::new(),
install_instructions_url: p.install_instructions_url.to_string(),
install_hint: p.install_hint.to_string(),
},
)
.collect()
}
/// Return the static slice of preset harness IDs.
///
/// Used by `check_id_collision` in `custom_harnesses` to derive the reserved-ID
/// set from the single source of truth (`PRESET_HARNESSES`) rather than a
/// hand-maintained copy. Adding a preset automatically reserves its ID.
pub(crate) fn preset_harness_ids() -> &'static [&'static str] {
// `PRESET_HARNESSES` is `'static`; we project its `id` fields.
// Computed once via OnceLock to avoid repeated allocations on hot paths.
use std::sync::OnceLock;
static IDS: OnceLock<Vec<&'static str>> = OnceLock::new();
IDS.get_or_init(|| PRESET_HARNESSES.iter().map(|p| p.id).collect())
.as_slice()
}
/// Discover all ACP runtimes, optionally merging user-defined custom harnesses
/// from `custom_harnesses_dir`.
///
@@ -0,0 +1,335 @@
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::managed_agents::{
AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, HarnessSource,
};
use super::normalize_agent_args;
/// Static data for a well-known tier-2 ACP harness.
pub(super) struct PresetHarness {
pub(super) id: &'static str,
label: &'static str,
command: &'static str,
args: &'static [&'static str],
install_instructions_url: &'static str,
install_hint: &'static str,
/// Vendor CLI the ACP command wraps, when the preset is an adapter.
///
/// Consulted only when the adapter is absent, so `AdapterMissing` replaces
/// `NotInstalled` when the CLI is present but the adapter is not. `None`
/// when the command is itself the vendor CLI.
underlying_cli: Option<&'static str>,
}
/// Build one preset catalog entry through an injectable command resolver.
pub(super) fn preset_catalog_entry(
def: &PresetHarness,
resolve: impl Fn(&str) -> Option<PathBuf>,
) -> AcpRuntimeCatalogEntry {
let (availability, command, binary_path) = match resolve(def.command) {
Some(path) => (
AcpAvailabilityStatus::Available,
Some(def.command.to_string()),
Some(path.display().to_string()),
),
None => {
let underlying_cli_found = def
.underlying_cli
.map(|cli| resolve(cli).is_some())
.unwrap_or(false);
if underlying_cli_found {
(AcpAvailabilityStatus::AdapterMissing, None, None)
} else {
(AcpAvailabilityStatus::NotInstalled, None, None)
}
}
};
let underlying_cli_path = def
.underlying_cli
.and_then(resolve)
.map(|path| path.display().to_string());
AcpRuntimeCatalogEntry {
id: def.id.to_string(),
label: def.label.to_string(),
// No remote URL — all preset icons are bundled assets.
avatar_url: String::new(),
availability,
command,
binary_path,
default_args: normalize_agent_args(
def.command,
def.args.iter().map(|arg| arg.to_string()).collect(),
),
mcp_command: None,
model_env_var: None,
provider_env_var: None,
thinking_env_var: None,
install_hint: def.install_hint.to_string(),
install_instructions_url: def.install_instructions_url.to_string(),
can_auto_install: false,
// Presets carry one flat install hint, so builtin external-CLI copy
// would name the wrong missing component for adapter presets.
requires_external_cli: false,
underlying_cli_path,
node_required: false,
auth_status: AuthStatus::NotApplicable,
login_hint: None,
source: HarnessSource::Preset,
definition_env: Default::default(),
}
}
pub(super) const PRESET_HARNESSES: &[PresetHarness] = &[
PresetHarness {
id: "devin",
label: "Devin",
command: "devin",
args: &["acp"],
install_instructions_url: "https://docs.devin.ai/cli",
install_hint: "Buzz talks to Devin through the official Devin CLI's ACP mode (devin acp).",
underlying_cli: None,
},
PresetHarness {
id: "cursor",
label: "Cursor",
command: "cursor-agent",
args: &["acp"],
install_instructions_url: "https://cursor.com/downloads",
install_hint: "Buzz talks to Cursor through the cursor-agent CLI's ACP mode.",
underlying_cli: None,
},
PresetHarness {
id: "omp",
label: "Oh My Pi",
command: "omp",
args: &["acp"],
install_instructions_url: "https://github.com/can1357/oh-my-pi",
install_hint: "Buzz talks to Oh My Pi through its CLI's ACP mode (omp acp).",
underlying_cli: None,
},
PresetHarness {
id: "grok",
label: "Grok Build",
command: "grok",
args: &["agent", "--always-approve", "stdio"],
install_instructions_url: "https://build.x.ai/docs",
install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.",
underlying_cli: None,
},
PresetHarness {
id: "opencode",
label: "OpenCode",
command: "opencode",
args: &["acp"],
install_instructions_url: "https://opencode.ai/docs",
install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).",
underlying_cli: None,
},
PresetHarness {
id: "kimi",
label: "Kimi Code",
command: "kimi",
args: &["acp"],
install_instructions_url: "https://kimi.ai/download",
install_hint: "Buzz talks to Kimi Code through its CLI's ACP mode (kimi acp).",
underlying_cli: None,
},
PresetHarness {
id: "amp",
label: "Amp",
command: "amp-acp",
args: &[],
install_instructions_url: "https://github.com/tao12345666333/amp-acp",
install_hint: "Buzz talks to the Amp CLI through the amp-acp adapter. Follow the setup guide to install the adapter so the amp-acp command is on your PATH.",
underlying_cli: Some("amp"),
},
PresetHarness {
id: "hermes",
label: "Hermes Agent",
command: "hermes-acp",
args: &[],
install_instructions_url: "https://hermes-agent.nousresearch.com",
install_hint: "Buzz talks to Hermes Agent through its hermes-acp command.",
underlying_cli: None,
},
PresetHarness {
id: "openclaw",
label: "OpenClaw",
command: "openclaw",
args: &["acp"],
install_instructions_url: "https://docs.openclaw.ai/start/getting-started",
install_hint: "Buzz talks to OpenClaw through its ACP mode (openclaw acp), which relies on the OpenClaw Gateway daemon. Follow the setup guide to install both.\n\n\
⚠️ Execution-locus note: `openclaw acp` runs tools inside the \
OpenClaw Gateway daemon, not in the Desktop process. \
Desktop-injected BUZZ_* env vars are visible to the `openclaw` \
harness process itself, but do NOT automatically reach the \
Gateway's execution environment. If your tools or agent logic \
needs BUZZ_* credentials at execution time, set them on the \
Gateway's own environment separately.",
underlying_cli: None,
},
];
/// Return preset definitions for the spawn/readiness registry.
pub(crate) fn preset_harness_definitions(
) -> Vec<crate::managed_agents::custom_harnesses::HarnessDefinition> {
PRESET_HARNESSES
.iter()
.map(
|preset| crate::managed_agents::custom_harnesses::HarnessDefinition {
id: preset.id.to_string(),
label: preset.label.to_string(),
command: preset.command.to_string(),
args: preset.args.iter().map(|arg| arg.to_string()).collect(),
env: Default::default(),
install_instructions_url: preset.install_instructions_url.to_string(),
install_hint: preset.install_hint.to_string(),
},
)
.collect()
}
/// Return preset IDs from the catalog's single source of truth.
pub(crate) fn preset_harness_ids() -> &'static [&'static str] {
static IDS: OnceLock<Vec<&'static str>> = OnceLock::new();
IDS.get_or_init(|| PRESET_HARNESSES.iter().map(|preset| preset.id).collect())
.as_slice()
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::managed_agents::{AcpAvailabilityStatus, AuthStatus, HarnessSource};
use super::{preset_catalog_entry, PresetHarness, PRESET_HARNESSES};
/// Amp-shaped preset: an ACP adapter wrapping a separately installed CLI.
const ADAPTER_PRESET: PresetHarness = PresetHarness {
id: "amp-test",
label: "Amp Test",
command: "amp-acp",
args: &[],
install_instructions_url: "https://example.com/install",
install_hint: "Install the amp-acp npm adapter.",
underlying_cli: Some("amp"),
};
#[test]
fn devin_preset_uses_official_native_acp_invocation() {
let preset = PRESET_HARNESSES
.iter()
.find(|preset| preset.id == "devin")
.expect("Devin preset should be present");
assert_eq!(preset.label, "Devin");
assert_eq!(preset.command, "devin");
assert_eq!(preset.args, &["acp"]);
assert_eq!(preset.underlying_cli, None);
assert_eq!(preset.install_instructions_url, "https://docs.devin.ai/cli");
let entry = preset_catalog_entry(preset, |command| {
(command == "devin").then(|| PathBuf::from("/usr/local/bin/devin"))
});
assert_eq!(entry.availability, AcpAvailabilityStatus::Available);
assert_eq!(entry.command.as_deref(), Some("devin"));
assert_eq!(entry.default_args, vec!["acp"]);
assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/devin"));
assert_eq!(entry.auth_status, AuthStatus::NotApplicable);
assert_eq!(entry.source, HarnessSource::Preset);
let missing_entry = preset_catalog_entry(preset, |_| None);
assert_eq!(
missing_entry.availability,
AcpAvailabilityStatus::NotInstalled
);
assert!(missing_entry.command.is_none());
assert_eq!(missing_entry.default_args, vec!["acp"]);
}
#[test]
fn devin_preset_is_exposed_in_the_runtime_catalog() {
use crate::managed_agents::custom_harnesses::registry_test_lock;
// Discovery touches process-global command-resolution and the loaded
// harness registry. Serialize with the other discovery tests.
let _path_guard = crate::managed_agents::lock_path_mutex();
let _registry_guard = registry_test_lock();
let entry = super::super::discover_acp_runtimes_from(None)
.into_iter()
.find(|entry| entry.id == "devin")
.expect("Devin preset should appear in the runtime catalog");
assert_eq!(entry.label, "Devin");
assert_eq!(entry.default_args, vec!["acp"]);
assert_eq!(entry.install_instructions_url, "https://docs.devin.ai/cli");
assert_eq!(entry.source, HarnessSource::Preset);
}
#[test]
fn adapter_missing_when_underlying_cli_present() {
let entry = preset_catalog_entry(&ADAPTER_PRESET, |command| {
(command == "amp").then(|| PathBuf::from("/usr/local/bin/amp"))
});
assert_eq!(entry.availability, AcpAvailabilityStatus::AdapterMissing);
assert!(entry.command.is_none());
assert!(entry.binary_path.is_none());
assert_eq!(
entry.underlying_cli_path.as_deref(),
Some("/usr/local/bin/amp")
);
assert!(!entry.requires_external_cli);
assert_eq!(entry.install_hint, "Install the amp-acp npm adapter.");
}
#[test]
fn not_installed_when_adapter_and_cli_are_missing() {
let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None);
assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled);
assert!(entry.underlying_cli_path.is_none());
assert!(!entry.requires_external_cli);
}
#[test]
fn available_when_adapter_and_cli_are_present() {
let entry = preset_catalog_entry(&ADAPTER_PRESET, |command| match command {
"amp-acp" => Some(PathBuf::from("/usr/local/bin/amp-acp")),
"amp" => Some(PathBuf::from("/usr/local/bin/amp")),
_ => None,
});
assert_eq!(entry.availability, AcpAvailabilityStatus::Available);
assert_eq!(entry.command.as_deref(), Some("amp-acp"));
assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp"));
assert_eq!(
entry.underlying_cli_path.as_deref(),
Some("/usr/local/bin/amp")
);
}
#[test]
fn adapter_presence_is_enough_for_availability() {
let entry = preset_catalog_entry(&ADAPTER_PRESET, |command| {
(command == "amp-acp").then(|| PathBuf::from("/usr/local/bin/amp-acp"))
});
assert_eq!(entry.availability, AcpAvailabilityStatus::Available);
assert_eq!(entry.command.as_deref(), Some("amp-acp"));
assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp"));
assert!(entry.underlying_cli_path.is_none());
}
#[test]
fn preset_without_underlying_cli_stays_simple() {
let preset = PresetHarness {
underlying_cli: None,
..ADAPTER_PRESET
};
let entry = preset_catalog_entry(&preset, |_| None);
assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled);
assert!(!entry.requires_external_cli);
assert!(entry.underlying_cli_path.is_none());
}
}
@@ -6,9 +6,9 @@ use super::{
codex_adapter_is_outdated, create_time_agent_command_override, default_agent_command,
effective_agent_command, find_nvm_default_bin, find_via_login_shell,
is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args,
parse_semver_tag, preset_catalog_entry, probe_codex_acp_version, record_agent_command,
refresh_login_shell_path, try_record_agent_command, PresetHarness, BUZZ_AGENT_AVATAR_URL,
CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL,
parse_semver_tag, probe_codex_acp_version, record_agent_command, refresh_login_shell_path,
try_record_agent_command, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL,
GOOSE_AVATAR_URL,
};
use crate::managed_agents::AcpAvailabilityStatus;
@@ -187,90 +187,6 @@ fn classifies_cli_missing_when_adapter_found_but_cli_absent() {
assert_eq!(path.as_deref(), Some("/opt/homebrew/bin/codex-acp"));
}
/// Amp-shaped preset: an ACP adapter (`amp-acp`) wrapping a separately
/// installed vendor CLI (`amp`).
const ADAPTER_PRESET: PresetHarness = PresetHarness {
id: "amp-test",
label: "Amp Test",
command: "amp-acp",
args: &[],
install_instructions_url: "https://example.com/install",
install_hint: "Install the amp-acp npm adapter.",
underlying_cli: Some("amp"),
};
#[test]
fn preset_entry_adapter_missing_when_underlying_cli_present() {
// Vendor CLI resolves, adapter does not — the state Tyler's Amp
// hand-test hit. Must NOT degrade to the misleading NotInstalled.
let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| {
(cmd == "amp").then(|| PathBuf::from("/usr/local/bin/amp"))
});
assert_eq!(entry.availability, AcpAvailabilityStatus::AdapterMissing);
assert!(entry.command.is_none());
assert!(entry.binary_path.is_none());
assert_eq!(
entry.underlying_cli_path.as_deref(),
Some("/usr/local/bin/amp")
);
assert!(!entry.requires_external_cli);
assert_eq!(entry.install_hint, "Install the amp-acp npm adapter.");
}
#[test]
fn preset_entry_not_installed_when_both_missing() {
let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None);
assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled);
assert!(entry.underlying_cli_path.is_none());
assert!(!entry.requires_external_cli);
}
#[test]
fn preset_entry_available_when_adapter_and_cli_present() {
let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| match cmd {
"amp-acp" => Some(PathBuf::from("/usr/local/bin/amp-acp")),
"amp" => Some(PathBuf::from("/usr/local/bin/amp")),
_ => None,
});
assert_eq!(entry.availability, AcpAvailabilityStatus::Available);
assert_eq!(entry.command.as_deref(), Some("amp-acp"));
assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp"));
assert_eq!(
entry.underlying_cli_path.as_deref(),
Some("/usr/local/bin/amp")
);
}
#[test]
fn preset_entry_stays_available_when_adapter_present_but_cli_absent() {
// Wren's regression guard: today an `amp-acp` install without `amp`
// is Available and selectable. Feeding underlying_cli through the
// FULL classify_runtime predicate would flip this to CliMissing
// (unselectable, with backwards install copy) — the adapter-missing
// arm is the only one presets consume.
let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| {
(cmd == "amp-acp").then(|| PathBuf::from("/usr/local/bin/amp-acp"))
});
assert_eq!(entry.availability, AcpAvailabilityStatus::Available);
assert_eq!(entry.command.as_deref(), Some("amp-acp"));
assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp"));
assert!(entry.underlying_cli_path.is_none());
}
#[test]
fn preset_entry_without_underlying_cli_stays_simple() {
// Most presets: the command IS the vendor CLI. No external-CLI flag,
// absent command means plain NotInstalled.
let preset = PresetHarness {
underlying_cli: None,
..ADAPTER_PRESET
};
let entry = preset_catalog_entry(&preset, |_| None);
assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled);
assert!(!entry.requires_external_cli);
assert!(entry.underlying_cli_path.is_none());
}
fn persona_with_runtime(id: &str, runtime: Option<&str>) -> crate::managed_agents::AgentDefinition {
crate::managed_agents::AgentDefinition {
id: id.to_string(),
@@ -17,6 +17,7 @@ const RUNTIME_LOGOS: Record<string, string> = {
// Public-path logos for bundled presets. Served from /harness-logos/ at runtime.
// Keys match the preset `id` values emitted by the backend PRESET_HARNESSES.
export const PRESET_LOGOS: Record<string, string> = {
devin: "/harness-logos/devin.svg",
omp: "/harness-logos/omp.svg",
grok: "/harness-logos/grok.svg",
opencode: "/harness-logos/opencode.svg",
@@ -24,15 +24,15 @@ const desktopRoot = path.resolve(
"../../../..",
);
const discoveryRs = readFileSync(
path.join(desktopRoot, "src-tauri/src/managed_agents/discovery.rs"),
const presetsRs = readFileSync(
path.join(desktopRoot, "src-tauri/src/managed_agents/discovery/presets.rs"),
"utf8",
);
const presetBlock = discoveryRs.match(
const presetBlock = presetsRs.match(
/const PRESET_HARNESSES: &\[PresetHarness\] = &\[([\s\S]*?)\n\];/,
);
assert.ok(presetBlock, "could not locate PRESET_HARNESSES in discovery.rs");
assert.ok(presetBlock, "could not locate PRESET_HARNESSES in presets.rs");
const presetIds = [...presetBlock[1].matchAll(/^\s{8}id: "([^"]+)",$/gm)].map(
(match) => match[1],