use crate::args::{CliError, CommandKind, InputPayload, StreamingInput, StreamingInputSource};
use crate::cli_io::input::{guess_input_format, prepare_streaming_inputs, resolve_input_format};
use crate::commands::metadata::execute_metadata_command;
use crate::commands::run::should_render_root_help_on_empty_interactive_invocation;
use crate::execute::execute_command;
use crate::{errors, parser, web_payload, web_server};
use std::fs;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn request_web_server_once(state: web_server::WebServerState, target: &str) -> Vec<u8> {
let server = web_server::WebServer::bind_for_test(state).expect("server should bind");
let addr = server.local_addr();
let handle = std::thread::spawn(move || {
server
.serve_once_for_test()
.expect("server should handle one request");
});
let mut stream = TcpStream::connect(addr).expect("client should connect");
write!(
stream,
"GET {target} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n"
)
.expect("request should write");
stream.flush().expect("request should flush");
let mut response = Vec::new();
stream
.read_to_end(&mut response)
.expect("response should read");
handle.join().expect("server thread should join");
response
}
fn test_web_server_state() -> web_server::WebServerState {
web_server::WebServerState {
token: "test-token".to_string(),
result: web_server::WebServerResult::text(
"input.json".to_string(),
".".to_string(),
"json".to_string(),
r#"{"ok":true}"#.to_string(),
),
}
}
fn assert_response_contains(response: &[u8], expected: &str) {
let text = std::str::from_utf8(response).expect("response should be utf8");
assert!(
text.contains(expected),
"response did not contain {expected:?}:\n{text}"
);
}
fn response_body(response: &[u8]) -> &[u8] {
response
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map(|index| &response[index + 4..])
.expect("response should include header terminator")
}
fn canonical_error_text(error: &CliError) -> String {
let CliError::Canonical(failure) = error else {
panic!("expected canonical failure, received {error:?}");
};
format!("{:?}: {}", failure.category(), failure.message())
}
fn test_asset_dir(files: &[(&str, &[u8])]) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should be monotonic enough for tests")
.as_nanos();
let root = std::env::temp_dir().join(format!("treease-cli-test-assets-{unique}"));
fs::create_dir_all(&root).expect("test asset root should be creatable");
for (relative, bytes) in files {
let path = root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("test asset parent should be creatable");
}
fs::write(path, bytes).expect("test asset file should write");
}
root
}
#[test]
fn web_server_serves_result_only_with_matching_token() {
let missing = request_web_server_once(test_web_server_state(), "/cli/result");
assert_response_contains(&missing, "HTTP/1.1 403 Forbidden");
let wrong = request_web_server_once(test_web_server_state(), "/cli/result?token=wrong");
assert_response_contains(&wrong, "HTTP/1.1 403 Forbidden");
let matching = request_web_server_once(test_web_server_state(), "/cli/result?token=test-token");
assert_response_contains(&matching, "HTTP/1.1 200 OK");
assert_response_contains(&matching, "Content-Type: application/json; charset=utf-8");
assert_response_contains(&matching, r#""source_label":"input.json""#);
assert_response_contains(&matching, r#""text":"{\"ok\":true}""#);
}
#[test]
fn web_server_serves_cli_metadata_and_source_separately() {
let missing_meta = request_web_server_once(test_web_server_state(), "/cli/meta");
assert_response_contains(&missing_meta, "HTTP/1.1 403 Forbidden");
let wrong_source = request_web_server_once(test_web_server_state(), "/cli/source?token=wrong");
assert_response_contains(&wrong_source, "HTTP/1.1 403 Forbidden");
let meta = request_web_server_once(test_web_server_state(), "/cli/meta?token=test-token");
assert_response_contains(&meta, "HTTP/1.1 200 OK");
assert_response_contains(&meta, "Content-Type: application/json; charset=utf-8");
assert_response_contains(&meta, "Access-Control-Allow-Origin: *");
assert_response_contains(&meta, r#""source_url":"/cli/source?token=test-token""#);
let source = request_web_server_once(test_web_server_state(), "/cli/source?token=test-token");
assert_response_contains(&source, "HTTP/1.1 200 OK");
assert_response_contains(&source, "Content-Type: application/json; charset=utf-8");
assert_eq!(response_body(&source), br#"{"ok":true}"#);
}
#[test]
fn web_server_graph_url_reuses_remote_editor_and_local_source() {
let server =
web_server::WebServer::bind_for_test(test_web_server_state()).expect("server should bind");
let editor_url = server.editor_url();
assert!(
editor_url.starts_with(
"https://treease.com/editor?textUrl=http%3A%2F%2Flocalhost.treease.com%3A"
)
);
assert!(editor_url.contains("%2Fcli%2Fsource%3Ftoken%3Dtest-token"));
assert!(editor_url.ends_with("&lang=json&ui=editor%2Cviewer"));
}
#[test]
fn default_invocation_parses_expression_and_files() {
let raw = vec![
"treease".to_string(),
".foo".to_string(),
"a.json".to_string(),
];
let parsed = parser::parse_cli_args(&raw).expect("parse should succeed");
assert_eq!(parsed.command, CommandKind::Run);
assert_eq!(parsed.expression, ".foo");
assert_eq!(parsed.files, vec!["a.json"]);
}
#[test]
fn empty_interactive_invocation_prefers_help_over_stdin() {
assert!(should_render_root_help_on_empty_interactive_invocation(
&["treease".to_string()],
true
));
assert!(!should_render_root_help_on_empty_interactive_invocation(
&["treease".to_string()],
false
));
assert!(!should_render_root_help_on_empty_interactive_invocation(
&["treease".to_string(), ".foo".to_string()],
true
));
}
#[test]
fn format_and_indent_options_parse() {
let raw = vec![
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"-I".to_string(),
"2".to_string(),
".foo".to_string(),
];
let parsed = parser::parse_cli_args(&raw).expect("parse should succeed");
assert_eq!(parsed.input_format.as_deref(), Some("yaml"));
assert_eq!(parsed.output_format.as_deref(), Some("json"));
assert_eq!(parsed.indent, Some(2));
assert_eq!(parsed.expression, ".foo");
}
#[test]
fn input_format_guess_prefers_explicit_override() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"--input-format".to_string(),
"yaml".to_string(),
".foo".to_string(),
])
.expect("parse should succeed");
let payload = InputPayload {
name: "data.json".to_string(),
bytes: br#"{"foo":1}"#.to_vec(),
};
let input = resolve_input_format(&parsed, &payload).expect("input format should resolve");
assert_eq!(input, "yaml");
}
#[test]
fn input_format_guess_uses_filename_extension_before_content() {
let payload = InputPayload {
name: "data.yaml".to_string(),
bytes: br#"{"foo":1}"#.to_vec(),
};
let guessed = guess_input_format(&payload);
assert_eq!(guessed.as_deref(), Some("yaml"));
}
#[test]
fn input_format_guess_uses_content_for_stdin() {
let payload = InputPayload {
name: "<stdin>".to_string(),
bytes: b"foo: 1\nbar: 2\n".to_vec(),
};
let guessed = guess_input_format(&payload);
assert_eq!(guessed.as_deref(), Some("yaml"));
}
#[test]
fn input_format_guess_uses_content_for_suffixless_file() {
let payload = InputPayload {
name: "config".to_string(),
bytes: b"foo = 1\nbar = 2\n".to_vec(),
};
let guessed = guess_input_format(&payload);
assert_eq!(guessed.as_deref(), Some("toml"));
}
#[test]
fn input_format_defaults_to_json_when_guess_fails() {
let parsed = parser::parse_cli_args(&["treease".to_string(), ".foo".to_string()])
.expect("parse should succeed");
let payload = InputPayload {
name: "README".to_string(),
bytes: b"plain text without recognizable structure".to_vec(),
};
let input = resolve_input_format(&parsed, &payload).expect("input format should resolve");
assert_eq!(input, "json");
}
#[test]
fn suffixless_yaml_input_executes_without_explicit_input_format() {
let parsed = parser::parse_cli_args(&["treease".to_string(), ".foo".to_string()])
.expect("parse should succeed");
let inputs = vec![InputPayload {
name: "sample".to_string(),
bytes: b"foo: 1\n".to_vec(),
}];
let output = execute_command(&parsed, &inputs).expect("command should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "1\n");
}
#[test]
fn streaming_run_path_reads_file_contents_at_execution_time() {
let root = test_asset_dir(&[("input.yaml", b"foo: 1\n")]);
let path = root.join("input.yaml");
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
".foo".to_string(),
path.to_string_lossy().into_owned(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
fs::write(&path, b"foo: 2\n").expect("test input should update");
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming command should succeed");
assert!(printed);
assert_eq!(String::from_utf8(output).unwrap(), "2\n");
}
#[test]
fn oversized_json_integer_does_not_block_unrelated_field_query() {
let parsed =
parser::parse_cli_args(&["treease".to_string(), ".BaseResp.StatusCode".to_string()])
.expect("parse should succeed");
let inputs = vec![InputPayload {
name: "base.json".to_string(),
bytes: br#"{"BaseResp":{"StatusCode":0},"Huge":9999999999999999999}"#.to_vec(),
}];
let output = execute_command(&parsed, &inputs).expect("command should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "0\n");
}
#[test]
fn null_input_evaluates_expression_without_stdin() {
let parsed =
parser::parse_cli_args(&["treease".to_string(), "-n".to_string(), "1".to_string()])
.expect("parse should succeed");
let output = execute_command(&parsed, &[]).expect("command should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "1\n");
}
#[test]
fn null_input_empty_object_renders_object_not_array() {
let parsed =
parser::parse_cli_args(&["treease".to_string(), "-n".to_string(), "{}".to_string()])
.expect("parse should succeed");
let output = execute_command(&parsed, &[]).expect("command should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "{}\n");
}
#[test]
fn null_input_object_literal_renders_object() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-n".to_string(),
"{\"wrap\": \"frog\"}".to_string(),
])
.expect("parse should succeed");
let output = execute_command(&parsed, &[]).expect("command should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "wrap: frog\n");
}
#[test]
fn null_input_path_creation_filters_derived_targets_without_fallback() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-n".to_string(),
"(.a.b = \"foo\") | (.d.e = \"bar\")".to_string(),
])
.expect("parse should succeed");
let output = execute_command(&parsed, &[]).expect("derived targets should be filtered");
assert_eq!(String::from_utf8(output).unwrap(), "null\n");
}
#[test]
fn cli_executes_canonical_scalar_assignment_for_json_and_yaml() {
for (input_format, output_format, expression, source, expected) in [
(
"json",
"json",
".value = 42",
br#"{"value":1,"untouched":true}"#.as_slice(),
"{\n \"value\": 42,\n \"untouched\": true\n}\n",
),
(
"yaml",
"yaml",
".value = 42",
b"value: !custom old\nuntouched: true\n".as_slice(),
"value: !custom 42\nuntouched: true\n",
),
(
"yaml",
"yaml",
".value =c 42",
b"value: !custom old\nuntouched: true\n".as_slice(),
"value: 42\nuntouched: true\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
input_format.to_string(),
"-o".to_string(),
output_format.to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: format!("value.{input_format}"),
bytes: source.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("canonical scalar assignment should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
expected,
"{input_format} {expression}"
);
}
}
#[test]
fn cli_executes_json_whole_node_assignment_and_uniform_broadcast() {
for (expression, source, expected) in [
(
r#". = {"items": [1, 2]}"#,
br#"{"old":true}"#.as_slice(),
"{\n \"items\": [\n 1,\n 2\n ]\n}\n",
),
(
r#"(.left, .right) = (0, ["replacement"])"#,
br#"{"left":{"old":1},"right":[1,2],"untouched":true}"#.as_slice(),
"{\n \"left\": [\n \"replacement\"\n ],\n \"right\": [\n \"replacement\"\n ],\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("whole-node assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("whole-node assignment should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_json_located_rhs_snapshot_assignment() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
"(.left, .right) = .left.copy".to_string(),
])
.expect("located RHS assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"left":{"copy":{"value":1}},"right":0}"#.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("located RHS assignment should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"left\": {\n \"value\": 1\n },\n \"right\": {\n \"value\": 1\n }\n}\n"
);
}
#[test]
fn cli_executes_nested_target_local_located_rhs_assignment_for_json() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".payload.dst |= .src".to_string(),
])
.expect("nested located RHS assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"payload":{"dst":{"src":{"value":1},"old":true}},"untouched":true}"#.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("nested located RHS assignment should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"payload\": {\n \"dst\": {\n \"value\": 1\n }\n },\n \"untouched\": true\n}\n"
);
}
#[test]
fn cli_executes_json_target_local_update_assignment() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
"(.a, .b) |= . + 10".to_string(),
])
.expect("update assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"a":1,"b":2}"#.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("update assignment should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"a\": 11,\n \"b\": 12\n}\n"
);
}
#[test]
fn cli_executes_json_homogeneous_scalar_add_assignment() {
for (expression, source, expected) in [
(
".payload.dst += .increment",
br#"{"payload":{"dst":2},"increment":3,"untouched":true}"#.as_slice(),
"{\n \"payload\": {\n \"dst\": 5\n },\n \"increment\": 3,\n \"untouched\": true\n}\n",
),
(
r#".label += "bar""#,
br#"{"label":"foo","untouched":true}"#.as_slice(),
"{\n \"label\": \"foobar\",\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("add assignment should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_json_array_add_assignment_for_array_and_ordinary_scalar_rhs() {
for (expression, source, expected) in [
(
".dst += .rhs",
br#"{"dst":[1,[2]],"rhs":[3,[4]],"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1,\n [\n 2\n ],\n 3,\n [\n 4\n ]\n ],\n \"rhs\": [\n 3,\n [\n 4\n ]\n ],\n \"untouched\": true\n}\n",
),
(
".dst += [[3], 4]",
br#"{"dst":[1,2],"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1,\n 2,\n [\n 3\n ],\n 4\n ],\n \"untouched\": true\n}\n",
),
(
".dst += .rhs",
br#"{"dst":[1],"rhs":true,"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1,\n true\n ],\n \"rhs\": true,\n \"untouched\": true\n}\n",
),
(
".dst += 2",
br#"{"dst":[1],"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1,\n 2\n ],\n \"untouched\": true\n}\n",
),
(
".dst += .rhs",
br#"{"dst":[1],"rhs":2.5,"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1,\n 2.5\n ],\n \"rhs\": 2.5,\n \"untouched\": true\n}\n",
),
(
r#".dst += "value""#,
br#"{"dst":[1],"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1,\n \"value\"\n ],\n \"untouched\": true\n}\n",
),
(
".dst += null",
br#"{"dst":[1],"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1\n ],\n \"untouched\": true\n}\n",
),
(
".dst += .rhs",
br#"{"dst":[1],"rhs":null,"untouched":true}"#.as_slice(),
"{\n \"dst\": [\n 1\n ],\n \"rhs\": null,\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("array add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("array add assignment should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_json_array_add_for_located_and_derived_ordinary_scalar_rhs() {
for (expression, expected) in [
(".dst + .flag", "[\n 1,\n true\n]\n"),
(".dst + 2", "[\n 1,\n 2\n]\n"),
(".dst + .ratio", "[\n 1,\n 2.5\n]\n"),
(r#".dst + "value""#, "[\n 1,\n \"value\"\n]\n"),
(".dst + null", "[\n 1\n]\n"),
(".dst + .nil", "[\n 1\n]\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("array add should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"flag":true,"ratio":2.5,"nil":null}"#.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("array add should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_embeds_explicit_encoder_results_as_array_string_members() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2},"scalar":2,"text":"value","people":[{"name":"Ada","age":37}]}"#
.to_vec(),
}];
for (expression, expected) in [
(
".dst + (.object | to_json(0))",
"[\n 1,\n \"{\\\"x\\\":2}\"\n]\n",
),
(
".dst + (.object | to_yaml(2))",
"[\n 1,\n \"x: 2\\n\"\n]\n",
),
(
r#".dst + ({"x":3} | to_json(0))"#,
"[\n 1,\n \"{\\\"x\\\":3}\"\n]\n",
),
(".dst + (.scalar | to_json(0))", "[\n 1,\n \"2\"\n]\n"),
(
".dst + (.people | to_csv)",
"[\n 1,\n \"name,age\\nAda,37\\n\"\n]\n",
),
(".dst + (.text | @base64)", "[\n 1,\n \"dmFsdWU=\"\n]\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("array encoded-string add should parse");
let output =
execute_command(&parsed, &input).expect("encoded text should become one string member");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_appends_ordinary_objects_as_single_array_members() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"nested":[2,{"x":3}]}}"#.to_vec(),
}];
for (expression, expected) in [
(
".dst + .object",
"[\n 1,\n {\n \"nested\": [\n 2,\n {\n \"x\": 3\n }\n ]\n }\n]\n",
),
(
r#".dst + {"literal":{"values":[4,5]}}"#,
"[\n 1,\n {\n \"literal\": {\n \"values\": [\n 4,\n 5\n ]\n }\n }\n]\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("array object add should parse");
let output =
execute_command(&parsed, &input).expect("object should append as one array member");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_appends_explicit_encoder_results_as_array_string_members() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2},"text":"value"}"#.to_vec(),
}];
for (expression, expected_member) in [
(".dst += (.object | to_json(0))", "\"{\\\"x\\\":2}\""),
(".dst += (.object | to_yaml(2))", "\"x: 2\\n\""),
(r#".dst += ({"x":3} | to_json(0))"#, "\"{\\\"x\\\":3}\""),
(".dst += (.text | @base64)", "\"dmFsdWU=\""),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("encoded-string add assignment should parse");
let output = execute_command(&parsed, &input)
.expect("encoded text should append and publish as a string");
assert_eq!(
String::from_utf8(output).unwrap(),
format!(
"{{\n \"dst\": [\n 1,\n {expected_member}\n ],\n \"object\": {{\n \"x\": 2\n }},\n \"text\": \"value\"\n}}\n"
),
"{expression}"
);
}
}
#[test]
fn cli_encoded_string_add_assignment_uses_zero_or_last_rhs_result() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2}}"#.to_vec(),
}];
for (expression, expected) in [
(
".dst += ((.object | to_json(0)) | select(false))",
"{\n \"dst\": [\n 1\n ],\n \"object\": {\n \"x\": 2\n }\n}\n",
),
(
".dst += ((.object | to_json(0)), null)",
"{\n \"dst\": [\n 1\n ],\n \"object\": {\n \"x\": 2\n }\n}\n",
),
(
".dst += (null, (.object | to_json(0)))",
"{\n \"dst\": [\n 1,\n \"{\\\"x\\\":2}\"\n ],\n \"object\": {\n \"x\": 2\n }\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("encoded-string N-last assignment should parse");
let output = execute_command(&parsed, &input)
.expect("zero or last RHS result should determine the published document");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_encoded_string_add_assignment_reports_late_error_without_output() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".dst += ((.object | to_json(0)), error("late encoded append"))"#.to_string(),
])
.expect("late-error encoded-string assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2}}"#.to_vec(),
}];
let error =
execute_command(&parsed, &input).expect_err("late RHS error should fail atomically");
assert_eq!(
canonical_error_text(&error),
"Evaluation: late encoded append"
);
}
#[test]
fn cli_executes_ordinary_null_plus_nested_array_for_literal_and_located_values() {
for (expression, expected) in [
("null + [1, [2]]", "[\n 1,\n [\n 2\n ]\n]\n"),
(".nil + .rhs", "[\n 3,\n [\n 4,\n 5\n ]\n]\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("null plus array should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"nil":null,"rhs":[3,[4,5]]}"#.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("null plus array should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_json_null_add_assignment_for_root_and_nested_arrays() {
for (expression, source, expected) in [
(
". += [1, [2]]",
b"null".as_slice(),
"[\n 1,\n [\n 2\n ]\n]\n",
),
(
".payload.dst += .rhs",
br#"{"payload":{"dst":null},"rhs":[3,[4]],"untouched":true}"#.as_slice(),
"{\n \"payload\": {\n \"dst\": [\n 3,\n [\n 4\n ]\n ]\n },\n \"rhs\": [\n 3,\n [\n 4\n ]\n ],\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("null array add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("null array add assignment should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_null_array_add_assignment_uses_zero_or_last_rhs_result() {
for (expression, expected) in [
(
".dst += (.empty | select(false))",
"{\n \"dst\": null,\n \"empty\": null\n}\n",
),
(
".dst += ([1], null)",
"{\n \"dst\": null,\n \"empty\": null\n}\n",
),
(
".dst += (null, [1, [2]])",
"{\n \"dst\": [\n 1,\n [\n 2\n ]\n ],\n \"empty\": null\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("null array N-last assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":null,"empty":null}"#.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("zero or last RHS result should determine output");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_null_array_add_assignment_reports_late_error_without_output() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".dst += ([1], error("late null array"))"#.to_string(),
])
.expect("late-error null array assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":null}"#.to_vec(),
}];
let error =
execute_command(&parsed, &input).expect_err("late RHS error should fail atomically");
assert_eq!(canonical_error_text(&error), "Evaluation: late null array");
}
#[test]
fn cli_array_add_assignment_uses_the_last_null_or_scalar_rhs_result() {
for (expression, expected) in [
(".dst += (2, null)", "{\n \"dst\": [\n 1\n ]\n}\n"),
(
".dst += (null, 2)",
"{\n \"dst\": [\n 1,\n 2\n ]\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("array null N-last assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1]}"#.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("the last RHS result should determine output");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_batch_appends_each_documents_located_scalar_rhs_in_isolation() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("array scalar add assignment should parse");
let inputs = vec![
InputPayload {
name: "first.json".to_string(),
bytes: br#"{"dst":[1],"rhs":true}"#.to_vec(),
},
InputPayload {
name: "second.json".to_string(),
bytes: br#"{"dst":[2],"rhs":"second"}"#.to_vec(),
},
];
let output = execute_command(&parsed, &inputs)
.expect("each batch document should use its own scalar RHS");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1,\n true\n ],\n \"rhs\": true\n}\n{\n \"dst\": [\n 2,\n \"second\"\n ],\n \"rhs\": \"second\"\n}\n"
);
}
#[test]
fn cli_batch_preserves_each_documents_array_for_located_null_rhs() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("array null add assignment should parse");
let inputs = vec![
InputPayload {
name: "first.json".to_string(),
bytes: br#"{"dst":[1],"rhs":null}"#.to_vec(),
},
InputPayload {
name: "second.json".to_string(),
bytes: br#"{"dst":[2,3],"rhs":null}"#.to_vec(),
},
];
let output = execute_command(&parsed, &inputs)
.expect("each batch document should preserve its own array");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1\n ],\n \"rhs\": null\n}\n{\n \"dst\": [\n 2,\n 3\n ],\n \"rhs\": null\n}\n"
);
}
#[test]
fn cli_rejects_unapproved_json_array_add_operands() {
for (expression, expected_error) in [
(
".value + .dst",
"TypeMismatch: expected scalar, received array",
),
(
"(.object | to_json(0)) + .dst",
"TypeMismatch: expected scalar, received array",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("array add expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"value":2,"text":"value"}"#.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("unapproved array add operands should remain rejected");
assert_eq!(canonical_error_text(&error), expected_error, "{expression}");
}
}
#[test]
fn cli_encoded_string_add_assignment_does_not_create_a_missing_path() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".missing += (.object | to_json(0))".to_string(),
])
.expect("missing-path add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2}}"#.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("an empty target plan should remain a no-op");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1\n ],\n \"object\": {\n \"x\": 2\n }\n}\n"
);
}
#[test]
fn cli_uses_rhs_scalar_identity_when_json_add_assignment_lhs_is_null() {
for (source, expected) in [
(
br#"{"dst":null,"rhs":true}"#.as_slice(),
"{\n \"dst\": true,\n \"rhs\": true\n}\n",
),
(
br#"{"dst":null,"rhs":42}"#.as_slice(),
"{\n \"dst\": 42,\n \"rhs\": 42\n}\n",
),
(
br#"{"dst":null,"rhs":"value"}"#.as_slice(),
"{\n \"dst\": \"value\",\n \"rhs\": \"value\"\n}\n",
),
(
br#"{"dst":null,"rhs":"42"}"#.as_slice(),
"{\n \"dst\": \"42\",\n \"rhs\": \"42\"\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("null lhs should use the located rhs scalar identity");
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
}
#[test]
fn cli_executes_json_string_participating_add_assignment_coercion() {
for (source, expected) in [
(
br#"{"dst":"item","rhs":2}"#.as_slice(),
"{\n \"dst\": \"item2\",\n \"rhs\": 2\n}\n",
),
(
br#"{"dst":2,"rhs":"items"}"#.as_slice(),
"{\n \"dst\": \"2items\",\n \"rhs\": \"items\"\n}\n",
),
(
br#"{"dst":true,"rhs":"value"}"#.as_slice(),
"{\n \"dst\": \"truevalue\",\n \"rhs\": \"value\"\n}\n",
),
(
br#"{"dst":"12","rhs":3}"#.as_slice(),
"{\n \"dst\": \"123\",\n \"rhs\": 3\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("add assignment should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
}
#[test]
fn cli_batch_demotes_encoded_strings_only_when_they_participate_in_scalar_add() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"text":"prefix:","suffix":":suffix","object":{"id":1}}"#.to_vec(),
}];
for (expression, expected) in [
("(.object | to_json(0)) + .suffix", "{\"id\":1}:suffix\n"),
(".text + (.object | to_json(0))", "prefix:{\"id\":1}\n"),
(
".text += (.object | to_json(0))",
"{\n \"text\": \"prefix:{\\\"id\\\":1}\",\n \"suffix\": \":suffix\",\n \"object\": {\n \"id\": 1\n }\n}\n",
),
(".object | to_json(0)", "{\"id\":1}\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("encoded-string scalar expression should parse");
let output =
execute_command(&parsed, &input).expect("encoded-string scalar expression should run");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_demotes_encoded_strings_only_when_they_participate_in_scalar_add() {
for (expression, expected) in [
("(.object | to_json(0)) + .suffix", "{\"id\":1}:suffix\n"),
(".text + (.object | to_json(0))", "prefix:{\"id\":1}\n"),
(
".text += (.object | to_json(0))",
"{\n \"text\": \"prefix:{\\\"id\\\":1}\",\n \"suffix\": \":suffix\",\n \"object\": {\n \"id\": 1\n }\n}\n",
),
(".object | to_json(0)", "{\"id\":1}\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("encoded-string scalar expression should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"text":"prefix:","suffix":":suffix","object":{"id":1}}"#.to_vec(),
),
}];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming encoded-string scalar expression should run");
assert!(printed, "{expression}");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_keeps_encoded_string_plus_array_closed_for_batch_and_streaming() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
"(.object | to_json(0)) + .array".to_string(),
])
.expect("encoded-string array addition should parse");
let source = br#"{"object":{"id":1},"array":[2]}"#;
let batch_inputs = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let batch_error = execute_command(&parsed, &batch_inputs)
.expect_err("encoded strings must not become array operands");
assert_eq!(
canonical_error_text(&batch_error),
"TypeMismatch: expected scalar, received array"
);
let streaming_inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(source.to_vec()),
}];
let mut output = Vec::new();
let streaming_error =
crate::execute::execute_command_to_writer(&parsed, &streaming_inputs, &mut output)
.expect_err("streaming encoded strings must not become array operands");
assert_eq!(
canonical_error_text(&streaming_error),
"TypeMismatch: expected scalar, received array"
);
assert!(output.is_empty());
}
#[test]
fn streaming_cli_string_encoded_add_assignment_emits_no_partial_current_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".text += ((.object | to_json(0)), error("late scalar encoded add"))"#.to_string(),
])
.expect("late-error encoded-string scalar assignment should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"text":"prefix:","object":{"id":1},"untouched":true}"#.to_vec(),
),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("late RHS error should fail before publishing the assigned string");
assert_eq!(
canonical_error_text(&error),
"Evaluation: late scalar encoded add"
);
assert!(output.is_empty());
}
#[test]
fn cli_preserves_existing_json_string_for_derived_and_located_null_add_assignment() {
for (expression, source, expected) in [
(
".dst += null",
br#"{"dst":"value"}"#.as_slice(),
"{\n \"dst\": \"value\"\n}\n",
),
(
".dst += .rhs",
br#"{"dst":"42","rhs":null}"#.as_slice(),
"{\n \"dst\": \"42\",\n \"rhs\": null\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("null rhs should preserve the existing ordinary string");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_does_not_extend_right_null_add_assignment_to_bool_or_number() {
for (source, expected_error) in [
(
br#"{"dst":true}"#.as_slice(),
"TypeMismatch: expected number, received bool",
),
(
br#"{"dst":42}"#.as_slice(),
"TypeMismatch: expected number, received null",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += null".to_string(),
])
.expect("add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("right null should remain unsupported for bool and number");
assert_eq!(canonical_error_text(&error), expected_error);
}
}
#[test]
fn cli_reports_yaml_add_assignment_as_unsupported() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
".dst += .rhs".to_string(),
])
.expect("add assignment should parse");
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: b"dst: item\nrhs: 2\n".to_vec(),
}];
let error = execute_command(&parsed, &input).expect_err("YAML add assignment should fail");
assert_eq!(
canonical_error_text(&error),
"Unsupported: whole-node assignment is currently supported only for JSON output"
);
}
#[test]
fn cli_reports_non_json_array_null_add_assignment_as_unsupported() {
for (output_format, expected_error) in [
(
"yaml",
"Unsupported: whole-node assignment is currently supported only for JSON output",
),
(
"toml",
"Unsupported: whole-node assignment is currently supported only for JSON output",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
".dst += null".to_string(),
])
.expect("array null add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1]}"#.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("non-JSON array null add assignment should fail without fallback");
assert_eq!(
canonical_error_text(&error),
expected_error,
"{output_format}"
);
}
}
#[test]
fn cli_implicitly_encodes_ordinary_array_plus_null_for_yaml_and_toml() {
for (output_format, expected) in [("yaml", "dst:\n - 1\n"), ("toml", "dst = [1]\n")] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
r#"{"dst": (.dst + null)}"#.to_string(),
])
.expect("ordinary array plus null should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1]}"#.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("ordinary add should remain eligible for implicit encoding");
assert_eq!(
String::from_utf8(output).unwrap(),
expected,
"{output_format}"
);
}
}
#[test]
fn cli_implicitly_encodes_array_with_embedded_explicit_encoding_for_supported_outputs() {
for (output_format, expected) in [
(
"json",
"{\n \"dst\": [\n 1,\n \"{\\\"x\\\":2}\"\n ]\n}\n",
),
("yaml", "dst:\n - 1\n - \"{\\\"x\\\":2}\"\n"),
("toml", "dst = [1, \"{\\\"x\\\":2}\"]\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
r#"{"dst": (.dst + (.object | to_json(0)))}"#.to_string(),
])
.expect("ordinary encoded-string array add should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2}}"#.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("ordinary add should support implicit encoding");
assert_eq!(
String::from_utf8(output).unwrap(),
expected,
"{output_format}"
);
}
}
#[test]
fn cli_reports_non_json_encoded_string_add_assignment_as_unsupported() {
for output_format in ["yaml", "toml"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
".dst += (.object | to_json(0))".to_string(),
])
.expect("encoded-string add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2}}"#.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("non-JSON compound output should fail without fallback");
assert_eq!(
canonical_error_text(&error),
"Unsupported: whole-node assignment is currently supported only for JSON output",
"{output_format}"
);
}
}
#[test]
fn cli_implicitly_encodes_ordinary_null_plus_array_for_supported_outputs() {
for (output_format, expected) in [
(
"json",
"{\n \"dst\": [\n 1,\n [\n 2\n ]\n ]\n}\n",
),
("yaml", "dst:\n - 1\n - \n - 2\n"),
("toml", "dst = [1, [2]]\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
r#"{"dst": (.nil + .rhs)}"#.to_string(),
])
.expect("ordinary null plus array should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"nil":null,"rhs":[1,[2]]}"#.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("ordinary null plus array should support implicit encoding");
assert_eq!(
String::from_utf8(output).unwrap(),
expected,
"{output_format}"
);
}
}
#[test]
fn cli_executes_null_plus_array_independently_of_source_language() {
for (input_format, source) in [
("json", br#"{"nil":null,"rhs":[1,[2]]}"#.as_slice()),
("yaml", b"nil: null\nrhs: [1, [2]]\n".as_slice()),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
input_format.to_string(),
"-o".to_string(),
"json".to_string(),
".nil + .rhs".to_string(),
])
.expect("ordinary null plus array should parse");
let input = vec![InputPayload {
name: format!("value.{input_format}"),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input)
.expect("ordinary null plus array should ignore source language");
assert_eq!(
String::from_utf8(output).unwrap(),
"[\n 1,\n [\n 2\n ]\n]\n",
"{input_format}"
);
}
}
#[test]
fn cli_reports_non_json_null_array_add_assignment_as_unsupported() {
for (input_format, output_format, source) in [
("json", "yaml", br#"{"dst":null,"rhs":[1,[2]]}"#.as_slice()),
("yaml", "toml", b"dst: null\nrhs: [1, [2]]\n".as_slice()),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
input_format.to_string(),
"-o".to_string(),
output_format.to_string(),
".dst += .rhs".to_string(),
])
.expect("null array add assignment should parse");
let input = vec![InputPayload {
name: format!("value.{input_format}"),
bytes: source.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("non-JSON compound output should fail without fallback");
assert_eq!(
canonical_error_text(&error),
"Unsupported: whole-node assignment is currently supported only for JSON output",
"{input_format} to {output_format}"
);
}
}
#[test]
fn cli_executes_json_numeric_subtract_and_multiply_assignments() {
for (expression, source, expected) in [
(
".payload.dst -= .decrement",
br#"{"payload":{"dst":10},"decrement":3,"untouched":true}"#.as_slice(),
"{\n \"payload\": {\n \"dst\": 7\n },\n \"decrement\": 3,\n \"untouched\": true\n}\n",
),
(
".payload.dst *= .factor",
br#"{"payload":{"dst":6},"factor":4,"untouched":true}"#.as_slice(),
"{\n \"payload\": {\n \"dst\": 24\n },\n \"factor\": 4,\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("compound assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("compound assignment should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_reports_yaml_whole_node_assignment_as_unsupported() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
r#". = {"items": [1, 2]}"#.to_string(),
])
.expect("whole-node assignment should parse");
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: b"old: true\n".to_vec(),
}];
let error =
execute_command(&parsed, &input).expect_err("YAML whole-node assignment should fail");
assert_eq!(
canonical_error_text(&error),
"Unsupported: whole-node assignment is currently supported only for JSON output"
);
}
#[test]
fn cli_propagates_rhs_error_for_derived_only_lhs() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#"(1, 2) = error("boom")"#.to_string(),
])
.expect("derived-only assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"unchanged":true}"#.to_vec(),
}];
let error =
execute_command(&parsed, &input).expect_err("the failing rhs should still be evaluated");
assert_eq!(canonical_error_text(&error), "Evaluation: boom");
}
#[test]
fn cli_reports_canonical_scalar_assignment_as_unsupported_for_toml_output() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"toml".to_string(),
".value = 42".to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"value":1}"#.to_vec(),
}];
let error =
execute_command(&parsed, &input).expect_err("TOML scalar assignment should be unsupported");
assert_eq!(
canonical_error_text(&error),
"Unsupported: scalar assignment is not supported for TOML output"
);
}
#[test]
fn streaming_cli_writes_each_successful_scalar_assignment_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
".value = 42".to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\nvalue: 1\n---\nvalue: 2\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("every scalar assignment document should succeed");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"value: 42\n---\nvalue: 42\n"
);
}
#[test]
fn streaming_cli_isolates_located_rhs_per_yaml_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
".dst |= .src".to_string(),
])
.expect("located RHS assignment should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = StreamingInputSource::Stdin(
b"---\ndst:\n src: 1\n old: first\n---\ndst:\n src: [2, 3]\n old: second\n"
.to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("each document should resolve its own located RHS");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": 1\n}\n{\n \"dst\": [\n 2,\n 3\n ]\n}\n"
);
}
#[test]
fn streaming_cli_appends_derived_scalar_rhs_per_json_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += 3".to_string(),
])
.expect("array scalar add assignment should parse");
let inputs = vec![
StreamingInput {
name: "first.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[1]}"#.to_vec()),
},
StreamingInput {
name: "second.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[2]}"#.to_vec()),
},
];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("each JSON document should append the derived scalar RHS");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1,\n 3\n ]\n}\n{\n \"dst\": [\n 2,\n 3\n ]\n}\n"
);
}
#[test]
fn cli_batch_executes_canonical_shallow_object_merge_and_assignment() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"left":{"keep":1,"shared":{"left":true}},"right":{"shared":{"right":true},"added":2},"untouched":true}"#
.to_vec(),
}];
for (expression, expected) in [
(
".left + .right",
"{\n \"keep\": 1,\n \"shared\": {\n \"right\": true\n },\n \"added\": 2\n}\n",
),
(
".left += .right",
"{\n \"left\": {\n \"keep\": 1,\n \"shared\": {\n \"right\": true\n },\n \"added\": 2\n },\n \"right\": {\n \"shared\": {\n \"right\": true\n },\n \"added\": 2\n },\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("object merge should parse");
let output = execute_command(&parsed, &input).expect("object merge should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_executes_canonical_shallow_object_merge_and_assignment() {
for (expression, expected) in [
(
".left + .right",
"{\n \"keep\": 1,\n \"shared\": {\n \"right\": true\n },\n \"added\": 2\n}\n",
),
(
".left += .right",
"{\n \"left\": {\n \"keep\": 1,\n \"shared\": {\n \"right\": true\n },\n \"added\": 2\n },\n \"right\": {\n \"shared\": {\n \"right\": true\n },\n \"added\": 2\n },\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("object merge should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"left":{"keep":1,"shared":{"left":true}},"right":{"shared":{"right":true},"added":2},"untouched":true}"#
.to_vec(),
),
}];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming object merge should succeed");
assert!(printed, "{expression}");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_object_merge_assignment_emits_no_partial_current_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".left += (.right, error("late object merge"))"#.to_string(),
])
.expect("late-error object merge assignment should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"left":{"keep":1},"right":{"added":2},"untouched":true}"#.to_vec(),
),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("late RHS error should fail before publishing the merged document");
assert_eq!(
canonical_error_text(&error),
"Evaluation: late object merge"
);
assert!(output.is_empty());
}
#[test]
fn cli_batch_isolates_object_add_assignment_per_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .object".to_string(),
])
.expect("object add assignment should parse");
let inputs = vec![
InputPayload {
name: "first.json".to_string(),
bytes: br#"{"dst":[1],"object":{"id":1,"nested":[2]}}"#.to_vec(),
},
InputPayload {
name: "second.json".to_string(),
bytes: br#"{"dst":[3],"object":{"id":2,"nested":[4,{"x":5}]}}"#.to_vec(),
},
];
let output = execute_command(&parsed, &inputs)
.expect("each batch document should append its own object");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1,\n {\n \"id\": 1,\n \"nested\": [\n 2\n ]\n }\n ],\n \"object\": {\n \"id\": 1,\n \"nested\": [\n 2\n ]\n }\n}\n{\n \"dst\": [\n 3,\n {\n \"id\": 2,\n \"nested\": [\n 4,\n {\n \"x\": 5\n }\n ]\n }\n ],\n \"object\": {\n \"id\": 2,\n \"nested\": [\n 4,\n {\n \"x\": 5\n }\n ]\n }\n}\n"
);
}
#[test]
fn cli_batch_preserves_duplicate_object_entries_during_array_add_assignment() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .object".to_string(),
])
.expect("object add assignment should parse");
let inputs = vec![
InputPayload {
name: "first.yaml".to_string(),
bytes: b"dst: [0]\nobject:\n key: first\n key: second\n".to_vec(),
},
InputPayload {
name: "second.yaml".to_string(),
bytes: b"dst: [1]\nobject:\n key: third\n key: fourth\n".to_vec(),
},
];
let output = execute_command(&parsed, &inputs)
.expect("each batch document should preserve ordered duplicate entries");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 0,\n {\n \"key\": \"first\",\n \"key\": \"second\"\n }\n ],\n \"object\": {\n \"key\": \"first\",\n \"key\": \"second\"\n }\n}\n{\n \"dst\": [\n 1,\n {\n \"key\": \"third\",\n \"key\": \"fourth\"\n }\n ],\n \"object\": {\n \"key\": \"third\",\n \"key\": \"fourth\"\n }\n}\n"
);
}
#[test]
fn streaming_cli_isolates_object_add_assignment_per_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .object".to_string(),
])
.expect("object add assignment should parse");
let inputs = vec![
StreamingInput {
name: "first.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"dst":[1],"object":{"id":1,"nested":[2]}}"#.to_vec(),
),
},
StreamingInput {
name: "second.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"dst":[3],"object":{"id":2,"nested":[4,{"x":5}]}}"#.to_vec(),
),
},
];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("each streamed document should append its own object");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1,\n {\n \"id\": 1,\n \"nested\": [\n 2\n ]\n }\n ],\n \"object\": {\n \"id\": 1,\n \"nested\": [\n 2\n ]\n }\n}\n{\n \"dst\": [\n 3,\n {\n \"id\": 2,\n \"nested\": [\n 4,\n {\n \"x\": 5\n }\n ]\n }\n ],\n \"object\": {\n \"id\": 2,\n \"nested\": [\n 4,\n {\n \"x\": 5\n }\n ]\n }\n}\n"
);
}
#[test]
fn streaming_cli_object_add_assignment_emits_no_partial_current_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".dst += (.object, error("late object append"))"#.to_string(),
])
.expect("late-error object add assignment should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"dst":[1],"object":{"nested":[2,{"x":3}]}}"#.to_vec(),
),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("late RHS error should fail before publishing the current document");
assert_eq!(
canonical_error_text(&error),
"Evaluation: late object append"
);
assert!(output.is_empty());
}
#[test]
fn cli_batch_reports_nested_alias_object_operand_as_unsupported_without_output() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
".dst + .object".to_string(),
])
.expect("object add should parse");
let inputs = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: b"anchor: &value\n id: 1\ndst: [0]\nobject:\n nested: *value\n".to_vec(),
}];
let error = execute_command(&parsed, &inputs)
.expect_err("an object containing a nested alias must not produce batch output");
assert_eq!(
canonical_error_text(&error),
"Unsupported: object operands containing aliases are not supported"
);
}
#[test]
fn streaming_cli_reports_nested_alias_object_operand_as_unsupported_without_output() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
".dst + .object".to_string(),
])
.expect("object add should parse");
let inputs = vec![StreamingInput {
name: "value.yaml".to_string(),
input_format: "yaml".to_string(),
source: StreamingInputSource::Stdin(
b"anchor: &value\n id: 1\ndst: [0]\nobject:\n nested: *value\n".to_vec(),
),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("an object containing a nested alias must not produce streaming output");
assert_eq!(
canonical_error_text(&error),
"Unsupported: object operands containing aliases are not supported"
);
assert!(output.is_empty());
}
#[test]
fn cli_object_add_assignment_reports_closed_boundaries_as_typed_errors() {
let alias = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .ref".to_string(),
])
.expect("alias object add assignment should parse");
let alias_input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: b"dst: [0]\nobject: &object\n x: 1\nref: *object\n".to_vec(),
}];
let error = execute_command(&alias, &alias_input)
.expect_err("alias object identity must remain unsupported");
assert_eq!(
canonical_error_text(&error),
"Unsupported: object operands containing aliases are not supported"
);
for output_format in ["yaml", "toml"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
".dst += .object".to_string(),
])
.expect("object add assignment should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":[1],"object":{"x":2}}"#.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("non-JSON whole-node output should fail without fallback");
assert_eq!(
canonical_error_text(&error),
"Unsupported: whole-node assignment is currently supported only for JSON output",
"{output_format}"
);
}
}
#[test]
fn cli_batch_isolates_encoded_string_add_assignment_per_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += (.object | to_json(0))".to_string(),
])
.expect("encoded-string add assignment should parse");
let inputs = vec![
InputPayload {
name: "first.json".to_string(),
bytes: br#"{"dst":[1],"object":{"id":1}}"#.to_vec(),
},
InputPayload {
name: "second.json".to_string(),
bytes: br#"{"dst":[2],"object":{"id":2}}"#.to_vec(),
},
];
let output = execute_command(&parsed, &inputs)
.expect("each batch document should encode and append its own object");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1,\n \"{\\\"id\\\":1}\"\n ],\n \"object\": {\n \"id\": 1\n }\n}\n{\n \"dst\": [\n 2,\n \"{\\\"id\\\":2}\"\n ],\n \"object\": {\n \"id\": 2\n }\n}\n"
);
}
#[test]
fn streaming_cli_isolates_encoded_string_add_assignment_per_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += (.object | to_json(0))".to_string(),
])
.expect("encoded-string add assignment should parse");
let inputs = vec![
StreamingInput {
name: "first.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[1],"object":{"id":1}}"#.to_vec()),
},
StreamingInput {
name: "second.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[2],"object":{"id":2}}"#.to_vec()),
},
];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("each streamed document should encode and append its own object");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1,\n \"{\\\"id\\\":1}\"\n ],\n \"object\": {\n \"id\": 1\n }\n}\n{\n \"dst\": [\n 2,\n \"{\\\"id\\\":2}\"\n ],\n \"object\": {\n \"id\": 2\n }\n}\n"
);
}
#[test]
fn streaming_cli_encoded_string_add_assignment_emits_no_partial_current_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".dst += ((.object | to_json(0)), error("late encoded append"))"#.to_string(),
])
.expect("late-error encoded-string assignment should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[1],"object":{"x":2}}"#.to_vec()),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("late RHS error should fail before publishing the current document");
assert_eq!(
canonical_error_text(&error),
"Evaluation: late encoded append"
);
assert!(output.is_empty());
}
#[test]
fn streaming_cli_preserves_each_array_for_located_null_rhs() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("array null add assignment should parse");
let inputs = vec![
StreamingInput {
name: "first.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[1],"rhs":null}"#.to_vec()),
},
StreamingInput {
name: "second.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":[2,3],"rhs":null}"#.to_vec()),
},
];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("each streamed document should preserve its own array");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1\n ],\n \"rhs\": null\n}\n{\n \"dst\": [\n 2,\n 3\n ],\n \"rhs\": null\n}\n"
);
}
#[test]
fn cli_batch_executes_null_object_identity_and_assignment() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"dst":null,"object":{"keep":1,"nested":{"value":2}},"untouched":true}"#
.to_vec(),
}];
for (expression, expected) in [
(
".dst + .object",
"{\n \"keep\": 1,\n \"nested\": {\n \"value\": 2\n }\n}\n",
),
(
".dst += .object",
"{\n \"dst\": {\n \"keep\": 1,\n \"nested\": {\n \"value\": 2\n }\n },\n \"object\": {\n \"keep\": 1,\n \"nested\": {\n \"value\": 2\n }\n },\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("null object addition should parse");
let output = execute_command(&parsed, &input).expect("null object identity should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_executes_null_object_identity_and_assignment() {
for (expression, expected) in [
(
".dst + .object",
"{\n \"keep\": 1,\n \"nested\": {\n \"value\": 2\n }\n}\n",
),
(
".dst += .object",
"{\n \"dst\": {\n \"keep\": 1,\n \"nested\": {\n \"value\": 2\n }\n },\n \"object\": {\n \"keep\": 1,\n \"nested\": {\n \"value\": 2\n }\n },\n \"untouched\": true\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("null object addition should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"dst":null,"object":{"keep":1,"nested":{"value":2}},"untouched":true}"#
.to_vec(),
),
}];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming null object identity should succeed");
assert!(printed, "{expression}");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_keeps_reverse_object_null_addition_closed_for_batch_and_streaming() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".object + .dst".to_string(),
])
.expect("reverse object null addition should parse");
let source = br#"{"dst":null,"object":{"value":1}}"#;
let batch_inputs = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let batch_error = execute_command(&parsed, &batch_inputs)
.expect_err("object plus null must remain outside the identity contract");
assert_eq!(
canonical_error_text(&batch_error),
"TypeMismatch: expected scalar, received object"
);
let streaming_inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(source.to_vec()),
}];
let mut output = Vec::new();
let streaming_error =
crate::execute::execute_command_to_writer(&parsed, &streaming_inputs, &mut output)
.expect_err("streaming object plus null must remain outside the identity contract");
assert_eq!(
canonical_error_text(&streaming_error),
"TypeMismatch: expected scalar, received object"
);
assert!(output.is_empty());
}
#[test]
fn streaming_cli_null_object_assignment_emits_no_partial_current_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".dst += (.object, error("late null object assignment"))"#.to_string(),
])
.expect("late-error null object assignment should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"dst":null,"object":{"value":1},"untouched":true}"#.to_vec(),
),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("late RHS error should fail before publishing the assigned object");
assert_eq!(
canonical_error_text(&error),
"Evaluation: late null object assignment"
);
assert!(output.is_empty());
}
#[test]
fn cli_batch_isolates_null_primary_array_add_assignment_per_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("null array add assignment should parse");
let inputs = vec![
InputPayload {
name: "first.json".to_string(),
bytes: br#"{"dst":null,"rhs":[1]}"#.to_vec(),
},
InputPayload {
name: "second.json".to_string(),
bytes: br#"{"dst":null,"rhs":[2,[3]]}"#.to_vec(),
},
];
let output = execute_command(&parsed, &inputs)
.expect("each batch document should resolve its own array RHS");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1\n ],\n \"rhs\": [\n 1\n ]\n}\n{\n \"dst\": [\n 2,\n [\n 3\n ]\n ],\n \"rhs\": [\n 2,\n [\n 3\n ]\n ]\n}\n"
);
}
#[test]
fn streaming_cli_isolates_null_primary_array_add_assignment_per_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".dst += .rhs".to_string(),
])
.expect("null array add assignment should parse");
let inputs = vec![
StreamingInput {
name: "first.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":null,"rhs":[1]}"#.to_vec()),
},
StreamingInput {
name: "second.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":null,"rhs":[2,[3]]}"#.to_vec()),
},
];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("each streamed document should resolve its own array RHS");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"dst\": [\n 1\n ],\n \"rhs\": [\n 1\n ]\n}\n{\n \"dst\": [\n 2,\n [\n 3\n ]\n ],\n \"rhs\": [\n 2,\n [\n 3\n ]\n ]\n}\n"
);
}
#[test]
fn streaming_cli_null_array_add_assignment_emits_no_partial_current_document() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#".dst += ([1], error("late null array"))"#.to_string(),
])
.expect("late-error null array assignment should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"dst":null}"#.to_vec()),
}];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("late RHS error should fail before publishing the current document");
assert_eq!(canonical_error_text(&error), "Evaluation: late null array");
assert!(output.is_empty());
}
#[test]
fn streaming_cli_preserves_whole_node_assignment_output_before_rhs_error() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-o".to_string(),
"json".to_string(),
r#".target = ((select(.fail != true) | [1]), (select(.fail == true) | error("boom")))"#
.to_string(),
])
.expect("parse should succeed");
let inputs = vec![
StreamingInput {
name: "first.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
br#"{"target":{"old":true},"fail":false}"#.to_vec(),
),
},
StreamingInput {
name: "second.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"target":{"old":true},"fail":true}"#.to_vec()),
},
];
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second input should propagate its rhs error");
assert_eq!(canonical_error_text(&error), "Evaluation: boom");
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"target\": [\n 1\n ],\n \"fail\": false\n}\n"
);
}
#[test]
fn cli_preserves_zero_one_and_many_document_framing() {
for (expression, expected) in [
("select(false)", ""),
(".missing", "null\n"),
(".items[]", "1\n2\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let inputs = vec![InputPayload {
name: "<stdin>".to_string(),
bytes: br#"{"items":[1,2]}"#.to_vec(),
}];
let output = execute_command(&parsed, &inputs).expect("command should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_uses_canonical_map_select_and_prints_multi_result_scalars_separately() {
let user_expression = r#"map(select(.metadata.account != "89a8933410e86f415bb8ceb07b4ea26b"))"#;
let user_parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
user_expression.to_string(),
])
.expect("parse should succeed");
let user_input = vec![InputPayload {
name: "accounts.json".to_string(),
bytes: br#"[{"metadata":{"account":"89a8933410e86f415bb8ceb07b4ea26b"}},{"metadata":{"account":"other"}}]"#
.to_vec(),
}];
let user_output = execute_command(&user_parsed, &user_input).expect("command should succeed");
assert_eq!(
String::from_utf8(user_output).unwrap(),
concat!(
"[\n",
" {\n",
" \"metadata\": {\n",
" \"account\": \"other\"\n",
" }\n",
" }\n",
"]\n"
)
);
let missing_account_input = vec![InputPayload {
name: "accounts.json".to_string(),
bytes: br#"[{"metadata":{"account":"89a8933410e86f415bb8ceb07b4ea26b"}},{"metadata":{"account":"other"}},{"metadata":{}},{}]"#
.to_vec(),
}];
let missing_account_output = execute_command(&user_parsed, &missing_account_input)
.expect("canonical missing paths should produce null placeholders");
assert_eq!(
String::from_utf8(missing_account_output).unwrap(),
concat!(
"[\n",
" {\n \"metadata\": {\n \"account\": \"other\"\n }\n },\n",
" {\n \"metadata\": {}\n },\n",
" {}\n",
"]\n"
)
);
let empty_parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
"map(select(false))".to_string(),
])
.expect("parse should succeed");
let empty_input = vec![InputPayload {
name: "items.json".to_string(),
bytes: b"[1,2]".to_vec(),
}];
let empty_output =
execute_command(&empty_parsed, &empty_input).expect("command should succeed");
assert_eq!(String::from_utf8(empty_output).unwrap(), "[]\n");
let multi_result_parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".items[]".to_string(),
])
.expect("parse should succeed");
let multi_result_input = vec![InputPayload {
name: "items.json".to_string(),
bytes: br#"{"items":[1,2]}"#.to_vec(),
}];
let multi_result_output =
execute_command(&multi_result_parsed, &multi_result_input).expect("command should succeed");
assert_eq!(String::from_utf8(multi_result_output).unwrap(), "1\n2\n");
}
#[test]
fn cli_preserves_binding_multi_result_document_framing() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
".[] as $item | ($item, $item + 10)".to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "items.yaml".to_string(),
bytes: b"---\n[1, 2]\n---\n[3]\n".to_vec(),
}];
let output = execute_command(&parsed, &input).expect("binding should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"1\n11\n2\n12\n---\n3\n13\n"
);
}
#[test]
fn cli_regex_test_preserves_multi_result_and_yaml_document_framing() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
r#".[] | test("^cat"; "g")"#.to_string(),
])
.expect("regex test should parse");
let input = vec![InputPayload {
name: "values.yaml".to_string(),
bytes: b"---\n[cat, dog]\n---\n[catalog, bird]\n".to_vec(),
}];
let output = execute_command(&parsed, &input).expect("regex test should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"true\nfalse\n---\ntrue\nfalse\n"
);
}
#[test]
fn cli_regex_test_reports_exact_typed_and_flag_errors() {
for (expression, source, expected) in [
(
r#"test("1")"#,
b"1\n".as_slice(),
"TypeMismatch: expected string, received number",
),
(
r#"test("^cat$"; "i")"#,
b"cat\n".as_slice(),
"Evaluation: invalid regular expression flags",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("regex test should parse");
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: source.to_vec(),
}];
let error = execute_command(&parsed, &input).expect_err("invalid regex use should fail");
assert_eq!(canonical_error_text(&error), expected, "{expression}");
}
}
#[test]
fn cli_regex_substitutes_globally_and_handles_unicode_empty_patterns() {
for (expression, source, expected) in [
(r#"sub("a"; "x")"#, b"banana\n".as_slice(), "bxnxnx\n"),
(r#"sub(""; "_")"#, "ä½ å¥½\n".as_bytes(), "_ä½ _好_\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
expression.to_string(),
])
.expect("regex substitution should parse");
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("regex substitution should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_regex_sub_preserves_multi_result_framing_and_reports_exact_errors() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
r#".[] | sub("a"; "x")"#.to_string(),
])
.expect("regex substitution should parse");
let input = vec![InputPayload {
name: "values.yaml".to_string(),
bytes: b"---\n[banana, cat]\n---\n[apple]\n".to_vec(),
}];
let output = execute_command(&parsed, &input).expect("regex substitution should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"bxnxnx\ncxt\n---\nxpple\n"
);
for (expression, source, expected) in [
(
r#"sub("a"; "x")"#,
b"1\n".as_slice(),
"TypeMismatch: expected string, received number",
),
(
r#"sub("["; "x")"#,
b"abc\n".as_slice(),
"Evaluation: invalid regular expression pattern",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("regex substitution should parse");
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: source.to_vec(),
}];
let error =
execute_command(&parsed, &input).expect_err("invalid regex substitution should fail");
assert_eq!(canonical_error_text(&error), expected, "{expression}");
}
}
#[test]
fn streaming_cli_preserves_regex_sub_output_before_the_first_error() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
r#"sub("a"; "x")"#.to_string(),
])
.expect("regex substitution should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\nbanana\n---\n1\n---\ncat\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should reject a non-string");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected string, received number"
);
assert_eq!(String::from_utf8(output).unwrap(), "bxnxnx\n");
}
#[test]
fn cli_regex_match_emits_metadata_and_global_results() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#""aba""#.to_vec(),
}];
for (expression, expected) in [
(
r#"match("(?P<x>a)(b)?")"#,
"{\n \"string\": \"ab\",\n \"offset\": 0,\n \"length\": 2,\n \"captures\": [\n {\n \"string\": \"a\",\n \"offset\": 0,\n \"length\": 1,\n \"name\": \"x\"\n },\n {\n \"string\": \"b\",\n \"offset\": 1,\n \"length\": 1\n }\n ]\n}\n",
),
(
r#"match("a"; "g")"#,
"{\n \"string\": \"a\",\n \"offset\": 0,\n \"length\": 1,\n \"captures\": []\n}\n{\n \"string\": \"a\",\n \"offset\": 2,\n \"length\": 1,\n \"captures\": []\n}\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("regex match should parse");
let output = execute_command(&parsed, &input).expect("regex match should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_regex_capture_preserves_named_unnamed_null_and_no_match_results() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#""ab a""#.to_vec(),
}];
for (expression, expected) in [
(
r#"capture("(?P<x>a)(b)?")"#,
"{\n \"x\": \"a\",\n \"\": \"b\"\n}\n",
),
(
r#"capture("(?P<x>a)(b)?"; "g")"#,
"{\n \"x\": \"a\",\n \"\": \"b\"\n}\n{\n \"x\": \"a\",\n \"\": null\n}\n",
),
(r#"capture("z")"#, ""),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("regex capture should parse");
let output = execute_command(&parsed, &input).expect("regex capture should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_preserves_regex_match_output_before_the_first_error() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
r#"match("a"; "g")"#.to_string(),
])
.expect("regex match should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source =
crate::args::StreamingInputSource::Stdin(b"---\naba\n---\n1\n---\ncat\n".to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should reject a non-string");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected string, received number"
);
assert_eq!(
String::from_utf8(output).unwrap(),
"{\n \"string\": \"a\",\n \"offset\": 0,\n \"length\": 1,\n \"captures\": []\n}\n{\n \"string\": \"a\",\n \"offset\": 2,\n \"length\": 1,\n \"captures\": []\n}\n"
);
for (expression, expected) in [
(
r#"match("a")"#,
"TypeMismatch: expected string, received number",
),
(
r#"capture("a"; "i")"#,
"Evaluation: invalid regular expression flags",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("regex operation should parse");
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: if expression.starts_with("match") {
b"1\n".to_vec()
} else {
b"abc\n".to_vec()
},
}];
let error = execute_command(&parsed, &input).expect_err("invalid regex use should fail");
assert_eq!(canonical_error_text(&error), expected, "{expression}");
}
}
#[test]
fn cli_preserves_composition_cardinality_order_and_late_errors() {
let input = vec![InputPayload {
name: "composition.json".to_string(),
bytes: br#"{"a":1}"#.to_vec(),
}];
for (expression, expected) in [
(r#"select(false) | error("rhs")"#, ""),
("(1,2) | (., . + 10)", "1\n2\n11\n12\n"),
("select(false), 1, select(false), 2", "1\n2\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("composition expression should parse");
let output = execute_command(&parsed, &input).expect("composition should execute");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#"1, error("late")"#.to_string(),
])
.expect("late-error expression should parse");
let error = execute_command(&parsed, &input).expect_err("late error must fail atomically");
assert_eq!(canonical_error_text(&error), "Evaluation: late");
}
#[test]
fn cli_executes_any_and_all_across_batch_inputs_and_yaml_documents() {
for (expression, expected) in [
("any", "true\ntrue\nfalse\n"),
("all", "false\ntrue\nfalse\n"),
("any_c(.)", "true\ntrue\nfalse\n"),
("all_c(.)", "false\ntrue\nfalse\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("collection predicate should parse");
let inputs = vec![
InputPayload {
name: "first.yaml".to_string(),
bytes: b"---\n[false, true]\n---\n[true, true]\n".to_vec(),
},
InputPayload {
name: "second.yaml".to_string(),
bytes: b"[false, false]\n".to_vec(),
},
];
let output = execute_command(&parsed, &inputs).expect("batch evaluation should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_preserves_any_and_all_empty_collection_identities() {
let input = vec![InputPayload {
name: "empty.json".to_string(),
bytes: b"[]".to_vec(),
}];
for (expression, expected) in [
("any", "false\n"),
("all", "true\n"),
("any_c(.)", "false\n"),
("all_c(.)", "true\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("collection predicate should parse");
let output = execute_command(&parsed, &input).expect("empty collection should evaluate");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_reports_any_and_all_type_mismatches() {
let input = vec![InputPayload {
name: "number.json".to_string(),
bytes: b"1".to_vec(),
}];
for expression in ["any", "all", "any_c(.)", "all_c(.)"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("collection predicate should parse");
let error =
execute_command(&parsed, &input).expect_err("non-array input should be rejected");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected array, received number",
"{expression}"
);
}
}
#[test]
fn streaming_cli_preserves_any_c_output_before_a_late_document_error() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"any_c((1 / .) > 0)".to_string(),
])
.expect("conditional collection predicate should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\n[1]\n---\n[0]\n---\n[1]\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should surface its late predicate error");
assert_eq!(canonical_error_text(&error), "Evaluation: division by zero");
assert_eq!(String::from_utf8(output).unwrap(), "true\n");
}
#[test]
fn cli_executes_min_and_max_across_batch_inputs_and_yaml_documents() {
let inputs = vec![
InputPayload {
name: "first.yaml".to_string(),
bytes: b"---\n[3, 1, 2]\n---\n[]\n---\n42\n".to_vec(),
},
InputPayload {
name: "second.yaml".to_string(),
bytes: b"[9, 4]\n".to_vec(),
},
];
for (expression, expected) in [("min", "1\n4\n"), ("max", "3\n9\n")] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("collection extrema should parse");
let output = execute_command(&parsed, &inputs).expect("collection extrema should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_reports_min_and_max_mixed_type_comparison_errors() {
let input = vec![InputPayload {
name: "mixed.json".to_string(),
bytes: br#"[3,"x",2]"#.to_vec(),
}];
for expression in ["min", "max"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("collection extrema should parse");
let error =
execute_command(&parsed, &input).expect_err("mixed types should not be comparable");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected matching number or string, received number",
"{expression}"
);
}
}
#[test]
fn streaming_cli_preserves_min_and_max_output_before_a_late_type_mismatch() {
for (expression, expected) in [("min", "1\n"), ("max", "3\n")] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("collection extrema should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\n[3, 1, 2]\n---\n[9, bad, 4]\n---\n[8, 7]\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should reject mixed comparison types");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected matching number or string, received number",
"{expression}"
);
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_restored_registry_operations() {
for (expression, source, expected) in [
("sort", "[3,1,2]", "[\n 1,\n 2,\n 3\n]\n"),
(
"sort_by(.rank)",
r#"[{"rank":2},{"rank":1}]"#,
"[\n {\n \"rank\": 1\n },\n {\n \"rank\": 2\n }\n]\n",
),
(
"group_by(.team)",
r#"[{"team":"b"},{"team":"a"},{"team":"b"}]"#,
"[\n [\n {\n \"team\": \"b\"\n },\n {\n \"team\": \"b\"\n }\n ],\n [\n {\n \"team\": \"a\"\n }\n ]\n]\n",
),
("trim", r#"" value ""#, "value\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("restored expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.as_bytes().to_vec(),
}];
let output = execute_command(&parsed, &input).expect("restored operation should execute");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_reverses_batch_and_yaml_documents_without_reversing_nested_arrays() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"reverse | to_json(0)".to_string(),
])
.expect("reverse should parse");
let inputs = vec![
InputPayload {
name: "first.yaml".to_string(),
bytes: b"---\n[1, [2, 3], 4]\n---\n[]\n".to_vec(),
},
InputPayload {
name: "second.yaml".to_string(),
bytes: b"[5, 6]\n".to_vec(),
},
];
let output = execute_command(&parsed, &inputs).expect("reverse should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"[4,[2,3],1]\n[]\n[6,5]\n"
);
}
#[test]
fn cli_reports_reverse_type_mismatches() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
"reverse".to_string(),
])
.expect("reverse should parse");
let input = vec![InputPayload {
name: "number.json".to_string(),
bytes: b"1".to_vec(),
}];
let error = execute_command(&parsed, &input).expect_err("reverse should require an array");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected array, received number"
);
}
#[test]
fn streaming_cli_preserves_reverse_output_before_a_late_type_mismatch() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"reverse | to_json(0)".to_string(),
])
.expect("reverse should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\n[1, 2]\n---\nnot-an-array\n---\n[3, 4]\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should reject a non-array");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected array, received string"
);
assert_eq!(String::from_utf8(output).unwrap(), "[2,1]\n");
}
#[test]
fn cli_executes_reduce_to_a_scalar_accumulator() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".[] as $item ireduce (0; . + $item)".to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "items.json".to_string(),
bytes: b"[1,2,3]".to_vec(),
}];
let output = execute_command(&parsed, &input).expect("reduce should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "6\n");
}
#[test]
fn cli_renders_canonical_string_interpolation_text() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#""hello \(.name):\(.items[])""#.to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "person.json".to_string(),
bytes: br#"{"name":"Ada","items":[1,2]}"#.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("interpolation should succeed");
assert_eq!(String::from_utf8(output).unwrap(), "hello Ada:1\n");
}
#[test]
fn cli_to_string_preserves_bounded_scalar_and_alias_text() {
let input = vec![InputPayload {
name: "value.yaml".to_string(),
bytes: b"number: 1E+03\nvalue: &source text\ncopy: *source\n".to_vec(),
}];
for (expression, expected) in [
(".number | to_string", "1E+03\n"),
(".copy | to_string", "*source\n"),
("(1 / 10000000) | to_string", "1e-07\n"),
(r#"("x" | to_json(0)) | to_string"#, "\"x\"\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("to-string expression should parse");
let output = execute_command(&parsed, &input).expect("bounded scalar should stringify");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_to_string_preserves_scalar_result_cardinality() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
r#"(null, true, 42, "text") | to_string"#.to_string(),
])
.expect("to-string cardinality expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: b"null".to_vec(),
}];
let output = execute_command(&parsed, &input).expect("every scalar should stringify");
assert_eq!(String::from_utf8(output).unwrap(), "null\ntrue\n42\ntext\n");
}
#[test]
fn cli_to_string_renders_container_tags() {
for (source, expected) in [
(b"[1]".as_slice(), "!!seq\n"),
(br#"{"a":1}"#.as_slice(), "!!map\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
"to_string".to_string(),
])
.expect("to-string expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("containers should render their tags");
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
}
#[test]
fn streaming_cli_to_string_serializes_container_results() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"(.number, .copy) | to_string".to_string(),
])
.expect("to-string expression should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = StreamingInputSource::Stdin(
b"---\nnumber: 1E+03\nvalue: &source text\ncopy: *source\n---\nnumber: [1]\ncopy: ignored\n---\nnumber: 2\ncopy: last\n"
.to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("to_string should serialize every supported value kind");
assert!(printed);
assert_eq!(
String::from_utf8(output).unwrap(),
"1E+03\n*source\n!!seq\nignored\n2\nlast\n"
);
}
#[test]
fn cli_executes_unicode_simple_upcase_and_downcase_with_typed_mismatches() {
for (expression, source, expected) in [
(
"upcase",
"Straße İı Å¿ ffi Σσς é ä¸ðŸ™‚",
"STRAßE İI S ffi ΣΣΣ É ä¸ðŸ™‚\n",
),
(
"downcase",
"Straße İı Å¿ ffi Σσς É ä¸ðŸ™‚",
"straße iı Å¿ ffi σσς é ä¸ðŸ™‚\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("change-case expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: serde_json::to_vec(source).expect("Unicode string should encode"),
}];
let output = execute_command(&parsed, &input).expect("change-case should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
for (expression, source, actual) in [
("upcase", b"null".as_slice(), "null"),
("downcase", b"true".as_slice(), "bool"),
("upcase", b"1".as_slice(), "number"),
("downcase", b"[1]".as_slice(), "array"),
("upcase", br#"{"a":1}"#.as_slice(), "object"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("change-case expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let error = execute_command(&parsed, &input)
.expect_err("change-case should reject non-string inputs");
assert_eq!(
canonical_error_text(&error),
format!("TypeMismatch: expected string, received {actual}"),
"{expression} {actual}"
);
}
}
#[test]
fn cli_preserves_change_case_yaml_multi_document_framing() {
for (expression, expected) in [
("upcase", "STRAßE İI S ffi ΣΣΣ É ä¸ðŸ™‚\n---\nSECOND\n"),
("downcase", "straße iı Å¿ ffi σσς é ä¸ðŸ™‚\n---\nsecond\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
expression.to_string(),
])
.expect("change-case expression should parse");
let input = vec![InputPayload {
name: "values.yaml".to_string(),
bytes: "---\n'Straße İı Å¿ ffi Σσς É ä¸ðŸ™‚'\n---\n'SeCoNd'\n"
.as_bytes()
.to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("multi-document change-case should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_preserves_change_case_output_before_the_first_typed_error() {
for (expression, expected) in [
("upcase", "STRAßE İI S ffi ΣΣΣ É ä¸ðŸ™‚\n"),
("downcase", "straße iı Å¿ ffi σσς é ä¸ðŸ™‚\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("change-case expression should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = StreamingInputSource::Stdin(
"---\n'Straße İı Å¿ ffi Σσς É ä¸ðŸ™‚'\n---\n7\n---\n'last'\n"
.as_bytes()
.to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should reject a number");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected string, received number"
);
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_both_to_number_spellings_for_decimal_and_exponent_values() {
for (expression, source, expected) in [
("to_number", "12.5", "12.5\n"),
("tonumber", "-.5", "-0.5\n"),
("to_number", "1e3", "1000\n"),
("tonumber", "2.5e-2", "0.025\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("to-number expression should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: serde_json::to_vec(source).expect("decimal string should encode"),
}];
let output = execute_command(&parsed, &input).expect("to-number should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
expected,
"{expression} {source}"
);
}
}
#[test]
fn streaming_cli_executes_both_to_number_spellings_for_decimal_and_exponent_values() {
for (expression, source, expected) in [
("to_number", "12.5", "12.5\n"),
("tonumber", "1e3", "1000\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("to-number expression should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(
serde_json::to_vec(source).expect("decimal string should encode"),
),
}];
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming to-number should succeed");
assert!(printed, "{expression} {source}");
assert_eq!(
String::from_utf8(output).unwrap(),
expected,
"{expression} {source}"
);
}
}
#[test]
fn cli_preserves_to_number_yaml_multi_document_framing() {
for expression in ["to_number", "tonumber"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
expression.to_string(),
])
.expect("to-number expression should parse");
let input = vec![InputPayload {
name: "values.yaml".to_string(),
bytes: b"---\n'12.5'\n---\n'1e3'\n".to_vec(),
}];
let output =
execute_command(&parsed, &input).expect("multi-document to-number should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"12.5\n---\n1000\n",
"{expression}"
);
}
}
#[test]
fn streaming_cli_preserves_to_number_output_before_the_first_typed_error() {
for expression in ["to_number", "tonumber"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("to-number expression should parse");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source =
StreamingInputSource::Stdin(b"---\n'12.5'\n---\n[1]\n---\n'1e3'\n".to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should reject an array");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected scalar, received array"
);
assert_eq!(String::from_utf8(output).unwrap(), "12.5\n", "{expression}");
}
}
#[test]
fn cli_reports_out_of_range_to_number_exponents_as_evaluation_errors() {
for expression in ["to_number", "tonumber"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("to-number expression should parse");
let source = serde_json::to_vec("1e400").expect("exponent string should encode");
let batch_inputs = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.clone(),
}];
let batch_error = execute_command(&parsed, &batch_inputs)
.expect_err("out-of-range exponent should fail batch conversion");
assert_eq!(
canonical_error_text(&batch_error),
"Evaluation: value cannot be converted to a number"
);
let streaming_inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(source),
}];
let mut output = Vec::new();
let streaming_error =
crate::execute::execute_command_to_writer(&parsed, &streaming_inputs, &mut output)
.expect_err("out-of-range exponent should fail streaming conversion");
assert_eq!(
canonical_error_text(&streaming_error),
"Evaluation: value cannot be converted to a number"
);
assert!(output.is_empty());
}
}
#[test]
fn cli_rejects_non_oracle_ascii_change_case_alias_names() {
for (expression, offset, lexeme) in [
("ascii_upcase", 0, "ascii_upcase"),
(".x | ascii_downcase", 5, "ascii_downcase"),
("asciiupcase", 0, "asciiupcase"),
(".x | asciidowncase", 5, "asciidowncase"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("CLI argument shape should parse");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"x":"Mixed"}"#.to_vec(),
}];
let error = execute_command(&parsed, &input).expect_err("alias should be rejected");
assert_eq!(
canonical_error_text(&error),
format!(
"Parse: invalid expression: ParticipleLexer(UnknownToken {{ offset: {offset}, lexeme: \"{lexeme}\" }})"
),
"{expression}"
);
}
}
#[test]
fn streaming_cli_reuses_string_participating_add_inside_reduce() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
".[] as $item ireduce (0; . + $item)".to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\n[1, 2]\n---\n[\"bad\"]\n---\n[3]\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("every document should reuse canonical string-participating addition");
assert_eq!(String::from_utf8(output).unwrap(), "3\n0bad\n3\n");
}
#[test]
fn cli_explicit_encoders_bypass_implicit_output_and_preserve_document_framing() {
let input = vec![InputPayload {
name: "items.json".to_string(),
bytes: br#"[{"id":1},{"id":2}]"#.to_vec(),
}];
for (expression, output_format, expected) in [
(".[] | to_json(0)", "yaml", "{\"id\":1}\n{\"id\":2}\n"),
(".[] | to_yaml(0)", "json", "id: 1\nid: 2\n"),
(".[] | to_toml", "json", "id = 1\nid = 2\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
output_format.to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let output = execute_command(&parsed, &input).expect("explicit encoder should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
let csv_input = vec![InputPayload {
name: "people.json".to_string(),
bytes: br#"[{"name":"Ada","age":37},{"name":"Bob","age":41}]"#.to_vec(),
}];
let csv_parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"yaml".to_string(),
"to_csv".to_string(),
])
.expect("parse should succeed");
let csv_output =
execute_command(&csv_parsed, &csv_input).expect("explicit CSV encoder should succeed");
assert_eq!(
String::from_utf8(csv_output).unwrap(),
"name,age\nAda,37\nBob,41\n"
);
}
#[test]
fn streaming_cli_writes_canonical_explicit_encoder_output() {
for (expression, source, expected) in [
("to_json(0)", br#"{"id":1}"#.as_slice(), "{\"id\":1}\n"),
(
"to_csv",
br#"[{"name":"Ada","age":37},{"name":"Bob","age":41}]"#.as_slice(),
"name,age\nAda,37\nBob,41\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"yaml".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(source.to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming explicit encoder should succeed");
assert!(printed);
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_canonical_base64_and_uri_scalar_codecs() {
let input = vec![InputPayload {
name: "payloads.json".to_string(),
bytes: r#"{"text":"hello world?/雪","base64":"aGVsbG8gd29ybGQ=","uri":"a+b%2Bc%2F~_-.","number":37,"array":[1]}"#
.as_bytes()
.to_vec(),
}];
for (expression, expected) in [
(".text | @base64", "aGVsbG8gd29ybGQ/L+mbqg==\n"),
(".base64 | @base64d", "hello world\n"),
(".text | to_uri", "hello+world%3F%2F%E9%9B%AA\n"),
(".uri | from_uri", "a b+c/~_-.\n"),
(".number | @urid", "37\n"),
(".array | @urid", "\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let output = execute_command(&parsed, &input).expect("scalar codec should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_recursive_container_contains() {
let input = vec![InputPayload {
name: "contains.json".to_string(),
bytes: br#"{"array":["cats",{"a":1,"b":2}],"object":{"a":["cats"],"b":2}}"#.to_vec(),
}];
for (expression, expected) in [
(r#".array | contains(["cat", {"a": 1}])"#, "true\n"),
(r#".array | contains([{"a": 2}])"#, "false\n"),
(r#".object | contains({"a": ["cat"]})"#, "true\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let output = execute_command(&parsed, &input).expect("recursive contains should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_canonical_split_and_join() {
for (expression, source, expected) in [
(
r#"split(",")"#,
br#""a,b""#.as_slice(),
"[\n \"a\",\n \"b\"\n]\n",
),
(
r#"join(",")"#,
br#"["a",1,true,null,[2],{"b":3}]"#.as_slice(),
"a,1,true,,,\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("split or join should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_reports_canonical_split_and_join_type_mismatches() {
for (expression, source, expected_type) in [
(r#"split(",")"#, br#"["a","b"]"#.as_slice(), "string"),
(r#"join(",")"#, br#""a,b""#.as_slice(), "array"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let error = execute_command(&parsed, &input).expect_err("invalid input type should fail");
let message = canonical_error_text(&error);
assert!(message.starts_with("TypeMismatch:"));
assert!(message.contains(&format!("expected {expected_type}")));
}
}
#[test]
fn streaming_cli_writes_canonical_split_and_join_output() {
for (expression, source, expected) in [
(
r#"split(",")"#,
br#""a,b""#.as_slice(),
"[\n \"a\",\n \"b\"\n]\n",
),
(
r#"join(",")"#,
br#"["a",1,true,null,[2],{"b":3}]"#.as_slice(),
"a,1,true,,,\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(source.to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming split or join should succeed");
assert!(printed);
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_executes_canonical_first_and_emits_no_bytes_for_zero_results() {
for (expression, source, expected) in [
("first", br#"[3,4]"#.as_slice(), "3\n"),
("first", br#"{"b":2,"a":1}"#.as_slice(), "b\n"),
(
"first(.enabled)",
br#"[{"enabled":false,"id":1},{"enabled":true,"id":2}]"#.as_slice(),
"{\n \"enabled\": true,\n \"id\": 2\n}\n",
),
("first", br#"[]"#.as_slice(), ""),
(
"first(.enabled)",
br#"[{"enabled":false,"id":1}]"#.as_slice(),
"",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: source.to_vec(),
}];
let output = execute_command(&parsed, &input).expect("first should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_reports_whether_canonical_first_printed_a_result() {
for (expression, source, expected, expected_printed) in [
("first", br#"[3,4]"#.as_slice(), "3\n", true),
(
"first(.enabled)",
br#"[{"enabled":false,"id":1},{"enabled":true,"id":2}]"#.as_slice(),
"{\n \"enabled\": true,\n \"id\": 2\n}\n",
true,
),
("first", br#"[]"#.as_slice(), "", false),
(
"first(.enabled)",
br#"[{"enabled":false,"id":1}]"#.as_slice(),
"",
false,
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(source.to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming first should succeed");
assert_eq!(printed, expected_printed, "{expression}");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn streaming_cli_exit_status_uses_canonical_result_truthiness() {
for (expression, expected_output, expected_exit_code) in [
("select(false)", "", 1),
("null", "null\n", 1),
("false", "false\n", 1),
("false, null", "false\nnull\n", 1),
("true, null", "true\nnull\n", 0),
("null, true", "null\ntrue\n", 0),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-e".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("exit-status expression should parse");
let inputs = vec![StreamingInput {
name: "value.json".to_string(),
input_format: "json".to_string(),
source: StreamingInputSource::Stdin(br#"{"value":1}"#.to_vec()),
}];
let mut output = Vec::new();
let status =
crate::execute::execute_command_to_writer_status(&parsed, &inputs, &mut output)
.expect("streaming exit-status expression should run");
assert_eq!(
String::from_utf8(output).unwrap(),
expected_output,
"{expression}"
);
assert_eq!(status.exit_code(true), expected_exit_code, "{expression}");
}
}
#[test]
fn cli_preserves_canonical_first_yaml_multi_document_framing() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
"first".to_string(),
])
.expect("parse should succeed");
let input = vec![InputPayload {
name: "values.yaml".to_string(),
bytes: b"---\n[3, 4]\n---\n[]\n---\n[{enabled: false}, {enabled: true, id: 2}]\n".to_vec(),
}];
let output = execute_command(&parsed, &input).expect("multi-document first should succeed");
assert_eq!(
String::from_utf8(output).unwrap(),
"3\n---\nenabled: false\n"
);
}
#[test]
fn streaming_cli_preserves_successful_first_output_before_the_first_error() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"first(split(\",\"))".to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(
b"---\n[\"a,b\"]\n---\n[null, [1,2]]\n---\n[\"c,d\"]\n".to_vec(),
);
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let error = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect_err("second document should fail");
assert_eq!(
canonical_error_text(&error),
"TypeMismatch: expected string, received array"
);
assert_eq!(String::from_utf8(output).unwrap(), "a,b\n");
}
#[test]
fn streaming_cli_writes_canonical_base64_and_uri_scalar_codec_output() {
for (expression, source, expected) in [
(
"@base64",
br#""hello world""#.as_slice(),
"aGVsbG8gd29ybGQ=\n",
),
("@base64d", br#""aGVsbG8=""#.as_slice(), "hello\n"),
("to_uri", br#""a b+c""#.as_slice(), "a+b%2Bc\n"),
("from_uri", br#""a%20b""#.as_slice(), "a b\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(source.to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming scalar codec should succeed");
assert!(printed);
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_canonical_decoders_preserve_result_framing_and_continue_evaluation() {
let input = vec![InputPayload {
name: "payloads.json".to_string(),
bytes: br#"{"csv":"name,age\nAda,37\nBob,41\n","json":"{\"items\":[1,2]}","yaml":"name: Ada\n"}"#.to_vec(),
}];
for (expression, expected) in [
(".csv | from_csv | (.[0].name, .[1].age)", "Ada\n41\n"),
(
".json | from_json | (.items[0] + 1, .items[1] + 1)",
"2\n3\n",
),
(".yaml | from_yaml | .name", "Ada\n"),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let output = execute_command(&parsed, &input).expect("canonical decoder should succeed");
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn cli_rejects_unregistered_toml_decoders_as_typed_parse_failures() {
use treease_core::evaluator::CanonicalFailureCategory;
let input = vec![InputPayload {
name: "payload.json".to_string(),
bytes: br#"{"toml":"name = \"Ada\"\n"}"#.to_vec(),
}];
for expression in [".toml | @tomld | .name", ".toml | from_toml | .name"] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("CLI argument parsing should retain the expression");
let error =
execute_command(&parsed, &input).expect_err("unregistered TOML decoder must not run");
let CliError::Canonical(failure) = error else {
panic!("unexpected CLI error for {expression}: {error:?}");
};
assert_eq!(
failure.category(),
CanonicalFailureCategory::Parse,
"{expression}"
);
}
}
#[test]
fn streaming_cli_writes_canonical_decoder_output() {
for (expression, source, expected) in [
("from_json | .id", br#""{\"id\":7}""#.as_slice(), "7\n"),
(
"from_csv | .[1].age",
br#""name,age\nAda,37\nBob,41\n""#.as_slice(),
"41\n",
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
expression.to_string(),
])
.expect("parse should succeed");
let inputs = prepare_streaming_inputs(&parsed).expect("streaming inputs should prepare");
let inputs = inputs
.into_iter()
.map(|mut input| {
input.source = crate::args::StreamingInputSource::Stdin(source.to_vec());
input
})
.collect::<Vec<_>>();
let mut output = Vec::new();
let printed = crate::execute::execute_command_to_writer(&parsed, &inputs, &mut output)
.expect("streaming decoder should succeed");
assert!(printed);
assert_eq!(String::from_utf8(output).unwrap(), expected, "{expression}");
}
}
#[test]
fn discovery_parent_command_requires_leaf_subcommand() {
let error = parser::parse_cli_args(&["treease".to_string(), "operators".to_string()])
.expect_err("parent discovery command should fail");
match error {
CliError::Eval(message) => {
assert!(message.contains("subcommand"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn operators_get_select_returns_json_metadata() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"operators".to_string(),
"get".to_string(),
"select".to_string(),
"--format".to_string(),
"json".to_string(),
])
.expect("operators get should parse");
let output = execute_metadata_command(&parsed).expect("metadata should render");
let value: serde_json::Value = serde_json::from_slice(&output).expect("valid json");
assert_eq!(value["name"], "select");
assert_eq!(value["category"], "special");
assert_eq!(value["syntax"], "select(EXPR)");
}
#[test]
fn cli_executes_compact_and_exposes_catalog_metadata() {
let input = vec![InputPayload {
name: "value.json".to_string(),
bytes: br#"{"x":[0,1]}"#.to_vec(),
}];
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"json".to_string(),
"-o".to_string(),
"json".to_string(),
".x | compact".to_string(),
])
.expect("compact should parse");
assert_eq!(
String::from_utf8(execute_command(&parsed, &input).expect("compact should execute"))
.unwrap(),
"[\n 1\n]\n"
);
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"operators".to_string(),
"get".to_string(),
"compact".to_string(),
"--format".to_string(),
"json".to_string(),
])
.expect("operators get should parse");
let output = execute_metadata_command(&parsed).expect("compact metadata should render");
let value: serde_json::Value = serde_json::from_slice(&output).expect("valid json");
assert_eq!(value["name"], "compact");
assert_eq!(value["syntax"], "compact");
}
#[test]
fn formats_get_yaml_returns_json_metadata() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"formats".to_string(),
"get".to_string(),
"yaml".to_string(),
"--format".to_string(),
"json".to_string(),
])
.expect("formats get should parse");
let output = execute_metadata_command(&parsed).expect("metadata should render");
let value: serde_json::Value = serde_json::from_slice(&output).expect("valid json");
assert_eq!(value["name"], "yaml");
assert_eq!(value["can_decode"], true);
assert_eq!(value["can_encode"], true);
}
#[test]
fn unsupported_format_error_has_code_and_hint() {
let err = CliError::UnsupportedFormat("foo".to_string());
let report = errors::error_report(&err);
assert_eq!(report.code, "UNSUPPORTED_FORMAT");
assert!(report.hint.contains("treease formats list"));
assert!(errors::render_text(&err).contains("UNSUPPORTED_FORMAT"));
}
#[test]
fn canonical_failures_keep_typed_categories_until_cli_reporting() {
use treease_core::evaluator::CanonicalFailureCategory;
for (expression, source, category) in [
(
"unknown_operator",
"{}".as_bytes(),
CanonicalFailureCategory::Parse,
),
(
". = 1",
"{}".as_bytes(),
CanonicalFailureCategory::Unsupported,
),
(
r#"test("x")"#,
"1".as_bytes(),
CanonicalFailureCategory::TypeMismatch,
),
(
r#"error("boom")"#,
"null".as_bytes(),
CanonicalFailureCategory::Evaluation,
),
] {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"yaml".to_string(),
expression.to_string(),
])
.expect("CLI argument parsing should retain the expression");
let inputs = vec![InputPayload {
name: "input.yaml".to_string(),
bytes: source.to_vec(),
}];
let error = execute_command(&parsed, &inputs).expect_err("canonical execution should fail");
let CliError::Canonical(failure) = &error else {
panic!("unexpected CLI error for {expression}: {error:?}");
};
assert_eq!(failure.category(), category, "{expression}");
let report = errors::error_report(&error);
assert_eq!(report.code, "EXECUTION_ERROR");
assert!(
report
.message
.starts_with(&format!("{}: ", category.abi_name())),
"{expression}: {}",
report.message
);
}
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"toml".to_string(),
"-o".to_string(),
"toml".to_string(),
".apple".to_string(),
])
.expect("CLI argument parsing should succeed");
let toml = vec![InputPayload {
name: "input.toml".to_string(),
bytes: b"apple = 2\n".to_vec(),
}];
let error = execute_command(&parsed, &toml).expect_err("TOML scalar output should fail");
let CliError::Canonical(failure) = &error else {
panic!("unexpected TOML format error: {error:?}");
};
assert_eq!(failure.category(), CanonicalFailureCategory::Format);
assert_eq!(
errors::error_report(&error).message,
"format: TOML yq results must have a top-level map"
);
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"-p".to_string(),
"yaml".to_string(),
".".to_string(),
])
.expect("CLI argument parsing should succeed");
let malformed = vec![InputPayload {
name: "input.yaml".to_string(),
bytes: b"[unterminated".to_vec(),
}];
let error = execute_command(&parsed, &malformed).expect_err("source decode should fail");
let CliError::Canonical(failure) = &error else {
panic!("unexpected source decode error: {error:?}");
};
assert_eq!(failure.category(), CanonicalFailureCategory::Format);
assert!(
errors::error_report(&error)
.message
.starts_with("format: source decode failed:")
);
}
#[test]
fn unknown_flag_error_has_code_and_hint() {
let err = CliError::UnknownFlag("--wat".to_string());
let report = errors::error_report(&err);
assert_eq!(report.code, "UNKNOWN_FLAG");
assert!(report.hint.contains("treease --help"));
}
#[test]
fn unknown_command_error_has_code_and_migration_hint() {
let err = CliError::UnknownCommand("eval-all".to_string());
let report = errors::error_report(&err);
assert_eq!(report.code, "UNKNOWN_COMMAND");
assert!(report.hint.contains("legacy eval subcommands were removed"));
assert!(
report
.hint
.contains("treease [OPTIONS] [EXPRESSION] [FILE]")
);
}
#[test]
fn removed_legacy_eval_commands_are_rejected() {
for command in ["e", "eval-all", "ea"] {
let error = parser::parse_cli_args(&["treease".to_string(), command.to_string()])
.expect_err("legacy eval command should fail");
match error {
CliError::UnknownCommand(name) => assert_eq!(name, command),
other => panic!("unexpected error for {command}: {other:?}"),
}
}
}
#[test]
fn web_command_parses_expression_file_and_format_options() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
"-p".to_string(),
"yaml".to_string(),
"-o".to_string(),
"json".to_string(),
"-I".to_string(),
"2".to_string(),
".service".to_string(),
"config.yaml".to_string(),
])
.expect("web command should parse");
assert_eq!(parsed.command, CommandKind::Web);
assert_eq!(parsed.expression, ".service");
assert_eq!(parsed.files, vec!["config.yaml"]);
assert_eq!(parsed.input_format.as_deref(), Some("yaml"));
assert_eq!(parsed.output_format.as_deref(), Some("json"));
assert_eq!(parsed.indent, Some(2));
}
#[test]
fn web_command_accepts_stdin_source() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
".".to_string(),
"-".to_string(),
])
.expect("web stdin should parse");
assert_eq!(parsed.command, CommandKind::Web);
assert_eq!(parsed.files, vec!["-"]);
}
#[test]
fn web_command_rejects_multiple_input_sources() {
let error = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
".".to_string(),
"a.yaml".to_string(),
"b.yaml".to_string(),
])
.expect_err("web should reject multiple files");
match error {
CliError::InvalidWebInputCount => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn root_invocation_with_web_file_name_is_not_prechecked_as_web_command() {
let parsed =
parser::parse_cli_args(&["treease".to_string(), ".".to_string(), "web".to_string()])
.expect("root invocation should parse");
assert_eq!(parsed.command, CommandKind::Run);
assert_eq!(parsed.expression, ".");
assert_eq!(parsed.files, vec!["web"]);
}
#[test]
fn web_command_rejects_root_execution_flags_that_do_not_apply() {
let error = parser::parse_cli_args(&[
"treease".to_string(),
"-e".to_string(),
"web".to_string(),
".".to_string(),
"a.yaml".to_string(),
])
.expect_err("web should reject exit-status");
match error {
CliError::UnsupportedWebFlag("--exit-status") => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn web_command_preserves_unknown_flag_error() {
let error = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
"--wat".to_string(),
".".to_string(),
"file.yaml".to_string(),
])
.expect_err("unknown web flag should fail");
match error {
CliError::UnknownFlag(flag) => assert_eq!(flag, "--wat"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn web_command_preserves_unknown_flag_with_value_error() {
let error = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
"--wat".to_string(),
"value".to_string(),
".".to_string(),
"file.yaml".to_string(),
])
.expect_err("unknown web flag should fail before input count validation");
match error {
CliError::UnknownFlag(flag) => assert_eq!(flag, "--wat"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn web_payload_uses_expression_output_and_output_format() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
"-o".to_string(),
"json".to_string(),
".foo".to_string(),
"input.yaml".to_string(),
])
.expect("web should parse");
let inputs = vec![InputPayload {
name: "input.yaml".to_string(),
bytes: b"foo:\n bar: 1\n".to_vec(),
}];
let payload = web_payload::build_cli_graph_result_payload(&parsed, &inputs)
.expect("web payload should be produced");
assert_eq!(payload.source_label, "input.yaml");
assert_eq!(payload.expression, ".foo");
assert_eq!(payload.language, "json");
assert_eq!(payload.text, "{\n \"bar\": 1\n}\n");
}
#[test]
fn web_payload_preserves_canonical_missing_path_result() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
".missing".to_string(),
"input.json".to_string(),
])
.expect("web should parse");
let inputs = vec![InputPayload {
name: "input.json".to_string(),
bytes: br#"{"foo":1}"#.to_vec(),
}];
let payload = web_payload::build_cli_graph_result_payload(&parsed, &inputs)
.expect("web payload should be produced");
assert_eq!(payload.language, "json");
assert_eq!(payload.text, "null\n");
}
#[test]
fn web_payload_supports_stdin_and_defaults_to_yaml() {
let parsed = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
".".to_string(),
"-".to_string(),
])
.expect("web stdin should parse");
let inputs = vec![InputPayload {
name: "<stdin>".to_string(),
bytes: b"foo: 1\n".to_vec(),
}];
let payload = web_payload::build_cli_graph_result_payload(&parsed, &inputs)
.expect("web payload should be produced");
assert_eq!(payload.source_label, "<stdin>");
assert_eq!(payload.language, "yaml");
assert_eq!(payload.text, "foo: 1\n");
}
#[test]
fn web_command_rejects_subcommand_position_unsupported_flags() {
for (flag, expected) in [
("--null-input", "--null-input"),
("--exit-status", "--exit-status"),
("--inplace", "--inplace"),
] {
let error = parser::parse_cli_args(&[
"treease".to_string(),
"web".to_string(),
flag.to_string(),
".".to_string(),
"file.yaml".to_string(),
])
.expect_err("web should reject unsupported execution flag");
match error {
CliError::UnsupportedWebFlag(actual) => assert_eq!(actual, expected),
other => panic!("unexpected error for {flag}: {other:?}"),
}
}
}