From 791c71c7ca324b51b0e39ddfe6a305aeae378848 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:18:09 +0100 Subject: [PATCH 1/7] mason: add carrier wire sample capture harness Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> --- crates/aft/src/subc/tool_provider.rs | 18 +- .../route_tests/carrier_samples.rs | 269 ++++++++++++++++++ .../rich_ui/carrier-samples/README.md | 95 +++++++ 3 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md 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..b1c0f239b --- /dev/null +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -0,0 +1,269 @@ +//! 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 = dir.path().join("tasks"); + 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 normalized_frame(frame: &Frame, temporary_root: &Path) -> Value { + let body = String::from_utf8(frame.body.clone()) + .unwrap() + .replace(temporary_root.to_str().unwrap(), "${FIXTURE_ROOT}"); + // Recompute only the byte length affected by replacing the temporary path. + 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; + assert_eq!( + reply.header.ty, + FrameType::Response, + "{}: {:?}", + case.name, + reply.body + ); + let result: cortexkit_role_tool_provider::result::ToolResult = + serde_json::from_slice(&reply.body).unwrap(); + cortexkit_role_tool_provider::result::validate(&result).unwrap(); + assert_eq!( + result.error_code.as_deref(), + case.error, + "{}: {}", + case.name, + String::from_utf8_lossy(&reply.body) + ); + json!({"request_frame":normalized_frame(&request, dir.path()),"reply_frame":normalized_frame(reply.frame(), dir.path())}) +} + +#[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 json = format!("{}\n", serde_json::to_string_pretty(&sample).unwrap()); + std::fs::write(sample_dir().join(format!("{}.json", case.name)), &json).unwrap(); + println!("CARRIER_SAMPLE {} {sample}", case.name); + } +} + +#[tokio::test] +async fn carrier_samples_match_wire() { + for case in cases() { + let path = sample_dir().join(format!("{}.json", case.name)); + 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..41f248eaa --- /dev/null +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md @@ -0,0 +1,95 @@ +# Phone carrier read-gate wire samples (CKIOS) + +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. + +## 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. + +The only normalization is replacement of the temporary fixture directory with +`${FIXTURE_ROOT}` wherever it occurs in body text. The header's body length is +recomputed for that replacement. 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 and no messages, result data, +durations or timestamps are normalized. + +## 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 +``` + +The ignored capture test explicitly overwrites the fourteen JSON samples and +prints each one as `CARRIER_SAMPLE ` for export from remote jobs. +The normal test never updates fixtures: it reruns all cases, validates tagged +results and expected success/error codes, and compares normalized frames, +including the exact serialized body strings, to these reviewed samples. From b1dda809f7c3d6b339ec098a0baa9273f752d8e1 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:20:07 +0100 Subject: [PATCH 2/7] mason: distinguish carrier admission error frames Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> --- .../route_tests/carrier_samples.rs | 28 ++++++++----------- .../rich_ui/carrier-samples/README.md | 8 ++++-- 2 files changed, 17 insertions(+), 19 deletions(-) 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 index b1c0f239b..9f3adb1d6 100644 --- a/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -215,23 +215,17 @@ async fn run_case(case: &Case) -> Value { dispatch, ) .await; - assert_eq!( - reply.header.ty, - FrameType::Response, - "{}: {:?}", - case.name, - reply.body - ); - let result: cortexkit_role_tool_provider::result::ToolResult = - serde_json::from_slice(&reply.body).unwrap(); - cortexkit_role_tool_provider::result::validate(&result).unwrap(); - assert_eq!( - result.error_code.as_deref(), - case.error, - "{}: {}", - case.name, - String::from_utf8_lossy(&reply.body) - ); + 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()),"reply_frame":normalized_frame(reply.frame(), dir.path())}) } diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md index 41f248eaa..8187c6910 100644 --- a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md @@ -10,6 +10,10 @@ negotiating `tool-result/v1`, with the `worker` preset and bound session 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 | @@ -90,6 +94,6 @@ cargo test -p agent-file-tools --lib -- carrier_samples::carrier_samples_match_w The ignored capture test explicitly overwrites the fourteen JSON samples and prints each one as `CARRIER_SAMPLE ` for export from remote jobs. -The normal test never updates fixtures: it reruns all cases, validates tagged -results and expected success/error codes, and compares normalized frames, +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. From c9bf4cc2b6ae9d53deaf70fc57a6257afcac8140 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:35:58 +0100 Subject: [PATCH 3/7] mason: normalize volatile carrier observation timing Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> --- .../route_tests/carrier_samples.rs | 42 +++++++++++++++++-- .../rich_ui/carrier-samples/README.md | 18 +++++--- 2 files changed, 50 insertions(+), 10 deletions(-) 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 index 9f3adb1d6..8ca4184b2 100644 --- a/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -130,7 +130,7 @@ fn fixture() -> (tempfile::TempDir, ProjectRootId, Arc) { } // A completed task avoids a live process and makes its duration reproducible. - let storage = dir.path().join("tasks"); + 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(), @@ -174,10 +174,19 @@ fn sample_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rich_ui/carrier-samples") } -fn normalized_frame(frame: &Frame, temporary_root: &Path) -> Value { - let body = String::from_utf8(frame.body.clone()) +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"\bWaited \d+ms").unwrap(); + body = rendered.replace_all(&body, "Waited 0ms").into_owned(); + } // Recompute only the byte length affected by replacing the temporary path. let header = &frame.header; let normalized = Frame::build_with_version( @@ -226,7 +235,32 @@ async fn run_case(case: &Case) -> Value { 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()),"reply_frame":normalized_frame(reply.frame(), dir.path())}) + 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":"Waited 9ms"}"#; + 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":"Waited 0ms"}"# + ); + 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":"Waited 9ms"}"# + ); } #[tokio::test] diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md index 8187c6910..b0c2ba869 100644 --- a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md @@ -74,12 +74,18 @@ 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. -The only normalization is replacement of the temporary fixture directory with -`${FIXTURE_ROOT}` wherever it occurs in body text. The header's body length is -recomputed for that replacement. 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 and no messages, result data, -durations or timestamps are normalized. +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. ## Reproduce and review updates From dc5e98216f8218808e0e53b9a7fb4eca3628c8fc Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:43:26 +0100 Subject: [PATCH 4/7] mason: clarify normalized frame length recalculation Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> --- .../aft/src/subc/tool_provider/route_tests/carrier_samples.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 8ca4184b2..7af333842 100644 --- a/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -187,7 +187,7 @@ fn normalized_frame(frame: &Frame, temporary_root: &Path, tool: &str) -> Value { let rendered = regex::Regex::new(r"\bWaited \d+ms").unwrap(); body = rendered.replace_all(&body, "Waited 0ms").into_owned(); } - // Recompute only the byte length affected by replacing the temporary path. + // Recompute the body byte length after normalizing volatile values. let header = &frame.header; let normalized = Frame::build_with_version( header.ver, From e56516066f33912eb31a581a45e13d5a895b7e73 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:09:46 +0100 Subject: [PATCH 5/7] mason: capture fourteen carrier read-gate wire samples The real watch golden exposed a normalization miss: JSON's escaped newline left no regex word boundary before Waited, so captures differed at 20ms versus 16ms. Match the rendered timing token directly and cover an escaped-newline input without changing the promised normalization scope. Seed samples from the route capture output and verify explicit recapture reproduces them byte for byte. Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> --- .../subc/tool_provider/route_tests/carrier_samples.rs | 9 ++++----- .../tests/fixtures/rich_ui/carrier-samples/README.md | 2 +- .../rich_ui/carrier-samples/aft_callgraph.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/aft_outline.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/aft_search.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/aft_zoom.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/bash_status.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/bash_watch.json | 10 ++++++++++ .../tests/fixtures/rich_ui/carrier-samples/glob.json | 10 ++++++++++ .../tests/fixtures/rich_ui/carrier-samples/grep.json | 10 ++++++++++ .../tests/fixtures/rich_ui/carrier-samples/read.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/refused-edit.json | 10 ++++++++++ .../rich_ui/carrier-samples/refused-github-issue.json | 10 ++++++++++ .../rich_ui/carrier-samples/refused-github-pr.json | 10 ++++++++++ .../carrier-samples/refused-invalid-arguments.json | 10 ++++++++++ .../fixtures/rich_ui/carrier-samples/refused-web.json | 10 ++++++++++ 16 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_callgraph.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_outline.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_search.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/aft_zoom.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_watch.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/glob.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/grep.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/read.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-edit.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-issue.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-github-pr.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-invalid-arguments.json create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/refused-web.json 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 index 7af333842..1e98159e0 100644 --- a/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -184,7 +184,7 @@ fn normalized_frame(frame: &Frame, temporary_root: &Path, tool: &str) -> Value { // 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"\bWaited \d+ms").unwrap(); + 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. @@ -240,8 +240,7 @@ async fn run_case(case: &Case) -> Value { #[test] fn carrier_sample_normalization_is_narrow() { - let body = - r#"{"started_at":17,"duration_ms":12,"waited":{"elapsed_ms":9},"text":"Waited 9ms"}"#; + 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(), @@ -254,12 +253,12 @@ fn carrier_sample_normalization_is_narrow() { 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":"Waited 0ms"}"# + 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":"Waited 9ms"}"# + r#"{"started_at":0,"duration_ms":12,"waited":{"elapsed_ms":9},"text":"status\nWaited 9ms\n"}"# ); } diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md index b0c2ba869..5bef6cd2d 100644 --- a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md @@ -1,4 +1,4 @@ -# Phone carrier read-gate wire samples (CKIOS) +# Phone carrier read-gate wire samples Captured from main commit **9782a74b254bdfbeeb0c7e09909b8d888609ffb1** using the production handlers and route admission code at that commit. The 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_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.\"}" + } +} From 651b1aff158e466ec24f99ec0ee46449f682678c Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:15:34 +0100 Subject: [PATCH 6/7] release notes and search-quality descriptor for carrier-samples Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> --- .../slice-descriptors/land-carrier-samples.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 benchmarks/aft-search/slice-descriptors/land-carrier-samples.json 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" + ] +} From 57651264d755d87e01390b0a6004dff6e99a33a4 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:04:04 +0100 Subject: [PATCH 7/7] mason: preserve native Windows carrier status sample Native Windows capture of all fourteen cases shows only bash_status differs: its reply includes the live-descendant-check limitation. Keep that clause and its encoded header length in a Windows-only variant; bash_watch and the other twelve cases retain the unchanged common samples. Native Windows capture now compares every case to its common frame pair and writes only differing variants, without changing normalization. Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com> (cherry picked from commit e80c3b497e851e8909020c1157080cc8853ff2f4) --- .../route_tests/carrier_samples.rs | 30 ++++++++++++++++-- .../rich_ui/carrier-samples/README.md | 31 +++++++++++++++++-- .../carrier-samples/bash_status.windows.json | 10 ++++++ 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 crates/aft/tests/fixtures/rich_ui/carrier-samples/bash_status.windows.json 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 index 1e98159e0..c0cc677c2 100644 --- a/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs +++ b/crates/aft/src/subc/tool_provider/route_tests/carrier_samples.rs @@ -174,6 +174,15 @@ 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() @@ -268,16 +277,31 @@ 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(sample_dir().join(format!("{}.json", case.name)), &json).unwrap(); - println!("CARRIER_SAMPLE {} {sample}", case.name); + 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_dir().join(format!("{}.json", case.name)); + let path = sample_path(&case); let expected: Value = serde_json::from_slice(&std::fs::read(&path).unwrap_or_else(|error| { panic!( diff --git a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md index 5bef6cd2d..1ebb807b0 100644 --- a/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md +++ b/crates/aft/tests/fixtures/rich_ui/carrier-samples/README.md @@ -87,6 +87,27 @@ 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 @@ -98,8 +119,14 @@ cargo test -p agent-file-tools --lib -- carrier_samples::capture_carrier_samples cargo test -p agent-file-tools --lib -- carrier_samples::carrier_samples_match_wire ``` -The ignored capture test explicitly overwrites the fourteen JSON samples and -prints each one as `CARRIER_SAMPLE ` for export from remote jobs. +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/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}" + } +}