diff --git a/benchmarks/aft-search/slice-descriptors/land-carrier-samples.json b/benchmarks/aft-search/slice-descriptors/land-carrier-samples.json new file mode 100644 index 000000000..ebd574711 --- /dev/null +++ b/benchmarks/aft-search/slice-descriptors/land-carrier-samples.json @@ -0,0 +1,8 @@ +{ + "targeted_mechanism": "none", + "kind": "harness", + "slice_class": "non_ranking", + "fixtures": [ + "harness-goldens" + ] +} diff --git a/crates/aft/src/subc/tool_provider.rs b/crates/aft/src/subc/tool_provider.rs index 955eacea7..56176ce73 100644 --- a/crates/aft/src/subc/tool_provider.rs +++ b/crates/aft/src/subc/tool_provider.rs @@ -4939,6 +4939,8 @@ mod tests { #[cfg(test)] mod route_tests { + mod carrier_samples; + use super::super::*; use super::*; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -5039,9 +5041,6 @@ mod route_tests { metrics: &Arc, dispatch: DispatchFn, ) -> WriterFrame { - let executor = Arc::new(Executor::new()); - assert!(executor.register_actor(identity.root.clone(), ctx)); - let routes = HashMap::from([(route_key(41, 1), identity)]); let frame = Frame::build( FrameType::Request, control_flags(), @@ -5051,6 +5050,19 @@ mod route_tests { serde_json::to_vec(&body).unwrap(), ) .unwrap(); + exchange_frame_with_actor(frame, identity, ctx, metrics, dispatch).await + } + + async fn exchange_frame_with_actor( + frame: Frame, + identity: RouteIdentity, + ctx: Arc, + metrics: &Arc, + dispatch: DispatchFn, + ) -> WriterFrame { + let executor = Arc::new(Executor::new()); + assert!(executor.register_actor(identity.root.clone(), ctx)); + let routes = HashMap::from([(route_key(41, 1), identity)]); // As on the frame loop, a scoped worker's saved confinement is read // off the loop first when memory cannot answer for it. let decoded = DecodedFrame { diff --git a/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs new file mode 100644 index 000000000..c0cc677c2 --- /dev/null +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -0,0 +1,320 @@ +//! Captured tool-provider frames for the read-only `reserved:occipital` identity. + +use super::*; +use crate::bash_background::persistence::{task_paths, write_task, PersistedTask}; +use crate::bash_background::BgTaskStatus; +use std::path::{Path, PathBuf}; + +const TASK_ID: &str = "bash-0123456789abcdef"; +const SOURCE: &str = "fn greeting() -> &'static str {\n \"hello phone\"\n}\n\nfn main() {\n println!(\"{}\", greeting());\n}\n"; + +struct Case { + name: &'static str, + tool: &'static str, + arguments: Value, + error: Option<&'static str>, +} + +fn cases() -> Vec { + let mut cases = vec![ + ("read", json!({"path":"main.rs"})), + ("aft_outline", json!({"target":"main.rs"})), + ( + "aft_zoom", + json!({"batch":[{"target":"main.rs","symbols":["greeting"]}]}), + ), + ("aft_search", json!({"pattern":"greeting"})), + ("grep", json!({"pattern":"greeting","include":"*.rs"})), + ("glob", json!({"pattern":"*.rs"})), + ( + "aft_callgraph", + json!({"path":"main.rs","symbol":"greeting","op":"callers"}), + ), + ("bash_status", json!({"taskId":TASK_ID})), + ("bash_watch", json!({"taskId":TASK_ID})), + ] + .into_iter() + .map(|(tool, arguments)| Case { + name: tool, + tool, + arguments, + error: None, + }) + .collect::>(); + assert_eq!( + cases.iter().map(|case| case.tool).collect::>(), + carrier_read_only::ALLOWED_TOOLS, + "Every admitted tool needs a successful wire sample" + ); + for (name, tool, arguments, error) in [ + ( + "refused-edit", + "edit", + json!({"path":"main.rs","edits":[]}), + "carrier_read_only", + ), + ( + "refused-web", + "aft_outline", + json!({"target":"https://example.com/phone"}), + "carrier_read_only", + ), + ( + "refused-github-issue", + "read", + json!({"path":"issue://935"}), + "carrier_read_only", + ), + ( + "refused-github-pr", + "read", + json!({"path":"pr://935"}), + "carrier_read_only", + ), + ( + "refused-invalid-arguments", + "read", + json!({"path":"main.rs","unexpected":true}), + "invalid_request", + ), + ] { + cases.push(Case { + name, + tool, + arguments, + error: Some(error), + }); + } + cases +} + +fn fixture() -> (tempfile::TempDir, ProjectRootId, Arc) { + let (dir, _) = test_support::test_root("carrier-samples"); + let project = dir.path().join("project"); + std::fs::create_dir(&project).unwrap(); + std::fs::write(project.join("main.rs"), SOURCE).unwrap(); + let root = ProjectRootId::from_path(&project).unwrap(); + let ctx = test_support::test_ctx(); + let config = json!({"indexes":{"trigram":true,"semantic":false,"callgraph":true}}); + let request: RawRequest = serde_json::from_value(json!({ + "id":"configure-carrier-samples", "command":"configure", "harness":"runner", + "project_root":project, "storage_dir":dir.path().join("storage"), + "config":[{"tier":"user","source":dir.path().join("aft.jsonc"),"doc":config.to_string()}] + })) + .unwrap(); + let response = crate::commands::configure::handle_configure(&request, &ctx); + assert!(response.success, "configure: {response:?}"); + crate::runtime_drain::drain_deferred_configure_maintenance(&ctx); + let deadline = Instant::now() + Duration::from_secs(30); + loop { + crate::runtime_drain::drain_search_index_events(&ctx); + crate::runtime_drain::drain_callgraph_store_events(&ctx); + let search_ready = ctx + .search_index() + .read() + .unwrap() + .as_ref() + .is_some_and(|index| index.ready); + let graph_ready = matches!( + ctx.callgraph_store_for_ops(), + crate::context::CallgraphStoreAccess::Ready(_) + ); + if search_ready && graph_ready { + break; + } + assert!( + Instant::now() < deadline, + "fixture indexes did not become ready" + ); + std::thread::sleep(Duration::from_millis(10)); + } + + // A completed task avoids a live process and makes its duration reproducible. + let storage = crate::bash_background::task_storage_dir(&ctx); + let paths = task_paths(&storage, "session-s", TASK_ID).unwrap(); + let mut task = PersistedTask::starting( + TASK_ID.into(), + "session-s".into(), + "printf 'hello phone\\n'".into(), + project.clone(), + Some(project), + None, + false, + false, + ); + task.mark_terminal(BgTaskStatus::Completed, Some(0), None); + task.duration_ms = Some(12); + write_task(&paths.json, &task).unwrap(); + std::fs::write(&paths.stdout, "hello phone\n").unwrap(); + std::fs::write(&paths.stderr, "").unwrap(); + std::fs::write(&paths.exit, "0\n").unwrap(); + ctx.bash_background() + .replay_session(&storage, "session-s") + .unwrap(); + (dir, root, ctx) +} + +fn dispatch(request: RawRequest, ctx: &AppContext) -> Response { + match request.command.as_str() { + "read" => crate::commands::read::handle_read(&request, ctx), + "outline" => crate::commands::outline::handle_outline(&request, ctx), + "zoom" => crate::commands::zoom::handle_zoom(&request, ctx), + "semantic_search" => { + crate::commands::semantic_search::handle_semantic_search(&request, ctx) + } + "grep" => crate::commands::grep::handle_grep(&request, ctx), + "glob" => crate::commands::glob::handle_glob(&request, ctx), + "callers" => crate::commands::callers::handle_callers(&request, ctx), + "bash_status" => crate::commands::bash_status::handle(&request, ctx), + other => panic!("Unexpected carrier dispatch: {other}"), + } +} + +fn sample_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rich_ui/carrier-samples") +} + +fn sample_path(case: &Case) -> PathBuf { + let windows = sample_dir().join(format!("{}.windows.json", case.name)); + if cfg!(windows) && windows.is_file() { + windows + } else { + sample_dir().join(format!("{}.json", case.name)) + } +} + +fn normalized_frame(frame: &Frame, temporary_root: &Path, tool: &str) -> Value { + let mut body = String::from_utf8(frame.body.clone()) + .unwrap() + .replace(temporary_root.to_str().unwrap(), "${FIXTURE_ROOT}"); + let timestamps = regex::Regex::new(r#"("(?:started_at|finished_at)":)\d+"#).unwrap(); + body = timestamps.replace_all(&body, "${1}0").into_owned(); + if tool == "bash_watch" { + // A completed task still reports scheduler-dependent time spent observing it. + let elapsed = regex::Regex::new(r#"("elapsed_ms":)\d+"#).unwrap(); + body = elapsed.replace_all(&body, "${1}0").into_owned(); + let rendered = regex::Regex::new(r"Waited \d+ms").unwrap(); + body = rendered.replace_all(&body, "Waited 0ms").into_owned(); + } + // Recompute the body byte length after normalizing volatile values. + let header = &frame.header; + let normalized = Frame::build_with_version( + header.ver, + header.ty, + header.flags, + header.channel, + header.epoch, + header.corr, + body.as_bytes().to_vec(), + ) + .unwrap(); + let header_hex: String = normalized + .header + .encode() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + json!({"header_hex":header_hex,"body_utf8":body}) +} + +async fn run_case(case: &Case) -> Value { + let (dir, root, ctx) = fixture(); + let mut identity = occipital_identity(&root, RouteRole::ToolProviderV1); + Arc::get_mut(&mut identity.0).unwrap().tool_result_body = true; + let request = Frame::build( + FrameType::Request, control_flags(), 41, 1, 7, + serde_json::to_vec(&json!({"op":"tool.call","name":case.tool,"preset":"worker","arguments":case.arguments})).unwrap(), + ).unwrap(); + let reply = exchange_frame_with_actor( + request.clone(), + identity, + ctx, + &Arc::new(DispatchPathMetrics::new()), + dispatch, + ) + .await; + if let Some(code) = case.error { + assert_eq!(reply.header.ty, FrameType::Error, "{}", case.name); + let error: subc_protocol::ErrorBody = serde_json::from_slice(&reply.body).unwrap(); + assert_eq!(error.code, code, "{}: {error:?}", case.name); + } else { + assert_eq!(reply.header.ty, FrameType::Response, "{}", case.name); + let result: cortexkit_role_tool_provider::result::ToolResult = + serde_json::from_slice(&reply.body).unwrap(); + cortexkit_role_tool_provider::result::validate(&result).unwrap(); + assert!(result.error_code.is_none(), "{}: {result:?}", case.name); + } + json!({"request_frame":normalized_frame(&request, dir.path(), case.tool),"reply_frame":normalized_frame(reply.frame(), dir.path(), case.tool)}) +} + +#[test] +fn carrier_sample_normalization_is_narrow() { + let body = r#"{"started_at":17,"duration_ms":12,"waited":{"elapsed_ms":9},"text":"status\nWaited 9ms\n"}"#; + let frame = Frame::build( + FrameType::Response, + control_flags(), + 41, + 1, + 7, + body.as_bytes().to_vec(), + ) + .unwrap(); + let watch = normalized_frame(&frame, Path::new("/unused-fixture-root"), "bash_watch"); + assert_eq!( + watch["body_utf8"], + r#"{"started_at":0,"duration_ms":12,"waited":{"elapsed_ms":0},"text":"status\nWaited 0ms\n"}"# + ); + let status = normalized_frame(&frame, Path::new("/unused-fixture-root"), "bash_status"); + assert_eq!( + status["body_utf8"], + r#"{"started_at":0,"duration_ms":12,"waited":{"elapsed_ms":9},"text":"status\nWaited 9ms\n"}"# + ); +} + +#[tokio::test] +#[ignore = "explicitly regenerate the reviewed CKIOS carrier wire samples"] +async fn capture_carrier_samples() { + std::fs::create_dir_all(sample_dir()).unwrap(); + for case in cases() { + let sample = run_case(&case).await; + let common = sample_dir().join(format!("{}.json", case.name)); + let path = if cfg!(windows) { + let baseline: Value = serde_json::from_slice(&std::fs::read(&common).unwrap()).unwrap(); + if sample == baseline { + println!("CARRIER_SAMPLE_UNCHANGED {}", case.name); + continue; + } + // Preserve genuine platform output without rewriting the common captures. + sample_dir().join(format!("{}.windows.json", case.name)) + } else { + common + }; + let json = format!("{}\n", serde_json::to_string_pretty(&sample).unwrap()); + std::fs::write(&path, &json).unwrap(); + println!( + "CARRIER_SAMPLE {} {sample}", + path.file_stem().unwrap().to_str().unwrap() + ); + } +} + +#[tokio::test] +async fn carrier_samples_match_wire() { + for case in cases() { + let path = sample_path(&case); + let expected: Value = + serde_json::from_slice(&std::fs::read(&path).unwrap_or_else(|error| { + panic!( + "{}: {error}; run capture_carrier_samples explicitly", + path.display() + ) + })) + .unwrap(); + assert_eq!( + run_case(&case).await, + expected, + "carrier sample {}", + case.name + ); + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md new file mode 100644 index 000000000..1ebb807b0 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md @@ -0,0 +1,132 @@ +# Phone carrier read-gate wire samples + +Captured from main commit **9782a74b254bdfbeeb0c7e09909b8d888609ffb1** +using the production handlers and route admission code at that commit. The +capture adds tests only; it does not change the gate or the tool implementations. + +These are `tool.call` frames on the untrusted `reserved:occipital` carrier route, +negotiating `tool-result/v1`, with the `worker` preset and bound session +`session-s`. They are not the separate, projected `rich_ui` operation results in +the adjacent schema-freeze fixtures. In particular, the task tools here take +`taskId`, not the rich-UI viewed-call `task` binding. + +Successful replies have tagged `tool-result/v1` bodies. Admission refusals are +protocol `Error` frames with `code` and `message`, even when `tool-result/v1` +is negotiated; they are not fabricated tool-result error envelopes. + +## Cases + +| File | Request | Expected outcome | +| --- | --- | --- | +| `read.json` | Read `main.rs` | Success | +| `aft_outline.json` | Outline `main.rs` | Success | +| `aft_zoom.json` | Zoom to `greeting` in `main.rs` | Success | +| `aft_search.json` | Search for literal pattern `greeting` | Success | +| `grep.json` | Grep Rust files for `greeting` | Success | +| `glob.json` | Glob `*.rs` | Success | +| `aft_callgraph.json` | Find callers of `greeting` | Success | +| `bash_status.json` | Observe a completed, session-owned task | Success | +| `bash_watch.json` | Watch that completed task | Success | +| `refused-edit.json` | Call non-admitted `edit` | `carrier_read_only` | +| `refused-web.json` | Outline an HTTPS target | `carrier_read_only` | +| `refused-github-issue.json` | Read an `issue://` target | `carrier_read_only` | +| `refused-github-pr.json` | Read a `pr://` target | `carrier_read_only` | +| `refused-invalid-arguments.json` | Read with an unknown argument | `invalid_request` | + +The read-only gate has no numeric argument limit of its own. The invalid case +captures the tool-provider schema admission refusal after the read-only gate. +HTTPS **read** is not used as the web refusal: this gate specifically rejects +web targets for outline/zoom, and GitHub targets for read/outline/zoom. + +## Fixture and capture boundary + +Each case gets an isolated project containing only this `main.rs`: + +```rust +fn greeting() -> &'static str { + "hello phone" +} + +fn main() { + println!("{}", greeting()); +} +``` + +Trigram and callgraph indexes are ready before each request; semantic embedding +is disabled. The task cases observe persisted stdout `hello phone\n`, empty +stderr and exit code 0. The task has a fixed 12 ms duration; no shell process or +network request is needed. + +The tests reuse the existing carrier-gate route harness: an actual request +`Frame` enters `handle_tool_call`, reaches real command handlers, and passes +through final `project_tool_result()` serialization. This is an in-process +route capture, not a live phone/socket session or a fabricated handler response. + +## JSON frame representation and normalization + +Each file contains `request_frame` and `reply_frame`. Each frame has: + +- `header_hex`: the complete binary frame header, hex encoded. +- `body_utf8`: the complete serialized JSON body as a string, **not** a parsed + and reserialized body object. Parse this string to consume the tagged result. + +To recover frame bytes, hex-decode `header_hex` and append the UTF-8 bytes of +`body_utf8`. JSON escaping in the outer sample file is only the storage wrapper; +the inner body's key order, whitespace, text and envelope fields are preserved. + +Normalization is limited to: + +- The temporary fixture directory becomes `${FIXTURE_ROOT}` in body text. +- Task `started_at`/`finished_at` timestamps, when present, become numeric `0`. +- Only for `bash_watch`, measured `waited.elapsed_ms` becomes numeric `0`, and + the corresponding rendered `Waited Nms` becomes `Waited 0ms`. This is measured + observation time, not the fixture task's fixed `duration_ms` of 12. + +The header's body length is recomputed after normalization. Frame channel 41, +epoch 1, correlation 7, derived request id `subc-41-7` and task id +`bash-0123456789abcdef` are fixed by the harness, not rewritten. No fields are +dropped; all other messages, result data and durations are preserved. + +## Platform variants + +All fourteen cases were captured natively on Windows. Only `bash_status` +differs from the common samples: + +| Windows variant | Preserved platform difference | +| --- | --- | +| `bash_status.windows.json` | Task status includes `· live descendant check n/a on Windows (background tasks do not use a Job Object)`. | + +`bash_watch` and the other twelve cases match their common samples after the +existing normalization, so they have no Windows variants. + +On Windows, the normal test selects `.windows.json` when that variant +exists; otherwise it uses the common `.json`. Other platforms always use +the common sample. Platform-specific reply text is preserved, not normalized. + +The native Windows capture reruns all fourteen cases and compares each complete +normalized frame pair with its common sample. It writes a `.windows.json` file +only when the pair differs, and prints `CARRIER_SAMPLE_UNCHANGED ` for an +identical pair. It never overwrites the common JSON files on Windows. + +## Reproduce and review updates + +Run from the repository root on Linux (the worker guide requires remote Cargo +jobs; print `git log -1` first and use 1 or 2 vCPUs): + +```sh +git log -1 +cargo test -p agent-file-tools --lib -- carrier_samples::capture_carrier_samples --ignored --nocapture +cargo test -p agent-file-tools --lib -- carrier_samples::carrier_samples_match_wire +``` + +Run the same capture and comparison commands natively on Windows using +`runon: "windows"` to regenerate or verify Windows variants; cross-compilation +cannot capture platform-specific replies. + +On non-Windows platforms, the ignored capture test explicitly overwrites the +fourteen common JSON samples. On Windows, it writes only differing variants. +It prints each written sample as `CARRIER_SAMPLE [.windows] ` for +export from remote jobs. +The normal test never updates fixtures: it reruns all cases, validates successful +tagged results and admission error codes, and compares normalized frames, +including the exact serialized body strings, to these reviewed samples. diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_callgraph.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_callgraph.json new file mode 100644 index 000000000..67119c974 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_callgraph.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "7d0000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"aft_callgraph\",\"preset\":\"worker\",\"arguments\":{\"path\":\"main.rs\",\"symbol\":\"greeting\",\"op\":\"callers\"}}" + }, + "reply_frame": { + "header_hex": "660000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"1 caller · 1 file group\\nmain.rs\\n ↳ main:6\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_outline.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_outline.json new file mode 100644 index 000000000..76c27d781 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_outline.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "5a0000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"aft_outline\",\"preset\":\"worker\",\"arguments\":{\"target\":\"main.rs\"}}" + }, + "reply_frame": { + "header_hex": "760000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"main.rs\\n fn greeting() -> &'static str 1:3\\n fn main() 5:7\\n\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_search.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_search.json new file mode 100644 index 000000000..72fd22f9f --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_search.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "5b0000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"aft_search\",\"preset\":\"worker\",\"arguments\":{\"pattern\":\"greeting\"}}" + }, + "reply_frame": { + "header_hex": "1f0100000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"main.rs\\n1: fn greeting() -> &'static str {\\n6: println!(\\\"{}\\\", greeting());\\n\\n1 files match (2 matching lines); one result per file, definitions first.\\n[interpreted_as: regex]\\ntrigram: ready\\nsemantic: unavailable: disabled\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_zoom.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_zoom.json new file mode 100644 index 000000000..cee18c500 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_zoom.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "7a0000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"aft_zoom\",\"preset\":\"worker\",\"arguments\":{\"batch\":[{\"target\":\"main.rs\",\"symbols\":[\"greeting\"]}]}}" + }, + "reply_frame": { + "header_hex": "d50000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"main.rs:1-3 [function greeting]\\n\\n1: fn greeting() -> &'static str {\\n2: \\\"hello phone\\\"\\n3: }\\n4: \\n5: fn main() {\\n6: println!(\\\"{}\\\", greeting());\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.json new file mode 100644 index 000000000..09c6cc8b4 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "680000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"bash_status\",\"preset\":\"worker\",\"arguments\":{\"taskId\":\"bash-0123456789abcdef\"}}" + }, + "reply_frame": { + "header_hex": "770000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"Task bash-0123456789abcdef: completed (exit 0) 0s\\nhello phone\\n\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.windows.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.windows.json new file mode 100644 index 000000000..32e2195ff --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.windows.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "680000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"bash_status\",\"preset\":\"worker\",\"arguments\":{\"taskId\":\"bash-0123456789abcdef\"}}" + }, + "reply_frame": { + "header_hex": "ca0000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"Task bash-0123456789abcdef: completed (exit 0) 0s · live descendant check n/a on Windows (background tasks do not use a Job Object)\\nhello phone\\n\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_watch.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_watch.json new file mode 100644 index 000000000..df6724139 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_watch.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "670000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"bash_watch\",\"preset\":\"worker\",\"arguments\":{\"taskId\":\"bash-0123456789abcdef\"}}" + }, + "reply_frame": { + "header_hex": "b70000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"Task bash-0123456789abcdef: completed (exit 0) 0s\\nWaited 0ms (limit 1800000ms); task exited (completed, exit 0).\\nhello phone\\n\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/glob.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/glob.json new file mode 100644 index 000000000..9dd0858d5 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/glob.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "510000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"glob\",\"preset\":\"worker\",\"arguments\":{\"pattern\":\"*.rs\"}}" + }, + "reply_frame": { + "header_hex": "560000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"1 file matching *.rs\\n\\nmain.rs\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/grep.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/grep.json new file mode 100644 index 000000000..d42eaec55 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/grep.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "660000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"grep\",\"preset\":\"worker\",\"arguments\":{\"pattern\":\"greeting\",\"include\":\"*.rs\"}}" + }, + "reply_frame": { + "header_hex": "a50000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"main.rs\\n1: fn greeting() -> &'static str {\\n6: println!(\\\"{}\\\", greeting());\\n\\nFound 2 matches in 1 file\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/read.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/read.json new file mode 100644 index 000000000..893e5844c --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/read.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "510000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"read\",\"preset\":\"worker\",\"arguments\":{\"path\":\"main.rs\"}}" + }, + "reply_frame": { + "header_hex": "ba0000000201002900010000000700000000000000", + "body_utf8": "{\"content\":[{\"type\":\"text\",\"text\":\"1: fn greeting() -> &'static str {\\n2: \\\"hello phone\\\"\\n3: }\\n4: \\n5: fn main() {\\n6: println!(\\\"{}\\\", greeting());\\n7: }\\n\"}],\"isError\":false}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-edit.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-edit.json new file mode 100644 index 000000000..0550f095f --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-edit.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "5c0000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"edit\",\"preset\":\"worker\",\"arguments\":{\"path\":\"main.rs\",\"edits\":[]}}" + }, + "reply_frame": { + "header_hex": "700000000205002900010000000700000000000000", + "body_utf8": "{\"code\":\"carrier_read_only\",\"message\":\"This route is read-only: edit is not available to the rich-UI provider.\"}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-issue.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-issue.json new file mode 100644 index 000000000..9aa3a4a03 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-issue.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "550000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"read\",\"preset\":\"worker\",\"arguments\":{\"path\":\"issue://935\"}}" + }, + "reply_frame": { + "header_hex": "7d0000000205002900010000000700000000000000", + "body_utf8": "{\"code\":\"carrier_read_only\",\"message\":\"This route is read-only: issue:// targets are not available to the rich-UI provider.\"}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-pr.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-pr.json new file mode 100644 index 000000000..49c87cae8 --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-pr.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "520000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"read\",\"preset\":\"worker\",\"arguments\":{\"path\":\"pr://935\"}}" + }, + "reply_frame": { + "header_hex": "7a0000000205002900010000000700000000000000", + "body_utf8": "{\"code\":\"carrier_read_only\",\"message\":\"This route is read-only: pr:// targets are not available to the rich-UI provider.\"}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-invalid-arguments.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-invalid-arguments.json new file mode 100644 index 000000000..4a26159ea --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-invalid-arguments.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "630000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"read\",\"preset\":\"worker\",\"arguments\":{\"path\":\"main.rs\",\"unexpected\":true}}" + }, + "reply_frame": { + "header_hex": "700000000205002900010000000700000000000000", + "body_utf8": "{\"code\":\"invalid_request\",\"message\":\"argument is absent from the served schema\",\"detail\":{\"field\":\"unexpected\"}}" + } +} diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-web.json b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-web.json new file mode 100644 index 000000000..de8e8988f --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-web.json @@ -0,0 +1,10 @@ +{ + "request_frame": { + "header_hex": "6c0000000200002900010000000700000000000000", + "body_utf8": "{\"op\":\"tool.call\",\"name\":\"aft_outline\",\"preset\":\"worker\",\"arguments\":{\"target\":\"https://example.com/phone\"}}" + }, + "reply_frame": { + "header_hex": "780000000205002900010000000700000000000000", + "body_utf8": "{\"code\":\"carrier_read_only\",\"message\":\"This route is read-only: URL targets are not available to the rich-UI provider.\"}" + } +}