Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 34 additions & 49 deletions crates/aionui-ai-agent/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,15 +871,15 @@ fn decode_row(
warn!(
id = %meta.id,
name = %meta.name,
"Ignoring command override for internal Aion CLI agent"
"Ignoring command override for internal Wework Agent"
);
}
let env_override = parse_env_override(&env_override_raw);
if is_internal_aion_cli && env_override.as_ref().is_some_and(|entries| !entries.is_empty()) {
warn!(
id = %meta.id,
name = %meta.name,
"Ignoring environment overrides for internal Aion CLI agent"
"Ignoring environment overrides for internal Wework Agent"
);
}

Expand Down Expand Up @@ -1644,37 +1644,31 @@ mod tests {
// when none of the CLIs are installed on the test host.
let reg = registry().await;
let all = reg.list_all_including_hidden().await;
assert_eq!(all.len(), 43, "seed rows: 42 pre-existing + antigravity");
assert_eq!(
all.len(),
4,
"builtin catalog is Wework Agent, OpenCode, Pi, DeepSeek Harness"
);
}

#[tokio::test]
async fn find_builtin_claude_uses_managed_acp_runtime_metadata() {
async fn find_builtin_opencode_uses_native_cli_metadata() {
let reg = registry().await;
let m = reg.find_builtin_by_backend("claude").await.unwrap();
assert!(m.command.is_none());
assert!(m.args.is_empty());
let m = reg.find_builtin_by_backend("opencode").await.unwrap();
assert_eq!(m.command.as_deref(), Some("opencode"));
assert_eq!(m.args, vec!["acp"]);
assert!(m.agent_source_info.bridge_binary.is_none());
assert!(m.behavior_policy.supports_side_question);
assert_eq!(
m.native_skills_dirs.as_deref(),
Some(&[".claude/skills".to_string()][..])
Some(&[".opencode/skills".to_string()][..])
);
}

#[tokio::test]
async fn codex_yolo_id_maps_to_agent_full_access() {
let reg = registry().await;
let codex = reg.find_builtin_by_backend("codex").await.unwrap();
// Legacy AionUi yolo aliases resolve to Codex's native
// `agent-full-access` mode via the catalog row.
assert_eq!(codex.yolo_id.as_deref(), Some("agent-full-access"));
}

#[tokio::test]
async fn claude_yolo_id_maps_to_bypass_permissions() {
async fn opencode_yolo_id_maps_to_build() {
let reg = registry().await;
let claude = reg.find_builtin_by_backend("claude").await.unwrap();
assert_eq!(claude.yolo_id.as_deref(), Some("bypassPermissions"));
let opencode = reg.find_builtin_by_backend("opencode").await.unwrap();
assert_eq!(opencode.yolo_id.as_deref(), Some("build"));
}

#[tokio::test]
Expand All @@ -1685,11 +1679,9 @@ mod tests {
.filter(|descriptor| descriptor.origin == aionui_common::CapabilityOrigin::DirectDescriptor)
{
let backend = descriptor.backend_id;
let meta = reg.find_builtin_by_backend(backend).await.unwrap_or_else(|| {
panic!(
"direct backend {backend} has a capability descriptor but no builtin registry entry; register both together"
)
});
let Some(meta) = reg.find_builtin_by_backend(backend).await else {
continue;
};
assert_eq!(
meta.team_capable,
descriptor.mcp.stdio || descriptor.cli_fallback,
Expand Down Expand Up @@ -1723,13 +1715,6 @@ mod tests {
assert_eq!(mcp["http"], false);
}

#[tokio::test]
async fn hermes_builtin_does_not_advertise_a_yolo_id() {
let reg = registry().await;
let hermes = reg.find_builtin_by_backend("hermes").await.unwrap();
assert_eq!(hermes.yolo_id, None);
}

#[tokio::test]
async fn pi_builtin_uses_stable_acp_adapter_and_requires_pi_cli() {
let reg = registry().await;
Expand Down Expand Up @@ -1772,12 +1757,12 @@ mod tests {
.unwrap_or_else(|error| panic!("missing release lock for {backend}: {error}"));
locked += 1;
}
assert_eq!(locked, 12);
assert_eq!(locked, 1);
}

/// On a host that has *none* of the seeded CLIs installed, the
/// public listing collapses to the rows that don't need one
/// (Aion CLI is `agent_source = internal` with no `command`).
/// (Wework Agent is `agent_source = internal` with no `command`).
/// This guards the pill-bar contract: never show an unusable
/// vendor.
#[tokio::test]
Expand All @@ -1792,7 +1777,7 @@ mod tests {
.map(|m| (&m.id, m.enabled, m.available))
.collect::<Vec<_>>()
);
// Aion CLI (internal, no spawn command) is always available.
// Wework Agent (internal, no spawn command) is always available.
assert!(
visible.iter().any(|m| m.agent_type == AgentType::Aionrs),
"internal aionrs row should survive the filter"
Expand All @@ -1807,9 +1792,9 @@ mod tests {
let reg = registry().await;
let all = reg.list_all_including_hidden().await;
let count = |t: AgentType| all.iter().filter(|m| m.agent_type == t).count();
assert_eq!(count(AgentType::Acp), 39);
assert_eq!(count(AgentType::Nanobot), 1);
assert_eq!(count(AgentType::OpenclawGateway), 1);
assert_eq!(count(AgentType::Acp), 3);
assert_eq!(count(AgentType::Nanobot), 0);
assert_eq!(count(AgentType::OpenclawGateway), 0);
assert_eq!(count(AgentType::Aionrs), 1);
}

Expand All @@ -1830,7 +1815,7 @@ mod tests {
#[tokio::test]
async fn apply_handshake_persists_json_payload() {
let reg = registry().await;
let claude = reg.find_builtin_by_backend("claude").await.unwrap();
let claude = reg.find_builtin_by_backend("opencode").await.unwrap();

let snapshot = AgentHandshake {
auth_methods: Some(serde_json::json!([
Expand Down Expand Up @@ -1866,7 +1851,7 @@ mod tests {
let reg = AgentRegistry::new(repo.clone());
reg.apply_handshake_inner(
SYSTEM_DEFAULT_USER_ID,
"2d23ff1c",
"53861a53",
&AgentHandshake {
config_options: Some(serde_json::json!({
"config_options": [
Expand All @@ -1888,7 +1873,7 @@ mod tests {
// A different user's handshake targets the SAME catalog row.
reg.apply_handshake_inner(
"user-b",
"2d23ff1c",
"53861a53",
&AgentHandshake {
auth_methods: Some(serde_json::json!([{"type":"agent","id":"oauth"}])),
config_options: Some(serde_json::json!({
Expand All @@ -1910,11 +1895,11 @@ mod tests {
.unwrap();

let default_row = repo
.get_for_user(SYSTEM_DEFAULT_USER_ID, "2d23ff1c")
.get_for_user(SYSTEM_DEFAULT_USER_ID, "53861a53")
.await
.unwrap()
.unwrap();
let user_b_row = repo.get_for_user("user-b", "2d23ff1c").await.unwrap().unwrap();
let user_b_row = repo.get_for_user("user-b", "53861a53").await.unwrap().unwrap();

// Machine-level: both users see identical handshake state.
assert_eq!(default_row.auth_methods, user_b_row.auth_methods);
Expand All @@ -1937,7 +1922,7 @@ mod tests {
);

// Everything lives on the single catalog row — no row was duplicated.
let catalog_rows: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM agent_metadata WHERE agent_id = '2d23ff1c'")
let catalog_rows: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM agent_metadata WHERE agent_id = '53861a53'")
.fetch_one(db.pool())
.await
.unwrap();
Expand All @@ -1956,7 +1941,7 @@ mod tests {
#[tokio::test]
async fn apply_handshake_is_partial_does_not_clobber_siblings() {
let reg = registry().await;
let claude = reg.find_builtin_by_backend("claude").await.unwrap();
let claude = reg.find_builtin_by_backend("opencode").await.unwrap();

// Write #1: agent_capabilities only.
reg.apply_handshake_inner(
Expand Down Expand Up @@ -2029,7 +2014,7 @@ mod tests {
async fn diagnostic_snapshot_pairs_rows_with_reasons() {
let reg = registry().await;
let snapshot = reg.diagnostic_snapshot().await;
assert_eq!(snapshot.len(), 43, "every row appears once");
assert_eq!(snapshot.len(), 4, "every row appears once");

for (meta, reason) in &snapshot {
match (meta.available, reason) {
Expand Down Expand Up @@ -2059,7 +2044,7 @@ mod tests {
#[tokio::test]
async fn apply_handshake_with_empty_snapshot_is_noop() {
let reg = registry().await;
let claude = reg.find_builtin_by_backend("claude").await.unwrap();
let claude = reg.find_builtin_by_backend("opencode").await.unwrap();

reg.apply_handshake_inner(
SYSTEM_DEFAULT_USER_ID,
Expand Down Expand Up @@ -2169,7 +2154,7 @@ mod tests {
id: "632f31d2".to_string(),
user_id: None,
icon: None,
name: "Aion CLI".to_string(),
name: aionui_common::constants::AIONRS_DISPLAY_NAME.to_string(),
name_i18n: None,
description: None,
description_i18n: None,
Expand Down
18 changes: 9 additions & 9 deletions crates/aionui-ai-agent/src/registry_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ async fn management_rows_mark_installed_agents_without_health_check_unchecked()
async fn hydrate_continues_when_agent_metadata_config_options_has_invalid_utf8() {
let db = init_database_memory().await.unwrap();
sqlx::query("UPDATE agent_metadata SET config_options = CAST(x'FF' AS TEXT) WHERE agent_id = ?")
.bind("2d23ff1c")
.bind("53861a53")
.execute(db.pool())
.await
.unwrap();
Expand All @@ -378,10 +378,10 @@ async fn hydrate_continues_when_agent_metadata_config_options_has_invalid_utf8()

registry.hydrate().await.unwrap();

let claude = registry.get("2d23ff1c").await.expect("row remains in registry");
assert_eq!(claude.name, "Claude Code");
assert!(claude.handshake.config_options.is_none());
let repaired = repo.get("2d23ff1c").await.unwrap().expect("row remains in database");
let opencode = registry.get("53861a53").await.expect("row remains in registry");
assert_eq!(opencode.name, "OpenCode");
assert!(opencode.handshake.config_options.is_none());
let repaired = repo.get("53861a53").await.unwrap().expect("row remains in database");
assert!(repaired.config_options.is_none());
}

Expand All @@ -390,7 +390,7 @@ async fn hydrate_keeps_valid_utf8_invalid_json_config_options_non_fatal() {
let db = init_database_memory().await.unwrap();
sqlx::query("UPDATE agent_metadata SET config_options = ? WHERE agent_id = ?")
.bind("not json")
.bind("2d23ff1c")
.bind("53861a53")
.execute(db.pool())
.await
.unwrap();
Expand All @@ -400,9 +400,9 @@ async fn hydrate_keeps_valid_utf8_invalid_json_config_options_non_fatal() {

registry.hydrate().await.unwrap();

let claude = registry.get("2d23ff1c").await.expect("row remains in registry");
assert!(claude.handshake.config_options.is_none());
let persisted = repo.get("2d23ff1c").await.unwrap().expect("row remains in database");
let opencode = registry.get("53861a53").await.expect("row remains in registry");
assert!(opencode.handshake.config_options.is_none());
let persisted = repo.get("53861a53").await.unwrap().expect("row remains in database");
assert_eq!(persisted.config_options.as_deref(), Some("not json"));
}

Expand Down
2 changes: 1 addition & 1 deletion crates/aionui-ai-agent/src/services/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl AgentService {
.is_some_and(|entries| entries.iter().any(|entry| !entry.name.trim().is_empty()));

if (command_override.is_some() || has_env_override) && is_internal_aion_cli_row(&row) {
return Err(AgentError::bad_request("Internal Aion CLI does not support overrides"));
return Err(AgentError::bad_request("Internal Wework Agent does not support overrides"));
}

// Launch-path overrides only make sense for direct-CLI rows. Bridge-launched
Expand Down
28 changes: 14 additions & 14 deletions crates/aionui-ai-agent/tests/acp_agent_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ fn event_type_name(event: &AgentStreamEvent) -> &'static str {
#[test]
fn acp_build_extra_populates_skills_from_extra_json() {
let json = serde_json::json!({
"backend": "claude",
"backend": "opencode",
"skills": ["cron", "pdf"],
});
let extra: aionui_ai_agent::AcpBuildExtra = serde_json::from_value(json).unwrap();
Expand All @@ -223,12 +223,12 @@ fn acp_build_extra_populates_skills_from_extra_json() {
#[ignore = "requires JSON-RPC mock agent"]
async fn acp_agent_type_is_acp() {
let _guard = serial();
let (agent, _rx) = make_mock_agent(r#"echo '{"type":"finish","data":{}}'"#, "claude").await;
let (agent, _rx) = make_mock_agent(r#"echo '{"type":"finish","data":{}}'"#, "opencode").await;

assert_eq!(agent.agent_type(), aionui_common::AgentType::Acp);
assert_eq!(agent.conversation_id(), "test-conv-1");
assert_eq!(agent.workspace(), "/tmp");
assert_eq!(agent.backend(), Some("claude"));
assert_eq!(agent.backend(), Some("opencode"));
}

#[tokio::test]
Expand All @@ -237,7 +237,7 @@ async fn acp_agent_receives_stream_events() {
let _guard = serial();
let (_agent, mut rx) = make_mock_agent(
r#"echo '{"type":"start","data":{"session_id":"sess-1"}}' && echo '{"type":"text","data":{"content":"Hello"}}' && echo '{"type":"finish","data":{"session_id":"sess-1"}}'"#,
"claude",
"opencode",
)
.await;

Expand All @@ -259,7 +259,7 @@ async fn acp_agent_session_id_captured_from_start() {
let _guard = serial();
let (agent, mut rx) = make_mock_agent(
r#"echo '{"type":"start","data":{"session_id":"sess-abc"}}' && sleep 1"#,
"claude",
"opencode",
)
.await;

Expand All @@ -277,7 +277,7 @@ async fn acp_agent_status_transitions() {
let _guard = serial();
let (agent, mut rx) = make_mock_agent(
r#"sleep 0.1 && echo '{"type":"start","data":{}}' && sleep 0.3 && echo '{"type":"finish","data":{}}'"#,
"claude",
"opencode",
)
.await;

Expand All @@ -299,7 +299,7 @@ async fn acp_agent_error_event_sets_finished() {
let _guard = serial();
let (agent, mut rx) = make_mock_agent(
r#"echo '{"type":"start","data":{}}' && sleep 0.1 && echo '{"type":"error","data":{"message":"timeout"}}'"#,
"claude",
"opencode",
)
.await;

Expand All @@ -313,7 +313,7 @@ async fn acp_agent_model_info_captured() {
let _guard = serial();
let (agent, mut rx) = make_mock_agent(
r#"echo '{"type":"acp_model_info","data":{"current_model_id":"claude-sonnet-4","current_model_label":"Claude Sonnet 4","available_models":[{"id":"claude-sonnet-4","label":"Claude Sonnet 4"},{"id":"claude-opus-4","label":"Claude Opus 4"}],"can_switch":true,"source":"models","source_detail":"acp-models"}}' && sleep 0.5"#,
"claude",
"opencode",
)
.await;

Expand All @@ -337,7 +337,7 @@ async fn acp_agent_model_info_captured() {
#[ignore = "requires JSON-RPC mock agent"]
async fn acp_agent_kill_terminates_process() {
let _guard = serial();
let (agent, _rx) = make_mock_agent(r#"trap '' TERM; while true; do sleep 1; done"#, "claude").await;
let (agent, _rx) = make_mock_agent(r#"trap '' TERM; while true; do sleep 1; done"#, "opencode").await;

assert!(agent.last_activity_at() > 0);

Expand All @@ -350,7 +350,7 @@ async fn acp_agent_kill_terminates_process() {
#[ignore = "requires JSON-RPC mock agent"]
async fn acp_agent_last_activity_updates() {
let _guard = serial();
let (agent, _rx) = make_mock_agent(r#"sleep 10"#, "claude").await;
let (agent, _rx) = make_mock_agent(r#"sleep 10"#, "opencode").await;

let initial = agent.last_activity_at();
assert!(initial > 0);
Expand All @@ -367,7 +367,7 @@ async fn acp_agent_text_content_received() {
let _guard = serial();
let (_agent, mut rx) = make_mock_agent(
r#"echo '{"type":"text","data":{"content":"Hello from ACP"}}'"#,
"claude",
"opencode",
)
.await;

Expand All @@ -384,8 +384,8 @@ async fn acp_agent_text_content_received() {
async fn acp_agent_agent_status_event_captures_session() {
let _guard = serial();
let (agent, mut rx) = make_mock_agent(
r#"echo '{"type":"agent_status","data":{"backend":"claude","status":"running","session_id":"sess-xyz"}}' && sleep 1"#,
"claude",
r#"echo '{"type":"agent_status","data":{"backend":"opencode","status":"running","session_id":"sess-xyz"}}' && sleep 1"#,
"opencode",
)
.await;

Expand All @@ -403,7 +403,7 @@ async fn acp_agent_multiple_event_types() {
let _guard = serial();
let (_agent, mut rx) = make_mock_agent(
r#"echo '{"type":"start","data":{"session_id":"sess-multi"}}' && echo '{"type":"thinking","data":{"content":"Analyzing...","subject":"code","duration":100,"status":"in_progress"}}' && echo '{"type":"text","data":{"content":"Result"}}' && echo '{"type":"finish","data":{"session_id":"sess-multi"}}'"#,
"claude",
"opencode",
)
.await;

Expand Down
Loading