mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Preserve explicit hidden harness starts
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
Reference in New Issue
Block a user