Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/bundled/builderlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ The bundled `block.builderlab` plugin adds Settings → Integrations → Builder
sign-out, and disposal.
- `login/` owns the login experience: pending state, cancellation, retry, email
display, and sign-out.
- `agents/` lists the signed-in account's remote agents beneath login. Requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required DCO sign-off

Commit d505d094d7c1cce8f1f18f56c1a84130604c87cc has no Signed-off-by trailer, so it violates the repository's per-commit DCO requirement and will not satisfy the hosted DCO check. Re-sign the commit using the verified effective author identity while preserving its actual authorship.

AGENTS.md reference: AGENTS.md:L151-L160

Useful? React with 👍 / 👎.

reuse the OAuth credential through native host HTTP; sign-out and navigation
discard late results. Listing does not enroll agents into Buzz communities.

The plugin uses the `BUZZ_BUILDERLAB_URL` [build input](../../../docs/configuration.md#builderlab-url-build-input)
as its server address and appends `/api/goose`.
Expand Down
80 changes: 80 additions & 0 deletions src/bundled/builderlab/agents/RemoteAgents.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";
import { act, cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, expect, it, vi } from "vitest";
import type { HostResponse } from "../../../features/host/service";
import { createOAuthSession } from "../oauth/session";
import { deferred } from "../test-helpers";
import { createAgentClient } from "./client";
import { RemoteAgents } from "./RemoteAgents";

afterEach(cleanup);
const row = {
agent_id: "one",
agent_name: "Helper",
agent_pubkey: "ab".repeat(32),
status: 2,
};
const response = (agents: unknown[], status = 200): HostResponse => ({
status,
headers: {},
body: JSON.stringify({ status: 1, agents }),
});
async function fixture() {
vi.stubEnv("VITE_BUZZ_BUILDERLAB_URL", "https://builderlab.example");
const session = createOAuthSession(async () => ({
value: "secret",
account: { email: "a@example.com" },
}));
await session.signIn();
const request = vi.fn(async () => response([row]));
const client = createAgentClient({ request, runCommand: vi.fn() }, session);
return { session, client, request };
}
afterEach(() => vi.unstubAllEnvs());
it("loads automatically, refreshes and hides account data on sign-out", async () => {
const h = await fixture();
render(<RemoteAgents {...h} active={() => true} />);
expect(await screen.findByText("Helper · Active")).toBeInTheDocument();
expect(screen.getByText(row.agent_pubkey)).toBeInTheDocument();
expect(document.body).not.toHaveTextContent("secret");
h.request.mockResolvedValue(response([]));
await userEvent
.setup()
.click(screen.getByRole("button", { name: "Refresh agents" }));
expect(await screen.findByText("No remote agents yet.")).toBeInTheDocument();
act(() => h.session.signOut());
expect(
screen.queryByRole("region", { name: "Remote agents" }),
).not.toBeInTheDocument();
});
it("shows a held loading state and retries a failed read", async () => {
const h = await fixture();
const held = deferred<HostResponse>();
h.request.mockReturnValueOnce(held.promise);
render(<RemoteAgents {...h} active={() => true} />);
expect(screen.getByRole("status")).toHaveTextContent("Loading remote agents");
expect(
screen.getByRole("button", { name: "Refresh agents" }),
).toHaveAttribute("aria-disabled", "true");
await act(async () => held.resolve(response([], 503)));
expect(screen.getByRole("alert")).toHaveTextContent("HTTP 503");
await userEvent.setup().click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByText("Helper · Active")).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
it.each(["sign-out", "unmount"])(
"ignores held reads after %s",
async (action) => {
const h = await fixture();
const held = deferred<HostResponse>();
h.request.mockReturnValue(held.promise);
const mounted = render(<RemoteAgents {...h} active={() => true} />);
if (action === "sign-out") act(() => h.session.signOut());
else mounted.unmount();
await act(async () => held.resolve(response([row])));
expect(screen.queryByText("Helper · Active")).not.toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
},
);
105 changes: 105 additions & 0 deletions src/bundled/builderlab/agents/RemoteAgents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useEffect, useState, useSyncExternalStore } from "react";
import { Button } from "../../../shared/design-system/ui/Button";
import type { LoginSnapshot, OAuthSession } from "../oauth/session";
import type { AgentClient, RemoteAgent } from "./client";

export function RemoteAgents({
client,
session,
active,
}: {
client: AgentClient;
session: OAuthSession;
active(): boolean;
}) {
const login = useSyncExternalStore(session.subscribe, session.snapshot);
return login.status === "signed-in" ? (
<AgentList client={client} account={login.account} active={active} />
) : null;
}

function AgentList({
client,
account,
active,
}: {
client: AgentClient;
account: LoginSnapshot["account"];
active(): boolean;
}) {
const [revision, setRevision] = useState(0);
const [agents, setAgents] = useState<readonly RemoteAgent[]>();
const [error, setError] = useState<string>();
const [loading, setLoading] = useState(true);
// biome-ignore lint/correctness/useExhaustiveDependencies: Account changes and explicit refreshes must restart the read.
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setAgents(undefined);
setError(undefined);
void client.list(controller.signal).then(
(rows) => {
if (!controller.signal.aborted) {
setAgents(rows);
setLoading(false);
}
},
(reason) => {
if (!controller.signal.aborted) {
setError(
reason instanceof Error ? reason.message : "Could not load agents.",
);
setLoading(false);
}
},
);
return () => controller.abort();
}, [client, account, revision]);
return (
<section
data-buzz-ui=""
className="mt-6 flex flex-col gap-3"
aria-label="Remote agents"
>
<h3 className="text-heading-sm text-primary">Remote agents</h3>
{loading && (
<p role="status" className="text-body-sm text-secondary">
Loading remote agents…
</p>
)}
{error && (
<p role="alert" className="text-body-sm text-danger">
{error}
</p>
)}
{agents?.length === 0 && (
<p className="text-body-sm text-secondary">No remote agents yet.</p>
)}
{agents && agents.length > 0 && (
<ul className="flex flex-col gap-3">
{agents.map((agent) => (
<li key={agent.id} className="flex flex-col gap-1">
<div className="text-body-sm text-primary">
{agent.name} · {agent.status}
</div>
<span className="break-all text-mono text-secondary">
{agent.pubkey}
</span>
</li>
))}
</ul>
)}
<div>
<Button
variant="outline"
loading={loading}
onClick={() => {
if (active()) setRevision((value) => value + 1);
}}
>
{error ? "Retry" : "Refresh agents"}
</Button>
</div>
</section>
);
}
152 changes: 152 additions & 0 deletions src/bundled/builderlab/agents/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import type { Host, HostResponse } from "../../../features/host/service";
import { createOAuthSession } from "../oauth/session";
import { deferred } from "../test-helpers";
import { createAgentClient } from "./client";

