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
949 changes: 870 additions & 79 deletions crates/buzz-acp/src/acp.rs

Large diffs are not rendered by default.

6,425 changes: 5,736 additions & 689 deletions crates/buzz-acp/src/lib.rs

Large diffs are not rendered by default.

587 changes: 572 additions & 15 deletions crates/buzz-acp/src/observer.rs

Large diffs are not rendered by default.

3,042 changes: 2,677 additions & 365 deletions crates/buzz-acp/src/pool.rs

Large diffs are not rendered by default.

1,998 changes: 1,710 additions & 288 deletions crates/buzz-acp/src/queue.rs

Large diffs are not rendered by default.

6,418 changes: 5,299 additions & 1,119 deletions crates/buzz-acp/src/relay.rs

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions crates/buzz-acp/tests/startup_signal_cleanup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#![cfg(unix)]

use nix::errno::Errno;
use nix::sys::signal::{kill, killpg, Signal};
use nix::unistd::Pid;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

struct TestProcess {
harness: Child,
adapter_pgid: Option<Pid>,
temp_dir: PathBuf,
}

impl Drop for TestProcess {
fn drop(&mut self) {
let _ = self.harness.kill();
let _ = self.harness.wait();
if let Some(pgid) = self.adapter_pgid {
let _ = killpg(pgid, Signal::SIGKILL);
}
let _ = fs::remove_dir_all(&self.temp_dir);
}
}

fn wait_until(mut condition: impl FnMut() -> bool, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if condition() {
return true;
}
std::thread::sleep(Duration::from_millis(20));
}
condition()
}

#[test]
fn repeated_sigterm_during_blocked_eager_initialize_reaps_adapter_process_group() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock")
.as_nanos();
let temp_dir =
std::env::temp_dir().join(format!("buzz-acp-sigterm-{}-{nonce}", std::process::id()));
fs::create_dir(&temp_dir).expect("create test directory");
let adapter_script = temp_dir.join("blocked-adapter.sh");
let adapter_pid_file = temp_dir.join("adapter.pid");
fs::write(
&adapter_script,
"#!/bin/sh\nset -eu\nprintf '%s\\n' \"$$\" > \"$BUZZ_TEST_ADAPTER_PID_FILE\"\nexec /bin/sleep 300\n",
)
.expect("write blocked adapter");
let mut permissions = fs::metadata(&adapter_script)
.expect("adapter metadata")
.permissions();
permissions.set_mode(0o700);
fs::set_permissions(&adapter_script, permissions).expect("make adapter executable");

let harness = Command::new(env!("CARGO_BIN_EXE_buzz-acp"))
.env_clear()
.env("BUZZ_TEST_ADAPTER_PID_FILE", &adapter_pid_file)
.arg("--private-key")
.arg("0000000000000000000000000000000000000000000000000000000000000001")
.arg("--agent-command")
.arg(&adapter_script)
.arg("--agent-args")
.arg("ignored")
.arg("--relay-url")
.arg("ws://127.0.0.1:9")
.arg("--no-presence")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn buzz-acp harness");
let mut process = TestProcess {
harness,
adapter_pgid: None,
temp_dir,
};

assert!(
wait_until(|| adapter_pid_file.exists(), Duration::from_secs(10)),
"blocked adapter did not publish its PID"
);
let adapter_pid: i32 = fs::read_to_string(&adapter_pid_file)
.expect("read adapter PID")
.trim()
.parse()
.expect("parse adapter PID");
let adapter_pgid = Pid::from_raw(adapter_pid);
process.adapter_pgid = Some(adapter_pgid);
assert_eq!(
killpg(adapter_pgid, None::<Signal>),
Ok(()),
"adapter process group must exist before SIGTERM"
);

kill(Pid::from_raw(process.harness.id() as i32), Signal::SIGTERM).expect("signal harness");
// The listener must remain installed after the graceful request. A second
// signal escalates cleanup without taking the default process-exit path,
// which would abandon process-group verification.
std::thread::sleep(Duration::from_millis(20));
if process.harness.try_wait().ok().flatten().is_none() {
kill(Pid::from_raw(process.harness.id() as i32), Signal::SIGTERM)
.expect("repeat signal harness during cleanup");
}
assert!(
wait_until(
|| process.harness.try_wait().ok().flatten().is_some(),
Duration::from_secs(10),
),
"harness did not exit after SIGTERM"
);
assert!(
wait_until(
|| killpg(adapter_pgid, None::<Signal>) == Err(Errno::ESRCH),
Duration::from_secs(5),
),
"adapter process group {adapter_pid} survived harness SIGTERM"
);
process.adapter_pgid = None;
}
13 changes: 13 additions & 0 deletions desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ with a TypeScript lookup table or an id comparison in a component.
published or removed. A queued update must stay visibly queued, and the
catalog itself must render only relay-confirmed publications — never an
optimistic local persona.
11. **Live model-switch success requires request-correlated, per-channel
application proof.** Every switch operation generates an unpredictable
request ID and accepts only signed, fresh `control_result` frames carrying
that exact request ID and model. `sent` and `recycling` are progress only;
each target channel must independently report `switched`, while explicit
terminal failures fail fast. A timeout is `pending`, never success. The
terminal-proof claim journal is relay/community-scoped process state:
`resetCommunityState()` must replace it on every community switch so a
claim observed on the outgoing relay cannot suppress proof on the incoming
relay.

## The tests that enforce this

Expand All @@ -128,6 +138,9 @@ with a TypeScript lookup table or an id comparison in a component.
`isCacheableDiscoveryResponse`, `deriveModelDiscoveryPending`,
`isSuccessfulEmptyDiscovery`. If the "reopen to retry" copy becomes inert
again, these tests will catch it.
- `lib/liveSwitchOutcome.test.mjs` — exact request correlation, signed
freshness, per-channel `switched` proof, pending-on-timeout behavior, replay
claims, and community-reset replacement.
- `desktop/tests/e2e/onboarding-agent-defaults.spec.ts` — onboarding behavior
acceptance coverage for readiness, failure states, defaults, navigation,
successful-empty vs failed optional-model discovery, and persistence races.
Expand Down
Loading