fix(agents): add route-level tests, restore run-location context for instance edits

AgentEditDialog now wraps instance-present paths in AgentRunLocationProvider
(matching AgentDialog's instance-edit arm) so RespondToField's remote-backend
access warning has context when the agent runs on a remote machine.

Three new routing tests in agentDialogRouting.test.mjs pin:
  - instance-with-definition → AgentRunLocationProvider wrapping AgentInstanceEditDialog
  - instance-only             → same (unlinked-agent UpdateManagedAgentInput path)
  - definition-only           → AgentEditDefinitionOnlyDialog sub-component

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-08 13:03:43 -04:00
parent 12b517dfcd
commit 6f0d07f638
2 changed files with 142 additions and 10 deletions

View File

@@ -47,8 +47,10 @@ import type {
} from "@/shared/api/types";
import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent";
import type { AgentDefinitionSubmitOptions } from "./AgentDefinitionDialog";
import { runLocationForBackend } from "@/features/agents/lib/agentAccessWarning";
import { AgentDefinitionDialog } from "./AgentDefinitionDialog";
import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog";
import { AgentRunLocationProvider } from "./AgentRunLocationContext";
import { editPersonaDialogState } from "./personaDialogState";
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -103,18 +105,26 @@ export function AgentEditDialog({
// parallelism, env vars, harness pin, auto-restart, start-on-launch, instance
// name) and owns a well-tested save path. Route all instance-present contexts
// here so no I/L field is accidentally omitted.
//
// Wrap in AgentRunLocationProvider so RespondToField's access warning can
// name the machine the agent actually runs on (mirrors AgentDialog's instance-
// edit arm — backend → run location for the remote-backend warning path).
if (ctx.kind === "instance-with-definition" || ctx.kind === "instance-only") {
return (
<AgentInstanceEditDialog
agent={ctx.instance}
initialFocus={initialFocus}
open={open}
onOpenChange={onOpenChange}
onUpdated={onUpdated}
// R4 back-door deleted: avatar lives on the merged surface (definition
// section is visible in the profile panel when a definition exists).
onEditLinkedPersona={undefined}
/>
<AgentRunLocationProvider
runLocation={runLocationForBackend(ctx.instance.backend)}
>
<AgentInstanceEditDialog
agent={ctx.instance}
initialFocus={initialFocus}
open={open}
onOpenChange={onOpenChange}
onUpdated={onUpdated}
// R4 back-door deleted: avatar lives on the merged surface (definition
// section is visible in the profile panel when a definition exists).
onEditLinkedPersona={undefined}
/>
</AgentRunLocationProvider>
);
}

View File

@@ -5,6 +5,7 @@ import { AgentDialog } from "./AgentDialog.tsx";
import { AgentDefinitionDialog } from "./AgentDefinitionDialog.tsx";
import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog.tsx";
import { AgentRunLocationProvider } from "./AgentRunLocationContext.tsx";
import { AgentEditDialog } from "./AgentEditDialog.tsx";
// ── Phase 1B.3c routing pinning ─────────────────────────────────────────────
//
@@ -110,3 +111,124 @@ test("create mode routes to the internal create router, not a form directly", ()
);
assert.equal(element.type.name, "AgentCreateDialogRouter");
});
// ── Phase 1 merged surface routing (AgentEditDialog) ────────────────────────
//
// AgentEditDialog collapses R1–R7 onto one entry point. Like AgentDialog, the
// instance-present arms are hook-free so they can be called directly and the
// returned element tree inspected. The definition-only arm delegates to a
// sub-component that owns hooks, so we only confirm the type name there.
//
// These tests prove:
// • instance-with-definition → AgentInstanceEditDialog (all I/L fields live here)
// • instance-only → AgentInstanceEditDialog (unlinked agents save correctly)
// • definition-only → AgentEditDefinitionOnlyDialog sub-component
//
// They also confirm the AgentRunLocationProvider wrapper is present on instance
// paths (so RespondToField's remote-backend warning works).
const minimalDefinition = {
id: "def-abc",
displayName: "Bot",
avatarUrl: "",
systemPrompt: "Do stuff.",
runtime: "goose",
model: null,
provider: null,
isBuiltIn: false,
isActive: true,
namePool: [],
envVars: {},
respondTo: null,
respondToAllowlist: [],
parallelism: null,
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-01T00:00:00Z",
};
const minimalInstance = {
pubkey: "pk-0000",
name: "Bot",
avatarUrl: "",
systemPrompt: "Do stuff.",
model: null,
provider: null,
envVars: {},
respondTo: null,
respondToAllowlist: [],
parallelism: null,
autoRestartOnConfigChange: false,
startOnAppLaunch: false,
};
test("AgentEditDialog: instance-with-definition routes instance to AgentInstanceEditDialog", () => {
const ctx = {
kind: "instance-with-definition",
definition: minimalDefinition,
instance: minimalInstance,
};
const element = AgentEditDialog({
ctx,
open: true,
onOpenChange: noop,
});
// Wrapped in AgentRunLocationProvider for respondTo warning.
assert.equal(element.type, AgentRunLocationProvider);
const form = element.props.children;
assert.equal(
form.type,
AgentInstanceEditDialog,
"linked agent edit must reach AgentInstanceEditDialog (all I/L fields)",
);
assert.equal(form.props.agent, minimalInstance);
assert.equal(form.props.open, true);
assert.equal(
form.props.onEditLinkedPersona,
undefined,
"R4 back-door must be deleted",
);
});
test("AgentEditDialog: instance-only routes to AgentInstanceEditDialog (unlinked save)", () => {
const ctx = { kind: "instance-only", instance: minimalInstance };
const element = AgentEditDialog({
ctx,
open: true,
onOpenChange: noop,
});
// Wrapped in AgentRunLocationProvider for respondTo warning.
assert.equal(element.type, AgentRunLocationProvider);
const form = element.props.children;
assert.equal(
form.type,
AgentInstanceEditDialog,
"unlinked agent edit must reach AgentInstanceEditDialog (UpdateManagedAgentInput path)",
);
assert.equal(form.props.agent, minimalInstance);
});
test("AgentEditDialog: definition-only routes to definition sub-component", () => {
const ctx = { kind: "definition-only", definition: minimalDefinition };
const element = AgentEditDialog({
ctx,
open: true,
onOpenChange: noop,
});
// Hook-bearing sub-component: confirm by name rather than reference.
assert.equal(
typeof element.type,
"function",
"definition-only must route to a React component",
);
assert.equal(
element.type.name,
"AgentEditDefinitionOnlyDialog",
"definition-only must use the coordinator-wired sub-component",
);
});