mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
- acceptedEffortValues on AcpRuntimeCatalogEntry: Goose carries canonical off|low|medium|high|max (source: goose thinking.rs:277-308); null for buzz-agent (per-model catalog). Single authority for UI, spawn bridge, and reader. - normalizeEffortValue() in buzzAgentConfig.ts: aliases none|disabled→off, med→medium, xhigh→max (case-insensitive); null acceptedValues = pass-through (buzz-agent path unchanged). - ALL_KNOWN_EFFORT_KEYS in buzzAgentConfig.ts mirrors Rust constant; used for record/persona runtime-transition cleanup. - agentConfigCore.ts: native key wins; legacy-only pre-migration save shown via effortValue fallback; legacyConsumedKey suppresses duplicate advanced row. optionSource harnessNative when acceptedEffortValues present. - AgentConfigFields.tsx: descriptor-driven global/onboarding path for harnessNative runtimes (isHarnessNativeEffort): canonical options from acceptedEffortValues, currentEffort from normalised descriptor value, effortValidForAutoClear from acceptedEffortValues. Unconditional BUZZ_AGENT_THINKING_EFFORT deletes guarded by !isHarnessNativeEffort (Delta 5: global-scope coexistence). allStructuredKeys includes legacyConsumedKey; effort clear deletes native + legacy; bakedStructuredKeys() replaces static set. - buzzAgentModelTuningFields.tsx: applyHarnessNativeEffortChange() exported production helper — single spec for the mutation contract. legacyKey=null at global/onboarding scope (Delta 5), legacyKey=BUZZ_AGENT_THINKING_EFFORT at record/persona scope. EffortSelectField gains canonicalValues prop to bypass Buzz 7-value catalog. HarnessNativeEffortFields calls production helper. - personaRuntimeModel.ts: resolveInheritedRuntimeSubmission gains excludePersonaEnvKeys param. Strips harness-native effort keys from persona layer on inherit-transition so the record never acquires a frozen copy of persona effort (effort inherits at spawn; it is not a credential). - AgentInstanceEditDialog.tsx: passes excludePersonaEnvKeys with runtime's thinkingEnvVar + ALL_KNOWN_EFFORT_KEYS when runtime has acceptedEffortValues. - bakedEnvHelpers.ts: getInheritedAgentDefaults accepts nativeEffortKey option; baked lookup is native-only for non-buzz-agent runtimes (Delta-4 boundary). bakedStructuredKeys() exported for AgentConfigFields. - useAgentDialogDefaults.ts: accepts nativeEffortKey; seeds inherited env under native key. - AgentDefinitionDialog + AgentInstanceEditDialog: pass prev/next native effort keys to selectionOnRuntimeChange; handleInheritHarnessChange clears all known effort keys on pin↔inherit toggle. - runtimeModelProviderSelection.ts: selectionOnRuntimeChange clears prev native + next native + legacy at record/persona scope; skip when native === legacy (buzz-agent same-key invariant). - agentConfigOptions.tsx: resetConfigForHarnessChange preserves all effort keys (global-scope Delta-5 rule; each runtime's descriptor strips foreign keys). - agent_config.rs: is_safe_to_reveal derives allowed effort keys from ALL_KNOWN_EFFORT_KEYS so GOOSE_THINKING_EFFORT is unmasked at baked tier. - AGENTS.md rule-2 'until PR 2.7' clause removed: native-key persistence lands in this commit. - Test coverage: applyHarnessNativeEffortChange record/persona + global scope pins; resolveInheritedRuntimeSubmission inherit-no-materialize (native + legacy), concrete-override-wins, non-effort-merged, full-submit-payload-clean; global Goose descriptor optionSource/value/invalid/no-legacy pins; runtimeModelProviderSelection useLiteralKeys cleanup. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
330 lines
10 KiB
JavaScript
330 lines
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
selectionOnModelDropdownChange,
|
|
selectionOnProviderDropdownChange,
|
|
selectionOnRuntimeChange,
|
|
} from "./runtimeModelProviderSelection.ts";
|
|
|
|
const base = {
|
|
provider: "",
|
|
model: "",
|
|
isCustomProviderEditing: false,
|
|
isCustomModelEditing: false,
|
|
envVars: {},
|
|
};
|
|
|
|
// --- selectionOnRuntimeChange ---
|
|
|
|
test("runtime change to a provider-locked runtime, full reset (Persona/Edit): clears provider, custom flags, and managed API key", () => {
|
|
const next = selectionOnRuntimeChange(
|
|
{
|
|
...base,
|
|
provider: "anthropic",
|
|
model: "claude-4",
|
|
isCustomProviderEditing: true,
|
|
isCustomModelEditing: true,
|
|
envVars: { ANTHROPIC_API_KEY: "sk-1", KEEP: "x" },
|
|
},
|
|
{
|
|
previousRuntime: "buzz-agent",
|
|
nextRuntime: "claude",
|
|
nextRuntimeCanChooseProvider: false,
|
|
lockedRuntimeReset: "full",
|
|
},
|
|
);
|
|
assert.equal(next.provider, "");
|
|
assert.equal(next.isCustomProviderEditing, false);
|
|
assert.equal(next.isCustomModelEditing, false);
|
|
assert.deepEqual(next.envVars, { KEEP: "x" });
|
|
});
|
|
|
|
test("runtime change to a provider-locked runtime, provider-only reset (Create): keeps env vars and custom-model flag", () => {
|
|
const next = selectionOnRuntimeChange(
|
|
{
|
|
...base,
|
|
provider: "anthropic",
|
|
envVars: { ANTHROPIC_API_KEY: "sk-1" },
|
|
isCustomModelEditing: true,
|
|
model: "my-custom",
|
|
},
|
|
{
|
|
previousRuntime: "buzz-agent",
|
|
nextRuntime: "claude",
|
|
nextRuntimeCanChooseProvider: false,
|
|
lockedRuntimeReset: "provider-only",
|
|
},
|
|
);
|
|
assert.equal(next.provider, "");
|
|
assert.equal(next.isCustomProviderEditing, false);
|
|
assert.deepEqual(next.envVars, { ANTHROPIC_API_KEY: "sk-1" });
|
|
});
|
|
|
|
test("runtime change between provider-selection runtimes keeps provider state", () => {
|
|
const current = {
|
|
...base,
|
|
provider: "anthropic",
|
|
envVars: { ANTHROPIC_API_KEY: "sk-1" },
|
|
};
|
|
const next = selectionOnRuntimeChange(current, {
|
|
previousRuntime: "goose",
|
|
nextRuntime: "buzz-agent",
|
|
nextRuntimeCanChooseProvider: true,
|
|
lockedRuntimeReset: "full",
|
|
});
|
|
assert.equal(next.provider, "anthropic");
|
|
assert.deepEqual(next.envVars, { ANTHROPIC_API_KEY: "sk-1" });
|
|
});
|
|
|
|
// --- selectionOnProviderDropdownChange ---
|
|
|
|
test("provider switch clears the previous managed API key and sets the provider", () => {
|
|
const next = selectionOnProviderDropdownChange(
|
|
{
|
|
...base,
|
|
provider: "anthropic",
|
|
envVars: { ANTHROPIC_API_KEY: "sk-1", KEEP: "x" },
|
|
},
|
|
{
|
|
runtime: "buzz-agent",
|
|
nextValue: "openai",
|
|
clearModelWhenApiKeyMissing: false,
|
|
},
|
|
);
|
|
assert.equal(next.provider, "openai");
|
|
assert.equal(next.isCustomProviderEditing, false);
|
|
assert.deepEqual(next.envVars, { KEEP: "x" });
|
|
});
|
|
|
|
test("custom-provider entry clears the managed key and enters custom editing", () => {
|
|
const next = selectionOnProviderDropdownChange(
|
|
{
|
|
...base,
|
|
provider: "anthropic",
|
|
envVars: { ANTHROPIC_API_KEY: "sk-1" },
|
|
},
|
|
{
|
|
runtime: "buzz-agent",
|
|
nextValue: "__custom_provider__",
|
|
clearModelWhenApiKeyMissing: false,
|
|
},
|
|
);
|
|
assert.equal(next.isCustomProviderEditing, true);
|
|
assert.equal(next.provider, "");
|
|
assert.deepEqual(next.envVars, {});
|
|
});
|
|
|
|
test("auto-provider selection maps to empty provider", () => {
|
|
const next = selectionOnProviderDropdownChange(
|
|
{ ...base, provider: "anthropic", envVars: { ANTHROPIC_API_KEY: "sk-1" } },
|
|
{
|
|
runtime: "buzz-agent",
|
|
nextValue: "__auto_provider__",
|
|
clearModelWhenApiKeyMissing: false,
|
|
},
|
|
);
|
|
assert.equal(next.provider, "");
|
|
assert.deepEqual(next.envVars, {});
|
|
});
|
|
|
|
test("Persona mode clears the model when the new provider's API key is missing", () => {
|
|
const next = selectionOnProviderDropdownChange(
|
|
{ ...base, model: "claude-4", provider: "" },
|
|
{
|
|
runtime: "buzz-agent",
|
|
nextValue: "anthropic",
|
|
clearModelWhenApiKeyMissing: true,
|
|
},
|
|
);
|
|
assert.equal(next.model, "");
|
|
});
|
|
|
|
test("Create/Edit mode keeps the model when the new provider's API key is missing", () => {
|
|
// claude-4 is scope-agnostic here: shouldClearKnownModelForSelectionScope
|
|
// only clears known models for the selection scope, and a custom string
|
|
// stays put — mirroring the dialogs' behavior without the persona flag.
|
|
const next = selectionOnProviderDropdownChange(
|
|
{ ...base, model: "my-custom-model", provider: "" },
|
|
{
|
|
runtime: "buzz-agent",
|
|
nextValue: "anthropic",
|
|
clearModelWhenApiKeyMissing: false,
|
|
},
|
|
);
|
|
assert.equal(next.model, "my-custom-model");
|
|
});
|
|
|
|
test("custom-model editing suppresses the model-scope clear on provider switch", () => {
|
|
const next = selectionOnProviderDropdownChange(
|
|
{ ...base, model: "anything", isCustomModelEditing: true },
|
|
{
|
|
runtime: "buzz-agent",
|
|
nextValue: "openai",
|
|
clearModelWhenApiKeyMissing: false,
|
|
},
|
|
);
|
|
assert.equal(next.model, "anything");
|
|
assert.equal(next.isCustomModelEditing, true);
|
|
});
|
|
|
|
// --- selectionOnModelDropdownChange ---
|
|
|
|
test("custom-model entry with clear (Persona) drops a known model", () => {
|
|
const next = selectionOnModelDropdownChange(
|
|
{ ...base, model: "known-model" },
|
|
{
|
|
nextValue: "__custom_model__",
|
|
clearKnownModelOnCustomEntry: true,
|
|
isModelCustom: false,
|
|
},
|
|
);
|
|
assert.equal(next.isCustomModelEditing, true);
|
|
assert.equal(next.model, "");
|
|
});
|
|
|
|
test("custom-model entry keeps an already-custom model (Persona) and any model (Edit)", () => {
|
|
const personaCustom = selectionOnModelDropdownChange(
|
|
{ ...base, model: "already-custom" },
|
|
{
|
|
nextValue: "__custom_model__",
|
|
clearKnownModelOnCustomEntry: true,
|
|
isModelCustom: true,
|
|
},
|
|
);
|
|
assert.equal(personaCustom.model, "already-custom");
|
|
|
|
const edit = selectionOnModelDropdownChange(
|
|
{ ...base, model: "known-model" },
|
|
{
|
|
nextValue: "__custom_model__",
|
|
clearKnownModelOnCustomEntry: false,
|
|
isModelCustom: false,
|
|
},
|
|
);
|
|
assert.equal(edit.model, "known-model");
|
|
assert.equal(edit.isCustomModelEditing, true);
|
|
});
|
|
|
|
test("auto-model selection clears the model; concrete selection sets it", () => {
|
|
const auto = selectionOnModelDropdownChange(
|
|
{ ...base, model: "old", isCustomModelEditing: true },
|
|
{
|
|
nextValue: "__auto_model__",
|
|
clearKnownModelOnCustomEntry: false,
|
|
isModelCustom: false,
|
|
},
|
|
);
|
|
assert.equal(auto.model, "");
|
|
assert.equal(auto.isCustomModelEditing, false);
|
|
|
|
const concrete = selectionOnModelDropdownChange(
|
|
{ ...base, model: "" },
|
|
{
|
|
nextValue: "gpt-5",
|
|
clearKnownModelOnCustomEntry: false,
|
|
isModelCustom: false,
|
|
},
|
|
);
|
|
assert.equal(concrete.model, "gpt-5");
|
|
});
|
|
|
|
// ── selectionOnRuntimeChange: effort-key cleanup ──────────────────────────
|
|
//
|
|
// At record/persona scope, a runtime change must clean up the previous
|
|
// runtime's native effort key AND the legacy key (BUZZ_AGENT_THINKING_EFFORT).
|
|
// The next runtime's native key is also cleared (stale leftover removal).
|
|
// Unrelated env keys are preserved.
|
|
|
|
test("selectionOnRuntimeChange_goose_to_buzz_clears_goose_native_and_legacy", () => {
|
|
// Goose→buzz-agent: GOOSE_THINKING_EFFORT (prev native) + BUZZ_AGENT_THINKING_EFFORT (legacy)
|
|
// are deleted. Unrelated keys survive.
|
|
const current = {
|
|
...base,
|
|
envVars: {
|
|
GOOSE_THINKING_EFFORT: "medium",
|
|
BUZZ_AGENT_THINKING_EFFORT: "high",
|
|
SOME_KEY: "value",
|
|
},
|
|
};
|
|
const next = selectionOnRuntimeChange(current, {
|
|
previousRuntime: "goose",
|
|
nextRuntime: "buzz-agent",
|
|
nextRuntimeCanChooseProvider: false,
|
|
lockedRuntimeReset: "full",
|
|
previousRuntimeNativeEffortKey: "GOOSE_THINKING_EFFORT",
|
|
nextRuntimeNativeEffortKey: "BUZZ_AGENT_THINKING_EFFORT",
|
|
});
|
|
assert.equal(
|
|
next.envVars.GOOSE_THINKING_EFFORT,
|
|
undefined,
|
|
"prev native effort key must be cleared",
|
|
);
|
|
assert.equal(
|
|
next.envVars.BUZZ_AGENT_THINKING_EFFORT,
|
|
undefined,
|
|
"legacy effort key must be cleared",
|
|
);
|
|
assert.equal(
|
|
next.envVars.SOME_KEY,
|
|
"value",
|
|
"unrelated env key must survive",
|
|
);
|
|
});
|
|
|
|
test("selectionOnRuntimeChange_buzz_to_goose_clears_goose_native_and_legacy", () => {
|
|
// buzz-agent→Goose: previousRuntimeNativeEffortKey is BUZZ_AGENT_THINKING_EFFORT
|
|
// which equals the legacy key — cleanup fires via the nextRuntimeNativeEffortKey path.
|
|
const current = {
|
|
...base,
|
|
envVars: {
|
|
GOOSE_THINKING_EFFORT: "low",
|
|
BUZZ_AGENT_THINKING_EFFORT: "high",
|
|
OTHER: "keep",
|
|
},
|
|
};
|
|
const next = selectionOnRuntimeChange(current, {
|
|
previousRuntime: "buzz-agent",
|
|
nextRuntime: "goose",
|
|
nextRuntimeCanChooseProvider: true,
|
|
lockedRuntimeReset: "full",
|
|
previousRuntimeNativeEffortKey: "BUZZ_AGENT_THINKING_EFFORT",
|
|
nextRuntimeNativeEffortKey: "GOOSE_THINKING_EFFORT",
|
|
});
|
|
// When prev native === legacy key, the else-if branch fires instead:
|
|
// it clears next native + legacy.
|
|
assert.equal(
|
|
next.envVars.GOOSE_THINKING_EFFORT,
|
|
undefined,
|
|
"next native effort key must be cleared",
|
|
);
|
|
assert.equal(
|
|
next.envVars.BUZZ_AGENT_THINKING_EFFORT,
|
|
undefined,
|
|
"legacy effort key must be cleared",
|
|
);
|
|
assert.equal(next.envVars.OTHER, "keep", "unrelated env key must survive");
|
|
});
|
|
|
|
test("selectionOnRuntimeChange_buzz_to_buzz_preserves_effort_keys", () => {
|
|
// buzz-agent→buzz-agent: no effort cleanup (native === legacy key,
|
|
// both branches skip). Effort keys survive.
|
|
const current = {
|
|
...base,
|
|
envVars: { BUZZ_AGENT_THINKING_EFFORT: "high" },
|
|
};
|
|
const next = selectionOnRuntimeChange(current, {
|
|
previousRuntime: "buzz-agent",
|
|
nextRuntime: "buzz-agent",
|
|
nextRuntimeCanChooseProvider: false,
|
|
lockedRuntimeReset: "full",
|
|
previousRuntimeNativeEffortKey: "BUZZ_AGENT_THINKING_EFFORT",
|
|
nextRuntimeNativeEffortKey: "BUZZ_AGENT_THINKING_EFFORT",
|
|
});
|
|
assert.equal(
|
|
next.envVars.BUZZ_AGENT_THINKING_EFFORT,
|
|
"high",
|
|
"buzz-agent effort key preserved on same-runtime switch",
|
|
);
|
|
});
|