use crate::common::HYGIENE;
use crate::common::MULTI_TAG;
use crate::common::OAS3;
use crate::common::vimanam;
use predicates::prelude::*;
const HYGIENE_MULTI_BODY: &str = "tests/fixtures/hygiene_multi_body_oas3.json";
#[test]
fn hygiene_report_is_appended_by_default() {
vimanam()
.arg(OAS3)
.assert()
.success()
.stdout(predicate::str::contains(
"\n---\n\n## Spec Hygiene Report\n",
))
.stdout(predicate::str::contains(
"**4 endpoints** across **2 services**",
))
.stdout(predicate::str::contains("| Deprecated | 1 |"))
.stdout(predicate::str::contains(
"### Deprecated (1)\n- `GET /store/orders`\n",
));
}
#[test]
fn no_report_suppresses_hygiene_report() {
vimanam()
.arg(OAS3)
.arg("--no-report")
.assert()
.success()
.stdout(predicate::str::contains("# Petstore API"))
.stdout(predicate::str::contains("Spec Hygiene Report").not())
.stdout(predicate::str::contains("\n---\n").not());
}
#[test]
fn hygiene_report_lists_every_check() {
let output = String::from_utf8(
vimanam()
.arg(HYGIENE)
.args(["--detail", "standard"])
.output()
.unwrap()
.stdout,
)
.unwrap();
let expected = "
---
## Spec Hygiene Report
**6 endpoints** across **2 services**
| Check | Count |
|-------|------:|
| Missing description | 1 |
| Missing operationId | 1 |
| No responses documented | 1 |
| Deprecated | 2 |
| Untagged (no service tag) | 1 |
| Duplicate operationIds | 1 |
| Parameters without description | 3 |
### Missing description (1)
- `GET /health`
### Missing operationId (1)
- `GET /health`
### No responses documented (1)
- `POST /users`
### Deprecated (2)
- `GET /ping`
- `DELETE /users/{id}`
### Untagged (no service tag) (1)
- `GET /health`
### Duplicate operationIds (1)
- `getUser`
- `DELETE /users/{id}`
- `GET /users/{id}`
### Parameters without description (3)
- `GET /users` — `limit`
- `POST /users` — `requestBody`
- `DELETE /users/{id}` — `id`
";
assert!(
output.ends_with(expected),
"report did not match.\nexpected tail:\n{expected}\nactual output:\n{output}"
);
assert!(output.starts_with("# Hygiene API\n"), "{output}");
}
#[test]
fn hygiene_report_respects_service_filter() {
vimanam()
.arg(HYGIENE)
.args(["--service-filter", "Health"])
.assert()
.success()
.stdout(predicate::str::contains(
"**1 endpoint** across **1 service**",
))
.stdout(predicate::str::contains("| Deprecated | 1 |"))
.stdout(predicate::str::contains(
"| Untagged (no service tag) | 0 |",
))
.stdout(predicate::str::contains("| Duplicate operationIds | 0 |"))
.stdout(predicate::str::contains(
"### Deprecated (1)\n- `GET /ping`\n",
))
.stdout(predicate::str::contains("GET /health").not());
}
#[test]
fn hygiene_report_respects_exclude_deprecated() {
vimanam()
.arg(HYGIENE)
.arg("--exclude-deprecated")
.assert()
.success()
.stdout(predicate::str::contains(
"**4 endpoints** across **1 service**",
))
.stdout(predicate::str::contains("| Deprecated | 0 |"))
.stdout(predicate::str::contains("| Duplicate operationIds | 0 |"))
.stdout(predicate::str::contains(
"| Parameters without description | 2 |",
))
.stdout(predicate::str::contains("### Deprecated").not())
.stdout(predicate::str::contains("### Duplicate operationIds").not());
}
#[test]
fn hygiene_report_is_appended_under_max_tokens() {
vimanam()
.arg(OAS3)
.args(["--detail", "full", "--max-tokens", "40"])
.assert()
.success()
.stderr(predicate::str::contains("token budget"))
.stdout(predicate::str::contains("## Spec Hygiene Report"));
}
#[test]
fn hygiene_report_is_written_to_output_file() {
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("out.md");
vimanam()
.arg(HYGIENE)
.args(["-o", out_path.to_str().unwrap()])
.assert()
.success();
let content = std::fs::read_to_string(&out_path).unwrap();
assert!(content.starts_with("# Hygiene API\n"), "{content}");
assert!(
content.contains("\n---\n\n## Spec Hygiene Report\n"),
"{content}"
);
assert!(
content.contains("| Duplicate operationIds | 1 |"),
"{content}"
);
}
#[test]
fn hygiene_report_is_deterministic() {
let run = || {
vimanam()
.arg(HYGIENE)
.args(["--detail", "full", "--sort", "none"])
.output()
.unwrap()
.stdout
};
let first = run();
for _ in 0..4 {
assert_eq!(first, run(), "report differed between identical runs");
}
}
#[test]
fn hygiene_report_counts_multi_media_type_body_once() {
let output = String::from_utf8(
vimanam()
.arg(HYGIENE_MULTI_BODY)
.args(["--detail", "standard"])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
output.contains("`requestBody (application/json)` | body |"),
"{output}"
);
assert!(
output.contains("`requestBody (application/xml)` | body |"),
"{output}"
);
assert!(
output.contains("| Parameters without description | 1 |"),
"{output}"
);
assert!(
output.ends_with(
"### Parameters without description (1)\n- `POST /items` — `requestBody (application/json)`\n"
),
"{output}"
);
assert_eq!(
output.matches("- `POST /items` — `requestBody").count(),
1,
"{output}"
);
}
#[test]
fn hygiene_report_is_separated_from_body_by_one_blank_line() {
for detail in ["summary", "basic", "standard", "full"] {
vimanam()
.arg(OAS3)
.args(["--detail", detail])
.assert()
.success()
.stdout(predicate::str::contains(
"\n\n---\n\n## Spec Hygiene Report",
))
.stdout(predicate::str::contains("\n\n\n---").not());
}
}
#[test]
fn hygiene_report_counts_multi_tag_endpoint_once() {
vimanam()
.arg(MULTI_TAG)
.assert()
.success()
.stdout(predicate::str::contains(
"**1 endpoint** across **2 services**",
));
}
#[test]
fn hygiene_report_on_empty_filtered_set_is_all_zero() {
let output = String::from_utf8(
vimanam()
.arg(OAS3)
.args(["--path-filter", "/nope"])
.output()
.unwrap()
.stdout,
)
.unwrap();
let expected = "\
## Spec Hygiene Report
**0 endpoints** across **0 services**
| Check | Count |
|-------|------:|
| Missing description | 0 |
| Missing operationId | 0 |
| No responses documented | 0 |
| Deprecated | 0 |
| Untagged (no service tag) | 0 |
| Duplicate operationIds | 0 |
| Parameters without description | 0 |
";
assert!(output.ends_with(expected), "{output}");
assert!(!output.contains("\n### "), "{output}");
}