Skip to content
Open
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
65 changes: 60 additions & 5 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1792,17 +1792,25 @@ impl AcpClient {
}
"available_commands_update" => {
// Advertised slash commands (ACP slash-commands extension).
// Logged for observability; UI surfacing is a follow-up.
let names: Vec<&str> = update["availableCommands"]
.as_array()
.map(|cmds| cmds.iter().filter_map(|c| c["name"].as_str()).collect())
.unwrap_or_default();
// Forward the complete latest list through the encrypted observer
// stream so Desktop can offer commands for the originating agent.
let Some(commands) = update["availableCommands"].as_array() else {
return false;
};
let names: Vec<&str> = commands
.iter()
.filter_map(|command| command["name"].as_str())
.collect();
tracing::info!(
target: "acp::update",
"available_commands_update: {} commands [{}]",
names.len(),
names.join(", ")
);
self.observe(
"available_commands_captured",
serde_json::json!({ "commands": commands }),
);
false
}
"session_info_update" => {
Expand Down Expand Up @@ -3749,6 +3757,53 @@ mod tests {
})
}

#[tokio::test]
async fn available_commands_update_emits_complete_semantic_snapshot() {
let mut client = spawn_inert_client().await;
let observer = ObserverHandle::in_process();
client.set_observer(Some(observer.clone()), 3);

let msg = serde_json::json!({
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "test-session",
"update": {
"sessionUpdate": "available_commands_update",
"availableCommands": [
{ "name": "review", "description": "Review changes" },
{ "name": "deploy", "description": "Ship it" }
]
}
}
});
let _ = client.handle_session_update(&msg);

let events = observer.snapshot();
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind, "available_commands_captured");
assert_eq!(events[0].agent_index, Some(3));
assert_eq!(
events[0].payload,
serde_json::json!({
"commands": [
{ "name": "review", "description": "Review changes" },
{ "name": "deploy", "description": "Ship it" }
]
})
);
let mut empty = msg.clone();
empty["params"]["update"]["availableCommands"] = serde_json::json!([]);
let _ = client.handle_session_update(&empty);
let events = observer.snapshot();
assert_eq!(events.len(), 2);
assert_eq!(events[1].payload, serde_json::json!({ "commands": [] }));

empty["params"]["update"]["availableCommands"] = serde_json::Value::Null;
let _ = client.handle_session_update(&empty);
assert_eq!(observer.snapshot().len(), 2);
}

