agent-joule/crates/joule_coded/tests/acp.rs
David Charlot 07cd0e287c joule_coded: serve on-device inference as an OpenAI-compatible metered endpoint
Expose the worker's sealed local generation (the L3 cascade tier) on the wire the
inference ecosystem speaks. New joule_coded::responses answers /v1/chat/completions
and /v1/responses via the Mesh standard's std-only server (mesh-openai/core/engine/
meter/router/schema, reused unchanged): mesh-core routes each request cheapest-
sufficient, meters the decode, and returns the sealed mesh/1 receipt in
usage.energy (microjoules, uj-per-accepted-token, honest provenance). The on-device
model is injected as `generate` — scripted in tests, JouleClaw's GGUF runtime in
production, the same injection run_loop uses.

- main.rs: start the server on its own thread when JOULE_CODE_RESPONSES_ADDR is set
  and a model is loaded (feature l3), so it never blocks the control protocol.
- lib.rs: pub mod responses.
- Cargo.toml: mesh-* deps (open-standards rev 0a8fb779, cached locally).
- tests/acp.rs, tests/fanout.rs: updated alongside.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 11:01:27 -04:00

87 lines
3.5 KiB
Rust

//! The headless ACP/L4 client, deterministically: drive `run_acp_prompt` against
//! a mock ACP agent (a tiny Python script that speaks the same newline-delimited
//! JSON-RPC handshake a real vendor CLI would) — proving the client does
//! initialize → session/new → session/prompt, assembles streamed assistant text,
//! and auto-answers a permission request, with no real vendor binary or login.
use joule_coded::acp::run_acp_prompt;
use std::io::Write;
/// A mock ACP agent: replies to initialize + session/new, then on session/prompt
/// asks one permission, streams two text chunks, and ends the turn.
const MOCK_AGENT: &str = r#"
import sys, json
def send(obj):
sys.stdout.write(json.dumps(obj) + "\n"); sys.stdout.flush()
for line in sys.stdin:
line = line.strip()
if not line: continue
msg = json.loads(line)
method, mid = msg.get("method"), msg.get("id")
if method == "initialize":
send({"jsonrpc":"2.0","id":mid,"result":{"protocolVersion":1,"agentCapabilities":{}}})
elif method == "session/new":
send({"jsonrpc":"2.0","id":mid,"result":{"sessionId":"sess-1"}})
elif method == "session/prompt":
# ask permission (agent->client request id 100)
send({"jsonrpc":"2.0","id":100,"method":"session/request_permission",
"params":{"sessionId":"sess-1","options":[
{"optionId":"allow","name":"Allow","kind":"allow_once"},
{"optionId":"reject","name":"Reject","kind":"reject_once"}]}})
# read the client's permission answer
ans = json.loads(sys.stdin.readline().strip())
# stream two assistant chunks
for chunk in ["Hello ", "from ACP"]:
send({"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"sess-1",
"update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":chunk}}}})
send({"jsonrpc":"2.0","id":mid,"result":{"stopReason":"end_turn"}})
break
"#;
#[test]
// Availability probe: blocking on `python3 --version` is fine in a test.
#[allow(clippy::disallowed_methods)]
fn headless_acp_prompt_handshakes_and_assembles_streamed_text() {
if std::process::Command::new("python3")
.arg("--version")
.output()
.is_err()
{
eprintln!("skipping: python3 not available");
return;
}
let dir = std::env::temp_dir().join(format!("jc-acp-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let script = dir.join("mock_agent.py");
std::fs::File::create(&script)
.unwrap()
.write_all(MOCK_AGENT.as_bytes())
.unwrap();
let outcome = run_acp_prompt(
"python3",
&[script.to_string_lossy().to_string()],
&dir,
"say hello",
true, // allow tools — approve the permission request
)
.expect("ACP prompt should complete against the mock agent");
assert_eq!(outcome.text, "Hello from ACP", "assembled streamed chunks");
assert_eq!(outcome.stop_reason, "end_turn");
assert_eq!(outcome.permissions_granted, 1, "auto-approved one permission");
// With tools refused, the same run still completes (the agent ends the turn).
let refused = run_acp_prompt(
"python3",
&[script.to_string_lossy().to_string()],
&dir,
"say hello",
false,
)
.expect("completes even when permission is refused");
assert_eq!(refused.permissions_granted, 0, "no permission granted when refused");
std::fs::remove_dir_all(&dir).ok();
}