Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
wai-app and Create / wai-app · does this change touch it (pull_request) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Has been cancelled
wai-audit collector and player integration / does this change touch it (pull_request) Has been cancelled
wai-audit collector and player integration / collector · linux (pull_request) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Has been cancelled
WAI operation catalogue / does this change touch it (pull_request) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI examples / WAI examples · does this change touch them (pull_request) Has been cancelled
WAI examples / WAI examples · portable · linux (pull_request) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Has been cancelled
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Has been cancelled
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Has been cancelled
wai-key-proxy / does this change touch it (pull_request) Has been cancelled
wai-key-proxy / proxy · linux (pull_request) Has been cancelled
wai-ledger service / does this change touch it (pull_request) Has been cancelled
wai-ledger service / service · linux (pull_request) Has been cancelled
wai-ledger service / service · macos (pull_request) Has been cancelled
WAI neural sink gates / does this change touch it (pull_request) Has been cancelled
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-quantum WebGPU member / does this change touch it (pull_request) Has been cancelled
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Has been cancelled
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Has been cancelled
wai-sink-android native sink library / does this change touch it (pull_request) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Has been cancelled
wai-sink-swift native sink package / does this change touch it (pull_request) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Has been cancelled
Merged #508 (wai/create-2, stacked on #505) before they reach main, as the coordinator directs: they merge before this branch, which builds the catalogue over them. - The catalogue holds every operation they add: mcp.start (wai mcp; it was agent.mcp), agent.setup, app.guide, app.check, app.export (wai app export, also wai create --from), app.stream, registry.show, docs.search, dev.stop, room.join, room.leave, session.show (it was room.session), and the room service's 40 operations at their routes. - Renamed to the verb rule: the MCP tool package_build is envelope_wrap (it does what wai wrap does; package.build stays for real packaging); the room service's challenges are watch.setup, conversation.setup and call.setup (no compound nouns); in the OpenAPI document, its contract test, the MCP table and tests, the docs. - wai::ops is folded into wai::catalogue: the tool table's tests hold it to the catalogue both ways (every tool an operation the catalogue says wai mcp serves, in its toolset, and every such operation a tool; a tool of MCP alone takes the catalogue's input), and the room service's contract test holds its OpenAPI the same way. - wai setup-agent and wai app check answer in the one envelope (wai: envelope/1, data), JSON with --json; wai mcp and setup-agent take --dry-run through the door; the binaries wai-relay and wai-room in the MCP dev room and Create's end-to-end script. Co-Authored-By: Claude Opus 5.5 <[email protected]>
192 lines
10 KiB
Rust
192 lines
10 KiB
Rust
//! `wai setup-agent` in a scratch project, run as an agent runs it (no terminal and `--json`, so
|
|
//! its output is the toolchain's JSON envelope, `envelope/1`, and it asks nothing):
|
|
//!
|
|
//! - a fresh project gets AGENTS.md and `.mcp.json`, and the configuration files of the agents
|
|
//! whose directories it holds, and no CLAUDE.md; a second run changes nothing;
|
|
//! - a CLAUDE.md the project has gains an import of AGENTS.md;
|
|
//! - a file that exists is changed only with `--yes` (or `--confirm`): without it nothing that
|
|
//! exists is written, the command exits 4 (confirmation required) with each proposed change and
|
|
//! the command that confirms; with it the block is added and everything else in the file is
|
|
//! kept; `--dry-run` writes nothing;
|
|
//! - `--toolset` registers the server with those toolsets;
|
|
//! - a JSON file it cannot read as JSON is left alone, with the entry to add by hand;
|
|
//! - nothing outside the project directory is touched, through a link or otherwise;
|
|
//! - its errors carry the table's exit status and a stable code: 2 usage, 5 not found.
|
|
|
|
use serde_json::{json, Value};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn scratch(tag: &str) -> PathBuf {
|
|
let d = std::env::temp_dir().join(format!("wai-cli-setup-agent-{tag}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
d.canonicalize().unwrap()
|
|
}
|
|
|
|
/// Run it in `dir`: its exit status and its JSON.
|
|
fn run(dir: &Path, extra: &[&str]) -> (i32, Value) {
|
|
let out = Command::new(env!("CARGO_BIN_EXE_wai")).arg("setup-agent").arg("--dir").arg(dir).args(extra).arg("--json").output().expect("run wai");
|
|
let text = String::from_utf8_lossy(&out.stdout);
|
|
let v: Value = serde_json::from_str(text.trim()).unwrap_or_else(|e| panic!("one JSON envelope with --json: {e}: {text}"));
|
|
(out.status.code().unwrap(), v)
|
|
}
|
|
|
|
fn files(v: &Value) -> &Vec<Value> {
|
|
assert_eq!((v["wai"].as_str(), v["op"].as_str()), (Some("envelope/1"), Some("agent.setup")), "the toolchain's envelope: {v}");
|
|
v["data"]["files"].as_array().unwrap()
|
|
}
|
|
|
|
fn action(v: &Value, path: &str) -> String {
|
|
files(v).iter().find(|f| f["path"] == path).map(|f| f["action"].as_str().unwrap().to_string()).unwrap_or_else(|| "absent".into())
|
|
}
|
|
|
|
fn read(p: PathBuf) -> String {
|
|
std::fs::read_to_string(p).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn a_fresh_project_is_set_up_and_a_second_run_changes_nothing() {
|
|
let d = scratch("fresh");
|
|
std::fs::create_dir(d.join(".vscode")).unwrap();
|
|
let (code, v) = run(&d, &[]);
|
|
assert_eq!(code, 0, "{v}");
|
|
assert_eq!(v["ok"], true);
|
|
for p in ["AGENTS.md", ".mcp.json", ".vscode/mcp.json"] {
|
|
assert_eq!(action(&v, p), "created", "{p}: {v}");
|
|
}
|
|
assert_eq!(action(&v, ".cursor/mcp.json"), "absent", "an agent whose directory is not there is left out");
|
|
assert_eq!(action(&v, "CLAUDE.md"), "absent", "no CLAUDE.md is made where the project has none");
|
|
assert!(!d.join("CLAUDE.md").exists());
|
|
let agents = read(d.join("AGENTS.md"));
|
|
assert!(agents.contains("## WAI") && agents.contains("wai mcp") && agents.contains("receipt_verify"), "{agents}");
|
|
let mcp: Value = serde_json::from_str(&read(d.join(".mcp.json"))).unwrap();
|
|
assert_eq!(mcp["mcpServers"]["wai"], json!({"command": "wai", "args": ["mcp"]}));
|
|
let vs: Value = serde_json::from_str(&read(d.join(".vscode/mcp.json"))).unwrap();
|
|
assert_eq!(vs["servers"]["wai"], json!({"type": "stdio", "command": "wai", "args": ["mcp"]}));
|
|
let (code, v) = run(&d, &[]);
|
|
assert_eq!(code, 0);
|
|
assert!(files(&v).iter().all(|f| f["action"] == "unchanged"), "{v}");
|
|
// --all writes every agent's file, a directory it needs made inside the project.
|
|
let (code, v) = run(&d, &["--all", "--confirm"]);
|
|
assert_eq!(code, 0, "{v}");
|
|
for p in [".cursor/mcp.json", ".gemini/settings.json", "opencode.json"] {
|
|
assert_eq!(action(&v, p), "created", "{p}");
|
|
}
|
|
let g: Value = serde_json::from_str(&read(d.join(".gemini/settings.json"))).unwrap();
|
|
assert_eq!(g["context"]["fileName"][0], "AGENTS.md", "{g}");
|
|
let oc: Value = serde_json::from_str(&read(d.join("opencode.json"))).unwrap();
|
|
assert_eq!(oc["mcp"]["wai"], json!({"type": "local", "command": ["wai", "mcp"], "enabled": true}));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
#[test]
|
|
fn an_existing_file_changes_only_when_confirmed() {
|
|
let d = scratch("existing");
|
|
let mine = "# My project\n\nRun `make test` before you commit.\n";
|
|
let claude = "# Notes for one agent\n";
|
|
std::fs::write(d.join("AGENTS.md"), mine).unwrap();
|
|
std::fs::write(d.join("CLAUDE.md"), claude).unwrap();
|
|
std::fs::write(d.join(".mcp.json"), r#"{"mcpServers": {"other": {"command": "other-server"}}}"#).unwrap();
|
|
// A dry run writes nothing, existing or not.
|
|
let (code, v) = run(&d, &["--dry-run"]);
|
|
assert_eq!(code, 0, "{v}");
|
|
assert_eq!((action(&v, "AGENTS.md"), action(&v, "CLAUDE.md")), ("would_update".into(), "would_update".into()));
|
|
assert_eq!(read(d.join("AGENTS.md")), mine);
|
|
// Without --confirm: what exists waits, and the command says how to confirm (exit 4).
|
|
let (code, v) = run(&d, &[]);
|
|
assert_eq!(code, 4, "confirmation required: {v}");
|
|
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str(), v["error"]["retryable"].as_bool()), (Some(false), Some("confirmation-required"), Some(false)), "{v}");
|
|
for p in ["AGENTS.md", "CLAUDE.md", ".mcp.json"] {
|
|
assert_eq!(action(&v, p), "needs_confirmation", "{p}");
|
|
}
|
|
assert_eq!(read(d.join("AGENTS.md")), mine, "nothing that exists was changed");
|
|
assert_eq!(read(d.join("CLAUDE.md")), claude, "nothing that exists was changed");
|
|
let proposed = v["proposed"].as_array().unwrap();
|
|
assert_eq!(proposed.len(), 3, "{v}");
|
|
let diff = proposed.iter().find(|f| f["path"] == "AGENTS.md").unwrap()["diff"].as_str().unwrap().to_string();
|
|
assert!(diff.starts_with("--- a/AGENTS.md\n+++ b/AGENTS.md\n") && diff.contains("+## WAI"), "{diff}");
|
|
let confirm = v["confirm"].as_str().unwrap();
|
|
assert!(confirm.ends_with("--yes") && confirm.contains(d.to_str().unwrap()), "{confirm}");
|
|
// With it, the block is added and the rest kept.
|
|
let (code, v) = run(&d, &["--yes"]);
|
|
assert_eq!(code, 0, "{v}");
|
|
assert_eq!(action(&v, "AGENTS.md"), "updated");
|
|
let c = read(d.join("CLAUDE.md"));
|
|
assert!(c.starts_with(claude) && c.lines().any(|l| l == "@AGENTS.md"), "a CLAUDE.md the project has imports AGENTS.md: {c}");
|
|
let agents = read(d.join("AGENTS.md"));
|
|
assert!(agents.starts_with(mine) && agents.contains("## WAI"), "{agents}");
|
|
let mcp: Value = serde_json::from_str(&read(d.join(".mcp.json"))).unwrap();
|
|
assert_eq!(mcp["mcpServers"]["other"]["command"], "other-server", "the other server is kept");
|
|
assert_eq!(mcp["mcpServers"]["wai"]["args"][0], "mcp");
|
|
// Once more: the block is replaced in place, not added twice.
|
|
let (code, _) = run(&d, &["--confirm"]);
|
|
assert_eq!(code, 0);
|
|
assert_eq!(read(d.join("AGENTS.md")).matches("## WAI").count(), 1);
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_that_is_not_plain_json_is_left_alone() {
|
|
let d = scratch("jsonc");
|
|
std::fs::create_dir(d.join(".vscode")).unwrap();
|
|
let jsonc = "{\n // the user's comment\n \"servers\": {}\n}\n";
|
|
std::fs::write(d.join(".vscode/mcp.json"), jsonc).unwrap();
|
|
let (code, v) = run(&d, &["--confirm"]);
|
|
assert_eq!(code, 0, "{v}");
|
|
assert_eq!(action(&v, ".vscode/mcp.json"), "skipped");
|
|
let f = files(&v).iter().find(|f| f["path"] == ".vscode/mcp.json").unwrap();
|
|
assert!(f["reason"].as_str().unwrap().contains("add by hand"), "{f}");
|
|
assert_eq!(read(d.join(".vscode/mcp.json")), jsonc);
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn nothing_outside_the_project_is_touched() {
|
|
let d = scratch("links");
|
|
let outside = scratch("links-outside");
|
|
std::fs::write(outside.join("AGENTS.md"), "outside\n").unwrap();
|
|
std::os::unix::fs::symlink(outside.join("AGENTS.md"), d.join("AGENTS.md")).unwrap();
|
|
std::os::unix::fs::symlink(&outside, d.join(".cursor")).unwrap();
|
|
let (code, v) = run(&d, &["--confirm", "--all"]);
|
|
assert_eq!(code, 0, "{v}");
|
|
assert_eq!(action(&v, "AGENTS.md"), "skipped");
|
|
assert_eq!(action(&v, ".cursor/mcp.json"), "skipped");
|
|
assert_eq!(read(outside.join("AGENTS.md")), "outside\n");
|
|
assert!(!outside.join("mcp.json").exists(), "nothing written through the linked directory");
|
|
let names: Vec<_> = std::fs::read_dir(&outside).unwrap().map(|e| e.unwrap().file_name()).collect();
|
|
assert_eq!(names.len(), 1, "{names:?}");
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
let _ = std::fs::remove_dir_all(&outside);
|
|
}
|
|
|
|
#[test]
|
|
fn the_toolsets_named_are_registered() {
|
|
let d = scratch("toolsets");
|
|
std::fs::write(d.join("opencode.json"), "{}\n").unwrap();
|
|
let (code, v) = run(&d, &["--toolset", "core,dev", "--confirm"]);
|
|
assert_eq!(code, 0, "{v}");
|
|
let mcp: Value = serde_json::from_str(&read(d.join(".mcp.json"))).unwrap();
|
|
assert_eq!(mcp["mcpServers"]["wai"], json!({"command": "wai", "args": ["mcp", "--toolset", "core,dev"]}));
|
|
let oc: Value = serde_json::from_str(&read(d.join("opencode.json"))).unwrap();
|
|
assert_eq!(oc["mcp"]["wai"]["command"], json!(["wai", "mcp", "--toolset", "core,dev"]));
|
|
assert!(read(d.join("AGENTS.md")).contains("`wai mcp --toolset core,dev`"));
|
|
let (code, v) = run(&d, &["--toolset", "quantum"]);
|
|
assert_eq!((code, v["error"]["code"].as_str()), (2, Some("usage")), "{v}");
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
#[test]
|
|
fn a_usage_error_is_structured() {
|
|
let d = scratch("usage");
|
|
let (code, v) = run(&d, &["--frobnicate"]);
|
|
assert_eq!(code, 2);
|
|
assert_eq!((v["wai"].as_str(), v["op"].as_str(), v["ok"].as_bool(), v["error"]["code"].as_str()), (Some("envelope/1"), Some("agent.setup"), Some(false), Some("usage")), "{v}");
|
|
assert!(v["error"]["message"].as_str().unwrap().contains("--frobnicate") && v["error"]["hint"].is_string(), "{v}");
|
|
let (code, v) = run(&d, &["--only", "README.md"]);
|
|
assert_eq!((code, v["error"]["code"].as_str()), (2, Some("usage")), "{v}");
|
|
let (code, v) = run(&d.join("not-there"), &[]);
|
|
assert_eq!((code, v["error"]["code"].as_str()), (5, Some("not-found")), "{v}");
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|