beforeEach(() =>
vi.stubEnv("VITE_BUZZ_BUILDERLAB_URL", "https://builderlab.example"),
);
afterEach(() => vi.unstubAllEnvs());
const row = {
agent_id: "agent-1",
agent_name: "Helper",
agent_pubkey: "ab".repeat(32),
status: "AGENT_STATUS_ACTIVE",
};
const response = (value: unknown, status = 200): HostResponse => ({
status,
headers: {},
body: JSON.stringify(value),
});
async function fixture() {
const session = createOAuthSession(async () => ({
value: "secret",
account: { email: "a@example.com" },
}));
await session.signIn();
const host: Host = {
runCommand: vi.fn(),
request: vi.fn(async () =>
response({ status: "LIST_AGENTS_STATUS_SUCCESS", agents: [row] }),
),
};
return {
session,
host,
client: createAgentClient(host, session),
signal: new AbortController().signal,
};
}
it("lists the authenticated account through the configured native host", async () => {
const h = await fixture();
expect(await h.client.list(h.signal)).toEqual([
{
id: row.agent_id,
name: row.agent_name,
pubkey: row.agent_pubkey,
status: "Active",
},
]);
expect(h.host.request).toHaveBeenCalledWith({
url: "https://builderlab.example/api/goose/v3/beekeeper/list-agents",
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-BB-Session-Credential": "secret",
},
body: "{}",
});
});
it.each([{}, { agents: [] }])(
"accepts an empty protobuf list %j",
async (value) => {
const h = await fixture();
vi.mocked(h.host.request).mockResolvedValue(
response({ status: 1, ...value }),
);
expect(await h.client.list(h.signal)).toEqual([]);
},
);
it.each([
["AGENT_STATUS_UNATTESTED", "Unattested"],
[1, "Unattested"],
["AGENT_STATUS_REVOKED", "Revoked"],
[3, "Revoked"],
["FUTURE_STATUS", "Unknown"],
[undefined, "Unknown"],
])(
"handles agent status %j without treating unknown as active",
async (status, expected) => {
const h = await fixture();
vi.mocked(h.host.request).mockResolvedValue(
response({ status: 1, agents: [{ ...row, status }] }),
);
expect((await h.client.list(h.signal))[0]?.status).toBe(expected);
},
);
it.each([
null,
{ status: 2 },
{ status: 1, agents: {} },
{ status: 1, agents: [null] },
{ status: 1, agents: [{ ...row, agent_pubkey: "bad" }] },
])("rejects invalid responses %j", async (value) => {
const h = await fixture();
vi.mocked(h.host.request).mockResolvedValue(response(value));
await expect(h.client.list(h.signal)).rejects.toThrow(
/invalid|did not return/,
);
});
it.each([401, 403, 500])(
"reports HTTP %s without disclosing the provider body",
async (status) => {
const h = await fixture();
vi.mocked(h.host.request).mockResolvedValue(
response({ error: "private-secret" }, status),
);
const error = await h.client.list(h.signal).catch((error: Error) => error);
expect(error).toBeInstanceOf(Error);
expect(String(error)).not.toContain("private-secret");
expect(h.session.snapshot().status).toBe(
status === 401 ? "signed-out" : "signed-in",
);
},
);
it.each(["sign-out", "new-login", "cancel", "dispose"])(
"discards a held result after %s",
async (action) => {
const h = await fixture();
const held = deferred<HostResponse>();
vi.mocked(h.host.request).mockReturnValue(held.promise);
const controller = new AbortController();
const pending = h.client.list(controller.signal);
if (action === "cancel") controller.abort();
else if (action === "dispose") h.session.dispose();
else {
h.session.signOut();
if (action === "new-login") await h.session.signIn();
}
held.resolve(
response(
{ status: 1, agents: [row] },
action === "new-login" ? 401 : 200,
),
);
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
if (action === "new-login")
expect(h.session.snapshot().status).toBe("signed-in");
},
);
it("does not dispatch signed-out or canceled requests", async () => {
const h = await fixture();
const controller = new AbortController();
controller.abort();
await expect(h.client.list(controller.signal)).rejects.toMatchObject({
name: "AbortError",
});
h.session.signOut();
await expect(h.client.list(h.signal)).rejects.toThrow("Sign in");
expect(h.host.request).not.toHaveBeenCalled();
});
Loading
Loading