231 lines
6.7 KiB
Rust
231 lines
6.7 KiB
Rust
use super::{
|
|
run_jq_filter, write_filter_output, write_json, write_json_line, write_list, TableFormat,
|
|
};
|
|
use comfy_table::Cell;
|
|
use serde::Serialize;
|
|
use serde_json::json;
|
|
use warp_cli::agent::OutputFormat;
|
|
use warp_cli::json_filter::parse_jq_filter;
|
|
|
|
#[derive(Serialize)]
|
|
struct TestItem {
|
|
id: &'static str,
|
|
subject: &'static str,
|
|
}
|
|
|
|
impl TableFormat for TestItem {
|
|
fn header() -> Vec<Cell> {
|
|
vec![Cell::new("ID"), Cell::new("SUBJECT")]
|
|
}
|
|
|
|
fn row(&self) -> Vec<Cell> {
|
|
vec![Cell::new(self.id), Cell::new(self.subject)]
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn write_list_emits_json_for_json_output_format() {
|
|
let mut output = Vec::new();
|
|
let items = [TestItem {
|
|
id: "message-1",
|
|
subject: "Build update",
|
|
}];
|
|
|
|
write_list(items, OutputFormat::Json, &mut output).unwrap();
|
|
|
|
let rendered = String::from_utf8(output).unwrap();
|
|
assert_eq!(rendered, r#"[{"id":"message-1","subject":"Build update"}]"#);
|
|
}
|
|
|
|
#[test]
|
|
fn write_list_emits_ndjson_for_ndjson_output_format() {
|
|
let mut output = Vec::new();
|
|
let items = [
|
|
TestItem {
|
|
id: "message-1",
|
|
subject: "Build update",
|
|
},
|
|
TestItem {
|
|
id: "message-2",
|
|
subject: "Pivot",
|
|
},
|
|
];
|
|
|
|
write_list(items, OutputFormat::Ndjson, &mut output).unwrap();
|
|
|
|
let rendered = String::from_utf8(output).unwrap();
|
|
assert_eq!(
|
|
rendered,
|
|
"{\"id\":\"message-1\",\"subject\":\"Build update\"}\n{\"id\":\"message-2\",\"subject\":\"Pivot\"}\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn write_json_emits_pretty_json_with_trailing_newline() {
|
|
let mut output = Vec::new();
|
|
let item = TestItem {
|
|
id: "message-1",
|
|
subject: "Build update",
|
|
};
|
|
|
|
write_json(&item, &mut output).unwrap();
|
|
|
|
let rendered = String::from_utf8(output).unwrap();
|
|
assert_eq!(
|
|
rendered,
|
|
"{\n \"id\": \"message-1\",\n \"subject\": \"Build update\"\n}\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn write_json_line_emits_compact_json_with_trailing_newline() {
|
|
let mut output = Vec::new();
|
|
let item = TestItem {
|
|
id: "message-1",
|
|
subject: "Build update",
|
|
};
|
|
|
|
write_json_line(&item, &mut output).unwrap();
|
|
|
|
let rendered = String::from_utf8(output).unwrap();
|
|
assert_eq!(
|
|
rendered,
|
|
"{\"id\":\"message-1\",\"subject\":\"Build update\"}\n"
|
|
);
|
|
}
|
|
/// A small fixture that matches the shape of `GET /api/v1/agent/runs`.
|
|
fn list_response_fixture() -> serde_json::Value {
|
|
json!({
|
|
"runs": [
|
|
{ "task_id": "01HX0000000000000000000001", "title": "Alpha", "state": "succeeded" },
|
|
{ "task_id": "01HX0000000000000000000002", "title": "Beta", "state": "failed" },
|
|
],
|
|
"page_info": { "has_next_page": false, "next_cursor": null }
|
|
})
|
|
}
|
|
|
|
/// Run a filter against `value` and return its output as a UTF-8 string.
|
|
fn run(value: serde_json::Value, filter: &str) -> String {
|
|
let filter = parse_jq_filter(filter).expect("filter compiles");
|
|
let mut buf = Vec::new();
|
|
run_jq_filter(value, &filter, &mut buf).expect("filter runs without error");
|
|
String::from_utf8(buf).expect("output is valid utf-8")
|
|
}
|
|
|
|
#[test]
|
|
fn identity_filter_matches_non_filtered_json_output() {
|
|
let fixture = list_response_fixture();
|
|
let filtered = run(fixture.clone(), ".");
|
|
|
|
let mut expected = Vec::new();
|
|
serde_json::to_writer_pretty(&mut expected, &fixture).unwrap();
|
|
expected.push(b'\n');
|
|
|
|
assert_eq!(filtered.as_bytes(), expected.as_slice());
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_string_is_unwrapped() {
|
|
let fixture = list_response_fixture();
|
|
let out = run(fixture, ".runs[0].task_id");
|
|
assert_eq!(out, "01HX0000000000000000000001\n");
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_number_is_unwrapped() {
|
|
let fixture = list_response_fixture();
|
|
let out = run(fixture, ".runs | length");
|
|
assert_eq!(out, "2\n");
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_bool_and_null_are_unwrapped() {
|
|
let fixture = list_response_fixture();
|
|
assert_eq!(run(fixture.clone(), ".page_info.has_next_page"), "false\n");
|
|
assert_eq!(run(fixture.clone(), ".page_info.next_cursor"), "null\n");
|
|
assert_eq!(run(fixture, "true"), "true\n");
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_scalar_outputs_each_on_own_line() {
|
|
let fixture = list_response_fixture();
|
|
let out = run(fixture, ".runs[].task_id");
|
|
assert_eq!(
|
|
out,
|
|
"01HX0000000000000000000001\n01HX0000000000000000000002\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_scalar_output_is_pretty_json() {
|
|
let fixture = list_response_fixture();
|
|
let out = run(fixture, ".runs[0]");
|
|
let expected = serde_json::to_string_pretty(&json!({
|
|
"task_id": "01HX0000000000000000000001",
|
|
"title": "Alpha",
|
|
"state": "succeeded",
|
|
}))
|
|
.unwrap();
|
|
assert_eq!(out, format!("{expected}\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn inner_scalars_stay_json_encoded() {
|
|
let fixture = list_response_fixture();
|
|
let out = run(fixture, ".runs");
|
|
assert!(
|
|
out.contains(r#""title": "Alpha""#),
|
|
"inner strings should remain JSON-encoded, got:\n{out}"
|
|
);
|
|
assert!(
|
|
out.contains(r#""task_id": "01HX0000000000000000000001""#),
|
|
"inner task_id should remain JSON-encoded, got:\n{out}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_filter_produces_no_output() {
|
|
let fixture = list_response_fixture();
|
|
let out = run(fixture, "empty");
|
|
assert_eq!(out, "");
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_error_is_surfaced_after_partial_output() {
|
|
// `.runs[].title | .[0]` succeeds on the first string but fails on a
|
|
// later element if we introduce a non-indexable value. Build a fixture
|
|
// where the second element has a numeric title, which triggers a runtime
|
|
// error when we try to index it as a string.
|
|
let fixture = json!({
|
|
"runs": [
|
|
{ "title": "hello" },
|
|
{ "title": 42 },
|
|
]
|
|
});
|
|
let filter = parse_jq_filter(".runs[].title | .[0:1]").expect("filter compiles");
|
|
let mut buf = Vec::new();
|
|
let result = run_jq_filter(fixture, &filter, &mut buf);
|
|
|
|
// The filter should fail at runtime on the integer title.
|
|
assert!(result.is_err(), "expected runtime error");
|
|
// The valid output from the first element should already be on the buffer.
|
|
let rendered = String::from_utf8(buf).unwrap();
|
|
assert!(
|
|
rendered.starts_with("h\n"),
|
|
"expected partial output before the error, got: {rendered:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn write_filter_output_respects_scalar_unwrapping_for_direct_vals() {
|
|
use jaq_json::Val;
|
|
|
|
// Exercise `write_filter_output` directly with jaq values to lock in the
|
|
// expected rendering of each scalar variant.
|
|
let mut buf = Vec::new();
|
|
write_filter_output(&Val::Null, &mut buf).unwrap();
|
|
write_filter_output(&Val::Bool(true), &mut buf).unwrap();
|
|
write_filter_output(&Val::Bool(false), &mut buf).unwrap();
|
|
assert_eq!(String::from_utf8(buf).unwrap(), "null\ntrue\nfalse\n");
|
|
}
|