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]>
330 lines
16 KiB
Rust
330 lines
16 KiB
Rust
//! An app made with Create, through `wai` built with the feature `app`:
|
|
//!
|
|
//! - `wai create <dir> --from app.oui` writes an ordinary WAI project: the app's files and its
|
|
//! runtime under `vendor/`, the project file laid out as every template's is and naming the
|
|
//! program and the room `wai dev` runs for it, and the licence; a template's project and an
|
|
//! exported one share their shape;
|
|
//! - `wai app check` passes on it, and refuses a project whose room is not its app's, whose
|
|
//! runtime changed, whose program has a refused line, or that names no room;
|
|
//! - `wai create --from` refuses a program with errors, one with no room, and a directory with
|
|
//! anything in it;
|
|
//! - `wai mcp --toolset app`: the tools are Create's operations (`app_guide`, `app_check`,
|
|
//! `app_stream`, `app_export`); a program streams through the bridge to a page connected with
|
|
//! the pairing code and to no page without it; an export is written inside the project
|
|
//! directory alone; `--read-only` runs no bridge and offers no tool that streams or writes.
|
|
#![cfg(feature = "app")]
|
|
|
|
use serde_json::{json, Value};
|
|
use sha2::{Digest, Sha256};
|
|
use std::io::{BufRead, BufReader, Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Child, ChildStdin, ChildStdout, Command, Output, Stdio};
|
|
|
|
const SHOW: &str = "root = App([show], \"Friday night\")\nshow = Show([player, chat, poll, people], \"Live from the studio\", 2)\nplayer = Player()\nchat = Chat(\"Talk\")\npoll = Poll(\"Which song next?\", [\"The first one\", \"The quiet one\"])\npeople = Participants()\n";
|
|
|
|
fn scratch(tag: &str) -> PathBuf {
|
|
let d = std::env::temp_dir().join(format!("wai-cli-app-{tag}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
d.canonicalize().unwrap()
|
|
}
|
|
|
|
fn hex(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
/// A Create runtime as `web/runtime.mjs` lays one out: a few files and their listing.
|
|
fn runtime(d: &Path) -> PathBuf {
|
|
let rt = d.join("runtime");
|
|
let files = [("create/wai-app-app.js", "export {};\n"), ("create/pkg/wai_app_bg.wasm", "\0asm"), ("room/wai-app-room-client.js", "// the room client\n")];
|
|
let mut listed = Vec::new();
|
|
for (p, text) in files {
|
|
let f = rt.join(p);
|
|
std::fs::create_dir_all(f.parent().unwrap()).unwrap();
|
|
std::fs::write(&f, text).unwrap();
|
|
listed.push(json!({"path": p, "sha256": hex(&Sha256::digest(text.as_bytes()))}));
|
|
}
|
|
std::fs::write(rt.join("create/wai-app-runtime.json"), json!({"files": listed}).to_string()).unwrap();
|
|
rt
|
|
}
|
|
|
|
fn wai(dir: &Path, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).output().expect("run wai")
|
|
}
|
|
|
|
fn text(o: &Output) -> String {
|
|
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
|
|
}
|
|
|
|
/// `wai app check` in `dir`: its exit status and its JSON.
|
|
fn app_check(dir: &Path) -> (i32, Value) {
|
|
let o = wai(dir, &["app", "check", "--json"]);
|
|
let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("{e}: {}", text(&o)));
|
|
assert_eq!((v["wai"].as_str(), v["op"].as_str()), (Some("envelope/1"), Some("app.check")), "{v}");
|
|
(o.status.code().unwrap(), v)
|
|
}
|
|
|
|
fn files_of(dir: &Path) -> Vec<String> {
|
|
fn walk(root: &Path, p: &Path, out: &mut Vec<String>) {
|
|
for e in std::fs::read_dir(p).unwrap().flatten() {
|
|
let q = e.path();
|
|
if q.is_dir() {
|
|
walk(root, &q, out);
|
|
} else {
|
|
out.push(q.strip_prefix(root).unwrap().to_string_lossy().replace('\\', "/"));
|
|
}
|
|
}
|
|
}
|
|
let mut out = Vec::new();
|
|
walk(dir, dir, &mut out);
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn an_export_is_an_ordinary_project_and_its_check_holds() {
|
|
let d = scratch("export");
|
|
let rt = runtime(&d);
|
|
std::fs::write(d.join("app.oui"), SHOW).unwrap();
|
|
let o = wai(&d, &["create", "show", "--from", "app.oui", "--runtime", rt.to_str().unwrap()]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let p = d.join("show");
|
|
let mut want: Vec<String> = ["app.oui", "index.html", "README.md", "AGENTS.md", ".gitignore", "vendor.json", "wai.json", "LICENSE"].iter().map(|s| s.to_string()).collect();
|
|
want.extend(["vendor/create/wai-app-app.js", "vendor/create/pkg/wai_app_bg.wasm", "vendor/room/wai-app-room-client.js"].iter().map(|s| s.to_string()));
|
|
want.sort();
|
|
assert_eq!(files_of(&p), want);
|
|
// The project file: the template's layout, naming the program and the show's room.
|
|
let wj = std::fs::read_to_string(p.join("wai.json")).unwrap();
|
|
assert_eq!(
|
|
wj,
|
|
"{\n \"title\": \"Friday night\",\n \"summary\": \"A WAI app made with Create: a show, with hosts on camera and an audience that watches and votes.\",\n \"app\": \"app.oui\",\n \"dev\": { \"site\": \".\", \"mode\": \"show\", \"audience\": 2 }\n}\n"
|
|
);
|
|
// A template's project has the same shape: its project file read by the same reader and
|
|
// laid out the same way, a README and the licence.
|
|
let o = wai(&d, &["create", "room", "--template", "video-room"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let t = std::fs::read_to_string(d.join("room/wai.json")).unwrap();
|
|
for f in [&wj, &t] {
|
|
let lines: Vec<&str> = f.lines().collect();
|
|
assert!(lines.first() == Some(&"{") && lines.last() == Some(&"}") && lines[1..lines.len() - 1].iter().all(|l| l.starts_with(" \"")), "{f}");
|
|
}
|
|
assert_eq!(std::fs::read(d.join("room/LICENSE")).unwrap(), std::fs::read(p.join("LICENSE")).unwrap());
|
|
for r in [p.join("README.md"), d.join("room/README.md")] {
|
|
assert!(std::fs::read_to_string(&r).unwrap().contains("Apache-2.0"), "{}", r.display());
|
|
}
|
|
// The program in the project is the canonical form of the one given.
|
|
assert!(std::fs::read_to_string(p.join("app.oui")).unwrap().starts_with("root = App("));
|
|
let (code, v) = app_check(&p);
|
|
assert_eq!(code, 0, "{v}");
|
|
assert_eq!(v["data"]["room"]["kind"], "show");
|
|
assert!(v["data"]["failures"].as_array().unwrap().is_empty(), "{v}");
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
#[test]
|
|
fn the_check_refuses_a_project_that_is_not_whole() {
|
|
let d = scratch("refused");
|
|
let rt = runtime(&d);
|
|
std::fs::write(d.join("app.oui"), SHOW).unwrap();
|
|
assert!(wai(&d, &["create", "good", "--from", "app.oui", "--runtime", rt.to_str().unwrap()]).status.success());
|
|
let good = d.join("good");
|
|
let cases: [Case; 5] = [
|
|
("whose room is not its app's", |p| edit(&p.join("wai.json"), "\"mode\": \"show\", \"audience\": 2", "\"mode\": \"room\""), "dev.mode is \"room\""),
|
|
("whose show has another audience", |p| edit(&p.join("wai.json"), "\"audience\": 2", "\"audience\": 7"), "dev.audience is 7"),
|
|
("that names no room", |p| edit(&p.join("wai.json"), ", \"mode\": \"show\", \"audience\": 2", ""), "names no room"),
|
|
("with a runtime file changed", |p| std::fs::write(p.join("vendor/create/wai-app-app.js"), "export {}; // changed\n").unwrap(), "vendor/create/wai-app-app.js: its SHA-256"),
|
|
("with a line the registry refuses", |p| edit(&p.join("app.oui"), "Participants()", "Hologram()"), "unknown-component"),
|
|
];
|
|
for (what, mutate, expect) in cases {
|
|
let b = d.join("broken");
|
|
let _ = std::fs::remove_dir_all(&b);
|
|
let o = Command::new("cp").args(["-R"]).arg(&good).arg(&b).output().unwrap();
|
|
assert!(o.status.success());
|
|
mutate(&b);
|
|
let (code, v) = app_check(&b);
|
|
let failures: String = v["data"]["failures"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect::<Vec<_>>().join("\n");
|
|
assert!(code == 1 && failures.contains(expect), "a project {what}: {code} {v}");
|
|
}
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
/// A project broken one way: what is wrong with it, how to break it, and what the check says.
|
|
type Case = (&'static str, fn(&Path), &'static str);
|
|
|
|
fn edit(p: &Path, from: &str, to: &str) {
|
|
let t = std::fs::read_to_string(p).unwrap();
|
|
assert_eq!(t.matches(from).count(), 1, "{from} in {}: {t}", p.display());
|
|
std::fs::write(p, t.replace(from, to)).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_watch_room_names_its_file_and_its_check_wants_it() {
|
|
let d = scratch("watch");
|
|
let rt = runtime(&d);
|
|
std::fs::write(d.join("w.oui"), "root = App([w], \"Film night\")\nw = WatchTogether(\"film.wav\", [Participants()])\n").unwrap();
|
|
assert!(wai(&d, &["create", "w", "--from", "w.oui", "--runtime", rt.to_str().unwrap()]).status.success());
|
|
let p = d.join("w");
|
|
assert!(std::fs::read_to_string(p.join("wai.json")).unwrap().contains("\"dev\": { \"site\": \".\", \"mode\": \"watch\", \"watch\": \"media/film.wav\" }"));
|
|
assert!(p.join("media/README.md").is_file());
|
|
let (code, v) = app_check(&p);
|
|
assert!(code == 1 && v["data"]["failures"].to_string().contains("media/film.wav is not there"), "{v}");
|
|
std::fs::write(p.join("media/film.wav"), b"RIFF").unwrap();
|
|
let (code, v) = app_check(&p);
|
|
assert_eq!(code, 0, "{v}");
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
#[test]
|
|
fn what_is_not_exported() {
|
|
let d = scratch("not");
|
|
let rt = runtime(&d);
|
|
let rt = rt.to_str().unwrap();
|
|
std::fs::write(d.join("bad.oui"), "root = App([r])\nr = Room([Poll(\"q\", [\"a\", \"b\"])])\n").unwrap();
|
|
std::fs::write(d.join("noroom.oui"), "root = App([t])\nt = Text(\"no room\")\n").unwrap();
|
|
std::fs::write(d.join("app.oui"), SHOW).unwrap();
|
|
for (prog, why) in [("bad.oui", "error"), ("noroom.oui", "no room")] {
|
|
let o = wai(&d, &["create", "x", "--from", prog, "--runtime", rt]);
|
|
assert!(o.status.code() == Some(2) && text(&o).contains(why), "{prog}: {}", text(&o));
|
|
assert!(!d.join("x").exists());
|
|
}
|
|
std::fs::create_dir(d.join("full")).unwrap();
|
|
std::fs::write(d.join("full/mine.txt"), "mine").unwrap();
|
|
let o = wai(&d, &["create", "full", "--from", "app.oui", "--runtime", rt]);
|
|
assert!(o.status.code() == Some(2) && text(&o).contains("not empty"), "{}", text(&o));
|
|
let o = wai(&d, &["create", "y", "--from", "app.oui", "--runtime", d.join("nowhere").to_str().unwrap()]);
|
|
assert!(o.status.code() == Some(2) && text(&o).contains("runtime"), "{}", text(&o));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------- the toolset
|
|
|
|
struct Client {
|
|
child: Child,
|
|
stdin: ChildStdin,
|
|
out: BufReader<ChildStdout>,
|
|
next: i64,
|
|
bridge: Option<(u16, String)>,
|
|
}
|
|
|
|
impl Client {
|
|
fn start(project: &Path, extra: &[&str]) -> Client {
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_wai"))
|
|
.args(["mcp", "--project"])
|
|
.arg(project)
|
|
.args(extra)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
let stdin = child.stdin.take().unwrap();
|
|
let out = BufReader::new(child.stdout.take().unwrap());
|
|
let mut c = Client { child, stdin, out, next: 1, bridge: None };
|
|
let init = c.request("initialize", json!({"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "cli_app test", "version": "1"}}));
|
|
assert_eq!(init["result"]["serverInfo"]["name"], "wai", "{init}");
|
|
c
|
|
}
|
|
|
|
/// The bridge's address and pairing code, as the server says them at start.
|
|
fn read_bridge(&mut self) {
|
|
let mut err = BufReader::new(self.child.stderr.take().unwrap());
|
|
let mut first = String::new();
|
|
err.read_line(&mut first).unwrap();
|
|
let port: u16 = first.split("http://127.0.0.1:").nth(1).and_then(|s| s.split(',').next()).and_then(|s| s.parse().ok()).expect(&first);
|
|
let code = first.trim().rsplit(' ').next().unwrap().to_string();
|
|
self.bridge = Some((port, code));
|
|
}
|
|
|
|
fn request(&mut self, method: &str, params: Value) -> Value {
|
|
let id = self.next;
|
|
self.next += 1;
|
|
writeln!(self.stdin, "{}", json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})).unwrap();
|
|
self.stdin.flush().unwrap();
|
|
let mut l = String::new();
|
|
assert!(self.out.read_line(&mut l).unwrap() > 0, "the server closed its output");
|
|
serde_json::from_str(&l).unwrap()
|
|
}
|
|
|
|
fn call(&mut self, name: &str, args: Value) -> Result<Value, String> {
|
|
let v = self.request("tools/call", json!({"name": name, "arguments": args}));
|
|
let r = &v["result"];
|
|
assert!(r.is_object(), "{name}: {v}");
|
|
if r["isError"] == json!(true) {
|
|
return Err(r["content"][0]["text"].as_str().unwrap_or("").to_string());
|
|
}
|
|
Ok(r["structuredContent"].clone())
|
|
}
|
|
|
|
fn names(&mut self) -> Vec<String> {
|
|
let l = self.request("tools/list", json!({}));
|
|
l["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect()
|
|
}
|
|
}
|
|
|
|
impl Drop for Client {
|
|
fn drop(&mut self) {
|
|
let _ = self.child.kill();
|
|
let _ = self.child.wait();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_through_the_app_toolset() {
|
|
let d = scratch("mcp");
|
|
let rt = runtime(&d);
|
|
let mut c = Client::start(&d, &["--toolset", "app"]);
|
|
c.read_bridge();
|
|
let (port, code) = c.bridge.clone().unwrap();
|
|
assert_eq!(c.names(), ["app_guide", "app_check", "app_stream", "app_export"]);
|
|
let g = c.call("app_guide", json!({})).unwrap();
|
|
assert!(g["guide"].as_str().unwrap().contains("Poll(") && g["code"] == code.as_str(), "{g}");
|
|
let ok = c.call("app_check", json!({"program": SHOW})).unwrap();
|
|
assert_eq!((ok["ok"].as_bool(), ok["room"]["kind"].as_str()), (Some(true), Some("show")), "{ok}");
|
|
let refused = c.call("app_check", json!({"program": "root = App([w])\nw = WatchTogether(\"f.mp4\", [Chat()])\n"})).unwrap();
|
|
assert!(refused["ok"] == false && refused["problems"][0].as_str().unwrap().starts_with("line 2: disallowed-child"), "{refused}");
|
|
// No page is connected: the stream says how to connect one.
|
|
assert!(c.call("app_stream", json!({"program": SHOW})).unwrap_err().contains(&code));
|
|
// A page without the code is refused; one with it reads the stream.
|
|
let get = |path: &str| {
|
|
let mut s = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
|
|
write!(s, "GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n").unwrap();
|
|
s
|
|
};
|
|
let mut resp = String::new();
|
|
get("/create/stream?code=wrong").read_to_string(&mut resp).unwrap();
|
|
assert!(resp.starts_with("HTTP/1.1 403"), "{resp}");
|
|
let mut page = BufReader::new(get(&format!("/create/stream?code={code}")));
|
|
let mut head = String::new();
|
|
page.read_line(&mut head).unwrap();
|
|
assert!(head.starts_with("HTTP/1.1 200"), "{head}");
|
|
let mut s = Err(String::new());
|
|
for _ in 0..100 {
|
|
s = c.call("app_stream", json!({"program": SHOW, "chunk": 40, "pace_ms": 0}));
|
|
if s.is_ok() {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
}
|
|
let s = s.unwrap();
|
|
assert_eq!(s["delivered"], 1, "{s}");
|
|
let mut seen = String::new();
|
|
while !seen.contains("event: end") {
|
|
let mut l = String::new();
|
|
page.read_line(&mut l).unwrap();
|
|
seen.push_str(&l);
|
|
}
|
|
assert!(seen.contains("event: start") && seen.contains("Friday night"), "{seen}");
|
|
// An export, inside the project directory alone.
|
|
let e = c.call("app_export", json!({"program": SHOW, "dir": "show", "runtime_dir": rt})).unwrap();
|
|
assert!(e["files"].as_array().unwrap().iter().any(|f| f == "wai.json"), "{e}");
|
|
assert_eq!(app_check(&d.join("show")).0, 0);
|
|
let outside = std::env::temp_dir().join(format!("wai-cli-app-outside-{}", std::process::id()));
|
|
let err = c.call("app_export", json!({"program": SHOW, "dir": outside, "runtime_dir": rt})).unwrap_err();
|
|
assert!(err.contains("outside the project directory") && !outside.exists(), "{err}");
|
|
// Read-only: no bridge, and nothing that streams or writes.
|
|
let mut ro = Client::start(&d, &["--toolset", "core,app", "--read-only"]);
|
|
let names = ro.names();
|
|
assert!(names.contains(&"app_check".to_string()) && !names.iter().any(|n| n == "app_stream" || n == "app_export"), "{names:?}");
|
|
assert!(ro.call("app_guide", json!({})).unwrap()["bridge"].is_null());
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|