mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## What When editing an agent, show where it runs. The edit dialog previously showed nothing about the backend; the "Where to run" section only existed in the create flow. This adds a read-only **Run on** section to `AgentInstanceEditDialog`: - **Local agents:** "This computer". - **Provider agents (e.g. Kubernetes):** the provider id plus its saved config rows — context, namespace, image, resources, etc. — with labels humanized from the stored keys and rows in provider-schema order (locators first, request/limit pairs adjacent, alphabetical spillover for unknown providers). - Copy states these are the settings **saved at creation** and that the run location can't be changed afterwards (a new agent is required). ## Design decisions (from thread review with @Wren + @Sami) - **No provider probe on edit.** `info` is executable work, and its schema reflects the plugin *today* (including a freshly generated random namespace default) — not what this agent was deployed with. The stored record is the only honest source. - **Saved settings, not effective settings.** Optional fields a record omits (e.g. `service_account`) are defaulted by the provider at deploy time; we render only what was persisted and never synthesize today's defaults. - **Safe rendering of opaque provider config.** Values render as safe scalars only; arrays/objects degrade to a summary row (React throws on object children — a hand-edited record must not crash the dialog). Falsy-but-present values (`0`, `false`) render honestly. Secret-shaped keys are redacted using the same word-split heuristic as the create-time `validate_provider_config` gate — one definition of "looks like a secret". The gate already blocks such keys on every app write path; display-side redaction is screenshot hygiene and covers hand-edited records. - **`backendAgentId` intentionally excluded:** deploy-time runtime state written on start, not saved creation intent. - **Read-only, no form state.** The backend is immutable post-create (`UpdateManagedAgentRequest` has no backend field), so the section renders straight from `agent.backend` with no reset effect. - `ADVANCED_FIELDS_MOTION_TRANSITION` was duplicated in both agent dialogs; hoisted to `agentConfigOptions` (also keeps the edit dialog inside the file-size ratchet). ## Testing - Unit contract for `summarizeRunOn` (9 tests): scalar honesty incl. `0`/`false`, structured-value fallback, secret redaction fail-safe, preferred ordering with spillover, key humanization. - Playwright spec (4 tests, registered in the smoke project): kubernetes agent with the exact eight-key record a real create flow persisted, local agent, blox agent (`workstation_name`), and redacted secret-shaped keys from a hypothetical future provider. - `pnpm typecheck`, `pnpm check`, full `pnpm test` (3937 pass) green at this head. - Live screenshots posted in the originating Buzz thread. --------- Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
213 lines
6.6 KiB
TypeScript
213 lines
6.6 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { waitForAnimations } from "../helpers/animations";
|
|
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
|
|
|
const SHOTS = "test-results/edit-agent-run-on";
|
|
|
|
/**
|
|
* The exact persisted shape of a real kubernetes agent created through the
|
|
* app ("Loni" in managed-agents.json, traced by Sami in review): eight keys,
|
|
* `inactivity_seconds` a JSON number, everything else strings, and the
|
|
* optional `service_account` ABSENT — no schema default means the create
|
|
* flow never seeds it, so honest fixtures omit it too.
|
|
*/
|
|
const KUBERNETES_CONFIG = {
|
|
context: "docker-desktop",
|
|
cpu_limit: "1",
|
|
cpu_request: "1",
|
|
image:
|
|
"ghcr.io/block/buzz-sprig:sha-6530b58@sha256:17facfc7608d8ddb33bc056c9aaba1098f4ef6abe5655702fbfd7584d1f74d76",
|
|
inactivity_seconds: 7200,
|
|
memory_limit: "1Gi",
|
|
memory_request: "1Gi",
|
|
namespace: "buzz-agents-gfq7aq",
|
|
};
|
|
|
|
async function openEditDialog(
|
|
page: import("@playwright/test").Page,
|
|
agentName: string,
|
|
) {
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page
|
|
.getByRole("button", { name: `${agentName} agent profile` })
|
|
.click();
|
|
await page.getByTestId("user-profile-edit-agent").click();
|
|
await expect(page.getByTestId("edit-agent-dialog")).toBeVisible();
|
|
}
|
|
|
|
test("editing a kubernetes agent shows its saved run-on settings", async ({
|
|
page,
|
|
}) => {
|
|
const agent = TEST_IDENTITIES.charlie;
|
|
await installMockBridge(page, {
|
|
managedAgents: [
|
|
{
|
|
pubkey: agent.pubkey,
|
|
name: "Remote Helper",
|
|
status: "running",
|
|
channelNames: ["general"],
|
|
respondTo: "owner-only",
|
|
backend: {
|
|
type: "provider",
|
|
id: "kubernetes",
|
|
config: KUBERNETES_CONFIG,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await openEditDialog(page, "Remote Helper");
|
|
|
|
const runOn = page.getByTestId("edit-agent-run-on");
|
|
await expect(runOn).toBeVisible();
|
|
await expect(runOn.getByTestId("edit-agent-run-on-location")).toHaveText(
|
|
"kubernetes",
|
|
);
|
|
|
|
// Every saved field renders as a labeled row with its stored value.
|
|
await expect(runOn.getByTestId("edit-agent-run-on-namespace")).toContainText(
|
|
"buzz-agents-gfq7aq",
|
|
);
|
|
await expect(runOn.getByTestId("edit-agent-run-on-context")).toContainText(
|
|
"docker-desktop",
|
|
);
|
|
await expect(runOn.getByTestId("edit-agent-run-on-image")).toContainText(
|
|
"ghcr.io/block/buzz-sprig",
|
|
);
|
|
await expect(
|
|
runOn.getByTestId("edit-agent-run-on-cpu_request"),
|
|
).toContainText("CPU request");
|
|
await expect(
|
|
runOn.getByTestId("edit-agent-run-on-inactivity_seconds"),
|
|
).toContainText("7200");
|
|
// Real records omit the optional service_account (no schema default): the
|
|
// section must show only what was saved, never synthesize a row for it.
|
|
await expect(
|
|
runOn.getByTestId("edit-agent-run-on-service_account"),
|
|
).toHaveCount(0);
|
|
|
|
// The section explains immutability instead of pretending to be a form.
|
|
await expect(runOn).toContainText("can't be changed afterwards");
|
|
|
|
await runOn.scrollIntoViewIfNeeded();
|
|
await waitForAnimations(page);
|
|
await page
|
|
.getByTestId("edit-agent-dialog")
|
|
.screenshot({ path: `${SHOTS}/kubernetes-run-on.png` });
|
|
});
|
|
|
|
test("editing a local agent names this computer, with no config rows", async ({
|
|
page,
|
|
}) => {
|
|
const agent = TEST_IDENTITIES.tyler;
|
|
await installMockBridge(page, {
|
|
managedAgents: [
|
|
{
|
|
pubkey: agent.pubkey,
|
|
name: "Local Helper",
|
|
status: "stopped",
|
|
channelNames: ["general"],
|
|
respondTo: "owner-only",
|
|
backend: { type: "local" },
|
|
},
|
|
],
|
|
});
|
|
await openEditDialog(page, "Local Helper");
|
|
|
|
const runOn = page.getByTestId("edit-agent-run-on");
|
|
await expect(runOn.getByTestId("edit-agent-run-on-location")).toHaveText(
|
|
"This computer",
|
|
);
|
|
await expect(runOn.getByTestId("edit-agent-run-on-namespace")).toHaveCount(0);
|
|
|
|
await runOn.scrollIntoViewIfNeeded();
|
|
await waitForAnimations(page);
|
|
await page
|
|
.getByTestId("edit-agent-dialog")
|
|
.screenshot({ path: `${SHOTS}/local-run-on.png` });
|
|
});
|
|
|
|
test("a blox agent's single saved field renders with a humanized label", async ({
|
|
page,
|
|
}) => {
|
|
// The other real provider shape on developer machines: blox records
|
|
// persist exactly one key, exercising provider-generic humanization.
|
|
const agent = TEST_IDENTITIES.bob;
|
|
await installMockBridge(page, {
|
|
managedAgents: [
|
|
{
|
|
pubkey: agent.pubkey,
|
|
name: "Blox Helper",
|
|
status: "running",
|
|
channelNames: ["general"],
|
|
respondTo: "owner-only",
|
|
backend: {
|
|
type: "provider",
|
|
id: "blox",
|
|
config: { workstation_name: "tlongwell-sprout-home" },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await openEditDialog(page, "Blox Helper");
|
|
|
|
const runOn = page.getByTestId("edit-agent-run-on");
|
|
await expect(runOn.getByTestId("edit-agent-run-on-location")).toHaveText(
|
|
"blox",
|
|
);
|
|
const row = runOn.getByTestId("edit-agent-run-on-workstation_name");
|
|
await expect(row).toContainText("Workstation name");
|
|
await expect(row).toContainText("tlongwell-sprout-home");
|
|
|
|
await runOn.scrollIntoViewIfNeeded();
|
|
await waitForAnimations(page);
|
|
await page
|
|
.getByTestId("edit-agent-dialog")
|
|
.screenshot({ path: `${SHOTS}/blox-run-on.png` });
|
|
});
|
|
|
|
test("secret-shaped keys from an untrusted provider render redacted", async ({
|
|
page,
|
|
}) => {
|
|
const agent = TEST_IDENTITIES.charlie;
|
|
await installMockBridge(page, {
|
|
managedAgents: [
|
|
{
|
|
pubkey: agent.pubkey,
|
|
name: "Future Provider Agent",
|
|
status: "running",
|
|
channelNames: ["general"],
|
|
respondTo: "owner-only",
|
|
backend: {
|
|
type: "provider",
|
|
id: "some-future-provider",
|
|
config: {
|
|
endpoint: "https://provider.example",
|
|
api_token: "tok-do-not-show",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await openEditDialog(page, "Future Provider Agent");
|
|
|
|
const runOn = page.getByTestId("edit-agent-run-on");
|
|
await expect(runOn.getByTestId("edit-agent-run-on-endpoint")).toContainText(
|
|
"https://provider.example",
|
|
);
|
|
const tokenRow = runOn.getByTestId("edit-agent-run-on-api_token");
|
|
await expect(tokenRow).toContainText("••••••••");
|
|
await expect(tokenRow).not.toContainText("tok-do-not-show");
|
|
// The raw secret must not appear anywhere in the dialog.
|
|
await expect(page.getByTestId("edit-agent-dialog")).not.toContainText(
|
|
"tok-do-not-show",
|
|
);
|
|
|
|
await runOn.scrollIntoViewIfNeeded();
|
|
await waitForAnimations(page);
|
|
await page
|
|
.getByTestId("edit-agent-dialog")
|
|
.screenshot({ path: `${SHOTS}/redacted-run-on.png` });
|
|
});
|