From 2b666681e8d85c471e97a86c2e2bc97eb8b3a288 Mon Sep 17 00:00:00 2001
From: Pinky
<5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Date: Tue, 22 Sep 2026 10:20:17 -0600
Subject: [PATCH 1/3] fix(agents): prepare desktop runtime and recover initial
status reads
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
---
docs/agent-control.md | 19 ++-
docs/contributing.md | 5 +-
scripts/build-agent-runtime.mjs | 44 +++++-
scripts/desktop-dev.mjs | 12 ++
src-tauri/tauri.conf.json | 2 +-
src/bundled/agents/AgentCreateDialog.tsx | 12 +-
src/bundled/agents/AgentsPage.test.tsx | 145 ++++++++++++++++++++
src/features/agents/control.test.ts | 121 +++++++++++++++-
src/features/agents/control.ts | 21 ++-
tests/integration/agent-runtime-fixture.mjs | 37 +++++
tests/integration/agent-runtime.test.mjs | 89 ++++++++++++
tests/integration/dev-commands.test.mjs | 29 +++-
tests/integration/worktree-icon.test.mjs | 7 +-
13 files changed, 524 insertions(+), 19 deletions(-)
create mode 100644 tests/integration/agent-runtime-fixture.mjs
create mode 100644 tests/integration/agent-runtime.test.mjs
diff --git a/docs/agent-control.md b/docs/agent-control.md
index 59af4fd70..0720fc633 100644
--- a/docs/agent-control.md
+++ b/docs/agent-control.md
@@ -8,7 +8,10 @@ agents. The only product entry point is ordinary desktop startup.
## Normal desktop workflow
Run from the feature worktree with `bin/just desktop`, not a management-only
-launcher. This opens **Buzz Foundation** using the ordinary live-development
+launcher. The command prepares the pinned agent runtime before starting Tauri;
+the first build may take several minutes. Later launches verify and reuse matching
+resources, rebuilding missing, stale or corrupt ones. Preparation failure stops
+launch rather than opening a desktop that cannot run agents. This opens **Buzz Foundation** using the ordinary live-development
configuration and persistent native settings. Coordinate the native rebuild/relaunch;
quit other Foundation copies first. Saved enabled agents can restore on startup.
Keep imported agents disabled and old Buzz running until an attended handover.
@@ -27,6 +30,8 @@ During a Create/profile wait, **Close** leaves the native operation running and
exposes the existing cards' recovery Stop. Closing before creation returns skips
automatic profile publication; refresh status and retry on the saved card. Late
completion never closes a subsequently opened dialog.
+Create is blocked with an explanation if this app’s runtime is unavailable;
+existing agents and profile retry remain intact.
The dev broker and native host must both support this flow. Packaged human
signing remains unavailable.
@@ -118,9 +123,11 @@ resources. Production has no disposable storage override or preview launch mode.
unavailable capability; no fetch fallback, local storage, signing or runner.
- `bundled/agents/AgentControlPanel.tsx`: compose with `{ control }` independently
of selected community or relay connectivity. It owns only observation and UI
- drafts. Its five-second refresh runs while visible/ready; reads coalesce. On
- errors it stops polling and exposes explicit Retry. Unmount clears the timer,
- not enabled intent or processes.
+ drafts. Its five-second refresh runs while visible/ready; reads coalesce. A read
+ rejected specifically because native startup is initializing or its lock is busy
+ stays pending for at most twenty 250ms waits. Genuine errors or exhausted retries
+ stop polling and expose explicit Retry; writes are never automatically retried.
+ Unmount clears the timer, not enabled intent or processes.
- Native host owns persistent state, credential custody, process groups, lock and
duplicate ownership checks, source import validation and sanitized diagnostics.
It must bound IPC operations and reject with deliberately user-facing strings;
@@ -224,7 +231,9 @@ resources. Production has no disposable storage override or preview launch mode.
## Runtime resources
-Build without launching any app or accessing old credentials:
+Ordinary `bin/just desktop` and `bin/pnpm tauri build` prepare these resources
+automatically. Direct Cargo builds do not run that JavaScript preparation step.
+To prepare/build without launching any app or accessing old credentials:
```sh
bin/pnpm install --frozen-lockfile
diff --git a/docs/contributing.md b/docs/contributing.md
index 88fcaafba..c9abf4df2 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -48,7 +48,10 @@ need their own validation.
e.g. `just web --port 1431 --host 127.0.0.1`. Vite uses the requested port
(default: 1430) or the next available port, allowing parallel browser development.
- `just desktop [args...]`: install locked dependencies and forward arguments to
- Tauri, e.g. `just desktop --port 1431 --no-watch`. The desktop adapter consumes
+ Tauri, e.g. `just desktop --port 1431 --no-watch`. Before launching, the adapter
+ builds the pinned agent runtime when missing/outdated, or verifies and reuses it.
+ A preparation failure stops launch; help does not prepare resources.
+ The desktop adapter consumes
`--port N` or `--port=N` to set both Vite's port and Tauri's development URL;
Tauri's own `--port` is for its static-file server, not Vite. Without this flag,
the existing Tauri configuration is unchanged (port 1430). Desktop requires the
diff --git a/scripts/build-agent-runtime.mjs b/scripts/build-agent-runtime.mjs
index 4e4d1b1f8..eee93e6cd 100644
--- a/scripts/build-agent-runtime.mjs
+++ b/scripts/build-agent-runtime.mjs
@@ -3,6 +3,9 @@ import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import {
readFile,
+ lstat,
+ access,
+ constants,
mkdir,
mkdtemp,
copyFile,
@@ -48,6 +51,46 @@ const target = (await run(join(root, "bin/rustc"), ["-vV"], true)).match(
/^host: (.+)$/m,
)?.[1];
if (!target) throw new Error("Could not resolve pinned Rust target");
+const destination = join(root, "src-tauri/resources/agent-runtime");
+const filenames = spec.tools.map((name) =>
+ process.platform === "win32" ? `${name}.exe` : name,
+);
+async function currentBundle() {
+ try {
+ if (!(await lstat(destination)).isDirectory()) return false;
+ const manifestPath = join(destination, "manifest.json");
+ const meta = await lstat(manifestPath);
+ if (!meta.isFile() || meta.size > 16384) return false;
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
+ if (
+ Object.keys(manifest).length !== 4 ||
+ manifest.version !== 1 ||
+ manifest.revision !== spec.revision ||
+ manifest.target !== target ||
+ Object.keys(manifest.files).length !== filenames.length
+ )
+ return false;
+ for (const filename of filenames) {
+ const path = join(destination, filename);
+ if (!(await lstat(path)).isFile()) return false;
+ await access(path, constants.X_OK);
+ const hash = createHash("sha256")
+ .update(await readFile(path))
+ .digest("hex");
+ if (manifest.files[filename] !== hash) return false;
+ }
+ return true;
+ } catch {
+ return false;
+ }
+}
+if (await currentBundle()) {
+ console.log(`Agent runtime ready (${spec.revision}, ${target})`);
+ process.exit(0);
+}
+console.log(
+ "Preparing the agent runtime; the first build can take several minutes.",
+);
await mkdir(join(root, "target"), { recursive: true });
const stage = await mkdtemp(join(root, "target/agent-runtime-stage-"));
try {
@@ -67,7 +110,6 @@ try {
"buzz-cli",
"git-credential-nostr",
]);
- const destination = join(root, "src-tauri/resources/agent-runtime");
await mkdir(destination, { recursive: true });
const files = {};
for (const name of spec.tools) {
diff --git a/scripts/desktop-dev.mjs b/scripts/desktop-dev.mjs
index e541ff66e..f405e8793 100644
--- a/scripts/desktop-dev.mjs
+++ b/scripts/desktop-dev.mjs
@@ -25,6 +25,18 @@ for (; index < args.length && args[index] !== "--"; index++) {
}
}
+// Prepare resources before Tauri can compile or observe an already-running Vite.
+// Its dev-server readiness timeout must not include a cold runtime build.
+if (!forwarded.some((arg) => arg === "--help" || arg === "-h")) {
+ const prepared = spawnSync(
+ process.execPath,
+ [fileURLToPath(new URL("./build-agent-runtime.mjs", import.meta.url))],
+ { stdio: "inherit" },
+ );
+ if (prepared.error) console.error(prepared.error.message);
+ if (prepared.signal) process.kill(process.pid, prepared.signal);
+ if (prepared.status !== 0) process.exit(prepared.status ?? 1);
+}
const config = {};
const icon = worktreeIcon(fileURLToPath(new URL("../", import.meta.url)));
if (icon) config.bundle = { icon: [icon] };
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 510871775..f1ce27a10 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -6,7 +6,7 @@
"build": {
"beforeDevCommand": "pnpm dev:desktop",
"devUrl": "http://localhost:1430",
- "beforeBuildCommand": "pnpm build",
+ "beforeBuildCommand": "node scripts/build-agent-runtime.mjs && pnpm build",
"frontendDist": "../dist"
},
"app": {
diff --git a/src/bundled/agents/AgentCreateDialog.tsx b/src/bundled/agents/AgentCreateDialog.tsx
index c1db3a046..a2bf7da7b 100644
--- a/src/bundled/agents/AgentCreateDialog.tsx
+++ b/src/bundled/agents/AgentCreateDialog.tsx
@@ -54,9 +54,10 @@ export function AgentCreateDialog({
state.data?.createAvailable &&
control.create
);
+ const runtimeBlocked = !state.data?.runtimeAvailable && !saved;
const blocked = busy || state.busy || state.status !== "ready";
const create = async () => {
- if (blocked || !available || !control.create) return;
+ if (blocked || runtimeBlocked || !available || !control.create) return;
setError(undefined);
setBusy(true);
try {
@@ -129,6 +130,13 @@ export function AgentCreateDialog({
an agent.
)}
+ {runtimeBlocked && (
+
+ This app’s agent runtime is unavailable. Repair or rebuild the
+ desktop app before creating an agent.
+ {state.data?.runtimeMessage && ` ${state.data.runtimeMessage}`}
+
+ )}
{busy && (
You can close this dialog to stop another agent. Saving
@@ -158,7 +166,7 @@ export function AgentCreateDialog({
diff --git a/src/bundled/agents/AgentsPage.test.tsx b/src/bundled/agents/AgentsPage.test.tsx
index 3c930f677..fd58c2ad8 100644
--- a/src/bundled/agents/AgentsPage.test.tsx
+++ b/src/bundled/agents/AgentsPage.test.tsx
@@ -22,6 +22,7 @@ const disposals: (() => void)[] = [];
afterEach(() => {
cleanup();
vi.restoreAllMocks();
+ vi.useRealTimers();
for (const dispose of disposals.splice(0)) dispose();
});
function setup(
@@ -591,3 +592,147 @@ for (const stage of ["create", "profile"] as const) {
});
}
}
+
+it("blocks creation before native writes when the runtime is missing and preserves the draft for recovery", async () => {
+ const prepare = vi.fn(async () => ({
+ id: "created",
+ pubkey: "cd".repeat(32),
+ }));
+ const commit = vi.fn();
+ const { f, control } = setup("connected", (fixture) => {
+ fixture.data.createAvailable = true;
+ fixture.data.runtimeAvailable = false;
+ fixture.data.runtimeMessage =
+ "Agent runtime is not packaged; build its resources first";
+ fixture.host.prepareCreate = prepare;
+ fixture.host.commitCreate = commit;
+ });
+ fireEvent.click(await screen.findByRole("button", { name: "Add agent" }));
+ const dialog = screen.getByRole("dialog", { name: "Create agent" });
+ fireEvent.change(within(dialog).getByLabelText("Name"), {
+ target: { value: "Calvin" },
+ });
+ const create = within(dialog).getByRole("button", { name: "Create agent" });
+ expect(create).toBeDisabled();
+ expect(within(dialog).getByRole("alert")).toHaveTextContent(
+ "agent runtime is unavailable",
+ );
+ const form = create.closest("form");
+ if (!form) throw Error("Missing create form");
+ await act(async () => fireEvent.submit(form));
+ expect(prepare).not.toHaveBeenCalled();
+ expect(commit).not.toHaveBeenCalled();
+ f.data.runtimeAvailable = true;
+ await act(async () => control.refresh());
+ expect(within(dialog).getByLabelText("Name")).toHaveValue("Calvin");
+ expect(create).toBeEnabled();
+ expect(within(dialog).queryByRole("alert")).toBeNull();
+});
+
+it("retries the same saved profile even if the runtime becomes unavailable", async () => {
+ const prepare = vi.fn(async () => ({
+ id: "created",
+ pubkey: "cd".repeat(32),
+ }));
+ const commit = vi.fn();
+ const profile = vi.fn();
+ vi.spyOn(communityApi, "communityRequest").mockResolvedValue({ auth: [] });
+ const { f, control } = setup("connected", (fixture) => {
+ fixture.data.createAvailable = true;
+ fixture.data.defaultWorkspace = "/fixture/workspace";
+ fixture.host.prepareCreate = prepare;
+ fixture.host.commitCreate = commit.mockImplementation(
+ async (_request, edit) => {
+ fixture.data.agents.push({
+ ...structuredClone(fixture.agent),
+ id: "created",
+ name: edit.name,
+ enabled: false,
+ status: "stopped",
+ profilePending: true,
+ });
+ return structuredClone(fixture.data);
+ },
+ );
+ fixture.host.publishProfile = profile
+ .mockRejectedValueOnce("Synthetic profile failure")
+ .mockImplementation(async () => {
+ const created = fixture.data.agents.find(
+ (agent) => agent.id === "created",
+ );
+ if (!created) throw Error("Missing created fixture");
+ created.profilePending = false;
+ return structuredClone(fixture.data);
+ });
+ });
+ const user = userEvent.setup();
+ await user.click(await screen.findByRole("button", { name: "Add agent" }));
+ const dialog = screen.getByRole("dialog", { name: "Create agent" });
+ await user.type(within(dialog).getByLabelText("Name"), "Calvin");
+ await user.click(
+ within(dialog).getByRole("button", { name: "Create agent" }),
+ );
+ await within(dialog).findByText(/Calvin is saved and stopped/);
+ expect(profile).toHaveBeenCalledExactlyOnceWith("created");
+ f.data.runtimeAvailable = false;
+ await act(async () => control.refresh());
+ const retry = within(dialog).getByRole("button", { name: "Retry profile" });
+ expect(retry).toBeEnabled();
+ await user.click(retry);
+ await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
+ expect(prepare).toHaveBeenCalledOnce();
+ expect(commit).toHaveBeenCalledOnce();
+ expect(profile.mock.calls).toEqual([["created"], ["created"]]);
+});
+
+for (const error of [
+ "Agent runtime is initializing; retry shortly",
+ "Another native agent operation is in progress",
+]) {
+ it(`shows agents without manual Retry after a transient native read: ${error}`, async () => {
+ vi.useFakeTimers();
+ let snapshot!: ReturnType;
+ setup("ready", (f) => {
+ snapshot = vi.spyOn(f.host, "snapshot").mockRejectedValueOnce(error);
+ });
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(0);
+ });
+ expect(screen.getByText("Reading local agent status…")).toBeVisible();
+ expect(screen.queryByRole("button", { name: "Retry status" })).toBeNull();
+ expect(snapshot).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(250);
+ });
+ expect(
+ screen.getAllByRole("article", { name: "Agent Fixture agent" }),
+ ).toHaveLength(2);
+ expect(screen.queryByRole("button", { name: "Retry status" })).toBeNull();
+ expect(snapshot).toHaveBeenCalledTimes(2);
+ });
+}
+it("shows Retry after persistent or genuine read failure without hiding the error", async () => {
+ vi.useFakeTimers();
+ const { f } = setup("ready", (f) => {
+ vi.spyOn(f.host, "snapshot").mockRejectedValue(
+ "Agent runtime is initializing; retry shortly",
+ );
+ });
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(5000);
+ });
+ expect(screen.getByRole("button", { name: "Retry status" })).toBeVisible();
+ expect(screen.getByText(/Could not refresh local agents/)).toBeVisible();
+ expect(f.host.snapshot).toHaveBeenCalledTimes(21);
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(10000);
+ });
+ expect(f.host.snapshot).toHaveBeenCalledTimes(21);
+ vi.mocked(f.host.snapshot).mockRejectedValue("Store is unreadable");
+ fireEvent.click(screen.getByRole("button", { name: "Retry status" }));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(5000);
+ });
+ expect(f.host.snapshot).toHaveBeenCalledTimes(22);
+ expect(screen.getByRole("button", { name: "Retry status" })).toBeVisible();
+});
diff --git a/src/features/agents/control.test.ts b/src/features/agents/control.test.ts
index 0f0a71269..46e62da0d 100644
--- a/src/features/agents/control.test.ts
+++ b/src/features/agents/control.test.ts
@@ -4,7 +4,10 @@ import { controlFixture } from "./control-testing";
import * as communityApi from "../communities/api";
import { agentDraft, agentEdit } from "../../bundled/agents/agent-edit";
-afterEach(() => vi.restoreAllMocks());
+afterEach(() => {
+ vi.restoreAllMocks();
+ vi.useRealTimers();
+});
function deferred() {
let resolve!: (value: T) => void;
const promise = new Promise((done) => {
@@ -742,3 +745,119 @@ it("a no-match mention is a native no-op and cannot erase an existing wake failu
expect(control.snapshot().mentionError).toBe(error);
control.dispose();
});
+
+const transientReads = [
+ "Agent runtime is initializing; retry shortly",
+ "Another native agent operation is in progress",
+];
+for (const error of transientReads) {
+ it(`keeps a coalesced read loading until native recovers from ${error}`, async () => {
+ vi.useFakeTimers();
+ const fixture = controlFixture();
+ const snapshot = vi
+ .spyOn(fixture.host, "snapshot")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error);
+ const control = createAgentControl(fixture.host);
+ const pending = control.refresh();
+ await vi.advanceTimersByTimeAsync(0);
+ expect(control.refresh()).toBe(pending);
+ expect(snapshot).toHaveBeenCalledTimes(1);
+ expect(control.snapshot()).toMatchObject({
+ status: "loading",
+ error: null,
+ });
+ await vi.advanceTimersByTimeAsync(250);
+ expect(snapshot).toHaveBeenCalledTimes(2);
+ expect(control.snapshot().status).toBe("loading");
+ await vi.advanceTimersByTimeAsync(250);
+ await pending;
+ expect(snapshot).toHaveBeenCalledTimes(3);
+ expect(control.snapshot().status).toBe("ready");
+ expect(control.snapshot().data).toEqual(fixture.data);
+ expect(fixture.calls).toEqual([{ action: "snapshot" }]);
+ control.dispose();
+ });
+}
+for (const previousSnapshot of [false, true]) {
+ it(`bounds transient reads and retains previous evidence: ${previousSnapshot}`, async () => {
+ vi.useFakeTimers();
+ const fixture = controlFixture();
+ const control = createAgentControl(fixture.host);
+ if (previousSnapshot) await control.refresh();
+ const snapshot = vi
+ .spyOn(fixture.host, "snapshot")
+ .mockRejectedValue(transientReads[0]);
+ const pending = control.refresh();
+ await vi.advanceTimersByTimeAsync(4999);
+ expect(snapshot).toHaveBeenCalledTimes(20);
+ expect(control.snapshot().status).toBe(
+ previousSnapshot ? "ready" : "loading",
+ );
+ await vi.advanceTimersByTimeAsync(1);
+ await pending;
+ expect(snapshot).toHaveBeenCalledTimes(21);
+ expect(control.snapshot().status).toBe("error");
+ expect(control.snapshot().data).toEqual(
+ previousSnapshot ? fixture.data : null,
+ );
+ await vi.advanceTimersByTimeAsync(10000);
+ expect(snapshot).toHaveBeenCalledTimes(21);
+ snapshot.mockResolvedValue(fixture.data);
+ await control.refresh();
+ expect(control.snapshot().status).toBe("ready");
+ control.dispose();
+ });
+}
+it("does not retry a genuine read failure or replay a write with a transient-looking error", async () => {
+ vi.useFakeTimers();
+ const fixture = controlFixture();
+ const control = createAgentControl(fixture.host);
+ const snapshot = vi
+ .spyOn(fixture.host, "snapshot")
+ .mockRejectedValueOnce("Unreadable store");
+ await control.refresh();
+ expect(control.snapshot().status).toBe("error");
+ await vi.advanceTimersByTimeAsync(5000);
+ expect(snapshot).toHaveBeenCalledTimes(1);
+ await control.refresh();
+ const action = vi
+ .spyOn(fixture.host, "action")
+ .mockRejectedValue(transientReads[1]);
+ await expect(control.action(fixture.agent.id, "start")).rejects.toThrow(
+ "Could not confirm",
+ );
+ await vi.advanceTimersByTimeAsync(5000);
+ expect(action).toHaveBeenCalledTimes(1);
+ expect(snapshot).toHaveBeenCalledTimes(2);
+ expect(control.snapshot().status).toBe("error");
+ control.dispose();
+});
+for (const boundary of ["dispose", "newer write"] as const) {
+ it(`retires a waiting status read on ${boundary}`, async () => {
+ vi.useFakeTimers();
+ const fixture = controlFixture();
+ const control = createAgentControl(fixture.host);
+ await control.refresh();
+ const snapshot = vi
+ .spyOn(fixture.host, "snapshot")
+ .mockRejectedValue(transientReads[1]);
+ const pending = control.refresh();
+ await vi.advanceTimersByTimeAsync(0);
+ expect(snapshot).toHaveBeenCalledTimes(1);
+ if (boundary === "dispose") control.dispose();
+ else await control.action(fixture.agent.id, "stop");
+ const before = control.snapshot();
+ await vi.advanceTimersByTimeAsync(5000);
+ await pending;
+ expect(snapshot).toHaveBeenCalledTimes(1);
+ expect(control.snapshot()).toBe(before);
+ if (boundary === "newer write") {
+ expect(control.snapshot().data?.agents[0]?.status).toBe("stopped");
+ expect(
+ fixture.calls.filter((call) => call.action === "stop"),
+ ).toHaveLength(1);
+ }
+ control.dispose();
+ });
+}
diff --git a/src/features/agents/control.ts b/src/features/agents/control.ts
index 6ec0a6c16..e2c71cdf5 100644
--- a/src/features/agents/control.ts
+++ b/src/features/agents/control.ts
@@ -182,10 +182,27 @@ export function createAgentControl(
const current = generation;
if (!state.data) update({ status: "loading", error: null });
const pending = Promise.resolve()
- .then(() => host.snapshot())
+ .then(async () => {
+ // Only read-only native startup/contention failures are transient. Keep
+ // the coalesced read loading for up to twenty 250ms waits, not a UI error.
+ for (let attempt = 0; !disposed && current === generation; attempt++) {
+ try {
+ return await host.snapshot();
+ } catch (error) {
+ if (disposed || current !== generation) return;
+ if (
+ attempt === 20 ||
+ (error !== "Agent runtime is initializing; retry shortly" &&
+ error !== "Another native agent operation is in progress")
+ )
+ throw error;
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ }
+ }
+ })
.then(
(data) => {
- if (current === generation) ready(data);
+ if (data && current === generation) ready(data);
},
() => {
if (current === generation)
diff --git a/tests/integration/agent-runtime-fixture.mjs b/tests/integration/agent-runtime-fixture.mjs
new file mode 100644
index 000000000..b16beb093
--- /dev/null
+++ b/tests/integration/agent-runtime-fixture.mjs
@@ -0,0 +1,37 @@
+import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
+import path from "node:path";
+
+// Real preparation script and manifest; only the compiler toolchain is synthetic.
+export function runtimeFixture(directory) {
+ for (const name of ["scripts", "runtime", "bin"])
+ mkdirSync(path.join(directory, name), { recursive: true });
+ for (const name of [
+ "scripts/build-agent-runtime.mjs",
+ "runtime/agent-runtime.json",
+ ])
+ copyFileSync(
+ new URL(`../../${name}`, import.meta.url),
+ path.join(directory, name),
+ );
+ const tool = (name, body) =>
+ writeFileSync(
+ path.join(directory, "bin", name),
+ `#!${process.execPath}\n${body}\n`,
+ { mode: 0o755 },
+ );
+ tool("rustc", 'console.log("host: fixture-target");');
+ tool(
+ "cargo",
+ `
+const fs = require("node:fs");
+const path = require("node:path");
+fs.appendFileSync("build-calls.jsonl", JSON.stringify(process.argv.slice(2)) + "\\n");
+if (fs.existsSync("fail-build")) process.exit(17);
+const stage = process.argv[process.argv.indexOf("--root") + 1];
+fs.mkdirSync(path.join(stage, "bin"), { recursive: true });
+const spec = JSON.parse(fs.readFileSync("runtime/agent-runtime.json", "utf8"));
+for (const name of spec.tools) fs.writeFileSync(path.join(stage, "bin",
+ process.platform === "win32" ? name + ".exe" : name), "fixture " + name, { mode: 0o755 });
+`,
+ );
+}
diff --git a/tests/integration/agent-runtime.test.mjs b/tests/integration/agent-runtime.test.mjs
new file mode 100644
index 000000000..8b6e861d5
--- /dev/null
+++ b/tests/integration/agent-runtime.test.mjs
@@ -0,0 +1,89 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import {
+ chmodSync,
+ existsSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import test from "node:test";
+import { runtimeFixture } from "./agent-runtime-fixture.mjs";
+
+test("runtime preparation builds missing resources, reuses verified files, and repairs stale or corrupt bundles", (t) => {
+ const directory = mkdtempSync(path.join(tmpdir(), "buzz-agent-runtime-"));
+ t.after(() => rmSync(directory, { recursive: true, force: true }));
+ runtimeFixture(directory);
+ const run = () => {
+ const result = spawnSync(
+ process.execPath,
+ ["scripts/build-agent-runtime.mjs"],
+ {
+ cwd: directory,
+ encoding: "utf8",
+ timeout: 10_000,
+ },
+ );
+ assert.ifError(result.error);
+ assert.equal(result.status, 0, result.stderr);
+ return result.stdout;
+ };
+ const bundle = path.join(directory, "src-tauri/resources/agent-runtime");
+ const manifestPath = path.join(bundle, "manifest.json");
+ const count = () =>
+ readFileSync(path.join(directory, "build-calls.jsonl"), "utf8")
+ .trim()
+ .split("\n").length;
+ assert.match(run(), /Verified inputs staged/);
+ assert.equal(count(), 1);
+ assert.match(run(), /Agent runtime ready/);
+ assert.equal(count(), 1, "warm preparation must not invoke Cargo");
+ const filename =
+ process.platform === "win32" ? "buzz-agent.exe" : "buzz-agent";
+ const binary = path.join(bundle, filename);
+ for (const mutation of [
+ () => writeFileSync(binary, "corrupt"),
+ () => rmSync(binary),
+ () => writeFileSync(manifestPath, "not json"),
+ () =>
+ writeFileSync(
+ manifestPath,
+ " ".repeat(16385) + readFileSync(manifestPath, "utf8"),
+ ),
+ () => {
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
+ manifest.unexpected = true;
+ writeFileSync(manifestPath, JSON.stringify(manifest));
+ },
+ ...["revision", "target", "version"].map((key) => () => {
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
+ manifest[key] = "outdated";
+ writeFileSync(manifestPath, JSON.stringify(manifest));
+ }),
+ ...(process.platform === "win32" ? [] : [() => chmodSync(binary, 0o644)]),
+ ]) {
+ const before = count();
+ mutation();
+ assert.match(run(), /Verified inputs staged/);
+ assert.equal(count(), before + 1);
+ assert.ok(existsSync(binary));
+ assert.match(run(), /Agent runtime ready/);
+ assert.equal(count(), before + 1);
+ }
+});
+
+test("packaged desktop build prepares runtime before frontend compilation", () => {
+ const config = JSON.parse(
+ readFileSync(
+ new URL("../../src-tauri/tauri.conf.json", import.meta.url),
+ "utf8",
+ ),
+ );
+ assert.equal(
+ config.build.beforeBuildCommand,
+ "node scripts/build-agent-runtime.mjs && pnpm build",
+ );
+});
diff --git a/tests/integration/dev-commands.test.mjs b/tests/integration/dev-commands.test.mjs
index 0eeb746fa..887da5a4a 100644
--- a/tests/integration/dev-commands.test.mjs
+++ b/tests/integration/dev-commands.test.mjs
@@ -13,8 +13,9 @@ import {
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
+import { runtimeFixture } from "./agent-runtime-fixture.mjs";
-function recipe(name, ...args) {
+function recipeWithRuntime(failRuntime, name, ...args) {
const directory = mkdtempSync(path.join(tmpdir(), "buzz-dev-command-"));
const callsFile = path.join(directory, "calls.jsonl");
try {
@@ -31,11 +32,16 @@ function recipe(name, ...args) {
new URL("../../scripts/worktree-icon.mjs", import.meta.url),
path.join(directory, "scripts/worktree-icon.mjs"),
);
- // Run the real recipes and adapter, recording only the package-manager boundary.
+ runtimeFixture(directory);
+ if (failRuntime) writeFileSync(path.join(directory, "fail-build"), "");
+ // Run the real recipes, adapter and preparation; never open a native app.
symlinkSync(process.execPath, path.join(directory, "node"));
writeFileSync(
path.join(directory, "pnpm"),
- `#!${process.execPath}\nrequire("node:fs").appendFileSync(process.env.BUZZ_TEST_CALLS, JSON.stringify(process.argv.slice(2)) + "\\n");\n`,
+ `#!${process.execPath}\nrequire("node:fs").appendFileSync(process.env.BUZZ_TEST_CALLS, JSON.stringify(process.argv.slice(2)) + "\\n");
+if (process.argv[2] === "tauri" && !process.argv.includes("--help") && !process.argv.includes("-h")) {
+ if (!require("node:fs").existsSync("src-tauri/resources/agent-runtime/manifest.json")) process.exit(19);
+}\n`,
{ mode: 0o755 },
);
// Set PATH inside the recipe shell: Hermit proxies restore their own PATH.
@@ -69,12 +75,27 @@ function recipe(name, ...args) {
const calls = existsSync(callsFile)
? readFileSync(callsFile, "utf8").trim().split("\n").map(JSON.parse)
: [];
- return { ...result, calls };
+ const built = existsSync(path.join(directory, "build-calls.jsonl"));
+ return { ...result, calls, built };
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
+function recipe(name, ...args) {
+ return recipeWithRuntime(false, name, ...args);
+}
+
+test("runtime preparation failure prevents desktop launch, while help does not build", () => {
+ const result = recipeWithRuntime(true, "desktop");
+ assert.notEqual(result.status, 0);
+ assert.equal(result.built, true);
+ assert.deepEqual(result.calls, [["install", "--frozen-lockfile"]]);
+ const help = recipeWithRuntime(true, "desktop", "--help");
+ assert.equal(help.status, 0, help.stderr);
+ assert.equal(help.built, false);
+});
+
function launched(target, ...args) {
const result = recipe(target, ...args);
assert.equal(result.status, 0, result.stderr);
diff --git a/tests/integration/worktree-icon.test.mjs b/tests/integration/worktree-icon.test.mjs
index 5c090957e..b1aa728dc 100644
--- a/tests/integration/worktree-icon.test.mjs
+++ b/tests/integration/worktree-icon.test.mjs
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
+import { runtimeFixture } from "./agent-runtime-fixture.mjs";
// Real Git worktrees and subprocesses; only Swift rendering and pnpm are stubs.
// The macOS-only launcher wiring also runs on a Mac, without opening an app.
@@ -71,7 +72,7 @@ function fixture(t) {
);
env.PATH = `${tools}${path.delimiter}${env.PATH}`;
for (const cwd of [main, linked]) {
- mkdirSync(path.join(cwd, "scripts"));
+ runtimeFixture(cwd);
for (const file of ["desktop-dev.mjs", "worktree-icon.mjs"]) {
copyFileSync(
new URL(`../../scripts/${file}`, import.meta.url),
@@ -168,7 +169,9 @@ test("macOS launcher combines icon and port before explicit config and runner ar
const launch = () =>
JSON.parse(
run(linked, ["scripts/desktop-dev.mjs", "--port=1431", ...forwarded])
- .stdout,
+ .stdout.trim()
+ .split("\n")
+ .at(-1),
);
const call = launch();
assert.deepEqual(call.slice(0, 3), ["tauri", "dev", "--config"]);
From 4c84ff1a21ef6e07a6416fb5040b06bed5b8c2b2 Mon Sep 17 00:00:00 2001
From: Pinky
<5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Date: Tue, 22 Sep 2026 10:51:55 -0600
Subject: [PATCH 2/3] test(gifs): wait for cleared-search results before
selection
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
---
tests/browser/gifs.spec.mjs | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/tests/browser/gifs.spec.mjs b/tests/browser/gifs.spec.mjs
index cc0f28780..c29d3c6a4 100644
--- a/tests/browser/gifs.spec.mjs
+++ b/tests/browser/gifs.spec.mjs
@@ -52,12 +52,16 @@ test("relay-backed GIF tab searches KLIPY and inserts URL-only media", async ({
});
});
await page.route("**/api/relay/*/gifs", async (route) => {
- requests.push(route.request().postDataJSON());
+ const request = route.request().postDataJSON();
+ requests.push(request);
await route.fulfill({
json: {
result: true,
data: {
- data: [result(1, "Hello", 240), result(2, "Celebrate", 320)],
+ data: [
+ result(1, request.query ? "Hello" : "Trending Hello", 240),
+ result(2, "Celebrate", 320),
+ ],
},
},
});
@@ -252,15 +256,22 @@ test("relay-backed GIF tab searches KLIPY and inserts URL-only media", async ({
await clear.click();
await expect(search).toHaveValue("");
await expect(search).toBeFocused();
+ // Clearing starts a debounced request. Old results have the same count, so
+ // wait for the cleared query's rendered result before clicking its replacement.
+ const trending = page.getByRole("button", {
+ name: "Choose Trending Hello",
+ exact: true,
+ });
+ await expect(trending).toBeVisible();
await expect(
page.getByTestId("klipy-gif-grid").getByRole("button"),
).toHaveCount(2);
await page.screenshot({ path: testInfo.outputPath("gif-picker.png") });
- await page.getByRole("button", { name: "Choose Hello", exact: true }).click();
+ await trending.click();
await expect(draft).toHaveJSProperty(
"value",
- "",
+ "",
);
await expect(
page.getByRole("searchbox", { name: "Search GIFs" }),
From 58238811e3ca0fcc91adbdbcf70b607924324539 Mon Sep 17 00:00:00 2001
From: Pinky
<5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Date: Tue, 22 Sep 2026 11:14:07 -0600
Subject: [PATCH 3/3] test(channels): record warm-switch timing boundaries
Preserve the existing click-to-paint-opportunity budget and visibility check while recording synchronous dispatch, frame callbacks and first visibility in the existing evidence report. This is diagnostic evidence for an unexplained hosted timing failure, not a performance repair.
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
---
tests/browser/channel-opening.spec.mjs | 33 +++++++++++++++++++++-----
1 file changed, 27 insertions(+), 6 deletions(-)
diff --git a/tests/browser/channel-opening.spec.mjs b/tests/browser/channel-opening.spec.mjs
index b700ef66b..de634cec8 100644
--- a/tests/browser/channel-opening.spec.mjs
+++ b/tests/browser/channel-opening.spec.mjs
@@ -107,18 +107,26 @@ test("cold opening bypasses held DM labels; warm switching paints within 100ms w
const before = submittedHeads.length;
// Browser-clock click → first visible row → paint, excluding Playwright IPC.
for (const name of ["Alpha", "Beta", "Alpha", "Beta"]) {
- const visibleMs = await page
+ const timing = await page
.getByRole("button", { name, exact: true })
.evaluate(
async (button, { name, ids }) => {
const start = performance.now();
button.click();
+ const clickDispatchMs = performance.now() - start;
+ const frames = [];
+ let firstVisibleMs;
+ let paintOpportunity;
await new Promise((resolve, reject) => {
const deadline = setTimeout(
() => reject(new Error("warm switch did not paint")),
1000,
);
- const check = () => {
+ const check = (timestamp) => {
+ frames.push({
+ frameMs: timestamp - start,
+ callbackMs: performance.now() - start,
+ });
const composer = document.querySelector(
`[role="textbox"][aria-label="Message #${name}"]`,
);
@@ -140,14 +148,27 @@ test("cold opening bypasses held DM labels; warm switching paints within 100ms w
},
);
if (!visible || !composer) return requestAnimationFrame(check);
- requestAnimationFrame(() => {
+ firstVisibleMs = performance.now() - start;
+ requestAnimationFrame((timestamp) => {
+ paintOpportunity = {
+ frameMs: timestamp - start,
+ callbackMs: performance.now() - start,
+ };
clearTimeout(deadline);
resolve();
});
};
requestAnimationFrame(check);
});
- return performance.now() - start;
+ const warmVisibleMs = performance.now() - start;
+ return {
+ warmVisibleMs,
+ // Synchronous button.click() only, not all React/render work.
+ clickDispatchMs,
+ frames,
+ firstVisibleMs,
+ paintOpportunity,
+ };
},
{
name,
@@ -156,8 +177,8 @@ test("cold opening bypasses held DM labels; warm switching paints within 100ms w
.map((event) => event.id),
},
);
- app.report.measurements.push({ name, warmVisibleMs: visibleMs });
- expect(visibleMs).toBeLessThan(100);
+ app.report.measurements.push({ name, ...timing });
+ expect(timing.warmVisibleMs).toBeLessThan(100);
}
expect(submittedHeads).toHaveLength(before);
const diagnostics = page