322 lines
11 KiB
Rust
322 lines
11 KiB
Rust
mod helpers;
|
|
|
|
use serde_json::Value;
|
|
|
|
/// Recursively mask dynamic fields (timestamps, durations, versions) for stable comparison.
|
|
fn normalize_value(value: &mut Value) {
|
|
match value {
|
|
Value::Object(map) => {
|
|
for (key, val) in map.iter_mut() {
|
|
match key.as_str() {
|
|
"duration_ms" if val.is_number() => *val = Value::Number(0.into()),
|
|
"tool_version" if val.is_string() => *val = Value::String("MASKED".into()),
|
|
"cached_at" | "fetched_at" | "last_accessed" if val.is_string() => {
|
|
*val = Value::String("MASKED_TIMESTAMP".into())
|
|
}
|
|
_ => normalize_value(val),
|
|
}
|
|
}
|
|
}
|
|
Value::Array(arr) => {
|
|
for item in arr.iter_mut() {
|
|
normalize_value(item);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// Normalize robot JSON for golden comparison: mask all dynamic fields.
|
|
fn normalize_robot(mut json: Value) -> Value {
|
|
normalize_value(&mut json);
|
|
json
|
|
}
|
|
|
|
/// Assert that a robot success envelope has the correct structural invariants.
|
|
fn assert_robot_success_structure(json: &Value, expected_command: &str) {
|
|
// "ok" must be bool true
|
|
assert_eq!(
|
|
json.get("ok").and_then(|v| v.as_bool()),
|
|
Some(true),
|
|
"ok field must be true"
|
|
);
|
|
|
|
// "data" must be an object
|
|
assert!(
|
|
json.get("data").is_some_and(|v| v.is_object()),
|
|
"data field must be an object"
|
|
);
|
|
|
|
// "meta" must be an object with required fields
|
|
let meta = json.get("meta").expect("meta field is required");
|
|
assert!(meta.is_object(), "meta must be an object");
|
|
|
|
// meta.schema_version must be number == 1
|
|
let sv = meta
|
|
.get("schema_version")
|
|
.expect("meta.schema_version required");
|
|
assert!(sv.is_number(), "meta.schema_version must be a number");
|
|
assert_eq!(sv.as_u64(), Some(1), "meta.schema_version must equal 1");
|
|
|
|
// meta.tool_version must be a string
|
|
let tv = meta
|
|
.get("tool_version")
|
|
.expect("meta.tool_version required");
|
|
assert!(tv.is_string(), "meta.tool_version must be a string");
|
|
|
|
// meta.command must be a string matching expected
|
|
let cmd = meta.get("command").expect("meta.command required");
|
|
assert!(cmd.is_string(), "meta.command must be a string");
|
|
assert_eq!(
|
|
cmd.as_str().unwrap(),
|
|
expected_command,
|
|
"meta.command must match the command run"
|
|
);
|
|
|
|
// meta.duration_ms must be a number
|
|
let dur = meta.get("duration_ms").expect("meta.duration_ms required");
|
|
assert!(dur.is_number(), "meta.duration_ms must be a number");
|
|
}
|
|
|
|
/// Assert that a robot error envelope has the correct structural invariants.
|
|
fn assert_robot_error_structure(json: &Value) {
|
|
// "ok" must be bool false
|
|
assert_eq!(
|
|
json.get("ok").and_then(|v| v.as_bool()),
|
|
Some(false),
|
|
"ok field must be false on error"
|
|
);
|
|
|
|
// "data" should be absent or null
|
|
let data = json.get("data");
|
|
assert!(
|
|
data.is_none() || data.unwrap().is_null(),
|
|
"data field must be null or absent on error"
|
|
);
|
|
|
|
// "error" must be an object with code and message
|
|
let error = json.get("error").expect("error field is required");
|
|
assert!(error.is_object(), "error must be an object");
|
|
assert!(
|
|
error.get("code").is_some_and(|v| v.is_string()),
|
|
"error.code must be a string"
|
|
);
|
|
assert!(
|
|
error.get("message").is_some_and(|v| v.is_string()),
|
|
"error.message must be a string"
|
|
);
|
|
|
|
// "meta" structural invariants (same as success)
|
|
let meta = json.get("meta").expect("meta field is required");
|
|
assert!(meta.is_object(), "meta must be an object");
|
|
assert!(
|
|
meta.get("schema_version").is_some_and(|v| v.is_number()),
|
|
"meta.schema_version must be a number"
|
|
);
|
|
assert!(
|
|
meta.get("tool_version").is_some_and(|v| v.is_string()),
|
|
"meta.tool_version must be a string"
|
|
);
|
|
assert!(
|
|
meta.get("command").is_some_and(|v| v.is_string()),
|
|
"meta.command must be a string"
|
|
);
|
|
assert!(
|
|
meta.get("duration_ms").is_some_and(|v| v.is_number()),
|
|
"meta.duration_ms must be a number"
|
|
);
|
|
}
|
|
|
|
/// Load a golden snapshot file, parse as JSON.
|
|
fn load_golden(name: &str) -> Value {
|
|
let path = helpers::fixture_path(&format!("golden/{name}"));
|
|
let content = std::fs::read_to_string(&path)
|
|
.unwrap_or_else(|e| panic!("failed to read golden file {}: {e}", path.display()));
|
|
serde_json::from_str(&content)
|
|
.unwrap_or_else(|e| panic!("failed to parse golden file {}: {e}", path.display()))
|
|
}
|
|
|
|
/// Write a golden snapshot file if it does not exist (first-run bootstrap).
|
|
fn write_golden_if_missing(name: &str, value: &Value) {
|
|
let path = helpers::fixture_path(&format!("golden/{name}"));
|
|
if !path.exists() {
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent).expect("failed to create golden dir");
|
|
}
|
|
let pretty =
|
|
serde_json::to_string_pretty(value).expect("failed to serialize golden snapshot");
|
|
std::fs::write(&path, pretty).expect("failed to write golden snapshot");
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Golden structure tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_golden_list_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(&env, &["list", "petstore", "--robot"]).success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "list");
|
|
|
|
// Snapshot comparison
|
|
let normalized = normalize_robot(json);
|
|
write_golden_if_missing("list.json", &normalized);
|
|
let golden = load_golden("list.json");
|
|
assert_eq!(
|
|
normalized, golden,
|
|
"list robot output does not match golden snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_show_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(
|
|
&env,
|
|
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
|
|
)
|
|
.success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "show");
|
|
|
|
let normalized = normalize_robot(json);
|
|
write_golden_if_missing("show.json", &normalized);
|
|
let golden = load_golden("show.json");
|
|
assert_eq!(
|
|
normalized, golden,
|
|
"show robot output does not match golden snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_search_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(&env, &["search", "petstore", "pet", "--robot"]).success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "search");
|
|
|
|
let normalized = normalize_robot(json);
|
|
write_golden_if_missing("search.json", &normalized);
|
|
let golden = load_golden("search.json");
|
|
assert_eq!(
|
|
normalized, golden,
|
|
"search robot output does not match golden snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_schemas_list_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--robot"]).success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "schemas");
|
|
|
|
let normalized = normalize_robot(json);
|
|
write_golden_if_missing("schemas_list.json", &normalized);
|
|
let golden = load_golden("schemas_list.json");
|
|
assert_eq!(
|
|
normalized, golden,
|
|
"schemas list robot output does not match golden snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_schemas_show_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--show", "Pet", "--robot"]).success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "schemas");
|
|
|
|
let normalized = normalize_robot(json);
|
|
write_golden_if_missing("schemas_show.json", &normalized);
|
|
let golden = load_golden("schemas_show.json");
|
|
assert_eq!(
|
|
normalized, golden,
|
|
"schemas show robot output does not match golden snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_tags_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(&env, &["tags", "petstore", "--robot"]).success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "tags");
|
|
|
|
let normalized = normalize_robot(json);
|
|
write_golden_if_missing("tags.json", &normalized);
|
|
let golden = load_golden("tags.json");
|
|
assert_eq!(
|
|
normalized, golden,
|
|
"tags robot output does not match golden snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_aliases_robot_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
|
|
|
let a = helpers::run_cmd(&env, &["aliases", "--list", "--robot"]).success();
|
|
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
|
|
|
assert_robot_success_structure(&json, "aliases");
|
|
|
|
// Aliases output has dynamic timestamps so we only check structure,
|
|
// not golden snapshot equality. Normalize and check key shape.
|
|
let data = json.get("data").unwrap();
|
|
assert!(data.get("aliases").is_some_and(|v| v.is_array()));
|
|
assert!(data.get("count").is_some_and(|v| v.is_number()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_error_structure() {
|
|
let env = helpers::TestEnv::new();
|
|
// Do NOT fetch -- alias does not exist
|
|
|
|
let a = helpers::run_cmd(&env, &["list", "nonexistent", "--robot"]);
|
|
|
|
// Command should fail with non-zero exit
|
|
let a = a.failure();
|
|
|
|
// Error JSON goes to stderr
|
|
let stderr = std::str::from_utf8(&a.get_output().stderr).expect("stderr not UTF-8");
|
|
let json: Value = serde_json::from_str(stderr.trim()).expect("stderr is not valid robot JSON");
|
|
|
|
assert_robot_error_structure(&json);
|
|
|
|
// Verify error code
|
|
assert_eq!(
|
|
json["error"]["code"].as_str(),
|
|
Some("ALIAS_NOT_FOUND"),
|
|
"error code should be ALIAS_NOT_FOUND"
|
|
);
|
|
|
|
// Verify meta.command
|
|
assert_eq!(
|
|
json["meta"]["command"].as_str(),
|
|
Some("list"),
|
|
"meta.command should be list"
|
|
);
|
|
}
|