use std::fs;
use std::path::Path;
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use serde_json;
use std::collections::{BTreeMap, HashMap};
use xchecker::doctor::{CheckStatus, DoctorCheck, DoctorOutput};
use xchecker::receipt::ReceiptManager;
use xchecker::status::status::StatusManager;
use xchecker::types::{FileHash, PacketEvidence, PhaseId, StatusOutput};
#[test]
fn test_jcs_documentation() {
let contracts_path = Path::new("docs/reference/CONTRACTS.md");
assert!(
contracts_path.exists(),
"CONTRACTS.md should exist at docs/reference/CONTRACTS.md"
);
let contracts_content =
fs::read_to_string(contracts_path).expect("Failed to read CONTRACTS.md");
assert!(
contracts_content.contains("JCS") || contracts_content.contains("RFC 8785"),
"CONTRACTS.md should mention JCS or RFC 8785"
);
assert!(
contracts_content.contains("canonical") || contracts_content.contains("Canonical"),
"CONTRACTS.md should describe canonical emission"
);
assert!(
contracts_content.contains("deterministic") || contracts_content.contains("sorted"),
"CONTRACTS.md should mention deterministic key ordering or sorting"
);
assert!(
contracts_content.contains("jcs-rfc8785")
|| contracts_content.contains("canonicalization_backend"),
"CONTRACTS.md should mention the jcs-rfc8785 backend"
);
test_receipt_jcs_byte_identity();
test_status_jcs_byte_identity();
test_doctor_jcs_byte_identity();
}
fn test_receipt_jcs_byte_identity() {
let _temp_dir = xchecker::paths::with_isolated_home();
let base_path = xchecker::paths::xchecker_home()
.join("specs")
.join("test-jcs-receipt");
let manager = ReceiptManager::new(&base_path);
let outputs1 = vec![
FileHash {
path: "artifacts/10-design.md".to_string(),
blake3_canonicalized: "fedcba9876543210".to_string(),
},
FileHash {
path: "artifacts/00-requirements.md".to_string(),
blake3_canonicalized: "0123456789abcdef".to_string(),
},
];
let outputs2 = vec![
FileHash {
path: "artifacts/00-requirements.md".to_string(),
blake3_canonicalized: "0123456789abcdef".to_string(),
},
FileHash {
path: "artifacts/10-design.md".to_string(),
blake3_canonicalized: "fedcba9876543210".to_string(),
},
];
let packet = PacketEvidence {
files: vec![],
max_bytes: 65536,
max_lines: 1200,
};
let fixed_timestamp = chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let mut receipt1 = manager.create_receipt(
"test-jcs",
PhaseId::Requirements,
0,
outputs1,
"0.1.0",
"0.8.1",
"haiku",
None,
HashMap::new(),
packet.clone(),
None,
None,
vec![],
Some(false),
"native",
None,
None,
None,
None,
None, );
receipt1.emitted_at = fixed_timestamp;
let mut receipt2 = manager.create_receipt(
"test-jcs",
PhaseId::Requirements,
0,
outputs2,
"0.1.0",
"0.8.1",
"haiku",
None,
HashMap::new(),
packet,
None,
None,
vec![],
Some(false),
"native",
None,
None,
None,
None,
None, );
receipt2.emitted_at = fixed_timestamp;
let json1_value = serde_json::to_value(&receipt1).unwrap();
let json1_bytes = serde_json_canonicalizer::to_vec(&json1_value).unwrap();
let json1 = String::from_utf8(json1_bytes).unwrap();
let json2_value = serde_json::to_value(&receipt2).unwrap();
let json2_bytes = serde_json_canonicalizer::to_vec(&json2_value).unwrap();
let json2 = String::from_utf8(json2_bytes).unwrap();
assert_eq!(
json1, json2,
"JCS should produce byte-identical output regardless of field insertion order"
);
assert!(
!json1.contains(" "),
"JCS output should not contain extra whitespace"
);
assert!(
!json1.contains('\n'),
"JCS output should not contain newlines"
);
assert!(
json1.contains("\"canonicalization_backend\":\"jcs-rfc8785\""),
"Receipt should document JCS backend"
);
}
fn test_status_jcs_byte_identity() {
let fixed_timestamp = chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let mut config1 = BTreeMap::new();
config1.insert(
"model".to_string(),
xchecker::types::ConfigValue {
value: serde_json::Value::String("haiku".to_string()),
source: xchecker::types::ConfigSource::Default,
},
);
config1.insert(
"max_turns".to_string(),
xchecker::types::ConfigValue {
value: serde_json::Value::Number(6.into()),
source: xchecker::types::ConfigSource::Config,
},
);
let mut config2 = BTreeMap::new();
config2.insert(
"max_turns".to_string(),
xchecker::types::ConfigValue {
value: serde_json::Value::Number(6.into()),
source: xchecker::types::ConfigSource::Config,
},
);
config2.insert(
"model".to_string(),
xchecker::types::ConfigValue {
value: serde_json::Value::String("haiku".to_string()),
source: xchecker::types::ConfigSource::Default,
},
);
let status1 = StatusOutput {
schema_version: "1".to_string(),
emitted_at: fixed_timestamp,
runner: "native".to_string(),
runner_distro: None,
fallback_used: false,
canonicalization_version: "yaml-v1,md-v1".to_string(),
canonicalization_backend: "jcs-rfc8785".to_string(),
artifacts: vec![],
last_receipt_path: "receipts/requirements-20250101_000000.json".to_string(),
effective_config: config1,
lock_drift: None,
pending_fixups: None,
};
let status2 = StatusOutput {
schema_version: "1".to_string(),
emitted_at: fixed_timestamp,
runner: "native".to_string(),
runner_distro: None,
fallback_used: false,
canonicalization_version: "yaml-v1,md-v1".to_string(),
canonicalization_backend: "jcs-rfc8785".to_string(),
artifacts: vec![],
last_receipt_path: "receipts/requirements-20250101_000000.json".to_string(),
effective_config: config2,
lock_drift: None,
pending_fixups: None,
};
let json1 = StatusManager::emit_json(&status1).unwrap();
let json2 = StatusManager::emit_json(&status2).unwrap();
assert_eq!(
json1, json2,
"JCS should produce byte-identical output for StatusOutput"
);
assert!(
json1.contains("\"canonicalization_backend\":\"jcs-rfc8785\""),
"StatusOutput should document JCS backend"
);
}
fn test_doctor_jcs_byte_identity() {
let fixed_timestamp = chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let checks1 = vec![
DoctorCheck {
name: "zebra_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
DoctorCheck {
name: "alpha_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
];
let checks2 = vec![
DoctorCheck {
name: "alpha_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
DoctorCheck {
name: "zebra_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
];
let mut output1 = DoctorOutput {
schema_version: "1".to_string(),
emitted_at: fixed_timestamp,
ok: true,
checks: checks1,
cache_stats: None,
};
let mut output2 = DoctorOutput {
schema_version: "1".to_string(),
emitted_at: fixed_timestamp,
ok: true,
checks: checks2,
cache_stats: None,
};
output1.checks.sort_by(|a, b| a.name.cmp(&b.name));
output2.checks.sort_by(|a, b| a.name.cmp(&b.name));
let json1_value = serde_json::to_value(&output1).unwrap();
let json1_bytes = serde_json_canonicalizer::to_vec(&json1_value).unwrap();
let json1 = String::from_utf8(json1_bytes).unwrap();
let json2_value = serde_json::to_value(&output2).unwrap();
let json2_bytes = serde_json_canonicalizer::to_vec(&json2_value).unwrap();
let json2 = String::from_utf8(json2_bytes).unwrap();
assert_eq!(
json1, json2,
"JCS should produce byte-identical output for DoctorOutput"
);
}
#[test]
fn test_array_sorting_documentation() {
let contracts_path = Path::new("docs/reference/CONTRACTS.md");
assert!(
contracts_path.exists(),
"CONTRACTS.md should exist at docs/reference/CONTRACTS.md"
);
let contracts_content =
fs::read_to_string(contracts_path).expect("Failed to read CONTRACTS.md");
assert!(
contracts_content.contains("Array Ordering")
|| contracts_content.contains("array") && contracts_content.contains("sorted"),
"CONTRACTS.md should document array sorting/ordering"
);
assert!(
contracts_content.contains("outputs") && contracts_content.contains("path"),
"CONTRACTS.md should document that receipt outputs are sorted by path"
);
assert!(
contracts_content.contains("artifacts") && contracts_content.contains("path"),
"CONTRACTS.md should document that status artifacts are sorted by path"
);
assert!(
contracts_content.contains("checks") && contracts_content.contains("name"),
"CONTRACTS.md should document that doctor checks are sorted by name"
);
assert!(
contracts_content.contains("deterministic") || contracts_content.contains("stable"),
"CONTRACTS.md should mention deterministic/stable output from array sorting"
);
test_receipt_outputs_sorted();
test_status_artifacts_sorted();
test_doctor_checks_sorted();
test_receipt_jcs_byte_identity();
test_status_jcs_byte_identity();
test_doctor_jcs_byte_identity();
println!("✓ CONTRACTS.md accurately documents array sorting rules");
println!(
"✓ Documentation matches implementation (outputs by path, artifacts by path, checks by name)"
);
println!("✓ Existing tests enforce sorting");
println!("✓ Byte-identical JCS tests pass");
}
fn test_receipt_outputs_sorted() {
let _temp_dir = xchecker::paths::with_isolated_home();
let base_path = xchecker::paths::xchecker_home()
.join("specs")
.join("test-sorting");
let manager = ReceiptManager::new(&base_path);
let outputs = vec![
FileHash {
path: "artifacts/20-tasks.md".to_string(),
blake3_canonicalized: "cccccccccccccccc".to_string(),
},
FileHash {
path: "artifacts/00-requirements.md".to_string(),
blake3_canonicalized: "aaaaaaaaaaaaaaaa".to_string(),
},
FileHash {
path: "artifacts/10-design.md".to_string(),
blake3_canonicalized: "bbbbbbbbbbbbbbbb".to_string(),
},
];
let packet = PacketEvidence {
files: vec![],
max_bytes: 65536,
max_lines: 1200,
};
let receipt = manager.create_receipt(
"test-sorting",
PhaseId::Requirements,
0,
outputs,
"0.1.0",
"0.8.1",
"haiku",
None,
HashMap::new(),
packet,
None,
None,
vec![],
Some(false),
"native",
None,
None,
None,
None,
None, );
assert_eq!(receipt.outputs.len(), 3);
assert_eq!(receipt.outputs[0].path, "artifacts/00-requirements.md");
assert_eq!(receipt.outputs[1].path, "artifacts/10-design.md");
assert_eq!(receipt.outputs[2].path, "artifacts/20-tasks.md");
}
fn test_status_artifacts_sorted() {
use xchecker::types::ArtifactInfo;
let mut artifacts = [
ArtifactInfo {
path: "artifacts/20-tasks.md".to_string(),
blake3_first8: "cccccccc".to_string(),
},
ArtifactInfo {
path: "artifacts/00-requirements.md".to_string(),
blake3_first8: "aaaaaaaa".to_string(),
},
ArtifactInfo {
path: "artifacts/10-design.md".to_string(),
blake3_first8: "bbbbbbbb".to_string(),
},
];
artifacts.sort_by(|a, b| a.path.cmp(&b.path));
assert_eq!(artifacts[0].path, "artifacts/00-requirements.md");
assert_eq!(artifacts[1].path, "artifacts/10-design.md");
assert_eq!(artifacts[2].path, "artifacts/20-tasks.md");
}
fn test_doctor_checks_sorted() {
let mut checks = [
DoctorCheck {
name: "zebra_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
DoctorCheck {
name: "alpha_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
DoctorCheck {
name: "middle_check".to_string(),
status: CheckStatus::Pass,
details: "test".to_string(),
},
];
checks.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(checks[0].name, "alpha_check");
assert_eq!(checks[1].name, "middle_check");
assert_eq!(checks[2].name, "zebra_check");
}
#[test]
fn test_deprecation_policy() {
let contracts_path = Path::new("docs/reference/CONTRACTS.md");
assert!(
contracts_path.exists(),
"CONTRACTS.md should exist at docs/reference/CONTRACTS.md"
);
let contracts_content =
fs::read_to_string(contracts_path).expect("Failed to read CONTRACTS.md");
assert!(
contracts_content.contains("Deprecation") || contracts_content.contains("deprecation"),
"CONTRACTS.md should document deprecation policy"
);
assert!(
contracts_content.contains("lifecycle") || contracts_content.contains("Lifecycle"),
"CONTRACTS.md should describe schema version lifecycle"
);
assert!(
contracts_content.contains("dual")
|| contracts_content.contains("both") && contracts_content.contains("supported"),
"CONTRACTS.md should mention dual version support during transition"
);
assert!(
contracts_content.contains("6 months") || contracts_content.contains("month"),
"CONTRACTS.md should specify deprecation period duration"
);
assert!(
contracts_content.contains("Breaking") || contracts_content.contains("breaking"),
"CONTRACTS.md should describe breaking changes"
);
assert!(
contracts_content.contains("Additive")
|| contracts_content.contains("additive")
|| contracts_content.contains("optional"),
"CONTRACTS.md should describe additive changes"
);
assert!(
contracts_content.contains("version bump")
|| contracts_content.contains("incrementing"),
"CONTRACTS.md should mention version bumps for breaking changes"
);
assert!(
contracts_content.contains("schema_version"),
"CONTRACTS.md should mention schema_version field"
);
assert!(
contracts_content.contains("ignore unknown")
|| contracts_content.contains("additionalProperties"),
"CONTRACTS.md should describe forward compatibility via ignoring unknown fields"
);
assert!(
contracts_content.contains("backward") || contracts_content.contains("Backward"),
"CONTRACTS.md should describe backward compatibility"
);
let schema_files = vec![
"schemas/receipt.v1.json",
"schemas/status.v1.json",
"schemas/doctor.v1.json",
];
for schema_file in schema_files {
let schema_path = Path::new(schema_file);
assert!(
schema_path.exists(),
"Schema file {schema_file} should exist as documented in CONTRACTS.md"
);
let schema_name = schema_path.file_name().unwrap().to_str().unwrap();
assert!(
contracts_content.contains(schema_name),
"CONTRACTS.md should mention schema file {schema_name}"
);
}
assert!(
contracts_content.contains("docs/schemas/") || contracts_content.contains("example"),
"CONTRACTS.md should mention example payload files"
);
println!("✓ CONTRACTS.md documents deprecation policy");
println!(
"✓ Policy matches implementation approach (schema_version, forward/backward compatibility)"
);
println!("✓ All documented schema files exist");
}
}