Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user