mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.
## Contract
Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:
| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |
`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.
## Changes
**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper
**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`
**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins
**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.
**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.
**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.
**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.
## Tests
**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged
**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ
**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path
**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5
**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)
**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)
**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
266 lines
8.3 KiB
JavaScript
266 lines
8.3 KiB
JavaScript
/**
|
|
* Unit tests for tauri.ts — focused on `applyTauriRateLimitIfNeeded`, the
|
|
* extracted `relay rate-limited:` classifier that activates the shared
|
|
* rate-limit gate when Rust emits an HTTP 429 error prefix.
|
|
*
|
|
* Testing the exported production function (not a local copy) ensures any
|
|
* change to the classifier logic is immediately covered here.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
// ── Fake-timer + gate setup ───────────────────────────────────────────────────
|
|
|
|
let fakeNow = 0;
|
|
const pendingTimers = new Map();
|
|
let nextTimerId = 1;
|
|
|
|
function fakeSetTimeout(fn, ms) {
|
|
const id = nextTimerId++;
|
|
pendingTimers.set(id, { fn, fireAt: fakeNow + ms });
|
|
return id;
|
|
}
|
|
|
|
function fakeClearTimeout(id) {
|
|
pendingTimers.delete(id);
|
|
}
|
|
|
|
function tickTo(ms) {
|
|
fakeNow = ms;
|
|
for (const [id, { fn, fireAt }] of Array.from(pendingTimers.entries())) {
|
|
if (fireAt <= fakeNow) {
|
|
pendingTimers.delete(id);
|
|
fn();
|
|
}
|
|
}
|
|
}
|
|
|
|
const origDateNow = Date.now;
|
|
function setFakeNow(ms) {
|
|
fakeNow = ms;
|
|
Date.now = () => fakeNow;
|
|
}
|
|
|
|
globalThis.window = {
|
|
setTimeout: fakeSetTimeout,
|
|
clearTimeout: fakeClearTimeout,
|
|
};
|
|
|
|
setFakeNow(0);
|
|
|
|
const { isRateLimited, resetRateLimitGate } = await import(
|
|
"./relayRateLimitGate.ts"
|
|
);
|
|
|
|
// Import the production classifier from tauri.ts — tests must exercise the
|
|
// real function, not a local copy, so a logic change is always caught here.
|
|
const { applyTauriRateLimitIfNeeded } = await import("./tauri.ts");
|
|
|
|
function resetGate(startMs = 0) {
|
|
pendingTimers.clear();
|
|
nextTimerId = 1;
|
|
setFakeNow(startMs);
|
|
resetRateLimitGate();
|
|
}
|
|
|
|
// ── applyTauriRateLimitIfNeeded: relay rate-limited: prefix ───────────────────
|
|
|
|
test("relay rate-limited: prefix activates the rate-limit gate", () => {
|
|
resetGate(0);
|
|
applyTauriRateLimitIfNeeded("relay rate-limited: retry in 10s");
|
|
assert.equal(isRateLimited(), true, "gate must be active after 429 error");
|
|
});
|
|
|
|
test("relay rate-limited: prefix parses the retry hint and arms the gate duration", () => {
|
|
resetGate(0);
|
|
applyTauriRateLimitIfNeeded("relay rate-limited: retry in 7s");
|
|
// Gate should be active at 6s.
|
|
setFakeNow(6_000);
|
|
assert.equal(isRateLimited(), true);
|
|
// Gate should expire after 7s.
|
|
tickTo(7_001);
|
|
assert.equal(isRateLimited(), false);
|
|
});
|
|
|
|
test("relay rate-limited: with no hint uses the 10s default", () => {
|
|
resetGate(0);
|
|
applyTauriRateLimitIfNeeded("relay rate-limited: quota exceeded");
|
|
tickTo(9_999);
|
|
assert.equal(isRateLimited(), true);
|
|
tickTo(10_001);
|
|
assert.equal(isRateLimited(), false);
|
|
});
|
|
|
|
test("non-rate-limited error does not activate the gate", () => {
|
|
resetGate(0);
|
|
applyTauriRateLimitIfNeeded("relay returned 404 Not Found");
|
|
assert.equal(
|
|
isRateLimited(),
|
|
false,
|
|
"gate must remain inactive for unrelated errors",
|
|
);
|
|
});
|
|
|
|
test("relay rate-limited: prefix check is case-sensitive (Rust always emits lowercase)", () => {
|
|
resetGate(0);
|
|
// The prefix from Rust is always lowercase; mixed-case must not trigger it.
|
|
applyTauriRateLimitIfNeeded("Relay rate-limited: retry in 5s");
|
|
assert.equal(
|
|
isRateLimited(),
|
|
false,
|
|
"uppercase prefix must not activate gate (relay emits lowercase only)",
|
|
);
|
|
});
|
|
|
|
// ── fromRawAcpRuntimeCatalogEntry: custom row API-boundary (B-2) ─────────────
|
|
//
|
|
// These tests feed real raw custom catalog rows through fromRawAcpRuntimeCatalogEntry
|
|
// and verify the Rust→TypeScript mapping boundary: definition_env (snake_case)
|
|
// arrives as definitionEnv (camelCase), source "custom" is preserved, and the
|
|
// env round-trips end-to-end so a save-then-edit cycle cannot erase env.
|
|
|
|
const { fromRawAcpRuntimeCatalogEntry } = await import("./tauri.ts");
|
|
|
|
test("fromRawAcpRuntimeCatalogEntry maps definition_env to definitionEnv", () => {
|
|
const raw = {
|
|
id: "my-harness",
|
|
label: "My Harness",
|
|
availability: "available",
|
|
command: "my-bin",
|
|
source: "custom",
|
|
definition_env: { ANTHROPIC_API_KEY: "sk-test", MODEL: "claude-3" },
|
|
default_args: [],
|
|
can_auto_install: false,
|
|
requires_external_cli: false,
|
|
install_hint: "",
|
|
install_instructions_url: "",
|
|
};
|
|
const entry = fromRawAcpRuntimeCatalogEntry(raw);
|
|
assert.deepStrictEqual(entry.definitionEnv, {
|
|
ANTHROPIC_API_KEY: "sk-test",
|
|
MODEL: "claude-3",
|
|
});
|
|
assert.equal(entry.source, "custom");
|
|
});
|
|
|
|
test("fromRawAcpRuntimeCatalogEntry defaults definitionEnv to {} when absent", () => {
|
|
// Rust serialization skips empty BTreeMap, so definition_env will be absent
|
|
// for harnesses with no env defined — the mapper must default to {}.
|
|
const raw = {
|
|
id: "no-env-harness",
|
|
label: "No Env",
|
|
availability: "available",
|
|
command: "no-env-bin",
|
|
source: "custom",
|
|
default_args: [],
|
|
can_auto_install: false,
|
|
requires_external_cli: false,
|
|
install_hint: "",
|
|
install_instructions_url: "",
|
|
};
|
|
const entry = fromRawAcpRuntimeCatalogEntry(raw);
|
|
assert.deepStrictEqual(
|
|
entry.definitionEnv,
|
|
{},
|
|
"absent definition_env must map to empty object, not undefined",
|
|
);
|
|
});
|
|
|
|
test("fromRawAcpRuntimeCatalogEntry preserves source preset", () => {
|
|
const raw = {
|
|
id: "cursor",
|
|
label: "Cursor",
|
|
availability: "available",
|
|
command: "cursor",
|
|
source: "preset",
|
|
default_args: [],
|
|
can_auto_install: false,
|
|
requires_external_cli: false,
|
|
install_hint: "",
|
|
install_instructions_url: "",
|
|
};
|
|
const entry = fromRawAcpRuntimeCatalogEntry(raw);
|
|
assert.equal(entry.source, "preset");
|
|
assert.deepStrictEqual(entry.definitionEnv, {});
|
|
});
|
|
|
|
test("fromRawAcpRuntimeCatalogEntry env round-trips through edit payload shape", () => {
|
|
// Simulate the full save → re-open cycle: raw entry comes back from Rust
|
|
// with definition_env populated; the edit form reads entry.definitionEnv.
|
|
// Verify the env values are identical before and after the mapper.
|
|
const envValues = { OPENAI_API_KEY: "sk-live-abc", REGION: "us-east-1" };
|
|
const raw = {
|
|
id: "openai-harness",
|
|
label: "OpenAI",
|
|
availability: "not_installed",
|
|
command: "openai-agent",
|
|
source: "custom",
|
|
definition_env: envValues,
|
|
default_args: ["--acp"],
|
|
can_auto_install: false,
|
|
requires_external_cli: true,
|
|
install_hint: "Install the OpenAI CLI",
|
|
install_instructions_url: "https://platform.openai.com/docs",
|
|
};
|
|
const entry = fromRawAcpRuntimeCatalogEntry(raw);
|
|
// The edit form reads entry.definitionEnv; it must equal the original env.
|
|
assert.deepStrictEqual(
|
|
entry.definitionEnv,
|
|
envValues,
|
|
"env must round-trip: edit form must see the same values that Rust serialized",
|
|
);
|
|
});
|
|
|
|
// ── max_parallelism → maxParallelism mapping ──────────────────────────────────
|
|
|
|
test("fromRawAcpRuntimeCatalogEntry maps max_parallelism to maxParallelism when present", () => {
|
|
const raw = {
|
|
id: "openclaw",
|
|
label: "OpenClaw",
|
|
availability: "not_installed",
|
|
command: null,
|
|
source: "preset",
|
|
default_args: [],
|
|
can_auto_install: false,
|
|
requires_external_cli: false,
|
|
install_hint: "",
|
|
install_instructions_url: "",
|
|
max_parallelism: 5,
|
|
};
|
|
const entry = fromRawAcpRuntimeCatalogEntry(raw);
|
|
assert.equal(
|
|
entry.maxParallelism,
|
|
5,
|
|
"max_parallelism: 5 must map to maxParallelism: 5",
|
|
);
|
|
});
|
|
|
|
test("fromRawAcpRuntimeCatalogEntry omits maxParallelism when max_parallelism is absent", () => {
|
|
const raw = {
|
|
id: "goose",
|
|
label: "Goose",
|
|
availability: "available",
|
|
command: "goose",
|
|
source: "builtin",
|
|
default_args: [],
|
|
can_auto_install: false,
|
|
requires_external_cli: false,
|
|
install_hint: "",
|
|
install_instructions_url: "",
|
|
// No max_parallelism field — uncapped harness.
|
|
};
|
|
const entry = fromRawAcpRuntimeCatalogEntry(raw);
|
|
assert.equal(
|
|
entry.maxParallelism,
|
|
undefined,
|
|
"uncapped harness must have maxParallelism: undefined",
|
|
);
|
|
});
|
|
|
|
// ── Teardown ──────────────────────────────────────────────────────────────────
|
|
|
|
test("teardown — restore Date.now", () => {
|
|
Date.now = origDateNow;
|
|
assert.ok(true);
|
|
});
|