fix(desktop): always start agents and hide unavailable mesh (#1860)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-07-14 17:08:47 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent ab5cbc3692
commit 37bc962cb8
13 changed files with 195 additions and 229 deletions
@@ -85,8 +85,6 @@ type AgentDefinitionDialogProps = {
onSubmit: (
input: CreatePersonaInput | UpdatePersonaInput,
) => Promise<unknown>;
/** Rendered in the footers left slot. */
createFooterSlot?: React.ReactNode;
/** Rendered below the form fields in create mode only ("Where to run"). */
createRunSection?: React.ReactNode;
/** Extra create-mode submit gate (e.g. incomplete provider config). */
@@ -115,7 +113,6 @@ export function AgentDefinitionDialog({
runtimesLoading = false,
onOpenChange,
onSubmit,
createFooterSlot,
createRunSection,
createSubmitBlocked = false,
createRunOnMesh = false,
@@ -661,7 +658,7 @@ export function AgentDefinitionDialog({
title={title}
footer={
<div className="flex w-full items-center justify-between gap-3">
<div className="flex min-h-9 items-center">{createFooterSlot}</div>
<div className="flex min-h-9 items-center" />
<div className="flex items-center gap-2">
<Button
+36 -42
View File
@@ -6,14 +6,10 @@ import type {
ManagedAgent,
UpdatePersonaInput,
} from "@/shared/api/types";
import { Switch } from "@/shared/ui/switch";
import type { BackendIntent } from "../lib/instanceInputForDefinition";
import {
definitionCreateDialogState,
intentForStartToggle,
type AgentCreateIntent,
} from "./agentCreateIntent";
import type { AgentCreateIntent } from "./agentCreateIntent";
import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent";
import { useMeshAvailability } from "@/features/mesh-compute/hooks/useMeshAvailability";
import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog";
import { createPersonaDialogState } from "./personaDialogState";
import { AgentDefinitionDialog } from "./AgentDefinitionDialog";
@@ -79,10 +75,11 @@ type AgentDialogProps =
/**
* Unified entry point (Phase 1B.2/1B.3b/1B.3c): routes an intent to the form
* that owns it. The definition family renders AgentDefinitionDialog — create
* mode adds a "start after create" toggle, definition-edit passes the caller's
* PersonaDialogState-derived props through unchanged (edit/duplicate/import).
* instance-edit renders AgentInstanceEditDialog (persistent mount + `open`
* toggle — its reset lifecycle is keyed on [open, agent.pubkey]).
* mode always starts the agent and includes a WhereToRunSection;
* definition-edit passes the caller's PersonaDialogState-derived props
* through unchanged (edit/duplicate/import). instance-edit renders
* AgentInstanceEditDialog (persistent mount + `open` toggle — its reset
* lifecycle is keyed on [open, agent.pubkey]).
*/
export function AgentDialog(props: AgentDialogProps) {
if (props.mode === "instance-edit") {
@@ -112,47 +109,44 @@ function AgentCreateDialogRouter({
runtimesLoading,
onSubmitDefinition,
}: AgentDialogCreateProps) {
const [startAfterCreate, setStartAfterCreate] = React.useState(true);
const [runDraft, setRunDraft] = React.useState(emptyWhereToRunDraft);
// Stable identity across toggle flips — AgentDefinitionDialog re-initializes its
// fields whenever `initialValues` changes.
const { availability: meshAvailability } = useMeshAvailability();
const meshUnavailable =
meshAvailability != null && !meshAvailability.available;
// The cleanup effect below resets the persisted draft after this render. The
// submit path must not wait for that effect: it uses the local fallback
// immediately when a selected mesh target disappears.
const effectiveRunDraft =
meshUnavailable && runDraft.runOn === "mesh"
? emptyWhereToRunDraft
: runDraft;
// Persist the fallback after the synchronous guard has made the render safe.
// This cleanup keeps a future availability recovery from restoring an invalid
// mesh selection.
React.useEffect(() => {
if (meshUnavailable && runDraft.runOn === "mesh") {
setRunDraft(emptyWhereToRunDraft);
}
}, [meshUnavailable, runDraft.runOn]);
const initialValues = React.useMemo(
() => createPersonaDialogState().initialValues,
[],
);
const copy = definitionCreateDialogState(startAfterCreate);
const copy = createPersonaDialogState();
return (
<AgentDefinitionDialog
createFooterSlot={
<label
className="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground"
htmlFor="agent-dialog-start-toggle"
>
<Switch
checked={startAfterCreate}
data-testid="agent-dialog-start-toggle"
disabled={isDefinitionPending}
id="agent-dialog-start-toggle"
onCheckedChange={setStartAfterCreate}
/>
Start agent after creation
</label>
}
createRunSection={
// "Where to run" is instance state: with the start toggle off no
// instance exists, so the section disappears instead of dangling.
startAfterCreate ? (
<WhereToRunSection
draft={runDraft}
isPending={isDefinitionPending}
onDraftChange={setRunDraft}
/>
) : null
<WhereToRunSection
draft={runDraft}
isPending={isDefinitionPending}
meshAvailability={meshAvailability}
onDraftChange={setRunDraft}
/>
}
createSubmitBlocked={!canSubmitWhereToRun(runDraft, startAfterCreate)}
createRunOnMesh={startAfterCreate && runDraft.runOn === "mesh"}
createSubmitBlocked={!canSubmitWhereToRun(effectiveRunDraft)}
createRunOnMesh={effectiveRunDraft.runOn === "mesh"}
description={copy.description}
error={definitionError}
initialValues={initialValues}
@@ -161,8 +155,8 @@ function AgentCreateDialogRouter({
onSubmit={async (input) => {
const submitted = await onSubmitDefinition(
input,
intentForStartToggle(startAfterCreate),
resolveBackendIntent(runDraft, startAfterCreate),
"definition_start",
resolveBackendIntent(effectiveRunDraft),
);
if (submitted) {
onOpenChange(false);
@@ -63,7 +63,7 @@ export function AgentsView() {
// biome-ignore lint/correctness/useExhaustiveDependencies: mount-only shortcut subscription; openUnifiedCreate only calls stable setState-backed callbacks
React.useEffect(() => {
// The app-wide "create agent" shortcut routes to the unified definition
// flow (B5): one create path, with the start-after-create toggle on.
// flow (B5): one create path, always starting the agent after creation.
if (consumePendingOpenCreateAgent()) {
openUnifiedCreate();
}
@@ -4,6 +4,7 @@ import * as React from "react";
import { useBackendProvidersQuery } from "@/features/agents/hooks";
import { RelayMeshAgentSection } from "@/features/mesh-compute/ui/RelayMeshAgentSection";
import { probeBackendProvider } from "@/shared/api/tauri";
import type { MeshAvailability } from "@/shared/api/tauriMesh";
import { ProviderConfigFields } from "./ProviderConfigFields";
import { emptyWhereToRunDraft, type WhereToRunDraft } from "./whereToRunIntent";
@@ -16,11 +17,9 @@ import { emptyWhereToRunDraft, type WhereToRunDraft } from "./whereToRunIntent";
* (`resolveBackendIntent`) and gates the submit button
* (`canSubmitWhereToRun`).
*
* Only rendered while the start-after-create toggle is ON "where to run"
* is instance state, and with the toggle off no instance exists. The parent
* discards the draft at submit when the toggle is off (the stale-intent
* guard), so a selection made before toggling off can never silently ride
* a definition-only create.
* Always rendered in the create flow agents are always started after
* creation. The parent owns the availability-flip guard so visibility and the
* submit path share one snapshot; this section only renders that snapshot.
*
* Honest-copy note: unlike the legacy create dialog, the mesh preset never
* overwrites the definition's fields only the minted instance carries the
@@ -30,10 +29,12 @@ import { emptyWhereToRunDraft, type WhereToRunDraft } from "./whereToRunIntent";
export function WhereToRunSection({
draft,
isPending,
meshAvailability,
onDraftChange,
}: {
draft: WhereToRunDraft;
isPending: boolean;
meshAvailability: MeshAvailability | null;
onDraftChange: (next: WhereToRunDraft) => void;
}) {
const backendProvidersQuery = useBackendProvidersQuery();
@@ -174,6 +175,7 @@ export function WhereToRunSection({
{!isProviderMode ? (
<>
<RelayMeshAgentSection
availability={meshAvailability}
current={{
// The definition's own fields are never overwritten by the mesh
// preset — only the minted instance carries it — so the override
@@ -1,12 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
definitionCreateDialogState,
intentForStartToggle,
resolveCreateIntent,
} from "./agentCreateIntent.ts";
import { createPersonaDialogState } from "./personaDialogState.ts";
import { resolveCreateIntent } from "./agentCreateIntent.ts";
test("resolveCreateIntent defaults to quick-start for un-migrated callers", () => {
// PersonaDialog's duplicate path calls handleSubmit without an intent until
@@ -19,25 +14,3 @@ test("resolveCreateIntent passes explicit intents through", () => {
assert.equal(resolveCreateIntent("definition"), "definition");
assert.equal(resolveCreateIntent("definition_start"), "definition_start");
});
test("intentForStartToggle maps the toggle to definition-family intents", () => {
assert.equal(intentForStartToggle(true), "definition_start");
assert.equal(intentForStartToggle(false), "definition");
});
test("toggle-on dialog copy is exactly the legacy create copy", () => {
assert.deepEqual(
definitionCreateDialogState(true),
createPersonaDialogState(),
);
});
test("toggle-off dialog copy differs only in description", () => {
const legacy = createPersonaDialogState();
const definitionOnly = definitionCreateDialogState(false);
assert.equal(definitionOnly.title, legacy.title);
assert.equal(definitionOnly.submitLabel, legacy.submitLabel);
assert.deepEqual(definitionOnly.initialValues, legacy.initialValues);
assert.notEqual(definitionOnly.description, legacy.description);
assert.match(definitionOnly.description, /without starting/);
});
@@ -1,8 +1,3 @@
import {
createPersonaDialogState,
type PersonaDialogState,
} from "./personaDialogState";
/**
* What the user is creating from the unified create dialog.
*
@@ -23,30 +18,3 @@ export function resolveCreateIntent(
): AgentCreateIntent {
return intent ?? "definition_start";
}
/** Maps the "Start agent after create" toggle to a definition-family intent. */
export function intentForStartToggle(
startAfterCreate: boolean,
): AgentCreateIntent {
return startAfterCreate ? "definition_start" : "definition";
}
/**
* Dialog copy for the definition-family create dialog. The toggle-on copy is
* derived from `createPersonaDialogState` so it cannot drift from the legacy
* create flow it replaces.
*/
export function definitionCreateDialogState(
startAfterCreate: boolean,
): PersonaDialogState {
const legacy = createPersonaDialogState();
if (startAfterCreate) {
return legacy;
}
return {
...legacy,
description:
"Create an agent without starting it. You can start it from its card at any time.",
};
}
@@ -241,36 +241,6 @@ test("localMode_gate_bypassed_for_meshMode", () => {
);
});
test("localMode_gate_active_when_mesh_selected_but_startAfterCreate_off", () => {
// Regression: when a user selects relay-mesh then turns off "Start agent
// after creation", the run draft is hidden and the backend intent is
// discarded — the saved definition has no mesh backing. The submit gate
// must require provider/model in this case.
//
// AgentDialog.tsx computes: createRunOnMesh = startAfterCreate && runDraft.runOn === "mesh"
// With startAfterCreate=false that evaluates to false regardless of runDraft,
// so computeLocalModeGate receives useMesh:false and must enforce the gate.
const result = computeLocalModeGate({
envVars: {},
isProviderMode: false,
model: "",
provider: "",
runtimeId: "buzz-agent",
useMesh: false, // = startAfterCreate(false) && runDraft.runOn("mesh")
});
assert.equal(
result.satisfied,
false,
"gate must NOT be bypassed when startAfterCreate is off, even if runDraft was set to mesh",
);
assert.deepEqual(
result.missingNormalizedFields,
["provider", "model"],
"both provider and model must be flagged as missing for the stale-mesh path",
);
});
// ── IMPORTANT 2: requiredEnvKeys surfaces correctly ───────────────────────
test("localMode_requiredEnvKeys_surfaces_anthropicKey", () => {
@@ -44,69 +44,45 @@ function meshDraft(overrides = {}) {
};
}
// ── Stale-intent edge (Pinky pin 2) ─────────────────────────────────────────
test("start toggle off discards a provider selection at submit", () => {
assert.equal(
resolveBackendIntent(providerDraft(), false),
null,
"definition-only create must never carry a backend intent",
);
});
test("start toggle off discards a mesh selection at submit", () => {
assert.equal(resolveBackendIntent(meshDraft(), false), null);
});
test("start toggle off always allows submit regardless of draft state", () => {
// Incomplete provider config with the toggle off: no instance is minted,
// so the draft must not block the definition-only create.
const incomplete = providerDraft({ providerConfig: {} });
assert.equal(canSubmitWhereToRun(incomplete, false), true);
});
// ── Submit gating carries over (Pinky pin 3) ────────────────────────────────
// ── Submit gating ───────────────────────────────────────────────────────────
test("provider selection blocks submit until the probe completes", () => {
const unprobed = providerDraft({ probedProvider: null });
assert.equal(canSubmitWhereToRun(unprobed, true), false);
assert.equal(canSubmitWhereToRun(unprobed), false);
});
test("provider selection blocks submit while required config is missing", () => {
const missing = providerDraft({ providerConfig: { size: "3" } });
assert.equal(canSubmitWhereToRun(missing, true), false);
assert.equal(canSubmitWhereToRun(missing), false);
assert.equal(providerConfigComplete(missing), false);
});
test("complete provider config allows submit", () => {
assert.equal(canSubmitWhereToRun(providerDraft(), true), true);
assert.equal(canSubmitWhereToRun(providerDraft()), true);
});
test("mesh selection blocks submit without a concrete serve target", () => {
assert.equal(
canSubmitWhereToRun(meshDraft({ meshTarget: null }), true),
canSubmitWhereToRun(meshDraft({ meshTarget: null })),
false,
"a model name alone is not a startable mesh selection",
);
assert.equal(
canSubmitWhereToRun(meshDraft({ meshModelId: "" }), true),
false,
);
assert.equal(canSubmitWhereToRun(meshDraft(), true), true);
assert.equal(canSubmitWhereToRun(meshDraft({ meshModelId: "" })), false);
assert.equal(canSubmitWhereToRun(meshDraft()), true);
});
test("local never gates submit", () => {
assert.equal(canSubmitWhereToRun(emptyWhereToRunDraft, true), true);
assert.equal(canSubmitWhereToRun(emptyWhereToRunDraft), true);
});
// ── Intent resolution ────────────────────────────────────────────────────────
test("local draft resolves to null intent", () => {
assert.equal(resolveBackendIntent(emptyWhereToRunDraft, true), null);
assert.equal(resolveBackendIntent(emptyWhereToRunDraft), null);
});
test("provider draft resolves with coerced config values", () => {
const intent = resolveBackendIntent(providerDraft(), true);
const intent = resolveBackendIntent(providerDraft());
assert.deepEqual(intent, {
type: "provider",
id: "blox",
@@ -115,7 +91,7 @@ test("provider draft resolves with coerced config values", () => {
});
test("mesh draft resolves with target and patch", () => {
const intent = resolveBackendIntent(meshDraft(), true);
const intent = resolveBackendIntent(meshDraft());
assert.equal(intent.type, "mesh");
assert.equal(intent.modelId, "mesh/model:Q4");
assert.equal(intent.target.endpointAddr, "10.0.0.1:9337");
@@ -123,12 +99,6 @@ test("mesh draft resolves with target and patch", () => {
});
test("mesh draft without patch or target resolves to null, not a broken intent", () => {
assert.equal(
resolveBackendIntent(meshDraft({ meshPatch: null }), true),
null,
);
assert.equal(
resolveBackendIntent(meshDraft({ meshTarget: null }), true),
null,
);
assert.equal(resolveBackendIntent(meshDraft({ meshPatch: null })), null);
assert.equal(resolveBackendIntent(meshDraft({ meshTarget: null })), null);
});
@@ -45,16 +45,8 @@ export function providerConfigComplete(draft: WhereToRunDraft): boolean {
* legacy dialog's gates: provider mode blocks until the probe succeeds and
* required config is filled; mesh mode blocks until a concrete serve target
* (not just a model name) is selected. Local always passes.
*
* When `startAfterCreate` is false there is no instance, so the draft is
* irrelevant and submit is always allowed (the intent is discarded see
* resolveBackendIntent).
*/
export function canSubmitWhereToRun(
draft: WhereToRunDraft,
startAfterCreate: boolean,
): boolean {
if (!startAfterCreate) return true;
export function canSubmitWhereToRun(draft: WhereToRunDraft): boolean {
if (draft.runOn === "mesh") {
return draft.meshModelId.trim().length > 0 && draft.meshTarget != null;
}
@@ -63,17 +55,12 @@ export function canSubmitWhereToRun(
/**
* Resolve the draft into the BackendIntent the instance mint should carry.
*
* Returns null for local AND whenever `startAfterCreate` is false: with the
* start toggle off no instance exists, so a leftover provider/mesh selection
* must be discarded at submit never silently attached to a definition-only
* create (the stale-intent edge).
* Returns null for local no backend override needed.
*/
export function resolveBackendIntent(
draft: WhereToRunDraft,
startAfterCreate: boolean,
): BackendIntent | null {
if (!startAfterCreate || draft.runOn === "local") {
if (draft.runOn === "local") {
return null;
}
if (draft.runOn === "mesh") {
@@ -1,14 +1,17 @@
import * as React from "react";
import { AlertCircle, Network } from "lucide-react";
import { meshAgentPreset, type MeshServeTarget } from "@/shared/api/tauriMesh";
import {
meshAgentPreset,
type MeshAvailability,
type MeshServeTarget,
} from "@/shared/api/tauriMesh";
import { Switch } from "@/shared/ui/switch";
import {
detectMeshPresetOverrides,
meshAgentPresetPatch,
} from "../applyMeshAgentPreset";
import { useMeshAvailability } from "../hooks/useMeshAvailability";
/**
* The "Run on relay mesh" entry in the agent create flow (WhereToRunSection).
@@ -21,6 +24,7 @@ import { useMeshAvailability } from "../hooks/useMeshAvailability";
* component is purely a controller for the mesh-specific subset.
*/
export function RelayMeshAgentSection({
availability,
current,
useMesh,
targetEndpointAddr,
@@ -28,6 +32,11 @@ export function RelayMeshAgentSection({
onModelIdChange,
onTargetChange,
}: {
/**
* Parent-owned mesh availability. Sharing this snapshot keeps visibility and
* draft reset synchronized when availability changes mid-dialog.
*/
availability: MeshAvailability | null;
/**
* Current draft state of the *fields the preset would overwrite*. Used to
* compute the override warning ("Using Relay mesh overrides this
@@ -56,27 +65,19 @@ export function RelayMeshAgentSection({
) => void;
onTargetChange: (target: MeshServeTarget | null) => void;
}) {
const { availability, error } = useMeshAvailability();
const [presetError, setPresetError] = React.useState<string | null>(null);
const disabled = availability == null || !availability.available;
const disabledReason =
availability == null
? (error ?? "Checking relay mesh availability…")
: (availability.reason ?? "The relay mesh isn't available right now.");
// Compute overrides from the currently-selected model's preset, *not* from
// an arbitrary one — the warning must reflect what'll actually happen.
const [overrides, setOverrides] = React.useState<string[]>([]);
// Null means the first availability fetch hasn't succeeded — either still
// loading, or the backend was built without mesh-llm and never will resolve.
// Hide the card entirely rather than showing a permanently disabled toggle.
if (availability === null) {
// Hide the card when availability hasn't resolved (built without mesh-llm,
// still loading) or when the mesh is genuinely unavailable (no serve nodes).
if (availability === null || !availability.available) {
return null;
}
const targets = availability?.serveTargets ?? [];
const targets = availability.serveTargets;
const selectedValue = targetEndpointAddr;
async function pickTarget(endpointAddr: string) {
@@ -128,15 +129,12 @@ export function RelayMeshAgentSection({
Run on relay mesh
</label>
<p className="text-sm text-muted-foreground">
{disabled
? disabledReason
: "Use a member's shared compute — no API key needed."}
Use a member's shared compute no API key needed.
</p>
</div>
<Switch
checked={useMesh}
data-testid="agent-relay-mesh-toggle"
disabled={disabled}
id="agent-relay-mesh-toggle"
onCheckedChange={onUseMeshChange}
/>
+26 -4
View File
@@ -764,6 +764,12 @@ declare global {
command: string;
payload: unknown;
}>;
/** Results emitted only after a mocked mesh-availability request resolves. */
__BUZZ_E2E_MESH_AVAILABILITY_RESULTS__?: Array<{
admitted: boolean;
available: boolean;
reason: string | null;
}>;
__BUZZ_E2E_WEBVIEW_ZOOM__?: number;
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
@@ -8208,6 +8214,7 @@ export function maybeInstallE2eTauriMocks() {
window.__BUZZ_E2E_COMMANDS__ = [];
window.__BUZZ_E2E_COMMAND_PAYLOADS__ = [];
window.__BUZZ_E2E_COMMAND_LOG__ = [];
window.__BUZZ_E2E_MESH_AVAILABILITY_RESULTS__ = [];
window.__BUZZ_E2E_SIGNED_EVENTS__ = [];
window.__BUZZ_E2E_WEBVIEW_ZOOM__ = 1;
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ = ({
@@ -8454,12 +8461,16 @@ export function maybeInstallE2eTauriMocks() {
window.__BUZZ_E2E_COMMAND_LOG__?.push({ command, payload });
switch (command) {
case "mesh_availability":
return {
case "mesh_availability": {
const result = {
capable: true,
admitted: mockMeshState.admitted,
available: mockMeshState.admitted,
reason: mockMeshState.admitted ? null : mockMeshState.denyReason,
available: mockMeshState.admitted && mockMeshState.models.length > 0,
reason: !mockMeshState.admitted
? mockMeshState.denyReason
: mockMeshState.models.length === 0
? "no relay mesh serve targets are available"
: null,
models: mockMeshState.models,
serveTargets: mockMeshState.models.map((model) => ({
modelId: model.id,
@@ -8476,6 +8487,17 @@ export function maybeInstallE2eTauriMocks() {
deviceName: "Mock desktop",
})),
};
return Promise.resolve(result).then((resolved) => {
// Unlike the command log above, record this only after the mocked
// result has resolved so E2E can distinguish it from loading.
window.__BUZZ_E2E_MESH_AVAILABILITY_RESULTS__?.push({
admitted: resolved.admitted,
available: resolved.available,
reason: resolved.reason,
});
return resolved;
});
}
case "mesh_installed_models":
return mockMeshState.models;
case "mesh_node_status":
+89 -4
View File
@@ -13,6 +13,11 @@ type E2eWindow = Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
__TAURI_INTERNALS__?: { invoke?: unknown };
__BUZZ_E2E_COMMANDS__?: string[];
__BUZZ_E2E_MESH_AVAILABILITY_RESULTS__?: Array<{
admitted: boolean;
available: boolean;
reason: string | null;
}>;
__BUZZ_E2E_SIGNED_EVENTS__?: Array<{
content: string;
kind: number;
@@ -57,7 +62,11 @@ async function signedEvents(page: import("@playwright/test").Page) {
async function setMesh(
page: import("@playwright/test").Page,
mesh: { admitted?: boolean; denyReason?: string },
mesh: {
admitted?: boolean;
models?: Array<{ id: string; name: string | null }>;
denyReason?: string;
},
) {
await page.evaluate((m) => {
(window as E2eWindow).__BUZZ_E2E_SET_MESH__?.(m);
@@ -285,10 +294,23 @@ test("a non-member cannot enable relay-mesh — membership is the gate", async (
await openNewAgentMenu(page);
await page.getByRole("menuitem", { name: /^New agent$/ }).click();
// The relay-mesh toggle stays disabled — a non-member cannot even opt into
// running on the mesh, let alone spawn an agent against it.
// Wait for the denied result, not just command invocation, so the hidden
// assertion cannot pass while availability is still loading.
await expect
.poll(async () =>
page.evaluate(
() =>
(window as E2eWindow).__BUZZ_E2E_MESH_AVAILABILITY_RESULTS__?.some(
(result) => !result.admitted && !result.available,
) ?? false,
),
)
.toBe(true);
// The relay-mesh section is hidden entirely for non-members — availability
// controls visibility, not just the enabled state of the toggle.
const toggle = page.getByTestId("agent-relay-mesh-toggle");
await expect(toggle).toBeDisabled();
await expect(toggle).toHaveCount(0);
// The flow never reaches an ensure-or-spawn, because membership gates the
// entry point itself. Sanity-check we never created an agent on the mesh.
@@ -296,6 +318,69 @@ test("a non-member cannot enable relay-mesh — membership is the gate", async (
expect(seq).not.toContain("create_managed_agent");
});
test("mesh availability flip mid-dialog resets the draft — no stale mesh intent on submit", async ({
page,
}) => {
await gotoApp(page);
await page.getByTestId("open-agents-view").click();
await openNewAgentMenu(page);
await page.getByRole("menuitem", { name: /^New agent$/ }).click();
await page.locator("#persona-display-name").fill("Mesh flip agent");
// Select a mesh target while availability is good.
const toggle = page.getByTestId("agent-relay-mesh-toggle");
await expect(toggle).toBeEnabled({ timeout: 10_000 });
await toggle.click();
await page
.getByTestId("agent-relay-mesh-model")
.selectOption({ label: "SmolLM2 135M — Mock desktop" });
// Wait for the asynchronous preset lookup to complete, proving the selected
// mesh target would otherwise supply a submit-ready mesh BackendIntent.
const submit = page.getByTestId("persona-dialog-submit");
await expect(submit).toBeEnabled();
// Flip availability to unavailable mid-dialog (simulates discovery losing
// every serve target after the user already selected one). The parent must
// synchronously turn the selected mesh draft into a local draft for submit;
// the passive cleanup is only allowed to persist that already-safe state.
await setMesh(page, { models: [] });
// The mesh section hides and the local gate applies again; a name alone
// cannot create an unrunnable local agent.
await expect(toggle).toHaveCount(0, { timeout: 10_000 });
await expect(submit).toBeDisabled();
// Explicitly configure the local fallback. Databricks v2 has no typed API
// key; its required host is an endpoint, not a secret credential.
await page.locator("#persona-llm-provider").click();
await page
.getByRole("menuitemradio", { name: "Databricks v2", exact: true })
.click();
await page.locator("#persona-model").click();
await page
.getByRole("button", { name: "Custom model...", exact: true })
.click();
await page.getByLabel("Custom model ID").fill("mock-local-model");
await page
.getByLabel("Value for DATABRICKS_HOST")
.fill("https://example.cloud.databricks.com");
await expect(submit).toBeEnabled();
// Submit the explicitly-configured local agent. It must not retain mesh
// intent after availability disappeared.
const before = (await commands(page)).length;
await submit.click();
await expect
.poll(async () => (await commands(page)).slice(before))
.toContain("create_managed_agent");
// The local create must not prepare a relay-mesh client.
expect((await commands(page)).slice(before)).not.toContain(
"mesh_prepare_relay_mesh_client",
);
});
test("saved relay-mesh agents restart via the backend serve-target preflight", async ({
page,
}) => {
+2 -2
View File
@@ -165,8 +165,8 @@ test("create agent supports parallelism and system prompt overrides", async ({
await expect(page.locator("#persona-parallelism")).toBeVisible();
await page.locator("#persona-parallelism").fill("3");
// The start-after-create toggle defaults ON, so submitting mints a running
// instance whose behavioral quad resolves from the definition.
// Submitting mints a running instance whose behavioral quad resolves from
// the definition (agents always start after creation).
await page.getByTestId("persona-dialog-submit").click();
await expect(