mod common;
use common::{stderr, stdout, Sandbox};
use std::fs;
const V1_FIXTURE: &str = "tests/fixtures/v1_project.req";
#[test]
fn req_0116_v1_fixture_errors_with_migrate_hint_when_opted_out() {
let s = Sandbox::new();
let target = s.dir.path().join("project.req");
fs::copy(V1_FIXTURE, &target).expect("copy v1 fixture");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_req"))
.env("REQ_NO_AUTO_MIGRATE", "1")
.env_remove("REQ_FILE")
.args(["--file", target.to_str().unwrap(), "validate"])
.output()
.expect("invoke req");
assert!(
!out.status.success(),
"with REQ_NO_AUTO_MIGRATE=1, v1 file must be rejected"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("req migrate"),
"error must hint at `req migrate`, got: {}",
err
);
}
#[test]
fn req_0122_auto_migrate_on_first_load() {
let s = Sandbox::new();
let target = s.dir.path().join("project.req");
fs::copy(V1_FIXTURE, &target).expect("copy v1 fixture");
let out = common::req(&["--file", target.to_str().unwrap(), "validate"]);
assert!(
out.status.success(),
"auto-migrate should succeed on the v1 fixture; stderr={}",
stderr(&out)
);
let err = stderr(&out);
assert!(
err.contains("auto-migrating"),
"expected the auto-migrate banner on stderr, got: {}",
err
);
let migrated = fs::read_to_string(&target).unwrap();
assert!(
migrated.contains("\"_format\": \"req-v2\""),
"_format should be req-v2 after auto-migrate"
);
let backup = target.with_extension("req.bak-req-v1");
assert!(
backup.exists(),
"expected backup at {} after auto-migrate",
backup.display()
);
}
#[test]
fn req_0122_auto_migrate_opt_out_still_errors() {
let s = Sandbox::new();
let target = s.dir.path().join("project.req");
fs::copy(V1_FIXTURE, &target).expect("copy v1 fixture");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_req"))
.env("REQ_NO_AUTO_MIGRATE", "1")
.env_remove("REQ_FILE")
.args(["--file", target.to_str().unwrap(), "validate"])
.output()
.expect("invoke req");
assert!(
!out.status.success(),
"REQ_NO_AUTO_MIGRATE=1 must preserve the manual-migrate error"
);
let on_disk = fs::read_to_string(&target).unwrap();
assert!(
on_disk.contains("\"_format\": \"req-v1\""),
"opt-out path must leave the file at the original _format"
);
}
#[test]
fn req_0116_v1_fixture_migrates_to_v2_with_ids_preserved() {
let s = Sandbox::new();
let target = s.dir.path().join("project.req");
fs::copy(V1_FIXTURE, &target).expect("copy v1 fixture");
let target_s = target.to_str().unwrap().to_string();
let out = common::req(&["--file", &target_s, "migrate"]);
assert!(
out.status.success(),
"migrate v1 → v2 should succeed; stderr={}",
stderr(&out)
);
assert!(
stdout(&out).contains("req-v1 → req-v2"),
"expected the v1 → v2 banner, got: {}",
stdout(&out)
);
let val = common::req(&["--file", &target_s, "validate"]);
assert!(
val.status.success(),
"post-migrate validate failed: {}",
stderr(&val)
);
let list = common::req(&["--file", &target_s, "list", "--json"]);
let body = stdout(&list);
assert!(body.contains("REQ-0001"), "REQ-0001 missing: {}", body);
assert!(body.contains("REQ-0002"), "REQ-0002 missing: {}", body);
assert!(
body.contains("Anchor requirement one"),
"anchor title not preserved: {}",
body
);
let migrated = fs::read_to_string(&target).unwrap();
assert!(
migrated.contains("\"_format\": \"req-v2\""),
"_format should be req-v2 after migrate"
);
let backup = target.with_extension("req.bak-req-v1");
assert!(backup.exists(), "expected backup at {}", backup.display());
}
#[test]
fn req_0116_migrate_on_current_format_is_noop() {
let s = Sandbox::new();
s.init("p");
let before = fs::read(s.path()).unwrap();
let out = s.run(&["migrate"]);
assert!(out.status.success(), "stderr={}", stderr(&out));
assert!(
stdout(&out).contains("no migration needed"),
"expected no-op message, got: {}",
stdout(&out)
);
let after = fs::read(s.path()).unwrap();
assert_eq!(
before, after,
"migrate at current format must not modify the file"
);
}
#[test]
fn req_0116_migrate_rejects_unknown_newer_format() {
let s = Sandbox::new();
s.init("p");
let text = fs::read_to_string(s.path()).unwrap();
let bumped = text.replace("\"_format\": \"req-v2\"", "\"_format\": \"req-v99\"");
fs::write(s.path(), bumped).unwrap();
let out = s.run(&["migrate"]);
assert!(!out.status.success(), "newer format must error");
let err = stderr(&out);
assert!(
err.contains("newer than this binary") || err.contains("Upgrade the binary"),
"error should point to binary upgrade, got: {}",
err
);
}