#[tokio::test]
async fn active_run_id_sets_on_string() {
let mut client = spawn_inert_client().await;
Expand Down
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default defineConfig({
"**/composer-selection-formatting.spec.ts",
"**/composer-tooltip-dismiss.spec.ts",
"**/mentions.spec.ts",
"**/slash-command-autocomplete.spec.ts",
"**/mention-spacing.spec.ts",
"**/mention-clipboard.spec.ts",
"**/cloud-provenance.spec.ts",
Expand Down
205 changes: 205 additions & 0 deletions desktop/src/features/agents/agentCommandCatalog.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import assert from "node:assert/strict";
import { beforeEach, describe, it } from "node:test";

import {
getAgentCommandCatalog,
initAgentCommandCatalog,
resetAgentCommandCatalog,
parseAvailableCommandsPayload,
recordAvailableCommandsUpdate,
resetAgentCommandCatalogForTests,
} from "./agentCommandCatalog.ts";

const OWNER = "aa".repeat(32);
const OTHER_OWNER = "bb".repeat(32);
const AGENT = "cc".repeat(32);

function installLocalStorage() {
const values = new Map();
globalThis.window = {
localStorage: {
get length() {
return values.size;
},
getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null,
removeItem: (key) => values.delete(key),
setItem: (key, value) => values.set(key, String(value)),
},
};
}

describe("agent command catalog", () => {
beforeEach(() => {
installLocalStorage();
resetAgentCommandCatalogForTests();
initAgentCommandCatalog("test-community");
});

it("sanitizes, bounds, and deduplicates advertised commands", () => {
const commands = parseAvailableCommandsPayload({
commands: [
{ name: "/review", description: " Review changes " },
{ name: "REVIEW", description: "duplicate" },
{ name: "bad name" },
{ name: "deploy", description: 42 },
],
});

assert.deepEqual(commands, [
{ name: "review", description: "Review changes" },
{ name: "deploy", description: null },
]);
});

it("keeps the latest complete command list per owner and agent", () => {
assert.equal(
recordAvailableCommandsUpdate(OWNER, AGENT, {
seq: 8,
timestamp: "2026-07-23T08:00:00Z",
payload: { commands: [{ name: "review", description: "Review" }] },
}),
true,
);
assert.equal(
recordAvailableCommandsUpdate(OWNER, AGENT, {
seq: 7,
timestamp: "2026-07-23T07:00:00Z",
payload: { commands: [{ name: "stale" }] },
}),
false,
);

assert.deepEqual(getAgentCommandCatalog(OWNER).get(AGENT)?.commands, [
{ name: "review", description: "Review" },
]);
assert.equal(getAgentCommandCatalog(OTHER_OWNER).has(AGENT), false);
});

it("rejects hidden controls in command names and removes them from descriptions", () => {
assert.deepEqual(
parseAvailableCommandsPayload({
commands: [
{ name: "rev\u0000iew" },
{ name: "rev\u202eiew" },
{ name: "rev\u200biew" },
{ name: "review", description: "a\u001bb\u202ec" },
],
}),
[{ name: "review", description: "a b c" }],
);
});

it("enforces count, name, and description bounds", () => {
assert.equal(
parseAvailableCommandsPayload({
commands: Array.from({ length: 300 }, (_, i) => ({ name: `cmd-${i}` })),
}).length,
256,
);
assert.deepEqual(
parseAvailableCommandsPayload({ commands: [{ name: "a".repeat(129) }] }),
[],
);
assert.equal(
parseAvailableCommandsPayload({
commands: [{ name: "review", description: "a".repeat(600) }],
})[0].description.length,
512,
);
});

it("ignores malformed snapshots and uses sequence to break timestamp ties", () => {
const timestamp = "2026-07-23T08:00:00Z";
recordAvailableCommandsUpdate(OWNER, AGENT, {
seq: 2,
timestamp,
payload: { commands: [{ name: "review" }] },
});
for (const event of [
{ seq: 3, timestamp, payload: {} },
{ seq: 3, timestamp: "invalid", payload: { commands: [] } },
{ seq: 1, timestamp, payload: { commands: [] } },
])
assert.equal(recordAvailableCommandsUpdate(OWNER, AGENT, event), false);
assert.deepEqual(getAgentCommandCatalog(OWNER).get(AGENT).commands, [
{ name: "review", description: null },
]);
});

it("re-sanitizes persisted commands and tolerates unavailable storage", () => {
window.localStorage.setItem(
`buzz-agent-command-catalog.v1:test-community:${OWNER}`,
JSON.stringify({
version: 1,
agents: {
[AGENT]: {
commands: [{ name: "bad\u202e" }, { name: "review" }],
seq: 1,
timestamp: "2026-07-23T08:00:00Z",
},
},
}),
);
assert.deepEqual(getAgentCommandCatalog(OWNER).get(AGENT).commands, [
{ name: "review", description: null },
]);
resetAgentCommandCatalogForTests();
initAgentCommandCatalog("test-community");
window.localStorage.getItem = () => {
throw new Error("storage disabled");
};
assert.equal(getAgentCommandCatalog(OWNER).size, 0);
});

it("treats an empty update as authoritative removal of prior commands", () => {
recordAvailableCommandsUpdate(OWNER, AGENT, {
seq: 1,
timestamp: "2026-07-23T08:00:00Z",
payload: { commands: [{ name: "review" }] },
});
recordAvailableCommandsUpdate(OWNER, AGENT, {
seq: 2,
timestamp: "2026-07-23T08:01:00Z",
payload: { commands: [] },
});

assert.deepEqual(getAgentCommandCatalog(OWNER).get(AGENT)?.commands, []);
});

it("hydrates a persisted owner-scoped catalog after restart", () => {
recordAvailableCommandsUpdate(OWNER, AGENT, {
seq: 3,
timestamp: "2026-07-23T08:00:00Z",
payload: { commands: [{ name: "review" }] },
});
resetAgentCommandCatalogForTests();
initAgentCommandCatalog("test-community");

assert.deepEqual(getAgentCommandCatalog(OWNER).get(AGENT)?.commands, [
{ name: "review", description: null },
]);
});

it("isolates the same owner and agent across community switches and restores on return", () => {
const event = {
seq: 1,
timestamp: "2026-07-23T08:00:00Z",
payload: { commands: [{ name: "review" }] },
};
recordAvailableCommandsUpdate(OWNER, AGENT, event);
resetAgentCommandCatalog();
assert.equal(getAgentCommandCatalog(OWNER).size, 0);
assert.equal(recordAvailableCommandsUpdate(OWNER, AGENT, event), false);
initAgentCommandCatalog("other-community");
assert.equal(getAgentCommandCatalog(OWNER).size, 0);
recordAvailableCommandsUpdate(OWNER, AGENT, {
...event,
payload: { commands: [{ name: "deploy" }] },
});
initAgentCommandCatalog("test-community");
assert.deepEqual(getAgentCommandCatalog(OWNER).get(AGENT).commands, [
{ name: "review", description: null },
]);
});
});
Loading