Preserve explicit hidden harness starts

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-07-27 13:27:06 +01:00
parent 6bebda4f07
commit 6e2950029e
7 changed files with 104 additions and 21 deletions
@@ -235,6 +235,25 @@ fn deploy_resolver_ignores_defaults_from_a_different_implicit_runtime() {
);
}
#[test]
fn deploy_resolver_ignores_mismatched_defaults_without_a_stored_override() {
let mut record = bare_agent_record(Some("p1"), None, None);
record.agent_command = "buzz-agent".to_string();
record.agent_command_override = None;
let personas = vec![persona_record("p1", None, None)];
let global = crate::managed_agents::GlobalAgentConfig {
model: Some("global-model".to_string()),
provider: Some("global-provider".to_string()),
preferred_runtime: Some("goose".to_string()),
..Default::default()
};
assert_eq!(
resolve_deploy_model_provider(&record, &personas, &global),
(None, None)
);
}
#[test]
fn normalize_relay_mesh_rejects_empty_model_ref() {
let config = RelayMeshConfig {
@@ -235,19 +235,17 @@ pub(crate) fn global_model_provider_for_record<'a>(
return global_values;
}
let Some(selected_runtime) = record
.agent_command_override
let Some(preferred_runtime) = global
.preferred_runtime
.as_deref()
.and_then(crate::managed_agents::known_acp_runtime)
else {
return global_values;
};
let preferred_runtime = global
.preferred_runtime
.as_deref()
.and_then(crate::managed_agents::known_acp_runtime);
let selected_command = crate::managed_agents::record_agent_command(record, personas);
let selected_runtime = crate::managed_agents::known_acp_runtime(&selected_command);
if preferred_runtime.is_some_and(|preferred| std::ptr::eq(preferred, selected_runtime)) {
if selected_runtime.is_some_and(|selected| std::ptr::eq(preferred_runtime, selected)) {
global_values
} else {
(None, None)
@@ -491,6 +491,26 @@ fn runtime_fallback_does_not_inherit_defaults_from_a_different_preferred_runtime
);
}
#[test]
fn default_command_fallback_does_not_inherit_defaults_from_another_preferred_runtime() {
let mut record = bare_record();
record.persona_id = Some("p1".to_string());
record.agent_command = "buzz-agent".to_string();
record.agent_command_override = None;
let personas = vec![persona("p1", None, None)];
let global = GlobalAgentConfig {
model: Some("global-model".to_string()),
provider: Some("global-provider".to_string()),
preferred_runtime: Some("goose".to_string()),
..Default::default()
};
assert_eq!(
resolve_effective_model_provider(&record, &personas, &global),
(None, None)
);
}
#[test]
fn runtime_fallback_inherits_defaults_when_it_matches_the_preferred_runtime() {
let mut record = bare_record();
+3 -2
View File
@@ -99,8 +99,9 @@ with a TypeScript lookup table or an id comparison in a component.
`resolveStartRuntimeForDefinition` is the shared boundary that filters every
runtime-less start through the device's visible-runtime set; call it instead
of duplicating default selection in a start surface. Team deploys use the
same visible-runtime set. Definitions already pinned to a hidden runtime
remain runnable.
same visible-runtime set only for members that need an implicit fallback, so
a fully pinned team remains deployable even when every installed runtime is
hidden. Definitions already pinned to a hidden runtime remain runnable.
10. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
canResolveAllPersonaRuntimes,
collectRuntimeWarnings,
resolvePersonaRuntime,
} from "./resolvePersonaRuntime.ts";
@@ -177,3 +178,25 @@ test("collectRuntimeWarnings — override=false behaves identically to no overri
test("collectRuntimeWarnings — empty personas array always returns empty", () => {
assert.deepEqual(collectRuntimeWarnings([], runtimes, goose, true), []);
});
test("team resolution allows explicit runtimes without a fallback", () => {
assert.equal(
canResolveAllPersonaRuntimes(
[{ runtime: "goose" }, { runtime: "claude" }],
runtimes,
null,
),
true,
);
});
test("team resolution requires a fallback for runtime-less definitions", () => {
assert.equal(
canResolveAllPersonaRuntimes([{ runtime: null }], runtimes, null),
false,
);
assert.equal(
canResolveAllPersonaRuntimes([{ runtime: null }], runtimes, goose),
true,
);
});
@@ -142,3 +142,16 @@ export function collectRuntimeWarnings(
}
return warnings;
}
/** Whether every definition can resolve with the supplied optional fallback. */
export function canResolveAllPersonaRuntimes(
personas: readonly { runtime: string | null }[],
runtimes: readonly AcpRuntime[],
fallbackRuntime: AcpRuntime | null,
): boolean {
return personas.every(
(persona) =>
resolvePersonaRuntime(persona.runtime, runtimes, fallbackRuntime)
.runtime !== null,
);
}
@@ -14,6 +14,7 @@ import {
import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { useSelectableAcpRuntimes } from "@/features/agents/lib/runtimeVisibilityPreference";
import {
canResolveAllPersonaRuntimes,
collectRuntimeWarnings,
getDefaultPersonaRuntime,
resolvePersonaRuntime,
@@ -86,6 +87,11 @@ export function AddTeamToChannelDialog({
);
const resolved = teamPersonaResolution.resolvedPersonas;
const missingPersonaCount = teamPersonaResolution.missingPersonaCount;
const canResolveTeamRuntimes = canResolveAllPersonaRuntimes(
resolved,
runtimes,
defaultProvider,
);
// Surface warnings when a persona's preferred runtime is unavailable.
// This dialog has no runtime selector, so the fallback is always
@@ -121,7 +127,7 @@ export function AddTeamToChannelDialog({
channels.find((channel) => channel.id === channelId) ?? null;
async function handleDeploy() {
if (!team || !selectedChannel || !defaultProvider) {
if (!team || !selectedChannel || !canResolveTeamRuntimes) {
return;
}
@@ -136,21 +142,23 @@ export function AddTeamToChannelDialog({
runtimes,
defaultProvider,
);
const runtimeToUse = personaRuntime ?? defaultProvider;
if (!personaRuntime) {
throw new Error("No runtime is available for this team member.");
}
return {
runtime: {
id: runtimeToUse.id,
label: runtimeToUse.label,
command: runtimeToUse.command,
defaultArgs: runtimeToUse.defaultArgs,
mcpCommand: runtimeToUse.mcpCommand,
id: personaRuntime.id,
label: personaRuntime.label,
command: personaRuntime.command,
defaultArgs: personaRuntime.defaultArgs,
mcpCommand: personaRuntime.mcpCommand,
},
name: persona.displayName,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
harnessOverride: shouldPinSelectedRuntimeForDefinition(
persona.runtime,
runtimeToUse.id,
personaRuntime.id,
),
model: persona.model ?? undefined,
personaId: persona.id,
@@ -262,10 +270,11 @@ export function AddTeamToChannelDialog({
</p>
) : null}
{!defaultProvider && !providersQuery.isLoading ? (
{!canResolveTeamRuntimes && !providersQuery.isLoading ? (
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
No ACP runtimes found. Make sure an agent runtime (e.g. Goose)
is installed.
{runtimes.length === 0
? "No ACP runtimes found. Make sure an agent runtime (e.g. Goose) is installed."
: "No enabled fallback runtime is available. Turn on a harness to deploy runtime-less team members."}
</p>
) : null}
@@ -307,7 +316,7 @@ export function AddTeamToChannelDialog({
disabled={
!team ||
!selectedChannel ||
!defaultProvider ||
!canResolveTeamRuntimes ||
resolved.length === 0 ||
missingPersonaCount > 0 ||
channelsQuery.isLoading ||