From b8ccbe4c22e6614e0642e396e55cac384badba78 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 16 Sep 2026 15:19:24 +0530 Subject: [PATCH 1/2] Move the inline tests out of tally/runtime.rs and tally/connection.rs docs/proposed-rust-module-conventions.md item 2: extract inline test modules first -- mechanical, no production change. These were the two largest remaining inline test blocks among pinned production files: 1,951 lines in runtime.rs and 1,848 in connection.rs. Production lines are unchanged; the files drop to 3,490 and 2,024 lines. Each block moves to _tests.rs beside its parent, kept as the same child module (`#[cfg(test)] #[path = "..."] mod tests;`), so private access and test paths are unchanged, and relative include_str!/include_bytes! paths still resolve from the same directory. Checked, not asserted: - The moved bodies differ from the removed blocks only by four columns of indentation, compared against HEAD line by line. The eight multi-line string literals were left byte-identical rather than dedented, since their whitespace can be test data. - rustfmt then re-wrapped lines using the freed columns. Its only non-whitespace changes are four trailing commas; all 1,057 string literals are identical before and after. - Test names are identical before and after: 99 under default features, 110 with voucher-scan, which gates 11 of them. All pass under both. - cargo clippy --lib --tests -D warnings is clean. - Resealing changed exactly two pinned hashes, and the #436 coverage report counts exactly two newly unsealed test-only modules, listing nothing else. Unpinned test modules are the repository norm. Co-Authored-By: Claude Opus 5 --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 6 +- src-tauri/src/tally/connection.rs | 1852 +--------------- src-tauri/src/tally/connection_tests.rs | 1836 ++++++++++++++++ src-tauri/src/tally/runtime.rs | 1955 +---------------- src-tauri/src/tally/runtime_tests.rs | 1940 ++++++++++++++++ 6 files changed, 3784 insertions(+), 3807 deletions(-) create mode 100644 src-tauri/src/tally/connection_tests.rs create mode 100644 src-tauri/src/tally/runtime_tests.rs diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 61115c08..0728fc12 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "368223b42559082724513067a7133a94fa4983fdb110ecb18fb57c45b44d51b3", + "compatibility_surface_sha256": "d1f7d3d265c7bda1f97b03db15220fd8de9d195cfd7cf34c0466e879cd580db6", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index bcb5a265..6a3f44ec 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -731,7 +731,7 @@ }, { "path": "src-tauri/src/tally/connection.rs", - "sha256": "76abe63f9b2e8b1fdea9b8b98d818d5e03195f0c50fcfb67d0d6b8fc01e7059b" + "sha256": "b09660a80abafdf50f9e64323dec62a4004109f2bc8b8ea959b8544ce8832ab5" }, { "path": "src-tauri/src/tally/connector.rs", @@ -747,7 +747,7 @@ }, { "path": "src-tauri/src/tally/runtime.rs", - "sha256": "9f1e388637760ddb0aa5c4de884c9f9dbc66e5714ace4cdd6bdbd2949888a81b" + "sha256": "dd8794f1bdb1fbb2b66b21c53e53fe2823ff2a29b00b59ccf1ef2748c58f0101" }, { "path": "src-tauri/src/tally/runtime_control.rs", @@ -958,5 +958,5 @@ "sha256": "a8ac2714fecf51947f2822c8c46d7ce2e8602c732780ff60566a7771f0836f9a" } ], - "manifest_sha256": "368223b42559082724513067a7133a94fa4983fdb110ecb18fb57c45b44d51b3" + "manifest_sha256": "d1f7d3d265c7bda1f97b03db15220fd8de9d195cfd7cf34c0466e879cd580db6" } \ No newline at end of file diff --git a/src-tauri/src/tally/connection.rs b/src-tauri/src/tally/connection.rs index 464aef99..62fe351d 100644 --- a/src-tauri/src/tally/connection.rs +++ b/src-tauri/src/tally/connection.rs @@ -2020,1853 +2020,5 @@ fn detect_product(text: &str) -> TallyProduct { } #[cfg(test)] -mod tests { - #[tokio::test] - async fn capability_probe_preserves_captured_release_and_exclusive_license_tier() { - let capture = include_bytes!("../../crates/bridge-tally-protocol/tests/fixtures/agent/native-licensed-release-companies.utf16le.xml"); - let xml = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( - capture, - "text/xml; charset=utf-16", - bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, - capture.len(), - ) - .unwrap() - .text; - let observation = super::parse_company_gateway_capability_observation(&xml).unwrap(); - for (education, silver, gold, tier) in [ - (false, true, false, Some(super::LicenseTier::Silver)), - (false, false, true, Some(super::LicenseTier::Gold)), - (false, true, true, None), - (false, false, false, None), - (true, true, false, None), - ] { - let mut altered = observation.clone(); - altered.educational_mode = education; - altered.silver = silver; - altered.gold = gold; - assert_eq!( - super::GatewayProductModeEvidence::from_observation(altered).license_tier, - tier - ); - } - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = listener.local_addr().unwrap(); - let server = tokio::spawn(async move { - for response in [ - utf8_status_response("Unknown status banner"), - utf16_xml_response(&xml), - ] { - let (mut socket, _) = listener.accept().await.unwrap(); - let request = read_complete_http_request(&mut socket).await; - assert!(!request.is_empty()); - socket.write_all(&response).await.unwrap(); - } - }); - let (probe, wire) = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .unwrap() - .probe_with_wire_evidence() - .await - .unwrap(); - server.await.unwrap(); - assert!(wire.bytes >= capture.len()); - assert_eq!(probe.companies.len(), 16); - assert_eq!(probe.profile.profile_version, 4); - assert_eq!(probe.profile.product, "TallyPrime"); - assert_eq!(probe.profile.mode.as_deref(), Some("Licensed")); - assert_eq!(probe.profile.release.as_deref(), Some("7.1")); - assert_eq!(probe.profile.license_tier, Some(super::LicenseTier::Silver)); - assert_eq!( - probe.profile.transports[&TransportId::JsonEx].state, - CapabilityState::Unknown - ); - assert_eq!( - probe.profile.transports[&TransportId::JsonEx] - .safe_reason_code - .as_deref(), - Some("transport_not_probed") - ); - let mut old = serde_json::to_value(&probe.profile).unwrap(); - old.as_object_mut().unwrap().remove("license_tier"); - old["profile_version"] = serde_json::json!(3); - let old: bridge_tally_core::CapabilityProfile = serde_json::from_value(old).unwrap(); - assert_eq!(old.license_tier, None); - assert_ne!(old.profile_version, probe.profile.profile_version); - } - - #[test] - fn party_ledger_commitment_hashes_the_three_encoded_builder_requests() { - let master = NativeLedgerExportPeriod::new( - DateBoundaryProfile::ModeAgnostic, - TallyDate::parse("20260401").unwrap(), - TallyDate::parse("20260731").unwrap(), - ) - .unwrap(); - let balance = party_ledger_master_balance_period( - DateBoundaryProfile::ModeAgnostic, - TallyDate::parse("20260401").unwrap(), - TallyDate::parse("20260731").unwrap(), - ) - .unwrap(); - let requests = [ - super::render_party_ledger_master_request("Synthetic ₹ Books", &master), - super::render_native_ledger_snapshot_request("Synthetic ₹ Books", &balance), - super::render_native_group_snapshot_request("Synthetic ₹ Books"), - ]; - let hashes = requests - .iter() - .map(|request| { - let mut bytes = vec![0xff, 0xfe]; - bytes.extend(request.encode_utf16().flat_map(u16::to_le_bytes)); - super::sha256_hex(&bytes) - }) - .collect::>(); - let expected = super::sha256_hex(hashes.join(":").as_bytes()); - assert_eq!(super::party_ledger_request_commitment(&requests), expected); - for index in 0..3 { - let mut changed = requests.clone(); - changed[index].push(' '); - assert_ne!(super::party_ledger_request_commitment(&changed), expected); - } - } - - #[cfg(feature = "voucher-scan")] - use super::LedgerOpeningCoverageRead; - use super::{ - canonical_loopback_origin, decode_xml_bytes, detect_product, - has_presentation_equivalent_guid_siblings, normalize_discovered_companies, - party_ledger_master_balance_period, party_ledger_master_openings_agree, tally_endpoint, - unique_company_identities, TallyClient, TallyConfig, TallyProduct, VerifiedCompanyIdentity, - }; - use bridge_tally_core::{ - CapabilityFeatureId, CapabilityPackId, CapabilityState, EvidenceConfidence, TallyDate, - TransportId, - }; - use bridge_tally_protocol::native_outstandings::NativeLedgerExportPeriod; - use bridge_tally_protocol::outstandings_shared::DateBoundaryProfile; - use std::time::Duration; - use tally_protocol_simulator::{Fixture, ScenarioPlan, Simulator, WireEncoding}; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use tokio::net::TcpListener; - - const COMPANY_EXTENT_V2: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/agent/native-company-book-extents-with-number.utf8.xml" - ); - const CAPTURED_COMPANY_GUID: &str = "bb8ad19e-6aef-4239-a917-87fec0c6215e"; - - fn identity_from_company_extent(xml: &str) -> VerifiedCompanyIdentity { - let companies = bridge_tally_protocol::parse_companies_from_collection(xml) - .expect("captured company extent must parse as a Company collection"); - let company = companies - .iter() - .find(|company| company.guid.as_deref() == Some(CAPTURED_COMPANY_GUID)) - .expect("captured target company must remain present"); - VerifiedCompanyIdentity::from_observed_companies( - company.name.clone(), - company.guid.clone().expect("captured target company GUID"), - company - .company_number - .clone() - .expect("captured target company number"), - company - .books_from - .clone() - .expect("captured target company books-from"), - &companies, - ) - .expect("captured target company tuple must remain uniquely admissible") - } - - fn captured_company_identity() -> VerifiedCompanyIdentity { - identity_from_company_extent(COMPANY_EXTENT_V2) - } - - #[test] - fn party_master_snapshot_uses_the_next_common_admissible_boundary() { - let period = party_ledger_master_balance_period( - DateBoundaryProfile::ModeAgnostic, - TallyDate::parse("20260401").unwrap(), - TallyDate::parse("20260415").unwrap(), - ) - .expect("the derived common boundary is valid"); - assert_eq!(period.to().as_str(), "20260501"); - assert!(period.to() >= &TallyDate::parse("20260415").unwrap()); - } - - #[test] - fn party_master_opening_balance_comparison_accepts_equivalent_decimal_scale() { - assert!(party_ledger_master_openings_agree( - "0", - &bridge_tally_core::ExactDecimal::parse("0.00").unwrap(), - ) - .unwrap()); - } - - #[test] - fn party_master_opening_balance_comparison_withholds_a_real_numeric_mismatch() { - assert!(!party_ledger_master_openings_agree( - "0.01", - &bridge_tally_core::ExactDecimal::parse("0.00").unwrap(), - ) - .unwrap()); - } - - #[test] - fn party_master_opening_balance_comparison_rejects_an_unparseable_master_value() { - assert!(party_ledger_master_openings_agree( - "not-an-amount", - &bridge_tally_core::ExactDecimal::parse("0.00").unwrap(), - ) - .is_err()); - } - - fn utf16_xml_response(body: impl AsRef) -> Vec { - let body = bridge_tally_protocol::encode_tally_xml_request_utf16le(body.as_ref()); - let headers = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-16\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - body.len() - ); - [headers.as_bytes(), &body].concat() - } - - fn utf8_status_response(body: impl AsRef) -> Vec { - let body = body.as_ref().as_bytes(); - let headers = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - body.len() - ); - [headers.as_bytes(), body].concat() - } - - async fn read_complete_http_request( - socket: &mut (impl tokio::io::AsyncRead + Unpin), - ) -> Vec { - const HEADER_TERMINATOR: &[u8] = b"\r\n\r\n"; - const MAX_TEST_HEADER_BYTES: usize = 64 * 1024; - - let mut request = Vec::new(); - let mut chunk = [0_u8; 4096]; - let header_end = loop { - if let Some(offset) = request - .windows(HEADER_TERMINATOR.len()) - .position(|window| window == HEADER_TERMINATOR) - { - break offset + HEADER_TERMINATOR.len(); - } - let bytes_read = socket - .read(&mut chunk) - .await - .expect("read synthetic HTTP request headers"); - assert!( - bytes_read > 0, - "synthetic HTTP request ended before its headers were complete" - ); - request.extend_from_slice(&chunk[..bytes_read]); - assert!( - request.len() <= MAX_TEST_HEADER_BYTES, - "synthetic HTTP request headers exceeded {MAX_TEST_HEADER_BYTES} bytes" - ); - }; - - let headers = std::str::from_utf8(&request[..header_end - HEADER_TERMINATOR.len()]) - .expect("synthetic HTTP request headers must be UTF-8"); - let mut lines = headers.split("\r\n"); - let request_line = lines - .next() - .expect("synthetic HTTP request must contain a request line"); - let mut request_line_parts = request_line.split_ascii_whitespace(); - let method = request_line_parts - .next() - .expect("synthetic HTTP request method is missing"); - request_line_parts - .next() - .expect("synthetic HTTP request target is missing"); - let version = request_line_parts - .next() - .expect("synthetic HTTP request version is missing"); - assert_eq!(version, "HTTP/1.1", "synthetic request must use HTTP/1.1"); - assert!( - request_line_parts.next().is_none(), - "synthetic HTTP request line has unexpected fields" - ); - - let mut content_length = None; - for header in lines { - let (name, value) = header - .split_once(':') - .expect("synthetic HTTP request contains a malformed header"); - assert!( - !name.eq_ignore_ascii_case("transfer-encoding"), - "synthetic HTTP request must use Content-Length framing" - ); - if name.eq_ignore_ascii_case("content-length") { - assert!( - content_length.is_none(), - "synthetic HTTP request contains duplicate Content-Length headers" - ); - content_length = Some( - value - .trim() - .parse::() - .expect("synthetic HTTP request has an invalid Content-Length"), - ); - } - } - let content_length = match content_length { - Some(length) => length, - None if method == "GET" || method == "HEAD" => 0, - None => panic!("synthetic HTTP request body is missing Content-Length"), - }; - let request_end = header_end - .checked_add(content_length) - .expect("synthetic HTTP request length overflowed usize"); - assert!( - request.len() <= request_end, - "synthetic HTTP request contains bytes beyond Content-Length" - ); - - while request.len() < request_end { - let remaining = request_end - request.len(); - let chunk_length = remaining.min(chunk.len()); - let bytes_read = socket - .read(&mut chunk[..chunk_length]) - .await - .expect("read synthetic HTTP request body"); - assert!( - bytes_read > 0, - "synthetic HTTP request ended before its Content-Length body was complete" - ); - request.extend_from_slice(&chunk[..bytes_read]); - } - - request - } - - fn assert_company_collection_request_shape(request: &str) { - assert!(request.contains("Collection")); - for method in ["NAME", "GUID", "PRODUCTNAME"] { - assert!(request.contains(&format!("{method}"))); - } - for expression in [ - "EduMode : $$LicenseInfo:IsEducationalMode", - "Silver : $$LicenseInfo:IsSilver", - "Gold : $$LicenseInfo:IsGold", - ] { - assert!(request.contains(expression)); - } - } - - #[test] - fn detects_tallyprime_status() { - assert!(matches!( - detect_product("TallyPrime Server is Running"), - TallyProduct::TallyPrime - )); - } - - #[test] - fn detects_erp9_status() { - assert!(matches!( - detect_product("Tally ERP 9 Server is Running"), - TallyProduct::TallyErp9 - )); - } - - #[test] - fn product_marker_is_not_accepted_inside_unrelated_content() { - assert!(matches!( - detect_product("TallyPrime Server is Running"), - TallyProduct::Unknown - )); - assert!(matches!( - detect_product("prefix Tally ERP 9 Server is Running suffix"), - TallyProduct::Unknown - )); - } - - #[tokio::test] - async fn selected_read_request_digest_matches_the_dispatched_wire_entity() { - let plan = ScenarioPlan::new(Fixture::NormalExport).with_encoding(WireEncoding::Utf16Le); - let simulator = Simulator::spawn(plan).expect("spawn synthetic Tally endpoint"); - let client = TallyClient::new(TallyConfig { - host: simulator.address().ip().to_string(), - port: simulator.address().port(), - }) - .expect("build synthetic Tally client"); - - let observation = client - .qualify_selected_ledgers( - "BRIDGE SYNTHETIC BOOK", - "00000000-0000-4000-8000-000000000001", - ) - .await - .expect("qualify synthetic selected-ledger read"); - let dispatched = simulator.finish().expect("finish synthetic Tally exchange"); - - assert_eq!(observation.request_sha256, dispatched.request_body_sha256); - } - - #[test] - fn company_identity_normalization_rejects_invalid_and_ambiguous_guids() { - let normalized = normalize_discovered_companies(vec![ - crate::tally::TallyCompany { - name: " Synthetic A ".to_string(), - guid: Some(" GUID-1 ".to_string()), - company_number: Some("100005".to_string()), - books_from: Some("20250401".to_string()), - }, - crate::tally::TallyCompany { - name: "Synthetic B".to_string(), - guid: Some("guid-1".to_string()), - company_number: Some("100014".to_string()), - books_from: Some("20260401".to_string()), - }, - ]) - .expect("normalize company identities"); - assert_eq!(normalized[0].name, "Synthetic A"); - assert_eq!(normalized[0].guid.as_deref(), Some("GUID-1")); - assert!(unique_company_identities(&normalized)); - assert!(!has_presentation_equivalent_guid_siblings(&normalized)); - - assert!( - normalize_discovered_companies(vec![crate::tally::TallyCompany { - name: "Synthetic\nCompany".to_string(), - guid: Some("guid-2".to_string()), - company_number: None, - books_from: None, - }]) - .is_err() - ); - assert!( - normalize_discovered_companies(vec![crate::tally::TallyCompany { - name: "Synthetic Company".to_string(), - guid: Some("guid\n2".to_string()), - company_number: None, - books_from: None, - }]) - .is_err() - ); - } - - #[test] - fn presentation_equivalent_guid_siblings_are_not_stable_company_identities() { - let companies = vec![ - crate::tally::TallyCompany { - name: "Synthetic Company".to_string(), - guid: Some("guid-3".to_string()), - company_number: Some("100005".to_string()), - books_from: Some("20250401".to_string()), - }, - crate::tally::TallyCompany { - name: " synthetic company ".to_string(), - guid: Some("GUID-3".to_string()), - company_number: Some("100014".to_string()), - books_from: Some("20260401".to_string()), - }, - ]; - - assert!(unique_company_identities(&companies)); - assert!(has_presentation_equivalent_guid_siblings(&companies)); - } - - #[test] - fn identical_name_same_guid_distinct_books_are_not_stable_company_identities() { - let companies = vec![ - crate::tally::TallyCompany { - name: "Synthetic Company".to_string(), - guid: Some("guid-3".to_string()), - company_number: Some("100005".to_string()), - books_from: Some("20250401".to_string()), - }, - crate::tally::TallyCompany { - name: "Synthetic Company".to_string(), - guid: Some("GUID-3".to_string()), - company_number: Some("100014".to_string()), - books_from: Some("20260401".to_string()), - }, - ]; - - assert!(unique_company_identities(&companies)); - assert!(has_presentation_equivalent_guid_siblings(&companies)); - } - - #[test] - fn validates_tally_endpoint_components() { - assert_eq!( - tally_endpoint(&TallyConfig::default(), "/status") - .expect("localhost endpoint") - .as_str(), - "http://127.0.0.1:9000/status" - ); - let config = TallyConfig { - host: "::1".to_string(), - port: 9000, - }; - assert_eq!( - tally_endpoint(&config, "/status") - .expect("IPv6 endpoint") - .as_str(), - "http://[::1]:9000/status" - ); - for host in ["localhost", "127.0.0.1"] { - assert_eq!( - canonical_loopback_origin(&TallyConfig { - host: host.to_string(), - port: 9000, - }) - .expect("canonical loopback origin"), - "http://127.0.0.1:9000" - ); - } - assert_eq!( - canonical_loopback_origin(&TallyConfig { - host: "::1".to_string(), - port: 9000, - }) - .expect("canonical IPv6 loopback origin"), - "http://[::1]:9000" - ); - - for host in ["http://localhost", "localhost/path", "user@localhost", ""] { - let invalid = TallyConfig { - host: host.to_string(), - port: 9000, - }; - assert!(tally_endpoint(&invalid, "/status").is_err()); - } - - for host in [ - "192.168.1.10", - "10.0.0.5", - "169.254.1.1", - "224.0.0.1", - "8.8.8.8", - "tally.internal", - ] { - let remote = TallyConfig { - host: host.to_string(), - port: 9000, - }; - assert!(tally_endpoint(&remote, "/status").is_err()); - } - } - - #[test] - fn decodes_supported_xml_byte_order_marks_and_rejects_invalid_sequences() { - let utf8 = [b"\xEF\xBB\xBF".as_slice(), b""].concat(); - assert_eq!(decode_xml_bytes(utf8).expect("UTF-8 BOM"), ""); - - let document = "नमस्ते"; - let mut utf16le = vec![0xFF, 0xFE]; - utf16le.extend(document.encode_utf16().flat_map(u16::to_le_bytes)); - assert_eq!(decode_xml_bytes(utf16le).expect("UTF-16LE"), document); - - let mut utf16be = vec![0xFE, 0xFF]; - utf16be.extend(document.encode_utf16().flat_map(u16::to_be_bytes)); - assert_eq!(decode_xml_bytes(utf16be).expect("UTF-16BE"), document); - - assert!(decode_xml_bytes(vec![0xFF, 0xFE, 0x00]).is_err()); - assert!(decode_xml_bytes(vec![0x80]).is_err()); - } - - #[tokio::test] - async fn tally_requests_ignore_configured_proxy() { - let tally_listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let tally_address = tally_listener.local_addr().expect("Tally address"); - let proxy_listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic proxy"); - let proxy_address = proxy_listener.local_addr().expect("proxy address"); - - let tally_server = tokio::spawn(async move { - let accepted = tokio::time::timeout(Duration::from_secs(2), tally_listener.accept()) - .await - .expect("Tally request timed out") - .expect("accept Tally request"); - let (mut socket, _) = accepted; - let request = read_complete_http_request(&mut socket).await; - assert!( - String::from_utf8_lossy(&request).starts_with("GET /status HTTP/1.1"), - "request should go directly to the Tally endpoint" - ); - let body = "TallyPrime Server is Running"; - socket - .write_all(&utf8_status_response(body)) - .await - .expect("write Tally response"); - }); - - let proxy_server = tokio::spawn(async move { - match tokio::time::timeout(Duration::from_millis(750), proxy_listener.accept()).await { - Ok(Ok((mut socket, _))) => { - let response = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - socket - .write_all(response.as_bytes()) - .await - .expect("write proxy response"); - true - } - Ok(Err(error)) => panic!("accept proxy request: {error}"), - Err(_) => false, - } - }); - - let client = TallyClient::with_http_builder( - TallyConfig { - host: tally_address.ip().to_string(), - port: tally_address.port(), - }, - reqwest::Client::builder().proxy( - reqwest::Proxy::all(format!("http://{proxy_address}")) - .expect("synthetic proxy URL"), - ), - ); - - let status = client - .check_connection() - .await - .expect("check synthetic Tally connection"); - tally_server.await.expect("synthetic Tally server task"); - let proxy_received_request = proxy_server.await.expect("synthetic proxy task"); - - assert!(status.reachable, "direct Tally response should be accepted"); - assert!( - status.compatible, - "synthetic Tally status should be recognized" - ); - assert!( - !proxy_received_request, - "Tally traffic must never be sent through a configured proxy" - ); - } - - #[cfg(feature = "voucher-scan")] - #[tokio::test] - async fn paired_outstandings_reads_health_check_between_and_after_requests() { - const OPTIONAL_VOUCHERS: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/unit_a_optional_voucher_live.xml" - ); - const STATUS: &str = "TallyPrime Server is Running"; - let identity = captured_company_identity(); - - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - for (index, expected_prefix) in [ - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - "POST / HTTP/1.1", - "GET /status HTTP/1.1", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = - tokio::time::timeout(Duration::from_secs(2), listener.accept()) - .await - .expect("paired request timed out") - .expect("accept paired request"); - let request = read_complete_http_request(&mut socket).await; - assert!( - String::from_utf8_lossy(&request).starts_with(expected_prefix), - "request {index} did not follow the required read/health-check sequence" - ); - let body = match index { - 0 | 2 | 12 | 14 => COMPANY_EXTENT_V2, - 4 | 6 | 8 | 10 => OPTIONAL_VOUCHERS, - _ => STATUS, - }; - let response = if index % 2 == 0 { - utf16_xml_response(body) - } else { - utf8_status_response(body) - }; - socket - .write_all(&response) - .await - .expect("write paired response"); - } - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let extent = client - .fetch_company_book_extent(&identity) - .await - .expect("paired extent reads remain stable"); - let reporting_window = bridge_tally_protocol::outstandings::DateWindow::parse( - bridge_tally_protocol::outstandings::DateBoundaryProfile::ModeAgnostic, - "20260401", - "20260401", - ) - .expect("synthetic one-day window"); - let segment_window = reporting_window - .narrow_partitions() - .expect("one narrow partition") - .remove(0); - let observation = client - .fetch_outstandings_segment_pair( - extent.company(), - segment_window.clone(), - bridge_tally_protocol::outstandings::AlterIdRange::new(0, 101603) - .expect("non-empty synthetic range"), - ) - .await - .expect("paired segment reads remain stable"); - let verification = observation.verification; - let bridge_tally_protocol::outstandings::SegmentVerification::Complete(segment) = - verification - else { - panic!("captured voucher response did not verify: {verification:?}"); - }; - assert!( - !segment.vouchers().is_empty(), - "captured voucher fixture unexpectedly had no vouchers" - ); - let witness = client - .fetch_empty_partition_witness_pair(extent.company(), segment_window) - .await - .expect("paired empty-date witness reads remain stable"); - let bridge_tally_protocol::outstandings::WitnessPairVerification::Complete(witness) = - witness - else { - panic!("captured witness fixture did not verify: {witness:?}"); - }; - assert!( - !witness.vouchers().is_empty(), - "captured witness fixture unexpectedly had no identity rows" - ); - let closing_extent = client - .fetch_company_book_extent(&identity) - .await - .expect("closing paired extent reads remain stable"); - assert_eq!(closing_extent, extent, "synthetic book did not change"); - assert_eq!( - client.observed_body_bytes(), - Some( - u64::try_from( - bridge_tally_protocol::encode_tally_xml_request_utf16le(OPTIONAL_VOUCHERS) - .len(), - ) - .expect("encoded fixture length fits u64"), - ), - "closing extent evidence must not overwrite the larger voucher payload" - ); - server.await.expect("synthetic Tally server task"); - } - - #[cfg(feature = "voucher-scan")] - #[tokio::test] - async fn paired_ledger_opening_coverage_reports_intra_pair_drift() { - const STATUS: &str = "TallyPrime Server is Running"; - let identity = captured_company_identity(); - - let coverage = |name: &str| { - format!( - "
1
bb8ad19e-6aef-4239-a917-87fec0c6215e-00000001Yes0
" - ) - }; - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let responses = vec![ - COMPANY_EXTENT_V2.to_string(), - STATUS.to_string(), - COMPANY_EXTENT_V2.to_string(), - STATUS.to_string(), - coverage("Before Rename"), - STATUS.to_string(), - coverage("After Rename"), - STATUS.to_string(), - ]; - let server = tokio::spawn(async move { - for (index, body) in responses.into_iter().enumerate() { - let (mut socket, _) = - tokio::time::timeout(Duration::from_secs(2), listener.accept()) - .await - .expect("paired request timed out") - .expect("accept paired request"); - let request = read_complete_http_request(&mut socket).await; - let expected_prefix = if index % 2 == 0 { - "POST / HTTP/1.1" - } else { - "GET /status HTTP/1.1" - }; - assert!( - String::from_utf8_lossy(&request).starts_with(expected_prefix), - "request {index} did not follow the required read/health-check sequence" - ); - let response = if index % 2 == 0 { - utf16_xml_response(body) - } else { - utf8_status_response(body) - }; - socket - .write_all(&response) - .await - .expect("write paired response"); - } - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let extent = client - .fetch_company_book_extent(&identity) - .await - .expect("paired extent reads remain stable"); - assert!(matches!( - client - .fetch_ledger_opening_coverage(extent.company()) - .await - .expect("coverage responses parse"), - LedgerOpeningCoverageRead::Drifted - )); - server.await.expect("synthetic Tally server task"); - } - - /// The outstandings bracket (`fetch_company_book_extent`, feeding both - /// `fetch_outstandings_native` and `fetch_ledgers`) must fail closed with - /// a typed error when both paired reads agree but neither carries - /// `ALTMSTID`. This is the exact case the review flagged: two - /// witness-less extents compare equal, so the ordinary `first != second` - /// drift check alone cannot tell a stable book from one where a - /// GROUP/LEDGER master moved mid-window without a signal to detect it. - /// Uses only a targeted mutation of the captured V2 response. The - /// production parser must continue refusing a stable pair without the - /// master witness instead of accepting a fabricated positive fixture. - #[tokio::test] - async fn outstandings_bracket_fails_closed_when_altmstid_is_absent() { - const STATUS: &str = "TallyPrime Server is Running"; - const CAPTURED_TARGET_WITNESS: &str = concat!( - "bb8ad19e-6aef-4239-a917-87fec0c6215e\r\n", - " 100000\r\n", - " 101605\r\n", - " 328" - ); - const CAPTURED_TARGET_WITHOUT_WITNESS: &str = concat!( - "bb8ad19e-6aef-4239-a917-87fec0c6215e\r\n", - " 100000\r\n", - " 101605" - ); - let identity = captured_company_identity(); - assert_eq!( - COMPANY_EXTENT_V2.matches(CAPTURED_TARGET_WITNESS).count(), - 1, - "the exact captured target witness must appear once before mutation" - ); - let company_extent_without_altmstid = - COMPANY_EXTENT_V2.replacen(CAPTURED_TARGET_WITNESS, CAPTURED_TARGET_WITHOUT_WITNESS, 1); - assert!( - !company_extent_without_altmstid.contains(CAPTURED_TARGET_WITNESS), - "the captured target witness mutation must apply" - ); - let parsed_without_witness = - bridge_tally_protocol::outstandings_shared::parse_company_book_extent_v2( - &company_extent_without_altmstid, - &identity - .company_book_extent_expectation() - .expect("captured expectation"), - ) - .expect("the captured negative mutation must remain a parseable extent"); - assert!( - parsed_without_witness - .master_alter_id_high_water() - .is_none(), - "the target witness mutation must remove only the target ALTMSTID" - ); - - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let responses = [ - company_extent_without_altmstid.clone(), - STATUS.to_string(), - company_extent_without_altmstid, - STATUS.to_string(), - ]; - let server = tokio::spawn(async move { - for (index, body) in responses.into_iter().enumerate() { - let (mut socket, _) = - tokio::time::timeout(Duration::from_secs(2), listener.accept()) - .await - .expect("paired request timed out") - .expect("accept paired request"); - let request = read_complete_http_request(&mut socket).await; - let expected_prefix = if index % 2 == 0 { - "POST / HTTP/1.1" - } else { - "GET /status HTTP/1.1" - }; - assert!( - String::from_utf8_lossy(&request).starts_with(expected_prefix), - "request {index} did not follow the required read/health-check sequence" - ); - let response = if index % 2 == 0 { - utf16_xml_response(&body) - } else { - utf8_status_response(&body) - }; - socket - .write_all(&response) - .await - .expect("write paired response"); - } - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let error = client - .fetch_company_book_extent(&identity) - .await - .expect_err("a stable pair without ALTMSTID must still fail closed"); - server.await.expect("synthetic Tally server task"); - assert!( - error - .downcast_ref::() - .is_some_and(|error| { - *error - == bridge_tally_protocol::outstandings_shared::OutstandingsError::MasterWitnessAbsent - }), - "unexpected error: {error:#}" - ); - } - - #[tokio::test] - async fn http_success_with_tally_status_zero_is_not_an_empty_success() { - let identity = captured_company_identity(); - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - for (index, body) in [ - COMPANY_EXTENT_V2, - "TallyPrime Server is Running", - COMPANY_EXTENT_V2, - "TallyPrime Server is Running", - "
0
", - "TallyPrime Server is Running", - "
0
", - "TallyPrime Server is Running", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - let expected = if index == 1 || index == 3 || index == 5 || index == 7 { - "GET /status HTTP/1.1" - } else { - "POST / HTTP/1.1" - }; - assert!( - String::from_utf8_lossy(&request).starts_with(expected), - "ledger fetch did not preserve its extent/read sequence" - ); - if index == 4 || index == 6 { - let body_start = request - .windows(4) - .position(|window| window == b"\r\n\r\n") - .map(|offset| offset + 4) - .expect("native ledger POST has complete HTTP headers"); - let request_xml = bridge_tally_protocol::decode_tally_text_bytes_limited( - &request[body_start..], - request.len(), - ) - .expect("native ledger POST uses decodable UTF-16 XML") - .text; - assert!( - request_xml.contains(r#"20240401"#) - ); - assert!(request_xml.contains(r#"20260401"#)); - } - let response = if index == 1 || index == 3 || index == 5 || index == 7 { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write Tally response"); - } - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let error = client - .fetch_ledgers(&identity, DateBoundaryProfile::ModeAgnostic) - .await - .expect_err("STATUS 0 must not become an empty ledger result"); - server.await.expect("synthetic Tally server task"); - assert!( - error - .to_string() - .contains("native ledger collection did not report success"), - "unexpected error: {error:#}" - ); - } - - #[tokio::test] - async fn invalid_book_extent_stops_ledger_export_without_a_date_fallback() { - let identity = captured_company_identity(); - let invalid_extent = - COMPANY_EXTENT_V2.replacen(r#"20240401"#, "", 1); - assert_ne!(invalid_extent, COMPANY_EXTENT_V2); - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - for (index, body) in [ - invalid_extent.as_str(), - "TallyPrime Server is Running", - invalid_extent.as_str(), - "TallyPrime Server is Running", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept extent request"); - let request = read_complete_http_request(&mut socket).await; - let expected = if index % 2 == 0 { - "POST / HTTP/1.1" - } else { - "GET /status HTTP/1.1" - }; - assert!( - String::from_utf8_lossy(&request).starts_with(expected), - "request {index} did not follow the paired extent sequence" - ); - let response = if index % 2 == 0 { - utf16_xml_response(body) - } else { - utf8_status_response(body) - }; - socket - .write_all(&response) - .await - .expect("write extent response"); - } - assert!( - tokio::time::timeout(Duration::from_millis(200), listener.accept()) - .await - .is_err(), - "an invalid extent must stop before any native ledger request" - ); - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - client - .fetch_ledgers(&identity, DateBoundaryProfile::ModeAgnostic) - .await - .expect_err("missing BOOKSFROM must fail closed"); - server.await.expect("synthetic Tally server task"); - } - - #[tokio::test] - async fn education_profile_rejects_an_unsupported_books_from_before_ledger_export() { - let extent = COMPANY_EXTENT_V2.replacen( - r#"20240401"#, - r#"20240115"#, - 1, - ); - let identity = identity_from_company_extent(&extent); - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - for (index, body) in [ - extent.as_str(), - "TallyPrime Server is Running", - extent.as_str(), - "TallyPrime Server is Running", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept extent request"); - let request = read_complete_http_request(&mut socket).await; - let expected = if index % 2 == 0 { - "POST / HTTP/1.1" - } else { - "GET /status HTTP/1.1" - }; - assert!( - String::from_utf8_lossy(&request).starts_with(expected), - "request {index} did not follow the paired extent sequence" - ); - let response = if index % 2 == 0 { - utf16_xml_response(body) - } else { - utf8_status_response(body) - }; - socket - .write_all(&response) - .await - .expect("write extent response"); - } - assert!( - tokio::time::timeout(Duration::from_millis(200), listener.accept()) - .await - .is_err(), - "an Education-invalid BOOKSFROM must stop before the native ledger request" - ); - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let error = client - .fetch_ledgers(&identity, DateBoundaryProfile::EducationRestricted) - .await - .expect_err("unsupported boundary must not reach the ledger export"); - server.await.expect("synthetic Tally server task"); - assert!( - error - .to_string() - .contains("not supported by the endpoint compatibility profile"), - "unexpected error: {error:#}" - ); - } - - fn native_voucher_collection_xml(rows: &[&str]) -> String { - format!( - "
1
{}
", - rows.join("") - ) - } - - const SYNTHETIC_VOUCHER_ROW: &str = r#"20260401synthetic-company-guid-0000000111PaymentNoNoCash-100.00Yes"#; - - /// A native Voucher collection carries no envelope company GUID, and a - /// zero-row response has no per-row GUID either -- there is nothing for - /// `parse_native_voucher_source_records_with_evidence` to bind. Before - /// the fix, `fetch_vouchers` accepted such a response unauthenticated, - /// so a silently substituted or dropped company looked identical to a - /// genuinely empty window. This reproduces that: the voucher read comes - /// back empty, and the out-of-band book-extent bracket that should - /// confirm the pinned company instead observes a different one. - #[tokio::test] - async fn empty_voucher_response_is_rejected_when_pinned_company_cannot_be_confirmed() { - let identity = captured_company_identity(); - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let empty_vouchers = native_voucher_collection_xml(&[]); - let substituted_extent = - COMPANY_EXTENT_V2.replacen(CAPTURED_COMPANY_GUID, "substituted-company-guid", 1); - let status = "TallyPrime Server is Running"; - let steps: Vec<(&str, String, bool)> = vec![ - ("POST / HTTP/1.1", empty_vouchers, false), - ("POST / HTTP/1.1", substituted_extent.clone(), false), - ("GET /status HTTP/1.1", status.to_string(), true), - ("POST / HTTP/1.1", substituted_extent, false), - ("GET /status HTTP/1.1", status.to_string(), true), - ]; - let server = tokio::spawn(async move { - for (index, (expected_prefix, body, is_status)) in steps.into_iter().enumerate() { - let (mut socket, _) = - tokio::time::timeout(Duration::from_secs(2), listener.accept()) - .await - .unwrap_or_else(|_| { - panic!( - "request {index} timed out -- the empty-voucher rejection path \ - did not attempt to confirm the pinned company out of band" - ) - }) - .expect("accept synthetic Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!( - String::from_utf8_lossy(&request).starts_with(expected_prefix), - "request {index} did not follow the voucher-then-extent-bracket sequence" - ); - let response = if is_status { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write synthetic Tally response"); - } - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let error = client - .fetch_vouchers(&identity, "20260401", "20260401") - .await - .expect_err( - "an empty voucher response must not be accepted when the pinned company book \ - extent cannot be confirmed", - ); - server.await.expect("synthetic Tally server task"); - assert!( - error.to_string().contains( - "empty voucher response could not confirm the pinned company book extent" - ), - "unexpected error: {error:#}" - ); - } - - /// Same empty voucher response as above, but this time the out-of-band - /// book-extent bracket confirms the pinned company is still selected and - /// stable -- so the empty result is accepted. - #[tokio::test] - async fn empty_voucher_response_is_accepted_when_bracket_confirms_pinned_company() { - let identity = captured_company_identity(); - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let empty_vouchers = native_voucher_collection_xml(&[]); - let confirmed_extent = COMPANY_EXTENT_V2.to_string(); - let status = "TallyPrime Server is Running"; - let steps: Vec<(&str, String, bool)> = vec![ - ("POST / HTTP/1.1", empty_vouchers, false), - ("POST / HTTP/1.1", confirmed_extent.clone(), false), - ("GET /status HTTP/1.1", status.to_string(), true), - ("POST / HTTP/1.1", confirmed_extent, false), - ("GET /status HTTP/1.1", status.to_string(), true), - ]; - let request_count = steps.len(); - let server = tokio::spawn(async move { - for (index, (expected_prefix, body, is_status)) in steps.into_iter().enumerate() { - let (mut socket, _) = - tokio::time::timeout(Duration::from_secs(2), listener.accept()) - .await - .unwrap_or_else(|_| panic!("request {index} timed out")) - .expect("accept synthetic Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!( - String::from_utf8_lossy(&request).starts_with(expected_prefix), - "request {index} did not follow the voucher-then-extent-bracket sequence" - ); - let response = if is_status { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write synthetic Tally response"); - } - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let vouchers = client - .fetch_vouchers(&identity, "20260401", "20260401") - .await - .expect("an empty voucher response confirmed by the extent bracket must be accepted"); - server.await.expect("synthetic Tally server task"); - assert!(vouchers.is_empty()); - assert_eq!( - request_count, 5, - "the empty path must pay for exactly one voucher read plus the extent bracket" - ); - } - - /// A non-empty voucher response keeps its existing row-GUID binding and - /// must not pay for the extent bracket -- the common case stays a single - /// request. - #[tokio::test] - async fn non_empty_voucher_response_issues_no_extra_request() { - let identity = captured_company_identity(); - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let voucher_row = - SYNTHETIC_VOUCHER_ROW.replace("synthetic-company-guid", CAPTURED_COMPANY_GUID); - let non_empty_vouchers = native_voucher_collection_xml(&[&voucher_row]); - - let server = tokio::spawn(async move { - let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) - .await - .expect("voucher request timed out") - .expect("accept synthetic Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!( - String::from_utf8_lossy(&request).starts_with("POST / HTTP/1.1"), - "unexpected request for the non-empty voucher read" - ); - socket - .write_all(&utf16_xml_response(non_empty_vouchers)) - .await - .expect("write synthetic Tally response"); - - // A non-empty response must not pay for the extent bracket: no - // further connection should ever arrive. - let extra = tokio::time::timeout(Duration::from_millis(300), listener.accept()).await; - assert!( - extra.is_err(), - "non-empty voucher fetch issued an unexpected extra request" - ); - }); - - let client = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client"); - let vouchers = client - .fetch_vouchers(&identity, "20260401", "20260401") - .await - .expect("non-empty voucher fetch must still succeed exactly as today"); - server.await.expect("synthetic Tally server task"); - assert_eq!(vouchers.len(), 1); - } - - #[tokio::test] - async fn capability_probe_reports_only_observed_xml_support() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - for (index, body) in [ - "LOCAL STATUS HEURISTIC UNRECOGNIZED", - "
11
guid-110000120260401TallyPrimeNoYesNo
", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!(!request.is_empty(), "synthetic Tally request must not be empty"); - let response = if index == 0 { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write Tally response"); - } - }); - - let probe = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .probe() - .await - .expect("probe synthetic Tally endpoint"); - server.await.expect("synthetic Tally server task"); - - assert!(probe.connection.reachable); - assert!(!probe.connection.compatible); - assert_eq!(probe.companies.len(), 1); - assert_eq!( - probe.profile.transports[&TransportId::XmlHttp].state, - CapabilityState::Supported - ); - assert_eq!( - probe.profile.packs[&CapabilityPackId::CoreAccounting].state, - CapabilityState::Unknown - ); - assert_eq!(probe.profile.product, "TallyPrime"); - assert!(probe.profile.release.is_none()); - assert_eq!(probe.profile.mode.as_deref(), Some("Licensed")); - assert_eq!(probe.profile.profile_version, 4); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::ProductAndMode].state, - CapabilityState::Supported - ); - let boundary = crate::tally::TallyRuntime::default() - .master_ledger_export_boundary_profile_from_profile(Some(&probe.profile)); - assert_eq!(boundary, DateBoundaryProfile::ModeAgnostic); - assert!(NativeLedgerExportPeriod::new( - boundary, - TallyDate::parse("20240115").expect("valid mid-month date"), - TallyDate::parse("20240115").expect("valid mid-month date"), - ) - .is_ok()); - for transport in [TransportId::TdlCompanion, TransportId::Odbc] { - let evidence = &probe.profile.transports[&transport]; - assert_eq!(evidence.state, CapabilityState::Unknown); - assert_eq!(evidence.confidence, EvidenceConfidence::Unknown); - assert_eq!( - evidence.safe_reason_code.as_deref(), - Some("configuration_not_observed") - ); - } - assert_eq!( - probe.profile.features[&CapabilityFeatureId::EndpointReachability].state, - CapabilityState::Supported - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::LoadedCompanies].state, - CapabilityState::Supported - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity].state, - CapabilityState::Supported - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::EncodingBehaviour] - .safe_reason_code - .as_deref(), - Some("utf16_le_bom_observed") - ); - for feature in [ - CapabilityFeatureId::PracticalResponseLimit, - CapabilityFeatureId::LedgerRead, - CapabilityFeatureId::VoucherRead, - CapabilityFeatureId::Write, - ] { - assert_eq!( - probe.profile.features[&feature].state, - CapabilityState::Unknown - ); - } - } - - #[tokio::test] - async fn capability_probe_records_unavailable_product_mode_evidence_without_refusing() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - let responses = [ - utf8_status_response("TallyPrime Server is Running"), - utf16_xml_response( - "
10
Capability collection unavailable
", - ), - utf16_xml_response( - "Synthetic Companyguid-1", - ), - ]; - for response in responses { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!( - !request.is_empty(), - "synthetic Tally request must not be empty" - ); - socket - .write_all(&response) - .await - .expect("write Tally response"); - } - }); - - let probe = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .probe() - .await - .expect("unavailable product/mode evidence must not refuse the probe"); - server.await.expect("synthetic Tally server task"); - - assert_eq!(probe.profile.product, "Unknown"); - assert!(probe.profile.mode.is_none()); - assert_eq!( - crate::tally::TallyRuntime::default() - .master_ledger_export_boundary_profile_from_profile(Some(&probe.profile)), - DateBoundaryProfile::ModeAgnostic - ); - let evidence = &probe.profile.features[&CapabilityFeatureId::ProductAndMode]; - assert_eq!(evidence.state, CapabilityState::Unknown); - assert_eq!(evidence.confidence, EvidenceConfidence::Observed); - assert_eq!( - evidence.safe_reason_code.as_deref(), - Some("product_mode_evidence_unavailable") - ); - } - - /// `fetch_companies` now requests the native `Company` collection - /// (`ReadOnlyProfile::CompanyListV2`) instead of the legacy `CompanyListV1` - /// custom TDL report. This asserts the request itself carries that shape -- - /// `TYPE=Collection`, no `REPORT`/`FORM`/`PART`/`LINE`/`FIELD` stack, and no - /// `SVCURRENTCOMPANY` scoping a discovery read to one company -- and that a - /// company row's `NAME` attribute and nested `GUID` are trimmed and returned. - #[tokio::test] - async fn interactive_company_fetch_sends_the_native_collection_request() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - let body = r#"
11
guid-1
"#; - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - let body_start = request - .windows(4) - .position(|window| window == b"\r\n\r\n") - .map(|position| position + 4) - .expect("POST request has complete HTTP headers"); - let post_xml = bridge_tally_protocol::decode_tally_text_bytes_limited( - &request[body_start..], - request.len(), - ) - .expect("POST request uses decodable UTF-16 XML") - .text; - assert_company_collection_request_shape(&post_xml); - assert!(!post_xml.contains("
11
"#; - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!( - !request.is_empty(), - "synthetic Tally request must not be empty" - ); - socket - .write_all(&utf16_xml_response(body)) - .await - .expect("write Tally response"); - }); - - let error = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .fetch_companies() - .await - .expect_err("a company row without a GUID must fail closed"); - server.await.expect("synthetic Tally server task"); - assert!(error.to_string().contains("GUID")); - let retained = error - .downcast_ref::() - .expect("parser refusal retains its typed wire-evidence boundary"); - assert!(retained.evidence.bytes > 0); - } - - #[tokio::test] - async fn direct_company_bootstrap_uses_only_the_shaped_collection_identity() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - // `fetch_companies` (the first of the two reads below) now requests the - // native `Company` collection (`ReadOnlyProfile::CompanyListV2`), so its - // response carries that shape rather than the legacy `CompanyListV1` - // direct report. Its GUID must still not escape into the returned - // identity -- only the second, scoped `standard` read may do that. - let discovered = r#"
11
scoped-guid10000120260401
"#; - let standard = "
11
ledger-guidPrimaryscoped-guidSynthetic Company1033
"; - let server = tokio::spawn(async move { - for body in [discovered, standard] { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!(!request.is_empty()); - socket - .write_all(&utf16_xml_response(body)) - .await - .expect("write Tally response"); - } - }); - - let company = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .bootstrap_direct_company("Synthetic Company") - .await - .expect("strict scoped bootstrap should succeed"); - server.await.expect("synthetic Tally server task"); - - assert_eq!(company.name, "Synthetic Company"); - assert_eq!(company.guid.as_deref(), Some("scoped-guid")); - assert_eq!(company.company_number.as_deref(), Some("100001")); - assert_eq!(company.books_from.as_deref(), Some("20260401")); - } - - #[tokio::test] - async fn capability_probe_does_not_promote_a_direct_company_report() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - // This responder gives the same untrusted bare direct company - // report regardless of what is requested: once for the `V2` - // collection attempt (which the collection parser rejects, since - // it never satisfies `HEADER/STATUS`), and once more for the - // `V1` fallback that follows. - for (index, body) in [ - "LOCAL STATUS HEURISTIC UNRECOGNIZED", - "Synthetic Companyguid-1", - "Synthetic Companyguid-1", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!(!request.is_empty(), "synthetic Tally request must not be empty"); - let response = if index == 0 { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write Tally response"); - } - }); - - let probe = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .probe() - .await - .expect("probe synthetic Tally endpoint"); - server.await.expect("synthetic Tally server task"); - - assert!(probe.connection.reachable); - assert!(probe.companies.is_empty()); - assert_eq!( - probe.profile.transports[&TransportId::XmlHttp].state, - CapabilityState::Unknown - ); - assert_eq!( - probe.profile.transports[&TransportId::XmlHttp] - .safe_reason_code - .as_deref(), - Some("direct_company_report_untrusted") - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::CompanyRead].state, - CapabilityState::Unknown - ); - } - - #[tokio::test] - async fn capability_probe_does_not_promote_a_shaped_company_failure_to_xml_support() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - // Same shaped `STATUS=0` failure for both the `V2` collection - // attempt and the `V1` fallback that follows it. - for (index, body) in [ - "TallyPrime Server is Running", - "
0
Could not find Company ''
", - "
0
Could not find Company ''
", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - assert!(!request.is_empty(), "synthetic Tally request must not be empty"); - let response = if index == 0 { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write Tally response"); - } - }); - - let probe = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .probe() - .await - .expect("probe synthetic Tally endpoint"); - server.await.expect("synthetic Tally server task"); - - assert!(probe.companies.is_empty()); - let xml = &probe.profile.transports[&TransportId::XmlHttp]; - assert_eq!(xml.state, CapabilityState::Unknown); - assert_eq!(xml.confidence, EvidenceConfidence::Observed); - assert_eq!(xml.safe_reason_code.as_deref(), Some("company_not_loaded")); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::LoadedCompanies].state, - CapabilityState::Unknown - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity].state, - CapabilityState::Unknown - ); - } - - /// A gateway-shaped `Company` collection response with two loaded - /// synthetic companies and the `CMPINFO` counter trap included. Proves - /// `probe` requests `CompanyListV2` on the happy path - /// and trusts its success without ever falling back to the legacy - /// `CompanyListV1` report: the mock server has exactly one POST response - /// queued, so a fallback request would hang and fail this test. - #[tokio::test] - async fn capability_probe_marks_presentation_equivalent_guid_siblings_ambiguous() { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic Tally server"); - let address = listener.local_addr().expect("synthetic Tally address"); - let server = tokio::spawn(async move { - let mut requests = Vec::new(); - for (index, body) in [ - "TallyPrime Server is Running", - "\n
11
\n 0\n \n Synthetic Company Asynthetic-guid-a10000120260401TallyPrimeNoYesNo\n synthetic company a SYNTHETIC-GUID-A10000220270401TallyPrimeNoYesNo\n \n \n
", - ] - .into_iter() - .enumerate() - { - let (mut socket, _) = listener.accept().await.expect("accept Tally request"); - let request = read_complete_http_request(&mut socket).await; - requests.push(request); - let response = if index == 0 { - utf8_status_response(body) - } else { - utf16_xml_response(body) - }; - socket - .write_all(&response) - .await - .expect("write Tally response"); - } - requests - }); - - let probe = TallyClient::new(TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }) - .expect("build synthetic Tally client") - .probe() - .await - .expect("probe synthetic Tally endpoint"); - let requests = server.await.expect("synthetic Tally server task"); - - assert_eq!(probe.companies.len(), 2); - assert_eq!(probe.companies[0].name, "Synthetic Company A"); - assert_eq!(probe.companies[0].guid.as_deref(), Some("synthetic-guid-a")); - assert_eq!(probe.companies[1].name, "synthetic company a"); - assert_eq!(probe.companies[1].guid.as_deref(), Some("SYNTHETIC-GUID-A")); - assert_eq!(probe.profile.product, "TallyPrime"); - assert_eq!(probe.profile.mode.as_deref(), Some("Licensed")); - assert_eq!( - probe.profile.transports[&TransportId::XmlHttp].state, - CapabilityState::Supported - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity] - .safe_reason_code - .as_deref(), - Some("company_identity_display_scope_ambiguous") - ); - assert_eq!( - probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity].state, - CapabilityState::Unknown - ); - - let post_request = requests - .iter() - .find(|request| request.starts_with(b"POST")) - .expect("exactly one POST request was sent"); - let body_start = post_request - .windows(4) - .position(|window| window == b"\r\n\r\n") - .map(|position| position + 4) - .expect("POST request has complete HTTP headers"); - let post_xml = bridge_tally_protocol::decode_tally_text_bytes_limited( - &post_request[body_start..], - post_request.len(), - ) - .expect("POST request uses decodable UTF-16 XML"); - assert_company_collection_request_shape(&post_xml.text); - assert!(post_xml.text.contains("BridgeCompanyExtent")); - } -} +#[path = "connection_tests.rs"] +mod tests; diff --git a/src-tauri/src/tally/connection_tests.rs b/src-tauri/src/tally/connection_tests.rs new file mode 100644 index 00000000..97589be0 --- /dev/null +++ b/src-tauri/src/tally/connection_tests.rs @@ -0,0 +1,1836 @@ +#[tokio::test] +async fn capability_probe_preserves_captured_release_and_exclusive_license_tier() { + let capture = include_bytes!("../../crates/bridge-tally-protocol/tests/fixtures/agent/native-licensed-release-companies.utf16le.xml"); + let xml = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( + capture, + "text/xml; charset=utf-16", + bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, + capture.len(), + ) + .unwrap() + .text; + let observation = super::parse_company_gateway_capability_observation(&xml).unwrap(); + for (education, silver, gold, tier) in [ + (false, true, false, Some(super::LicenseTier::Silver)), + (false, false, true, Some(super::LicenseTier::Gold)), + (false, true, true, None), + (false, false, false, None), + (true, true, false, None), + ] { + let mut altered = observation.clone(); + altered.educational_mode = education; + altered.silver = silver; + altered.gold = gold; + assert_eq!( + super::GatewayProductModeEvidence::from_observation(altered).license_tier, + tier + ); + } + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + for response in [ + utf8_status_response("Unknown status banner"), + utf16_xml_response(&xml), + ] { + let (mut socket, _) = listener.accept().await.unwrap(); + let request = read_complete_http_request(&mut socket).await; + assert!(!request.is_empty()); + socket.write_all(&response).await.unwrap(); + } + }); + let (probe, wire) = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .unwrap() + .probe_with_wire_evidence() + .await + .unwrap(); + server.await.unwrap(); + assert!(wire.bytes >= capture.len()); + assert_eq!(probe.companies.len(), 16); + assert_eq!(probe.profile.profile_version, 4); + assert_eq!(probe.profile.product, "TallyPrime"); + assert_eq!(probe.profile.mode.as_deref(), Some("Licensed")); + assert_eq!(probe.profile.release.as_deref(), Some("7.1")); + assert_eq!(probe.profile.license_tier, Some(super::LicenseTier::Silver)); + assert_eq!( + probe.profile.transports[&TransportId::JsonEx].state, + CapabilityState::Unknown + ); + assert_eq!( + probe.profile.transports[&TransportId::JsonEx] + .safe_reason_code + .as_deref(), + Some("transport_not_probed") + ); + let mut old = serde_json::to_value(&probe.profile).unwrap(); + old.as_object_mut().unwrap().remove("license_tier"); + old["profile_version"] = serde_json::json!(3); + let old: bridge_tally_core::CapabilityProfile = serde_json::from_value(old).unwrap(); + assert_eq!(old.license_tier, None); + assert_ne!(old.profile_version, probe.profile.profile_version); +} + +#[test] +fn party_ledger_commitment_hashes_the_three_encoded_builder_requests() { + let master = NativeLedgerExportPeriod::new( + DateBoundaryProfile::ModeAgnostic, + TallyDate::parse("20260401").unwrap(), + TallyDate::parse("20260731").unwrap(), + ) + .unwrap(); + let balance = party_ledger_master_balance_period( + DateBoundaryProfile::ModeAgnostic, + TallyDate::parse("20260401").unwrap(), + TallyDate::parse("20260731").unwrap(), + ) + .unwrap(); + let requests = [ + super::render_party_ledger_master_request("Synthetic ₹ Books", &master), + super::render_native_ledger_snapshot_request("Synthetic ₹ Books", &balance), + super::render_native_group_snapshot_request("Synthetic ₹ Books"), + ]; + let hashes = requests + .iter() + .map(|request| { + let mut bytes = vec![0xff, 0xfe]; + bytes.extend(request.encode_utf16().flat_map(u16::to_le_bytes)); + super::sha256_hex(&bytes) + }) + .collect::>(); + let expected = super::sha256_hex(hashes.join(":").as_bytes()); + assert_eq!(super::party_ledger_request_commitment(&requests), expected); + for index in 0..3 { + let mut changed = requests.clone(); + changed[index].push(' '); + assert_ne!(super::party_ledger_request_commitment(&changed), expected); + } +} + +#[cfg(feature = "voucher-scan")] +use super::LedgerOpeningCoverageRead; +use super::{ + canonical_loopback_origin, decode_xml_bytes, detect_product, + has_presentation_equivalent_guid_siblings, normalize_discovered_companies, + party_ledger_master_balance_period, party_ledger_master_openings_agree, tally_endpoint, + unique_company_identities, TallyClient, TallyConfig, TallyProduct, VerifiedCompanyIdentity, +}; +use bridge_tally_core::{ + CapabilityFeatureId, CapabilityPackId, CapabilityState, EvidenceConfidence, TallyDate, + TransportId, +}; +use bridge_tally_protocol::native_outstandings::NativeLedgerExportPeriod; +use bridge_tally_protocol::outstandings_shared::DateBoundaryProfile; +use std::time::Duration; +use tally_protocol_simulator::{Fixture, ScenarioPlan, Simulator, WireEncoding}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; + +const COMPANY_EXTENT_V2: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/agent/native-company-book-extents-with-number.utf8.xml" +); +const CAPTURED_COMPANY_GUID: &str = "bb8ad19e-6aef-4239-a917-87fec0c6215e"; + +fn identity_from_company_extent(xml: &str) -> VerifiedCompanyIdentity { + let companies = bridge_tally_protocol::parse_companies_from_collection(xml) + .expect("captured company extent must parse as a Company collection"); + let company = companies + .iter() + .find(|company| company.guid.as_deref() == Some(CAPTURED_COMPANY_GUID)) + .expect("captured target company must remain present"); + VerifiedCompanyIdentity::from_observed_companies( + company.name.clone(), + company.guid.clone().expect("captured target company GUID"), + company + .company_number + .clone() + .expect("captured target company number"), + company + .books_from + .clone() + .expect("captured target company books-from"), + &companies, + ) + .expect("captured target company tuple must remain uniquely admissible") +} + +fn captured_company_identity() -> VerifiedCompanyIdentity { + identity_from_company_extent(COMPANY_EXTENT_V2) +} + +#[test] +fn party_master_snapshot_uses_the_next_common_admissible_boundary() { + let period = party_ledger_master_balance_period( + DateBoundaryProfile::ModeAgnostic, + TallyDate::parse("20260401").unwrap(), + TallyDate::parse("20260415").unwrap(), + ) + .expect("the derived common boundary is valid"); + assert_eq!(period.to().as_str(), "20260501"); + assert!(period.to() >= &TallyDate::parse("20260415").unwrap()); +} + +#[test] +fn party_master_opening_balance_comparison_accepts_equivalent_decimal_scale() { + assert!(party_ledger_master_openings_agree( + "0", + &bridge_tally_core::ExactDecimal::parse("0.00").unwrap(), + ) + .unwrap()); +} + +#[test] +fn party_master_opening_balance_comparison_withholds_a_real_numeric_mismatch() { + assert!(!party_ledger_master_openings_agree( + "0.01", + &bridge_tally_core::ExactDecimal::parse("0.00").unwrap(), + ) + .unwrap()); +} + +#[test] +fn party_master_opening_balance_comparison_rejects_an_unparseable_master_value() { + assert!(party_ledger_master_openings_agree( + "not-an-amount", + &bridge_tally_core::ExactDecimal::parse("0.00").unwrap(), + ) + .is_err()); +} + +fn utf16_xml_response(body: impl AsRef) -> Vec { + let body = bridge_tally_protocol::encode_tally_xml_request_utf16le(body.as_ref()); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-16\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + [headers.as_bytes(), &body].concat() +} + +fn utf8_status_response(body: impl AsRef) -> Vec { + let body = body.as_ref().as_bytes(); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + [headers.as_bytes(), body].concat() +} + +async fn read_complete_http_request(socket: &mut (impl tokio::io::AsyncRead + Unpin)) -> Vec { + const HEADER_TERMINATOR: &[u8] = b"\r\n\r\n"; + const MAX_TEST_HEADER_BYTES: usize = 64 * 1024; + + let mut request = Vec::new(); + let mut chunk = [0_u8; 4096]; + let header_end = loop { + if let Some(offset) = request + .windows(HEADER_TERMINATOR.len()) + .position(|window| window == HEADER_TERMINATOR) + { + break offset + HEADER_TERMINATOR.len(); + } + let bytes_read = socket + .read(&mut chunk) + .await + .expect("read synthetic HTTP request headers"); + assert!( + bytes_read > 0, + "synthetic HTTP request ended before its headers were complete" + ); + request.extend_from_slice(&chunk[..bytes_read]); + assert!( + request.len() <= MAX_TEST_HEADER_BYTES, + "synthetic HTTP request headers exceeded {MAX_TEST_HEADER_BYTES} bytes" + ); + }; + + let headers = std::str::from_utf8(&request[..header_end - HEADER_TERMINATOR.len()]) + .expect("synthetic HTTP request headers must be UTF-8"); + let mut lines = headers.split("\r\n"); + let request_line = lines + .next() + .expect("synthetic HTTP request must contain a request line"); + let mut request_line_parts = request_line.split_ascii_whitespace(); + let method = request_line_parts + .next() + .expect("synthetic HTTP request method is missing"); + request_line_parts + .next() + .expect("synthetic HTTP request target is missing"); + let version = request_line_parts + .next() + .expect("synthetic HTTP request version is missing"); + assert_eq!(version, "HTTP/1.1", "synthetic request must use HTTP/1.1"); + assert!( + request_line_parts.next().is_none(), + "synthetic HTTP request line has unexpected fields" + ); + + let mut content_length = None; + for header in lines { + let (name, value) = header + .split_once(':') + .expect("synthetic HTTP request contains a malformed header"); + assert!( + !name.eq_ignore_ascii_case("transfer-encoding"), + "synthetic HTTP request must use Content-Length framing" + ); + if name.eq_ignore_ascii_case("content-length") { + assert!( + content_length.is_none(), + "synthetic HTTP request contains duplicate Content-Length headers" + ); + content_length = Some( + value + .trim() + .parse::() + .expect("synthetic HTTP request has an invalid Content-Length"), + ); + } + } + let content_length = match content_length { + Some(length) => length, + None if method == "GET" || method == "HEAD" => 0, + None => panic!("synthetic HTTP request body is missing Content-Length"), + }; + let request_end = header_end + .checked_add(content_length) + .expect("synthetic HTTP request length overflowed usize"); + assert!( + request.len() <= request_end, + "synthetic HTTP request contains bytes beyond Content-Length" + ); + + while request.len() < request_end { + let remaining = request_end - request.len(); + let chunk_length = remaining.min(chunk.len()); + let bytes_read = socket + .read(&mut chunk[..chunk_length]) + .await + .expect("read synthetic HTTP request body"); + assert!( + bytes_read > 0, + "synthetic HTTP request ended before its Content-Length body was complete" + ); + request.extend_from_slice(&chunk[..bytes_read]); + } + + request +} + +fn assert_company_collection_request_shape(request: &str) { + assert!(request.contains("Collection")); + for method in ["NAME", "GUID", "PRODUCTNAME"] { + assert!(request.contains(&format!("{method}"))); + } + for expression in [ + "EduMode : $$LicenseInfo:IsEducationalMode", + "Silver : $$LicenseInfo:IsSilver", + "Gold : $$LicenseInfo:IsGold", + ] { + assert!(request.contains(expression)); + } +} + +#[test] +fn detects_tallyprime_status() { + assert!(matches!( + detect_product("TallyPrime Server is Running"), + TallyProduct::TallyPrime + )); +} + +#[test] +fn detects_erp9_status() { + assert!(matches!( + detect_product("Tally ERP 9 Server is Running"), + TallyProduct::TallyErp9 + )); +} + +#[test] +fn product_marker_is_not_accepted_inside_unrelated_content() { + assert!(matches!( + detect_product("TallyPrime Server is Running"), + TallyProduct::Unknown + )); + assert!(matches!( + detect_product("prefix Tally ERP 9 Server is Running suffix"), + TallyProduct::Unknown + )); +} + +#[tokio::test] +async fn selected_read_request_digest_matches_the_dispatched_wire_entity() { + let plan = ScenarioPlan::new(Fixture::NormalExport).with_encoding(WireEncoding::Utf16Le); + let simulator = Simulator::spawn(plan).expect("spawn synthetic Tally endpoint"); + let client = TallyClient::new(TallyConfig { + host: simulator.address().ip().to_string(), + port: simulator.address().port(), + }) + .expect("build synthetic Tally client"); + + let observation = client + .qualify_selected_ledgers( + "BRIDGE SYNTHETIC BOOK", + "00000000-0000-4000-8000-000000000001", + ) + .await + .expect("qualify synthetic selected-ledger read"); + let dispatched = simulator.finish().expect("finish synthetic Tally exchange"); + + assert_eq!(observation.request_sha256, dispatched.request_body_sha256); +} + +#[test] +fn company_identity_normalization_rejects_invalid_and_ambiguous_guids() { + let normalized = normalize_discovered_companies(vec![ + crate::tally::TallyCompany { + name: " Synthetic A ".to_string(), + guid: Some(" GUID-1 ".to_string()), + company_number: Some("100005".to_string()), + books_from: Some("20250401".to_string()), + }, + crate::tally::TallyCompany { + name: "Synthetic B".to_string(), + guid: Some("guid-1".to_string()), + company_number: Some("100014".to_string()), + books_from: Some("20260401".to_string()), + }, + ]) + .expect("normalize company identities"); + assert_eq!(normalized[0].name, "Synthetic A"); + assert_eq!(normalized[0].guid.as_deref(), Some("GUID-1")); + assert!(unique_company_identities(&normalized)); + assert!(!has_presentation_equivalent_guid_siblings(&normalized)); + + assert!( + normalize_discovered_companies(vec![crate::tally::TallyCompany { + name: "Synthetic\nCompany".to_string(), + guid: Some("guid-2".to_string()), + company_number: None, + books_from: None, + }]) + .is_err() + ); + assert!( + normalize_discovered_companies(vec![crate::tally::TallyCompany { + name: "Synthetic Company".to_string(), + guid: Some("guid\n2".to_string()), + company_number: None, + books_from: None, + }]) + .is_err() + ); +} + +#[test] +fn presentation_equivalent_guid_siblings_are_not_stable_company_identities() { + let companies = vec![ + crate::tally::TallyCompany { + name: "Synthetic Company".to_string(), + guid: Some("guid-3".to_string()), + company_number: Some("100005".to_string()), + books_from: Some("20250401".to_string()), + }, + crate::tally::TallyCompany { + name: " synthetic company ".to_string(), + guid: Some("GUID-3".to_string()), + company_number: Some("100014".to_string()), + books_from: Some("20260401".to_string()), + }, + ]; + + assert!(unique_company_identities(&companies)); + assert!(has_presentation_equivalent_guid_siblings(&companies)); +} + +#[test] +fn identical_name_same_guid_distinct_books_are_not_stable_company_identities() { + let companies = vec![ + crate::tally::TallyCompany { + name: "Synthetic Company".to_string(), + guid: Some("guid-3".to_string()), + company_number: Some("100005".to_string()), + books_from: Some("20250401".to_string()), + }, + crate::tally::TallyCompany { + name: "Synthetic Company".to_string(), + guid: Some("GUID-3".to_string()), + company_number: Some("100014".to_string()), + books_from: Some("20260401".to_string()), + }, + ]; + + assert!(unique_company_identities(&companies)); + assert!(has_presentation_equivalent_guid_siblings(&companies)); +} + +#[test] +fn validates_tally_endpoint_components() { + assert_eq!( + tally_endpoint(&TallyConfig::default(), "/status") + .expect("localhost endpoint") + .as_str(), + "http://127.0.0.1:9000/status" + ); + let config = TallyConfig { + host: "::1".to_string(), + port: 9000, + }; + assert_eq!( + tally_endpoint(&config, "/status") + .expect("IPv6 endpoint") + .as_str(), + "http://[::1]:9000/status" + ); + for host in ["localhost", "127.0.0.1"] { + assert_eq!( + canonical_loopback_origin(&TallyConfig { + host: host.to_string(), + port: 9000, + }) + .expect("canonical loopback origin"), + "http://127.0.0.1:9000" + ); + } + assert_eq!( + canonical_loopback_origin(&TallyConfig { + host: "::1".to_string(), + port: 9000, + }) + .expect("canonical IPv6 loopback origin"), + "http://[::1]:9000" + ); + + for host in ["http://localhost", "localhost/path", "user@localhost", ""] { + let invalid = TallyConfig { + host: host.to_string(), + port: 9000, + }; + assert!(tally_endpoint(&invalid, "/status").is_err()); + } + + for host in [ + "192.168.1.10", + "10.0.0.5", + "169.254.1.1", + "224.0.0.1", + "8.8.8.8", + "tally.internal", + ] { + let remote = TallyConfig { + host: host.to_string(), + port: 9000, + }; + assert!(tally_endpoint(&remote, "/status").is_err()); + } +} + +#[test] +fn decodes_supported_xml_byte_order_marks_and_rejects_invalid_sequences() { + let utf8 = [b"\xEF\xBB\xBF".as_slice(), b""].concat(); + assert_eq!(decode_xml_bytes(utf8).expect("UTF-8 BOM"), ""); + + let document = "नमस्ते"; + let mut utf16le = vec![0xFF, 0xFE]; + utf16le.extend(document.encode_utf16().flat_map(u16::to_le_bytes)); + assert_eq!(decode_xml_bytes(utf16le).expect("UTF-16LE"), document); + + let mut utf16be = vec![0xFE, 0xFF]; + utf16be.extend(document.encode_utf16().flat_map(u16::to_be_bytes)); + assert_eq!(decode_xml_bytes(utf16be).expect("UTF-16BE"), document); + + assert!(decode_xml_bytes(vec![0xFF, 0xFE, 0x00]).is_err()); + assert!(decode_xml_bytes(vec![0x80]).is_err()); +} + +#[tokio::test] +async fn tally_requests_ignore_configured_proxy() { + let tally_listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let tally_address = tally_listener.local_addr().expect("Tally address"); + let proxy_listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic proxy"); + let proxy_address = proxy_listener.local_addr().expect("proxy address"); + + let tally_server = tokio::spawn(async move { + let accepted = tokio::time::timeout(Duration::from_secs(2), tally_listener.accept()) + .await + .expect("Tally request timed out") + .expect("accept Tally request"); + let (mut socket, _) = accepted; + let request = read_complete_http_request(&mut socket).await; + assert!( + String::from_utf8_lossy(&request).starts_with("GET /status HTTP/1.1"), + "request should go directly to the Tally endpoint" + ); + let body = "TallyPrime Server is Running"; + socket + .write_all(&utf8_status_response(body)) + .await + .expect("write Tally response"); + }); + + let proxy_server = tokio::spawn(async move { + match tokio::time::timeout(Duration::from_millis(750), proxy_listener.accept()).await { + Ok(Ok((mut socket, _))) => { + let response = + "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + socket + .write_all(response.as_bytes()) + .await + .expect("write proxy response"); + true + } + Ok(Err(error)) => panic!("accept proxy request: {error}"), + Err(_) => false, + } + }); + + let client = TallyClient::with_http_builder( + TallyConfig { + host: tally_address.ip().to_string(), + port: tally_address.port(), + }, + reqwest::Client::builder().proxy( + reqwest::Proxy::all(format!("http://{proxy_address}")).expect("synthetic proxy URL"), + ), + ); + + let status = client + .check_connection() + .await + .expect("check synthetic Tally connection"); + tally_server.await.expect("synthetic Tally server task"); + let proxy_received_request = proxy_server.await.expect("synthetic proxy task"); + + assert!(status.reachable, "direct Tally response should be accepted"); + assert!( + status.compatible, + "synthetic Tally status should be recognized" + ); + assert!( + !proxy_received_request, + "Tally traffic must never be sent through a configured proxy" + ); +} + +#[cfg(feature = "voucher-scan")] +#[tokio::test] +async fn paired_outstandings_reads_health_check_between_and_after_requests() { + const OPTIONAL_VOUCHERS: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/unit_a_optional_voucher_live.xml" + ); + const STATUS: &str = "TallyPrime Server is Running"; + let identity = captured_company_identity(); + + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + for (index, expected_prefix) in [ + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + "POST / HTTP/1.1", + "GET /status HTTP/1.1", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) + .await + .expect("paired request timed out") + .expect("accept paired request"); + let request = read_complete_http_request(&mut socket).await; + assert!( + String::from_utf8_lossy(&request).starts_with(expected_prefix), + "request {index} did not follow the required read/health-check sequence" + ); + let body = match index { + 0 | 2 | 12 | 14 => COMPANY_EXTENT_V2, + 4 | 6 | 8 | 10 => OPTIONAL_VOUCHERS, + _ => STATUS, + }; + let response = if index % 2 == 0 { + utf16_xml_response(body) + } else { + utf8_status_response(body) + }; + socket + .write_all(&response) + .await + .expect("write paired response"); + } + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let extent = client + .fetch_company_book_extent(&identity) + .await + .expect("paired extent reads remain stable"); + let reporting_window = bridge_tally_protocol::outstandings::DateWindow::parse( + bridge_tally_protocol::outstandings::DateBoundaryProfile::ModeAgnostic, + "20260401", + "20260401", + ) + .expect("synthetic one-day window"); + let segment_window = reporting_window + .narrow_partitions() + .expect("one narrow partition") + .remove(0); + let observation = client + .fetch_outstandings_segment_pair( + extent.company(), + segment_window.clone(), + bridge_tally_protocol::outstandings::AlterIdRange::new(0, 101603) + .expect("non-empty synthetic range"), + ) + .await + .expect("paired segment reads remain stable"); + let verification = observation.verification; + let bridge_tally_protocol::outstandings::SegmentVerification::Complete(segment) = verification + else { + panic!("captured voucher response did not verify: {verification:?}"); + }; + assert!( + !segment.vouchers().is_empty(), + "captured voucher fixture unexpectedly had no vouchers" + ); + let witness = client + .fetch_empty_partition_witness_pair(extent.company(), segment_window) + .await + .expect("paired empty-date witness reads remain stable"); + let bridge_tally_protocol::outstandings::WitnessPairVerification::Complete(witness) = witness + else { + panic!("captured witness fixture did not verify: {witness:?}"); + }; + assert!( + !witness.vouchers().is_empty(), + "captured witness fixture unexpectedly had no identity rows" + ); + let closing_extent = client + .fetch_company_book_extent(&identity) + .await + .expect("closing paired extent reads remain stable"); + assert_eq!(closing_extent, extent, "synthetic book did not change"); + assert_eq!( + client.observed_body_bytes(), + Some( + u64::try_from( + bridge_tally_protocol::encode_tally_xml_request_utf16le(OPTIONAL_VOUCHERS).len(), + ) + .expect("encoded fixture length fits u64"), + ), + "closing extent evidence must not overwrite the larger voucher payload" + ); + server.await.expect("synthetic Tally server task"); +} + +#[cfg(feature = "voucher-scan")] +#[tokio::test] +async fn paired_ledger_opening_coverage_reports_intra_pair_drift() { + const STATUS: &str = "TallyPrime Server is Running"; + let identity = captured_company_identity(); + + let coverage = |name: &str| { + format!( + "
1
bb8ad19e-6aef-4239-a917-87fec0c6215e-00000001Yes0
" + ) + }; + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let responses = vec![ + COMPANY_EXTENT_V2.to_string(), + STATUS.to_string(), + COMPANY_EXTENT_V2.to_string(), + STATUS.to_string(), + coverage("Before Rename"), + STATUS.to_string(), + coverage("After Rename"), + STATUS.to_string(), + ]; + let server = tokio::spawn(async move { + for (index, body) in responses.into_iter().enumerate() { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) + .await + .expect("paired request timed out") + .expect("accept paired request"); + let request = read_complete_http_request(&mut socket).await; + let expected_prefix = if index % 2 == 0 { + "POST / HTTP/1.1" + } else { + "GET /status HTTP/1.1" + }; + assert!( + String::from_utf8_lossy(&request).starts_with(expected_prefix), + "request {index} did not follow the required read/health-check sequence" + ); + let response = if index % 2 == 0 { + utf16_xml_response(body) + } else { + utf8_status_response(body) + }; + socket + .write_all(&response) + .await + .expect("write paired response"); + } + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let extent = client + .fetch_company_book_extent(&identity) + .await + .expect("paired extent reads remain stable"); + assert!(matches!( + client + .fetch_ledger_opening_coverage(extent.company()) + .await + .expect("coverage responses parse"), + LedgerOpeningCoverageRead::Drifted + )); + server.await.expect("synthetic Tally server task"); +} + +/// The outstandings bracket (`fetch_company_book_extent`, feeding both +/// `fetch_outstandings_native` and `fetch_ledgers`) must fail closed with +/// a typed error when both paired reads agree but neither carries +/// `ALTMSTID`. This is the exact case the review flagged: two +/// witness-less extents compare equal, so the ordinary `first != second` +/// drift check alone cannot tell a stable book from one where a +/// GROUP/LEDGER master moved mid-window without a signal to detect it. +/// Uses only a targeted mutation of the captured V2 response. The +/// production parser must continue refusing a stable pair without the +/// master witness instead of accepting a fabricated positive fixture. +#[tokio::test] +async fn outstandings_bracket_fails_closed_when_altmstid_is_absent() { + const STATUS: &str = "TallyPrime Server is Running"; + const CAPTURED_TARGET_WITNESS: &str = concat!( + "bb8ad19e-6aef-4239-a917-87fec0c6215e\r\n", + " 100000\r\n", + " 101605\r\n", + " 328" + ); + const CAPTURED_TARGET_WITHOUT_WITNESS: &str = concat!( + "bb8ad19e-6aef-4239-a917-87fec0c6215e\r\n", + " 100000\r\n", + " 101605" + ); + let identity = captured_company_identity(); + assert_eq!( + COMPANY_EXTENT_V2.matches(CAPTURED_TARGET_WITNESS).count(), + 1, + "the exact captured target witness must appear once before mutation" + ); + let company_extent_without_altmstid = + COMPANY_EXTENT_V2.replacen(CAPTURED_TARGET_WITNESS, CAPTURED_TARGET_WITHOUT_WITNESS, 1); + assert!( + !company_extent_without_altmstid.contains(CAPTURED_TARGET_WITNESS), + "the captured target witness mutation must apply" + ); + let parsed_without_witness = + bridge_tally_protocol::outstandings_shared::parse_company_book_extent_v2( + &company_extent_without_altmstid, + &identity + .company_book_extent_expectation() + .expect("captured expectation"), + ) + .expect("the captured negative mutation must remain a parseable extent"); + assert!( + parsed_without_witness + .master_alter_id_high_water() + .is_none(), + "the target witness mutation must remove only the target ALTMSTID" + ); + + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let responses = [ + company_extent_without_altmstid.clone(), + STATUS.to_string(), + company_extent_without_altmstid, + STATUS.to_string(), + ]; + let server = tokio::spawn(async move { + for (index, body) in responses.into_iter().enumerate() { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) + .await + .expect("paired request timed out") + .expect("accept paired request"); + let request = read_complete_http_request(&mut socket).await; + let expected_prefix = if index % 2 == 0 { + "POST / HTTP/1.1" + } else { + "GET /status HTTP/1.1" + }; + assert!( + String::from_utf8_lossy(&request).starts_with(expected_prefix), + "request {index} did not follow the required read/health-check sequence" + ); + let response = if index % 2 == 0 { + utf16_xml_response(&body) + } else { + utf8_status_response(&body) + }; + socket + .write_all(&response) + .await + .expect("write paired response"); + } + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let error = client + .fetch_company_book_extent(&identity) + .await + .expect_err("a stable pair without ALTMSTID must still fail closed"); + server.await.expect("synthetic Tally server task"); + assert!( + error + .downcast_ref::() + .is_some_and(|error| { + *error + == bridge_tally_protocol::outstandings_shared::OutstandingsError::MasterWitnessAbsent + }), + "unexpected error: {error:#}" + ); +} + +#[tokio::test] +async fn http_success_with_tally_status_zero_is_not_an_empty_success() { + let identity = captured_company_identity(); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + for (index, body) in [ + COMPANY_EXTENT_V2, + "TallyPrime Server is Running", + COMPANY_EXTENT_V2, + "TallyPrime Server is Running", + "
0
", + "TallyPrime Server is Running", + "
0
", + "TallyPrime Server is Running", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + let expected = if index == 1 || index == 3 || index == 5 || index == 7 { + "GET /status HTTP/1.1" + } else { + "POST / HTTP/1.1" + }; + assert!( + String::from_utf8_lossy(&request).starts_with(expected), + "ledger fetch did not preserve its extent/read sequence" + ); + if index == 4 || index == 6 { + let body_start = request + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|offset| offset + 4) + .expect("native ledger POST has complete HTTP headers"); + let request_xml = bridge_tally_protocol::decode_tally_text_bytes_limited( + &request[body_start..], + request.len(), + ) + .expect("native ledger POST uses decodable UTF-16 XML") + .text; + assert!(request_xml.contains(r#"20240401"#)); + assert!(request_xml.contains(r#"20260401"#)); + } + let response = if index == 1 || index == 3 || index == 5 || index == 7 { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write Tally response"); + } + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let error = client + .fetch_ledgers(&identity, DateBoundaryProfile::ModeAgnostic) + .await + .expect_err("STATUS 0 must not become an empty ledger result"); + server.await.expect("synthetic Tally server task"); + assert!( + error + .to_string() + .contains("native ledger collection did not report success"), + "unexpected error: {error:#}" + ); +} + +#[tokio::test] +async fn invalid_book_extent_stops_ledger_export_without_a_date_fallback() { + let identity = captured_company_identity(); + let invalid_extent = + COMPANY_EXTENT_V2.replacen(r#"20240401"#, "", 1); + assert_ne!(invalid_extent, COMPANY_EXTENT_V2); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + for (index, body) in [ + invalid_extent.as_str(), + "TallyPrime Server is Running", + invalid_extent.as_str(), + "TallyPrime Server is Running", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept extent request"); + let request = read_complete_http_request(&mut socket).await; + let expected = if index % 2 == 0 { + "POST / HTTP/1.1" + } else { + "GET /status HTTP/1.1" + }; + assert!( + String::from_utf8_lossy(&request).starts_with(expected), + "request {index} did not follow the paired extent sequence" + ); + let response = if index % 2 == 0 { + utf16_xml_response(body) + } else { + utf8_status_response(body) + }; + socket + .write_all(&response) + .await + .expect("write extent response"); + } + assert!( + tokio::time::timeout(Duration::from_millis(200), listener.accept()) + .await + .is_err(), + "an invalid extent must stop before any native ledger request" + ); + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + client + .fetch_ledgers(&identity, DateBoundaryProfile::ModeAgnostic) + .await + .expect_err("missing BOOKSFROM must fail closed"); + server.await.expect("synthetic Tally server task"); +} + +#[tokio::test] +async fn education_profile_rejects_an_unsupported_books_from_before_ledger_export() { + let extent = COMPANY_EXTENT_V2.replacen( + r#"20240401"#, + r#"20240115"#, + 1, + ); + let identity = identity_from_company_extent(&extent); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + for (index, body) in [ + extent.as_str(), + "TallyPrime Server is Running", + extent.as_str(), + "TallyPrime Server is Running", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept extent request"); + let request = read_complete_http_request(&mut socket).await; + let expected = if index % 2 == 0 { + "POST / HTTP/1.1" + } else { + "GET /status HTTP/1.1" + }; + assert!( + String::from_utf8_lossy(&request).starts_with(expected), + "request {index} did not follow the paired extent sequence" + ); + let response = if index % 2 == 0 { + utf16_xml_response(body) + } else { + utf8_status_response(body) + }; + socket + .write_all(&response) + .await + .expect("write extent response"); + } + assert!( + tokio::time::timeout(Duration::from_millis(200), listener.accept()) + .await + .is_err(), + "an Education-invalid BOOKSFROM must stop before the native ledger request" + ); + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let error = client + .fetch_ledgers(&identity, DateBoundaryProfile::EducationRestricted) + .await + .expect_err("unsupported boundary must not reach the ledger export"); + server.await.expect("synthetic Tally server task"); + assert!( + error + .to_string() + .contains("not supported by the endpoint compatibility profile"), + "unexpected error: {error:#}" + ); +} + +fn native_voucher_collection_xml(rows: &[&str]) -> String { + format!( + "
1
{}
", + rows.join("") + ) +} + +const SYNTHETIC_VOUCHER_ROW: &str = r#"20260401synthetic-company-guid-0000000111PaymentNoNoCash-100.00Yes"#; + +/// A native Voucher collection carries no envelope company GUID, and a +/// zero-row response has no per-row GUID either -- there is nothing for +/// `parse_native_voucher_source_records_with_evidence` to bind. Before +/// the fix, `fetch_vouchers` accepted such a response unauthenticated, +/// so a silently substituted or dropped company looked identical to a +/// genuinely empty window. This reproduces that: the voucher read comes +/// back empty, and the out-of-band book-extent bracket that should +/// confirm the pinned company instead observes a different one. +#[tokio::test] +async fn empty_voucher_response_is_rejected_when_pinned_company_cannot_be_confirmed() { + let identity = captured_company_identity(); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let empty_vouchers = native_voucher_collection_xml(&[]); + let substituted_extent = + COMPANY_EXTENT_V2.replacen(CAPTURED_COMPANY_GUID, "substituted-company-guid", 1); + let status = "TallyPrime Server is Running"; + let steps: Vec<(&str, String, bool)> = vec![ + ("POST / HTTP/1.1", empty_vouchers, false), + ("POST / HTTP/1.1", substituted_extent.clone(), false), + ("GET /status HTTP/1.1", status.to_string(), true), + ("POST / HTTP/1.1", substituted_extent, false), + ("GET /status HTTP/1.1", status.to_string(), true), + ]; + let server = tokio::spawn(async move { + for (index, (expected_prefix, body, is_status)) in steps.into_iter().enumerate() { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) + .await + .unwrap_or_else(|_| { + panic!( + "request {index} timed out -- the empty-voucher rejection path \ + did not attempt to confirm the pinned company out of band" + ) + }) + .expect("accept synthetic Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!( + String::from_utf8_lossy(&request).starts_with(expected_prefix), + "request {index} did not follow the voucher-then-extent-bracket sequence" + ); + let response = if is_status { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write synthetic Tally response"); + } + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let error = client + .fetch_vouchers(&identity, "20260401", "20260401") + .await + .expect_err( + "an empty voucher response must not be accepted when the pinned company book \ + extent cannot be confirmed", + ); + server.await.expect("synthetic Tally server task"); + assert!( + error + .to_string() + .contains("empty voucher response could not confirm the pinned company book extent"), + "unexpected error: {error:#}" + ); +} + +/// Same empty voucher response as above, but this time the out-of-band +/// book-extent bracket confirms the pinned company is still selected and +/// stable -- so the empty result is accepted. +#[tokio::test] +async fn empty_voucher_response_is_accepted_when_bracket_confirms_pinned_company() { + let identity = captured_company_identity(); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let empty_vouchers = native_voucher_collection_xml(&[]); + let confirmed_extent = COMPANY_EXTENT_V2.to_string(); + let status = "TallyPrime Server is Running"; + let steps: Vec<(&str, String, bool)> = vec![ + ("POST / HTTP/1.1", empty_vouchers, false), + ("POST / HTTP/1.1", confirmed_extent.clone(), false), + ("GET /status HTTP/1.1", status.to_string(), true), + ("POST / HTTP/1.1", confirmed_extent, false), + ("GET /status HTTP/1.1", status.to_string(), true), + ]; + let request_count = steps.len(); + let server = tokio::spawn(async move { + for (index, (expected_prefix, body, is_status)) in steps.into_iter().enumerate() { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) + .await + .unwrap_or_else(|_| panic!("request {index} timed out")) + .expect("accept synthetic Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!( + String::from_utf8_lossy(&request).starts_with(expected_prefix), + "request {index} did not follow the voucher-then-extent-bracket sequence" + ); + let response = if is_status { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write synthetic Tally response"); + } + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let vouchers = client + .fetch_vouchers(&identity, "20260401", "20260401") + .await + .expect("an empty voucher response confirmed by the extent bracket must be accepted"); + server.await.expect("synthetic Tally server task"); + assert!(vouchers.is_empty()); + assert_eq!( + request_count, 5, + "the empty path must pay for exactly one voucher read plus the extent bracket" + ); +} + +/// A non-empty voucher response keeps its existing row-GUID binding and +/// must not pay for the extent bracket -- the common case stays a single +/// request. +#[tokio::test] +async fn non_empty_voucher_response_issues_no_extra_request() { + let identity = captured_company_identity(); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let voucher_row = + SYNTHETIC_VOUCHER_ROW.replace("synthetic-company-guid", CAPTURED_COMPANY_GUID); + let non_empty_vouchers = native_voucher_collection_xml(&[&voucher_row]); + + let server = tokio::spawn(async move { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(2), listener.accept()) + .await + .expect("voucher request timed out") + .expect("accept synthetic Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!( + String::from_utf8_lossy(&request).starts_with("POST / HTTP/1.1"), + "unexpected request for the non-empty voucher read" + ); + socket + .write_all(&utf16_xml_response(non_empty_vouchers)) + .await + .expect("write synthetic Tally response"); + + // A non-empty response must not pay for the extent bracket: no + // further connection should ever arrive. + let extra = tokio::time::timeout(Duration::from_millis(300), listener.accept()).await; + assert!( + extra.is_err(), + "non-empty voucher fetch issued an unexpected extra request" + ); + }); + + let client = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client"); + let vouchers = client + .fetch_vouchers(&identity, "20260401", "20260401") + .await + .expect("non-empty voucher fetch must still succeed exactly as today"); + server.await.expect("synthetic Tally server task"); + assert_eq!(vouchers.len(), 1); +} + +#[tokio::test] +async fn capability_probe_reports_only_observed_xml_support() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + for (index, body) in [ + "LOCAL STATUS HEURISTIC UNRECOGNIZED", + "
11
guid-110000120260401TallyPrimeNoYesNo
", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!(!request.is_empty(), "synthetic Tally request must not be empty"); + let response = if index == 0 { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write Tally response"); + } + }); + + let probe = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .probe() + .await + .expect("probe synthetic Tally endpoint"); + server.await.expect("synthetic Tally server task"); + + assert!(probe.connection.reachable); + assert!(!probe.connection.compatible); + assert_eq!(probe.companies.len(), 1); + assert_eq!( + probe.profile.transports[&TransportId::XmlHttp].state, + CapabilityState::Supported + ); + assert_eq!( + probe.profile.packs[&CapabilityPackId::CoreAccounting].state, + CapabilityState::Unknown + ); + assert_eq!(probe.profile.product, "TallyPrime"); + assert!(probe.profile.release.is_none()); + assert_eq!(probe.profile.mode.as_deref(), Some("Licensed")); + assert_eq!(probe.profile.profile_version, 4); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::ProductAndMode].state, + CapabilityState::Supported + ); + let boundary = crate::tally::TallyRuntime::default() + .master_ledger_export_boundary_profile_from_profile(Some(&probe.profile)); + assert_eq!(boundary, DateBoundaryProfile::ModeAgnostic); + assert!(NativeLedgerExportPeriod::new( + boundary, + TallyDate::parse("20240115").expect("valid mid-month date"), + TallyDate::parse("20240115").expect("valid mid-month date"), + ) + .is_ok()); + for transport in [TransportId::TdlCompanion, TransportId::Odbc] { + let evidence = &probe.profile.transports[&transport]; + assert_eq!(evidence.state, CapabilityState::Unknown); + assert_eq!(evidence.confidence, EvidenceConfidence::Unknown); + assert_eq!( + evidence.safe_reason_code.as_deref(), + Some("configuration_not_observed") + ); + } + assert_eq!( + probe.profile.features[&CapabilityFeatureId::EndpointReachability].state, + CapabilityState::Supported + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::LoadedCompanies].state, + CapabilityState::Supported + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity].state, + CapabilityState::Supported + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::EncodingBehaviour] + .safe_reason_code + .as_deref(), + Some("utf16_le_bom_observed") + ); + for feature in [ + CapabilityFeatureId::PracticalResponseLimit, + CapabilityFeatureId::LedgerRead, + CapabilityFeatureId::VoucherRead, + CapabilityFeatureId::Write, + ] { + assert_eq!( + probe.profile.features[&feature].state, + CapabilityState::Unknown + ); + } +} + +#[tokio::test] +async fn capability_probe_records_unavailable_product_mode_evidence_without_refusing() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + let responses = [ + utf8_status_response("TallyPrime Server is Running"), + utf16_xml_response( + "
10
Capability collection unavailable
", + ), + utf16_xml_response( + "Synthetic Companyguid-1", + ), + ]; + for response in responses { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!( + !request.is_empty(), + "synthetic Tally request must not be empty" + ); + socket + .write_all(&response) + .await + .expect("write Tally response"); + } + }); + + let probe = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .probe() + .await + .expect("unavailable product/mode evidence must not refuse the probe"); + server.await.expect("synthetic Tally server task"); + + assert_eq!(probe.profile.product, "Unknown"); + assert!(probe.profile.mode.is_none()); + assert_eq!( + crate::tally::TallyRuntime::default() + .master_ledger_export_boundary_profile_from_profile(Some(&probe.profile)), + DateBoundaryProfile::ModeAgnostic + ); + let evidence = &probe.profile.features[&CapabilityFeatureId::ProductAndMode]; + assert_eq!(evidence.state, CapabilityState::Unknown); + assert_eq!(evidence.confidence, EvidenceConfidence::Observed); + assert_eq!( + evidence.safe_reason_code.as_deref(), + Some("product_mode_evidence_unavailable") + ); +} + +/// `fetch_companies` now requests the native `Company` collection +/// (`ReadOnlyProfile::CompanyListV2`) instead of the legacy `CompanyListV1` +/// custom TDL report. This asserts the request itself carries that shape -- +/// `TYPE=Collection`, no `REPORT`/`FORM`/`PART`/`LINE`/`FIELD` stack, and no +/// `SVCURRENTCOMPANY` scoping a discovery read to one company -- and that a +/// company row's `NAME` attribute and nested `GUID` are trimmed and returned. +#[tokio::test] +async fn interactive_company_fetch_sends_the_native_collection_request() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + let body = r#"
11
guid-1
"#; + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + let body_start = request + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|position| position + 4) + .expect("POST request has complete HTTP headers"); + let post_xml = bridge_tally_protocol::decode_tally_text_bytes_limited( + &request[body_start..], + request.len(), + ) + .expect("POST request uses decodable UTF-16 XML") + .text; + assert_company_collection_request_shape(&post_xml); + assert!(!post_xml.contains("
11
"#; + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!( + !request.is_empty(), + "synthetic Tally request must not be empty" + ); + socket + .write_all(&utf16_xml_response(body)) + .await + .expect("write Tally response"); + }); + + let error = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .fetch_companies() + .await + .expect_err("a company row without a GUID must fail closed"); + server.await.expect("synthetic Tally server task"); + assert!(error.to_string().contains("GUID")); + let retained = error + .downcast_ref::() + .expect("parser refusal retains its typed wire-evidence boundary"); + assert!(retained.evidence.bytes > 0); +} + +#[tokio::test] +async fn direct_company_bootstrap_uses_only_the_shaped_collection_identity() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + // `fetch_companies` (the first of the two reads below) now requests the + // native `Company` collection (`ReadOnlyProfile::CompanyListV2`), so its + // response carries that shape rather than the legacy `CompanyListV1` + // direct report. Its GUID must still not escape into the returned + // identity -- only the second, scoped `standard` read may do that. + let discovered = r#"
11
scoped-guid10000120260401
"#; + let standard = "
11
ledger-guidPrimaryscoped-guidSynthetic Company1033
"; + let server = tokio::spawn(async move { + for body in [discovered, standard] { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!(!request.is_empty()); + socket + .write_all(&utf16_xml_response(body)) + .await + .expect("write Tally response"); + } + }); + + let company = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .bootstrap_direct_company("Synthetic Company") + .await + .expect("strict scoped bootstrap should succeed"); + server.await.expect("synthetic Tally server task"); + + assert_eq!(company.name, "Synthetic Company"); + assert_eq!(company.guid.as_deref(), Some("scoped-guid")); + assert_eq!(company.company_number.as_deref(), Some("100001")); + assert_eq!(company.books_from.as_deref(), Some("20260401")); +} + +#[tokio::test] +async fn capability_probe_does_not_promote_a_direct_company_report() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + // This responder gives the same untrusted bare direct company + // report regardless of what is requested: once for the `V2` + // collection attempt (which the collection parser rejects, since + // it never satisfies `HEADER/STATUS`), and once more for the + // `V1` fallback that follows. + for (index, body) in [ + "LOCAL STATUS HEURISTIC UNRECOGNIZED", + "Synthetic Companyguid-1", + "Synthetic Companyguid-1", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!(!request.is_empty(), "synthetic Tally request must not be empty"); + let response = if index == 0 { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write Tally response"); + } + }); + + let probe = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .probe() + .await + .expect("probe synthetic Tally endpoint"); + server.await.expect("synthetic Tally server task"); + + assert!(probe.connection.reachable); + assert!(probe.companies.is_empty()); + assert_eq!( + probe.profile.transports[&TransportId::XmlHttp].state, + CapabilityState::Unknown + ); + assert_eq!( + probe.profile.transports[&TransportId::XmlHttp] + .safe_reason_code + .as_deref(), + Some("direct_company_report_untrusted") + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::CompanyRead].state, + CapabilityState::Unknown + ); +} + +#[tokio::test] +async fn capability_probe_does_not_promote_a_shaped_company_failure_to_xml_support() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + // Same shaped `STATUS=0` failure for both the `V2` collection + // attempt and the `V1` fallback that follows it. + for (index, body) in [ + "TallyPrime Server is Running", + "
0
Could not find Company ''
", + "
0
Could not find Company ''
", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + assert!(!request.is_empty(), "synthetic Tally request must not be empty"); + let response = if index == 0 { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write Tally response"); + } + }); + + let probe = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .probe() + .await + .expect("probe synthetic Tally endpoint"); + server.await.expect("synthetic Tally server task"); + + assert!(probe.companies.is_empty()); + let xml = &probe.profile.transports[&TransportId::XmlHttp]; + assert_eq!(xml.state, CapabilityState::Unknown); + assert_eq!(xml.confidence, EvidenceConfidence::Observed); + assert_eq!(xml.safe_reason_code.as_deref(), Some("company_not_loaded")); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::LoadedCompanies].state, + CapabilityState::Unknown + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity].state, + CapabilityState::Unknown + ); +} + +/// A gateway-shaped `Company` collection response with two loaded +/// synthetic companies and the `CMPINFO` counter trap included. Proves +/// `probe` requests `CompanyListV2` on the happy path +/// and trusts its success without ever falling back to the legacy +/// `CompanyListV1` report: the mock server has exactly one POST response +/// queued, so a fallback request would hang and fail this test. +#[tokio::test] +async fn capability_probe_marks_presentation_equivalent_guid_siblings_ambiguous() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic Tally server"); + let address = listener.local_addr().expect("synthetic Tally address"); + let server = tokio::spawn(async move { + let mut requests = Vec::new(); + for (index, body) in [ + "TallyPrime Server is Running", + "\n
11
\n 0\n \n Synthetic Company Asynthetic-guid-a10000120260401TallyPrimeNoYesNo\n synthetic company a SYNTHETIC-GUID-A10000220270401TallyPrimeNoYesNo\n \n \n
", + ] + .into_iter() + .enumerate() + { + let (mut socket, _) = listener.accept().await.expect("accept Tally request"); + let request = read_complete_http_request(&mut socket).await; + requests.push(request); + let response = if index == 0 { + utf8_status_response(body) + } else { + utf16_xml_response(body) + }; + socket + .write_all(&response) + .await + .expect("write Tally response"); + } + requests + }); + + let probe = TallyClient::new(TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }) + .expect("build synthetic Tally client") + .probe() + .await + .expect("probe synthetic Tally endpoint"); + let requests = server.await.expect("synthetic Tally server task"); + + assert_eq!(probe.companies.len(), 2); + assert_eq!(probe.companies[0].name, "Synthetic Company A"); + assert_eq!(probe.companies[0].guid.as_deref(), Some("synthetic-guid-a")); + assert_eq!(probe.companies[1].name, "synthetic company a"); + assert_eq!(probe.companies[1].guid.as_deref(), Some("SYNTHETIC-GUID-A")); + assert_eq!(probe.profile.product, "TallyPrime"); + assert_eq!(probe.profile.mode.as_deref(), Some("Licensed")); + assert_eq!( + probe.profile.transports[&TransportId::XmlHttp].state, + CapabilityState::Supported + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity] + .safe_reason_code + .as_deref(), + Some("company_identity_display_scope_ambiguous") + ); + assert_eq!( + probe.profile.features[&CapabilityFeatureId::StableCompanyIdentity].state, + CapabilityState::Unknown + ); + + let post_request = requests + .iter() + .find(|request| request.starts_with(b"POST")) + .expect("exactly one POST request was sent"); + let body_start = post_request + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|position| position + 4) + .expect("POST request has complete HTTP headers"); + let post_xml = bridge_tally_protocol::decode_tally_text_bytes_limited( + &post_request[body_start..], + post_request.len(), + ) + .expect("POST request uses decodable UTF-16 XML"); + assert_company_collection_request_shape(&post_xml.text); + assert!(post_xml.text.contains("BridgeCompanyExtent")); +} diff --git a/src-tauri/src/tally/runtime.rs b/src-tauri/src/tally/runtime.rs index e35fb68c..7eddfa24 100644 --- a/src-tauri/src/tally/runtime.rs +++ b/src-tauri/src/tally/runtime.rs @@ -3486,1956 +3486,5 @@ fn map_execution_error(error: ReadExecutionError) -> anyhow::Erro } #[cfg(test)] -mod tests { - use super::*; - use crate::commands::VerifiedCompanyIdentity; - use crate::tally::TallyProduct; - use anyhow::Context; - use bridge_tally_core::CapabilityProfile; - use std::collections::BTreeMap; - use tally_protocol_simulator::Fixture; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - fn verified_identity(name: &str, guid: &str) -> VerifiedCompanyIdentity { - VerifiedCompanyIdentity::test_fixture(name, guid) - } - - fn captured_aarav_company_list_and_identity() -> (String, VerifiedCompanyIdentity) { - let bytes = include_bytes!("../../crates/bridge-tally-protocol/tests/fixtures/agent/native-licensed-release-companies.utf16le.xml"); - let xml = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( - bytes, - "text/xml; charset=utf-16", - bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, - bytes.len(), - ) - .expect("captured Company collection decodes") - .text; - let rows = parse_companies_from_collection(&xml).unwrap(); - let row = rows - .iter() - .find(|row| row.name == "Aarav Trading Company Demo") - .unwrap(); - let identity = VerifiedCompanyIdentity::from_observed_companies( - row.name.clone(), - row.guid.clone().unwrap(), - row.company_number.clone().unwrap(), - row.books_from.clone().unwrap(), - &rows, - ) - .unwrap(); - (xml, identity) - } - - #[test] - fn agent_company_list_evidence_hashes_the_encoded_utf16_response_bytes() { - let response = "
1
g-1120260401
".to_string(); - let encoded = bridge_tally_protocol::encode_tally_xml_request_utf16le(&response); - let expected_bytes = encoded.len(); - let expected_sha = Sha256::digest(&encoded) - .iter() - .map(|byte| format!("{byte:02x}")) - .collect::(); - let listed = - agent_company_list_from_response(response, expected_bytes, expected_sha.clone()) - .expect("company list"); - assert_eq!(listed.response_bytes, expected_bytes); - assert_eq!(listed.response_sha256, expected_sha); - assert_eq!(listed.companies.len(), 1); - } - - #[test] - fn party_ledger_master_evidence_includes_currency_probe_and_source_responses() { - let source = |master_response_sha256: &str| PartyLedgerMasterSource { - company: "Synthetic Books".to_string(), - company_guid: "company-guid".to_string(), - currency_assertion: OutstandingsCurrencyAssertion::Inr, - currency_decimal_places: 2, - from: TallyDate::parse("20260401").expect("source from"), - to: TallyDate::parse("20260731").expect("source to"), - rows: Vec::new(), - request_sha256: "0".repeat(64), - master_response_sha256: master_response_sha256.to_string(), - balance_response_sha256: "b".repeat(64), - group_response_sha256: "c".repeat(64), - master_response_bytes: 11, - balance_response_bytes: 13, - group_response_bytes: 17, - groups: Vec::new(), - }; - let currency = - RuntimeReadEvidence::paired("", sha256_hex(b"currency-response"), 19); - let baseline = TallyRuntime::party_ledger_master_source_evidence( - &source(&"a".repeat(64)), - currency.clone(), - ); - let changed = TallyRuntime::party_ledger_master_source_evidence( - &source(&"d".repeat(64)), - currency.clone(), - ); - let changed_currency = TallyRuntime::party_ledger_master_source_evidence( - &source(&"a".repeat(64)), - RuntimeReadEvidence::paired( - "", - sha256_hex(b"changed-currency-response"), - 23, - ), - ); - let source_scope = "0".repeat(64); - let source_responses = sha256_hex( - format!("{}:{}:{}", "a".repeat(64), "b".repeat(64), "c".repeat(64)).as_bytes(), - ); - assert_eq!( - baseline.request_sha256, - sha256_hex(format!("{}:{source_scope}", currency.request_sha256).as_bytes()) - ); - assert_eq!( - baseline.response_sha256, - sha256_hex(format!("{}:{source_responses}", currency.response_sha256).as_bytes()) - ); - assert_eq!(baseline.request_sha256, changed_currency.request_sha256); - assert_ne!(baseline.response_sha256, changed_currency.response_sha256); - assert_eq!(changed_currency.bytes, (11 + 13 + 17 + 23) * 2); - - assert_eq!(baseline.request_sha256, changed.request_sha256); - assert_ne!(baseline.response_sha256, changed.response_sha256); - assert_eq!(baseline.bytes, (11 + 13 + 17 + 19) * 2); - } - - #[test] - fn paired_evidence_commits_to_utf16le_request_body_not_utf8_source() { - let request = "₹ & Co"; - let mut encoded = vec![0xff, 0xfe]; - encoded.extend(request.encode_utf16().flat_map(u16::to_le_bytes)); - let evidence = RuntimeReadEvidence::paired(request, "r".repeat(64), 7); - assert_eq!(evidence.request_sha256, sha256_hex(&encoded)); - assert_ne!(evidence.request_sha256, sha256_hex(request.as_bytes())); - assert_eq!(evidence.bytes, 14); - } - - #[test] - fn outstandings_wire_evidence_changes_with_any_native_report_response() { - let receivable = RuntimeReadEvidence::paired("", sha256_hex(b"r1"), 3); - let payable = RuntimeReadEvidence::paired("", sha256_hex(b"p1"), 5); - let baseline = receivable.clone().combine(payable.clone()); - let changed = receivable.combine(RuntimeReadEvidence::paired( - "", - sha256_hex(b"p2"), - 5, - )); - - assert_eq!(baseline.bytes, 16, "both paired responses are counted"); - assert_ne!(baseline.response_sha256, changed.response_sha256); - assert_ne!(baseline.request_sha256, String::new()); - } - - fn utf16_xml_response(body: impl AsRef) -> Vec { - let body = bridge_tally_protocol::encode_tally_xml_request_utf16le(body.as_ref()); - utf16_xml_response_bytes(&body) - } - - fn utf16_xml_response_bytes(body: &[u8]) -> Vec { - let headers = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-16\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - body.len() - ); - [headers.as_bytes(), body].concat() - } - - fn utf8_status_response(body: impl AsRef) -> Vec { - let body = body.as_ref().as_bytes(); - let headers = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - body.len() - ); - [headers.as_bytes(), body].concat() - } - - async fn read_http_request(socket: &mut tokio::net::TcpStream) -> Vec { - let mut request = Vec::new(); - let mut buffer = [0_u8; 4096]; - let (header_end, content_length) = loop { - let read = socket.read(&mut buffer).await.expect("read request"); - assert!(read > 0, "request ended before headers completed"); - request.extend_from_slice(&buffer[..read]); - let Some(header_end) = request.windows(4).position(|bytes| bytes == b"\r\n\r\n") else { - continue; - }; - let header = std::str::from_utf8(&request[..header_end]) - .expect("request headers are valid UTF-8"); - let content_length = header - .lines() - .find_map(|line| { - let (name, value) = line.split_once(':')?; - name.eq_ignore_ascii_case("content-length") - .then_some(value.trim()) - }) - .map(|value| { - value - .parse::() - .expect("request Content-Length is numeric") - }) - .unwrap_or(0); - break (header_end + 4, content_length); - }; - while request.len() < header_end + content_length { - let read = socket.read(&mut buffer).await.expect("read request body"); - assert!(read > 0, "request ended before declared body completed"); - request.extend_from_slice(&buffer[..read]); - } - request - } - - #[test] - fn ageing_anchor_serializes_as_an_explicit_wire_contract() { - assert_eq!( - serde_json::to_value(OutstandingsAgeingAnchor::DueDate).unwrap(), - serde_json::json!("due_date") - ); - assert_eq!( - serde_json::to_value(OutstandingsAgeingAnchor::BillDate).unwrap(), - serde_json::json!("bill_date") - ); - } - - #[test] - fn outstandings_read_strategy_serializes_as_an_explicit_wire_contract() { - assert_eq!( - serde_json::to_value(OutstandingsReadStrategy::NativeBills).unwrap(), - serde_json::json!("native_bills") - ); - assert_eq!( - serde_json::to_value(OutstandingsReadStrategy::VoucherScan).unwrap(), - serde_json::json!("voucher_scan") - ); - } - - #[test] - fn single_company_forex_ledger_capture_returns_a_typed_partial() { - const FOREX_LEDGER_CAPTURE: &[u8] = include_bytes!( - "../../crates/bridge-tally-protocol/tests/fixtures/ledgers_forex_composite_live.utf16le.xml" - ); - let ledger_body = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( - FOREX_LEDGER_CAPTURE, - "text/xml; charset=utf-16", - bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, - FOREX_LEDGER_CAPTURE.len(), - ) - .expect("captured forex ledger response decodes") - .text; - let admitted = admit_native_ledger_snapshot( - parse_native_ledger_snapshot(&ledger_body) - .map_err(anyhow::Error::from) - .context("stable native ledger snapshot"), - ) - .expect("a foreign-currency ledger is an in-band partial"); - - assert!(matches!( - admitted, - NativeLedgerSnapshotAdmission::Partial(OutstandingsLoadResult::Partial { reason, .. }) - if reason.reason_code == "company_foreign_currency_ledger_balance" - && reason.foreign_currency_ledger_name.as_deref() == Some("FX USD Debtor 02") - )); - } - - #[tokio::test] - async fn single_company_read_returns_the_forex_capture_partial() { - const EXTENT: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/agent/native-company-book-extents-with-number.utf8.xml" - ); - const FOREX_LEDGER_CAPTURE: &[u8] = include_bytes!( - "../../crates/bridge-tally-protocol/tests/fixtures/ledgers_forex_composite_live.utf16le.xml" - ); - const RECEIVABLE: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_receivable_aarav.xml" - ); - const PAYABLE: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_payable_aarav.xml" - ); - const STATUS: &str = "TallyPrime Server is Running"; - let (company_list, identity) = captured_aarav_company_list_and_identity(); - - let extent = EXTENT; - - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic outstandings server"); - let address = listener.local_addr().expect("synthetic server address"); - let server = tokio::spawn(async move { - let mut source_post_index = 0; - let mut company_post_index = 0; - for index in 0..30 { - let (mut socket, _) = - tokio::time::timeout(std::time::Duration::from_secs(2), listener.accept()) - .await - .expect("outstandings request timed out") - .expect("accept outstandings request"); - let request = read_http_request(&mut socket).await; - let response = if request.starts_with(b"GET /status") { - utf8_status_response(STATUS) - } else { - assert!(request.starts_with(b"POST /"), "request {index}"); - let body_start = request - .windows(4) - .position(|bytes| bytes == b"\r\n\r\n") - .expect("complete request headers") - + 4; - let xml = bridge_tally_protocol::decode_tally_text_bytes_limited( - &request[body_start..], - request.len() - body_start, - ) - .expect("decode request") - .text; - if xml.contains("BridgeCompanyExtent") - && !xml.contains("") - { - company_post_index += 1; - utf16_xml_response(&company_list) - } else { - let response = match source_post_index { - 0 | 1 | 10 | 11 => utf16_xml_response(extent), - 2 | 3 => utf16_xml_response(RECEIVABLE), - 6 | 7 => utf16_xml_response(PAYABLE), - _ => utf16_xml_response_bytes(FOREX_LEDGER_CAPTURE), - }; - source_post_index += 1; - response - } - }; - socket.write_all(&response).await.expect("write response"); - } - assert_eq!(company_post_index, 4); - assert_eq!(source_post_index, 12); - }); - - let result = TallyRuntime::default() - .fetch_outstandings( - TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }, - &identity, - TallyDate::parse("20260401").expect("captured book as-of"), - OutstandingsCurrencyAssertion::Inr, - OutstandingsAgeingAnchor::DueDate, - ) - .await - .expect("foreign-currency capture returns an in-band partial"); - - assert!(matches!( - result, - OutstandingsLoadResult::Partial { reason, .. } - if reason.reason_code == "company_foreign_currency_ledger_balance" - && reason.foreign_currency_ledger_name.as_deref() == Some("FX USD Debtor 02") - )); - server.await.expect("synthetic outstandings server task"); - } - - #[test] - fn validation_lab_future_bill_stays_unaged_in_open_bill_output() { - let receivable = parse_native_bill_rows( - include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_receivable_validation_lab.xml" - ), - &TallyDate::parse("20250401").expect("captured BooksFrom"), - &TallyDate::parse("20260817").expect("capture as-of"), - ) - .expect("captured validation-book rows parse"); - let rows = all_open_bill_rows( - &receivable, - &[], - OutstandingsAgeingAnchor::DueDate, - &TallyDate::parse("20260817").expect("capture as-of"), - ); - let future = rows - .iter() - .find(|row| row.reference == "ALPHA-FUTURE") - .expect("captured future-due bill remains present"); - assert_eq!(future.amount.as_str(), "22222.00"); - assert_eq!(future.age_days, None); - - let bill_date_rows = all_open_bill_rows( - &receivable, - &[], - OutstandingsAgeingAnchor::BillDate, - &TallyDate::parse("20260817").expect("capture as-of"), - ); - let bill_date_future = bill_date_rows - .iter() - .find(|row| row.reference == "ALPHA-FUTURE") - .expect("captured future-due bill remains present for bill-date ageing"); - assert!( - bill_date_future.age_days.is_some(), - "the selected bill-date basis must not reuse the future due date" - ); - } - - #[test] - fn settled_native_bill_rows_do_not_reach_statement_or_export_sources() { - let books_from = TallyDate::parse("20250401").expect("synthetic BooksFrom"); - let as_of = TallyDate::parse("20260817").expect("synthetic as-of"); - let parsed = parse_native_bill_rows( - "1-Aug-26SETTLED\ - Synthetic Customer0.00\ - 1-Aug-2616", - &books_from, - &as_of, - ) - .expect("synthetic settled row parses at the wire boundary"); - - assert_eq!(parsed.len(), 1, "the fixture proves the native row existed"); - assert!( - all_open_bill_rows(&parsed, &[], OutstandingsAgeingAnchor::DueDate, &as_of).is_empty(), - "a zero closing balance is not an open bill" - ); - } - - #[test] - fn captured_validation_lab_bytes_render_a_complete_working_paper_end_to_end() { - const COMPANY: &str = "Bridge Validation Lab"; - const COMPANY_GUID: &str = "c6afd306-00e1-4f51-802a-babe44daddd3"; - let books_from = TallyDate::parse("20250401").expect("captured BooksFrom"); - let as_of = TallyDate::parse("20260801").expect("captured as-of date"); - let receivable_xml = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_receivable_validation_lab.xml" - ); - let payable_xml = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_payable_validation_lab.xml" - ); - let groups_xml = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/group_snapshot_validation_lab.xml" - ); - let ledgers_xml = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/native/ledger_snapshot_validation_lab.xml" - ); - - let receivable = parse_native_bill_rows(receivable_xml, &books_from, &as_of) - .expect("captured receivable rows parse"); - let payable = parse_native_bill_rows(payable_xml, &books_from, &as_of) - .expect("captured payable rows parse"); - // This captured Group body predates the request's response-bound - // compute. Add only that collection context for the parser contract; - // source-byte accounting below remains the exact captured response. - let groups_with_response_company_guid = groups_xml.replace( - "", - "c6afd306-00e1-4f51-802a-babe44daddd3", - ); - let groups = parse_native_group_snapshot(&groups_with_response_company_guid, COMPANY_GUID) - .expect("captured group identity and ancestry parse"); - let ledgers = - parse_native_ledger_snapshot(ledgers_xml).expect("captured ledger controls parse"); - let source_bytes = receivable_xml - .len() - .checked_add(payable_xml.len()) - .and_then(|value| value.checked_add(groups_xml.len())) - .and_then(|value| value.checked_add(ledgers_xml.len())) - .expect("captured source byte count fits usize"); - let computed = compute_native_outstandings( - COMPANY, - &receivable, - &payable, - NativeMasterSnapshot { - ledgers: &ledgers, - groups: NativeGroupSnapshot::Complete(&groups), - }, - NativeAgeingAnchor::DueDate, - &as_of, - source_bytes, - ) - .expect("captured native controls compute"); - assert_eq!( - native_crosscheck_partial_reason(&computed, &as_of), - None, - "captured overdue counters must positively prove the requested date" - ); - - let statement_open_bills = all_open_bill_rows( - &receivable, - &payable, - OutstandingsAgeingAnchor::DueDate, - &as_of, - ); - assert_eq!(statement_open_bills.len(), 6); - assert!(statement_open_bills.iter().all(|row| !row.amount.is_zero())); - let statement_unallocated_by_party = all_unallocated_parties(&computed.residuals); - let result = OutstandingsLoadResult::Complete { - report: Box::new(computed.report), - read_strategy: OutstandingsReadStrategy::NativeBills, - currency_assertion: OutstandingsCurrencyAssertion::Inr, - ageing_anchor: OutstandingsAgeingAnchor::DueDate, - synced_at_unix_ms: 1_777_000_000_000, - unallocated_total: Some(computed.residual_total), - statement_unallocated_by_party, - statement_open_bills, - }; - let source = crate::reports::outstandings_working_paper_store::source_from_complete_result( - &result, - COMPANY_GUID, - ) - .expect("captured source stays inside export budgets") - .expect("captured complete result substantiates a source"); - assert_eq!(source.source_bytes, 33_575); - assert_eq!(source.open_bills.len(), 6); - let paper = - crate::reports::outstandings_working_paper::build_outstandings_working_paper(source) - .expect("captured exact controls reconcile into a working paper"); - let workbook = crate::reports::outstandings_working_paper_xlsx::render_outstandings_working_paper_xlsx(&paper) - .expect("captured working paper renders"); - assert!(workbook.len() > 200); - assert_eq!(&workbook[0..2], b"PK"); - } - - #[test] - fn raw_overdue_disagreement_withholds_native_complete() { - let rows = parse_native_bill_rows( - "1-Jul-26MISMATCH\ - Synthetic Customer-100.00\ - 1-Jul-2631", - &TallyDate::parse("20250401").expect("synthetic BooksFrom"), - &TallyDate::parse("20260817").expect("synthetic as-of"), - ) - .expect("raw bill parses"); - let computed = compute_native_outstandings( - "Synthetic Company", - &rows, - &[], - NativeMasterSnapshot { - ledgers: &[], - groups: NativeGroupSnapshot::LegacyFixtureWithoutGroups, - }, - NativeAgeingAnchor::DueDate, - &TallyDate::parse("20260817").expect("synthetic as-of"), - 0, - ) - .expect("arithmetic still computes for diagnostic comparison"); - assert_eq!( - computed.overdue_crosscheck, - NativeOverdueCrosscheck::Inconsistent - ); - assert_eq!( - native_crosscheck_partial_reason(&computed, &TallyDate::parse("20260817").unwrap()), - Some(OutstandingsPartialReason::code( - "native_overdue_crosscheck_mismatch" - )) - ); - } - - #[test] - fn zero_bill_rows_with_a_ledger_residual_withhold_native_complete() { - let groups = parse_native_group_snapshot( - r#"
1
- 11111111-1111-1111-1111-111111111111-0000000111111111-1111-1111-1111-111111111111Primary -
"#, - "11111111-1111-1111-1111-111111111111", - ) - .expect("synthetic group snapshot parses"); - let ledgers = parse_native_ledger_snapshot( - r#"
1
- Sundry Debtors-100.000No -
"#, - ) - .expect("synthetic ledger snapshot parses"); - let requested_as_of = TallyDate::parse("20260817").expect("synthetic as-of"); - let computed = compute_native_outstandings( - "Synthetic Company", - &[], - &[], - NativeMasterSnapshot { - ledgers: &ledgers, - groups: NativeGroupSnapshot::Complete(&groups), - }, - NativeAgeingAnchor::DueDate, - &requested_as_of, - 0, - ) - .expect("the residual is a partial-result diagnostic, not a parser failure"); - - assert_eq!( - computed.overdue_crosscheck, - NativeOverdueCrosscheck::UnconfirmedAsOfWithoutBillReferences - ); - assert_eq!( - native_crosscheck_partial_reason(&computed, &requested_as_of), - Some(OutstandingsPartialReason::code( - WarningCode::NativeOutstandingsAsOfUnconfirmedWithoutBillReferences.as_str(), - )) - ); - } - - #[test] - fn raw_empty_bill_references_preserve_each_amount_and_get_explicit_label() { - let raw_bills = "\ - 1-Jul-26Synthetic Customer\ - -40.001-Jul-2630\ - 2-Jul-26 \n\t Synthetic Customer\ - -60.002-Jul-2629\ - "; - let as_of = TallyDate::parse("20260731").expect("synthetic as-of"); - let parsed = parse_native_bill_rows( - raw_bills, - &TallyDate::parse("20250401").expect("synthetic BooksFrom"), - &as_of, - ) - .expect("paired empty BILLREF values remain parseable"); - let rows = all_open_bill_rows(&parsed, &[], OutstandingsAgeingAnchor::DueDate, &as_of); - - assert_eq!(rows.len(), 2, "empty identities must not collapse rows"); - let total = rows - .iter() - .try_fold(ExactDecimal::zero(), |sum, row| { - sum.checked_add(&row.amount) - }) - .expect("synthetic bill total remains exact"); - assert_eq!(total.as_str(), "100", "neither amount may be lost"); - assert_eq!( - rows.iter() - .map(|row| row.reference.as_str()) - .collect::>(), - vec!["No reference reported", "No reference reported"], - "client-facing rows must disclose the missing identity", - ); - } - - #[test] - fn party_master_currency_assertion_rejects_a_changed_company_extent() { - const EXTENT: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/unit_a_company_extent_live.xml" - ); - let read_extent_xml = EXTENT.replacen( - r#"bb8ad19e-6aef-4239-a917-87fec0c6215e"#, - r#"bb8ad19e-6aef-4239-a917-87fec0c6215e1"#, - 1, - ); - let changed_extent_xml = read_extent_xml.replace( - "20260401", - "20260402", - ); - let read_extent = bridge_tally_protocol::outstandings_shared::parse_company_book_extent( - &read_extent_xml, - "Aarav Trading Company Demo", - "bb8ad19e-6aef-4239-a917-87fec0c6215e", - ) - .expect("captured extent parses"); - let changed_extent = bridge_tally_protocol::outstandings_shared::parse_company_book_extent( - &changed_extent_xml, - "Aarav Trading Company Demo", - "bb8ad19e-6aef-4239-a917-87fec0c6215e", - ) - .expect("changed synthetic extent parses"); - let assertion = CompanyCurrencyRead { - currency: CompanyCurrency { - symbol: "₹".to_string(), - mailing_name: "INR".to_string(), - currency_count: 1, - decimal_places: 2, - is_inr: true, - }, - extent: read_extent.clone(), - evidence: RuntimeReadEvidence::empty(), - } - .bind_party_ledger_master_assertion(OutstandingsCurrencyAssertion::Inr); - - assert_eq!( - assertion - .require_opening_extent(&read_extent) - .expect("the observed extent releases INR"), - PartyLedgerMasterCurrency { - assertion: OutstandingsCurrencyAssertion::Inr, - decimal_places: 2, - } - ); - let error = assertion - .require_opening_extent(&changed_extent) - .expect_err("a changed extent must not release INR"); - assert!(matches!( - error.downcast_ref::(), - Some(PairedReadValidationError::CurrencyToMasterExtent) - )); - } - - #[tokio::test] - async fn detect_base_currency_rejects_book_drift_after_the_currency_read() { - const EXTENT: &str = include_str!( - "../../crates/bridge-tally-protocol/tests/fixtures/agent/native-company-book-extents-with-number.utf8.xml" - ); - const CURRENCY: &[u8] = include_bytes!( - "../../crates/bridge-tally-protocol/tests/fixtures/currency_inr_modern_live.utf16le.xml" - ); - const STATUS: &str = "TallyPrime Server is Running"; - let (company_list, identity) = captured_aarav_company_list_and_identity(); - - let currency = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( - CURRENCY, - "text/xml; charset=utf-16", - bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, - CURRENCY.len(), - ) - .expect("captured currency response decodes") - .text; - - let opening_extent = EXTENT; - let closing_extent = opening_extent.replace( - "20260401", - "20260402", - ); - assert_ne!( - closing_extent, opening_extent, - "the drift mutation must apply" - ); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind synthetic currency server"); - let address = listener.local_addr().expect("synthetic server address"); - let server = tokio::spawn(async move { - let mut source_post_index = 0; - for index in 0..13 { - let (mut socket, _) = - tokio::time::timeout(std::time::Duration::from_secs(2), listener.accept()) - .await - .expect("currency request timed out") - .expect("accept currency request"); - let request = read_http_request(&mut socket).await; - let response = if request.starts_with(b"GET /status") { - utf8_status_response(STATUS) - } else { - assert!(request.starts_with(b"POST /"), "request {index}"); - let body_start = request - .windows(4) - .position(|bytes| bytes == b"\r\n\r\n") - .expect("complete request headers") - + 4; - let xml = bridge_tally_protocol::decode_tally_text_bytes_limited( - &request[body_start..], - request.len() - body_start, - ) - .expect("decode request") - .text; - if xml.contains("BridgeCompanyExtent") - && !xml.contains("") - { - utf16_xml_response(&company_list) - } else { - let response = match source_post_index { - 0 | 1 => utf16_xml_response(opening_extent), - 2 | 3 => utf16_xml_response(¤cy), - _ => utf16_xml_response(&closing_extent), - }; - source_post_index += 1; - response - } - }; - socket.write_all(&response).await.expect("write response"); - } - }); - - let runtime = TallyRuntime::default(); - let result = runtime - .detect_base_currency( - TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }, - &identity, - ) - .await; - - let error = result.expect_err("closing extent drift must reject the currency"); - assert!( - error.chain().any(|cause| matches!( - cause.downcast_ref::(), - Some(PairedReadValidationError::CurrencyExtent) - )), - "unexpected error: {error:#}" - ); - server.await.expect("synthetic currency server task"); - } - - fn synthetic_probe_result() -> TallyProbeResult { - TallyProbeResult { - connection: ConnectionStatus { - reachable: true, - compatible: false, - server_text: "Synthetic status".to_string(), - product: TallyProduct::Unknown, - error: None, - }, - companies: vec![TallyCompany { - name: "Synthetic Company".to_string(), - guid: Some("synthetic-guid".to_string()), - company_number: None, - books_from: None, - }], - profile: CapabilityProfile { - profile_version: 2, - product: "Unknown".to_string(), - release: None, - license_tier: None, - mode: None, - transports: BTreeMap::new(), - features: BTreeMap::new(), - packs: BTreeMap::new(), - }, - selected_read_scope: None, - passport_snapshot_id: None, - } - } - - #[test] - fn future_due_bill_from_raw_native_bytes_reaches_the_statement_source() { - let books_from = TallyDate::parse("20260401").expect("synthetic book start"); - let as_of = TallyDate::parse("20260731").expect("synthetic as-of date"); - let bills_xml = "\ - 1-Jul-26FUTURE-1Synthetic Party\ - -100.001-Aug-260\ - "; - let ledger_xml = "
1
"; - let receivable = parse_native_bill_rows(bills_xml, &books_from, &as_of) - .expect("a future due date in raw native bytes must parse"); - let ledgers = parse_native_ledger_snapshot(ledger_xml) - .expect("the synthetic raw ledger response must parse"); - let computed = compute_native_outstandings( - "Synthetic Company", - &receivable, - &[], - NativeMasterSnapshot { - ledgers: &ledgers, - groups: NativeGroupSnapshot::LegacyFixtureWithoutGroups, - }, - NativeAgeingAnchor::DueDate, - &as_of, - bills_xml.len() + ledger_xml.len(), - ) - .expect("a future-due bill must not abort the native computation"); - assert_eq!(computed.report.receivable_total.as_str(), "100"); - assert_eq!(computed.report.top_parties[0].oldest_bill_age_days, None); - assert_eq!(computed.report.ageing.days_0_30.as_str(), "100"); - assert_eq!(computed.report.ageing.days_31_60, ExactDecimal::zero()); - assert_eq!(computed.report.ageing.days_61_90, ExactDecimal::zero()); - assert_eq!(computed.report.ageing.days_90_plus, ExactDecimal::zero()); - assert_eq!(computed.report.open_receivable_bill_count, 1); - - let statement_rows = - all_open_bill_rows(&receivable, &[], OutstandingsAgeingAnchor::DueDate, &as_of); - assert_eq!( - statement_rows.len(), - 1, - "a future-due bill must remain available to the statement source" - ); - assert_eq!(statement_rows[0].amount.as_str(), "100.00"); - assert_eq!(statement_rows[0].age_days, None); - let statement = crate::reports::party_statement::build_party_statement( - "Synthetic Company", - as_of.as_str(), - "Synthetic Party", - &statement_rows, - &[], - ) - .expect("the future-due bill must build a party statement"); - assert_eq!(statement.bills.len(), 1); - assert_eq!(statement.bills[0].reference, "FUTURE-1"); - assert_eq!(statement.bills[0].age_days, None); - assert_eq!(statement.bills[0].bucket, None); - assert_eq!(statement.bill_total.as_str(), "100"); - - let destination = tempfile::tempdir().expect("synthetic destination"); - let approvals = - crate::reports::bulk_party_statement::PartyStatementDestinationApprovals::default(); - let approval_id = approvals - .issue(destination.path().to_path_buf()) - .expect("approve synthetic destination"); - let approved_destination = approvals - .consume(&approval_id, destination.path()) - .expect("consume synthetic approval"); - let bulk = crate::reports::bulk_party_statement::write_bulk_party_statements( - &approved_destination, - "Synthetic Company", - as_of.as_str(), - "xlsx", - &statement_rows, - &[], - |party_statement| { - crate::reports::party_statement_xlsx::render_party_statement_xlsx(party_statement) - .map_err(|error| error.to_string()) - }, - ) - .expect("a bulk run must write the future-due party statement"); - assert_eq!(bulk.written.len(), 1); - let workbook = std::fs::File::open(destination.path().join(&bulk.written[0].file_name)) - .expect("the bulk statement file exists"); - let mut archive = zip::ZipArchive::new(workbook).expect("bulk output is an XLSX archive"); - let mut workbook_text = String::new(); - for entry_name in ["xl/worksheets/sheet1.xml", "xl/sharedStrings.xml"] { - let mut entry = archive - .by_name(entry_name) - .expect("the XLSX statement entry exists"); - std::io::Read::read_to_string(&mut entry, &mut workbook_text) - .expect("the workbook XML is readable"); - } - assert!(workbook_text.contains("FUTURE-1")); - assert!(workbook_text.contains("Not due")); - assert!(workbook_text.contains("Unaged")); - } - - #[test] - fn education_snapshot_period_refusal_is_an_in_band_partial() { - let result = match admit_native_ledger_snapshot_period( - DateBoundaryProfile::EducationRestricted, - TallyDate::parse("20260401").expect("Education-valid book start"), - TallyDate::parse("20260415").expect("ordinary calendar as-of"), - ) { - NativeLedgerSnapshotPeriodAdmission::Period(_) => { - panic!("an Education-refused as-of must not construct a snapshot period") - } - NativeLedgerSnapshotPeriodAdmission::Partial(partial) => partial, - }; - assert!(matches!( - result, - OutstandingsLoadResult::Partial { reason, .. } - if reason.reason_code == "as_of_has_no_valid_window_boundary" - )); - } - - #[cfg(feature = "voucher-scan")] - #[test] - fn outstandings_read_failures_are_partial_and_deadlines_recommend_restart() { - assert_eq!( - outstandings_read_failure_reason(&anyhow::Error::new( - TallyTransportError::RequestTimedOut - )), - "tally_segment_deadline_restart_recommended" - ); - assert_eq!( - outstandings_read_failure_reason(&anyhow::Error::new( - TallyTransportError::RequestFailed - )), - "segment_request_failed" - ); - assert_eq!( - outstandings_read_failure_reason(&anyhow::Error::new( - TallyTransportError::HttpStatus { status: 500 } - )), - "segment_http_status_failure" - ); - assert_eq!( - outstandings_read_failure_reason(&anyhow::anyhow!("connection refused")), - "segment_read_failed" - ); - } - - #[cfg(feature = "voucher-scan")] - #[tokio::test] - async fn outstandings_read_transport_failure_feeds_breaker_and_preserves_typed_partial() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9120, - }; - let result = runtime - .execute( - config, - ReadOperation::VoucherExport, - ReadRetryPolicy::SINGLE_ATTEMPT, - |_client| async { - Err::(anyhow::Error::new( - OutstandingsReadTransportFailure { - reason_code: "tally_segment_deadline_restart_recommended", - source: anyhow::Error::new(TallyTransportError::RequestTimedOut), - }, - )) - }, - ) - .await; - let caller_value = partial_after_outstandings_read_transport_failure(result) - .expect("outstandings read transport failures stay in-band for the caller"); - assert!(matches!( - caller_value, - OutstandingsLoadResult::Partial { reason, .. } - if reason.reason_code == "tally_segment_deadline_restart_recommended" - )); - assert_eq!( - runtime.snapshots().expect("runtime snapshot")[0].consecutive_failures, - 1, - "the breaker must observe the transport failure before UI mapping" - ); - } - - #[cfg(feature = "voucher-scan")] - #[tokio::test] - async fn verification_partial_stays_partial_without_feeding_breaker() { - let runtime = TallyRuntime::default(); - let result = runtime - .execute( - TallyConfig { - host: "localhost".to_string(), - port: 9121, - }, - ReadOperation::VoucherExport, - ReadRetryPolicy::SINGLE_ATTEMPT, - |_client| async { - Ok::<_, anyhow::Error>(partial_result("paired_segment_mismatch")) - }, - ) - .await; - let caller_value = partial_after_outstandings_read_transport_failure(result) - .expect("verification partial remains an in-band result"); - assert!(matches!( - caller_value, - OutstandingsLoadResult::Partial { reason, .. } - if reason.reason_code == "paired_segment_mismatch" - )); - assert_eq!( - runtime.snapshots().expect("runtime snapshot")[0].consecutive_failures, - 0, - "a verification failure reached a responder and must not poison endpoint health" - ); - } - - #[cfg(feature = "voucher-scan")] - #[test] - fn closing_coverage_drift_is_not_reported_as_uncovered_opening_bills() { - assert_eq!( - closing_coverage_partial_reason(false, true), - Some("ledger_master_identity_changed_during_scan") - ); - assert_eq!( - closing_coverage_partial_reason(true, false), - Some("ledger_opening_bills_not_covered") - ); - assert_eq!(closing_coverage_partial_reason(true, true), None); - } - - #[cfg(feature = "voucher-scan")] - #[test] - fn intra_pair_ledger_coverage_drift_is_an_in_band_partial() { - assert_eq!( - paired_coverage_partial_reason(&LedgerOpeningCoverageRead::Drifted), - Some("ledger_master_identity_changed_during_scan") - ); - } - - #[cfg(feature = "voucher-scan")] - #[tokio::test] - async fn witness_transport_failure_feeds_breaker_and_preserves_typed_partial() { - let error = fetch_empty_partition_witness( - VoucherAlterIdHighWater::parse("1").expect("positive high-water"), - || async { Err(anyhow::Error::new(TallyTransportError::RequestTimedOut)) }, - ) - .await - .expect_err("a witness transport failure must cross the runtime health boundary"); - assert!(error.downcast_ref::().is_some()); - - let runtime = TallyRuntime::default(); - let result = runtime - .execute( - TallyConfig { - host: "localhost".to_string(), - port: 9122, - }, - ReadOperation::VoucherExport, - ReadRetryPolicy::SINGLE_ATTEMPT, - |_client| async { - Err::(outstandings_read_transport_failure( - anyhow::Error::new(TallyTransportError::RequestTimedOut), - )) - }, - ) - .await; - let caller_value = partial_after_outstandings_read_transport_failure(result) - .expect("witness transport failure stays in-band only after health accounting"); - - assert!(matches!( - caller_value, - OutstandingsLoadResult::Partial { reason, .. } - if reason.reason_code == "tally_segment_deadline_restart_recommended" - )); - assert_eq!( - runtime.snapshots().expect("runtime snapshot")[0].consecutive_failures, - 1, - "the breaker must observe the witness transport failure" - ); - } - - #[cfg(feature = "voucher-scan")] - #[tokio::test] - async fn witness_non_transport_failure_remains_an_in_band_partial() { - let partial = fetch_empty_partition_witness( - VoucherAlterIdHighWater::parse("1").expect("positive high-water"), - || async { Err(anyhow::anyhow!("witness response was malformed")) }, - ) - .await - .expect("non-transport witness failure stays in-band") - .expect_err("a malformed witness cannot complete a non-empty book"); - - assert_eq!( - partial.reason_code, - "empty_date_witness_profile_unavailable" - ); - } - - #[cfg(feature = "voucher-scan")] - #[test] - fn outstandings_date_boundaries_follow_detected_mode_and_fallback_to_i12() { - let mut profile = synthetic_probe_result().profile; - profile.product = "TallyPrime Edit Log".to_string(); - profile.mode = Some("Education".to_string()); - assert_eq!( - select_date_boundary_profile(Some(&profile)), - DateBoundaryProfile::EducationRestricted - ); - - profile.mode = Some("Licensed".to_string()); - assert_eq!( - select_date_boundary_profile(Some(&profile)), - DateBoundaryProfile::ModeAgnostic - ); - assert_eq!( - select_date_boundary_profile(None), - DateBoundaryProfile::ModeAgnostic - ); - - profile.product = "Unknown".to_string(); - profile.mode = Some("Education".to_string()); - assert_eq!( - select_date_boundary_profile(Some(&profile)), - DateBoundaryProfile::ModeAgnostic, - "inconsistent or incomplete detection must rely on I12 rather than inventing compatibility evidence" - ); - } - - /// An uncalibrated segment width no longer refuses the read -- it selects - /// the native bills path, which needs no width. What must NOT change is - /// that a non-loopback endpoint is still refused. - /// - /// This test previously asserted `outstandings_segment_sizing_uncalibrated` - /// and, by using a non-loopback host, proved the refusal happened BEFORE - /// endpoint admission. That refusal was the defect: production has no - /// calibrated width by construction, so every shipped build returned it for - /// every company on every book and the screen could only ever say "No Tally - /// data was read". The reason code is gone with it. - /// - /// The loopback guard is unchanged and still fails closed; it is simply now - /// the first guard the native path reaches. That is the property worth - /// pinning, so this asserts it directly rather than inferring it from an - /// ordering that no longer exists. - #[tokio::test] - async fn uncalibrated_outstandings_takes_the_native_path_and_still_refuses_a_non_loopback_endpoint( - ) { - let runtime = TallyRuntime::default(); - #[cfg(feature = "voucher-scan")] - assert!( - runtime.outstandings_segment_policy.is_none(), - "a default runtime must have no calibrated width -- that is what routes to the native path" - ); - - let error = runtime - .fetch_outstandings( - TallyConfig { - host: "not-a-loopback-endpoint".to_string(), - port: 9000, - }, - &verified_identity("Synthetic Company", "synthetic-guid"), - TallyDate::parse("20260731").unwrap(), - OutstandingsCurrencyAssertion::Inr, - OutstandingsAgeingAnchor::DueDate, - ) - .await - .expect_err("a non-loopback endpoint must never be contacted"); - assert!( - error.to_string().contains("non_loopback_forbidden"), - "loopback-only admission must still fail closed on the native path, got: {error}" - ); - } - - #[cfg(feature = "live-calibration-harness")] - #[tokio::test] - async fn calibrated_voucher_scan_withholds_totals_before_endpoint_admission_without_residual_coverage( - ) { - let result = TallyRuntime::for_billwise_lab_reconciliation_exit_check() - .fetch_outstandings( - TallyConfig { - host: "not-a-loopback-endpoint".to_string(), - port: 9000, - }, - &verified_identity("Synthetic Company", "synthetic-guid"), - TallyDate::parse("20260731").unwrap(), - OutstandingsCurrencyAssertion::Inr, - OutstandingsAgeingAnchor::DueDate, - ) - .await - .expect("missing coverage is an in-band partial result"); - assert!(matches!( - result, - OutstandingsLoadResult::Partial { reason, .. } - if reason.reason_code == "unallocated_direct_postings_not_covered" - )); - } - - #[cfg(all(feature = "voucher-scan", not(feature = "live-calibration-harness")))] - #[test] - fn default_build_has_no_outstandings_width_admission() { - let runtime = TallyRuntime::default(); - assert!(runtime.outstandings_segment_policy.is_none()); - assert!(runtime.outstandings_boundary_profile_override.is_none()); - } - - #[cfg(feature = "live-calibration-harness")] - #[test] - fn billwise_lab_exit_harness_uses_only_education_valid_boundaries() { - let runtime = TallyRuntime::for_billwise_lab_reconciliation_exit_check(); - assert_eq!( - runtime.outstandings_boundary_profile_override, - Some(DateBoundaryProfile::EducationRestricted) - ); - let reporting_window = DateWindow::parse( - runtime - .outstandings_boundary_profile_override - .expect("exit profile is fixed"), - "20240401", - "20260702", - ) - .expect("accepted corpus extent uses Education-valid boundaries"); - let partitions = reporting_window - .narrow_partitions() - .expect("Education-valid corpus partitions without day-3 synthesis"); - assert!(partitions.iter().all(|partition| { - matches!(&partition.from().as_str()[6..8], "01" | "02" | "31") - && matches!(&partition.to().as_str()[6..8], "01" | "02" | "31") - })); - - let unprofiled = - DateWindow::parse(DateBoundaryProfile::ModeAgnostic, "20240401", "20260702") - .unwrap() - .narrow_partitions() - .unwrap(); - assert!(unprofiled.iter().any(|partition| { - !matches!(&partition.from().as_str()[6..8], "01" | "02" | "31") - || !matches!(&partition.to().as_str()[6..8], "01" | "02" | "31") - })); - } - - #[test] - fn local_rejections_and_client_http_statuses_do_not_poison_endpoint_health() { - assert_eq!( - classify_failure(&anyhow::Error::new( - TallyRuntimeReadError::ApplicationResponseRejected, - )), - ReadFailureClass::Application - ); - for error in [ - TallyTransportError::RequestTooLarge { limit: 1024 }, - TallyTransportError::PolicyInvalid { code: "test" }, - TallyTransportError::EndpointInvalid { code: "test" }, - TallyTransportError::ClientInitializationFailed, - TallyTransportError::HttpStatus { status: 400 }, - ] { - assert_eq!( - classify_error(&anyhow::Error::new(error)), - HealthOutcome::ApplicationRejected - ); - } - for error in [ - TallyTransportError::ConnectionFailed, - TallyTransportError::RequestTimedOut, - TallyTransportError::HttpStatus { status: 503 }, - ] { - assert_eq!( - classify_error(&anyhow::Error::new(error)), - HealthOutcome::TransportFailure - ); - } - } - - #[test] - fn endpoint_identity_aliases_only_localhost_to_ipv4_loopback() { - let runtime = TallyRuntime::default(); - let localhost_identity = EndpointKey::from_config(&TallyConfig { - host: "localhost".to_string(), - port: 9000, - }) - .expect("localhost identity"); - let ipv4_identity = EndpointKey::from_config(&TallyConfig { - host: "127.0.0.1".to_string(), - port: 9000, - }) - .expect("IPv4 loopback identity"); - assert_eq!(localhost_identity, ipv4_identity); - let first = runtime - .session(TallyConfig { - host: "localhost".to_string(), - port: 9000, - }) - .expect("localhost session"); - let second = runtime - .session(TallyConfig { - host: "127.0.0.1".to_string(), - port: 9000, - }) - .expect("IPv4 loopback session"); - let third = runtime - .session(TallyConfig { - host: "::1".to_string(), - port: 9000, - }) - .expect("IPv6 loopback session"); - let fourth = runtime - .session(TallyConfig { - host: "127.0.0.2".to_string(), - port: 9000, - }) - .expect("alternate IPv4 loopback session"); - assert!(Arc::ptr_eq(&first, &second)); - assert!(!Arc::ptr_eq(&first, &third)); - assert!(!Arc::ptr_eq(&first, &fourth)); - assert!(!Arc::ptr_eq(&third, &fourth)); - let snapshots = runtime.snapshots().expect("runtime snapshots"); - assert_eq!(snapshots.len(), 3); - assert_eq!(snapshots[0].canonical_endpoint, "http://127.0.0.1:9000"); - assert_eq!(snapshots[1].canonical_endpoint, "http://127.0.0.2:9000"); - assert_eq!(snapshots[2].canonical_endpoint, "http://[::1]:9000"); - } - - #[test] - fn reviewed_probe_cache_preserves_observation_time_and_is_single_use() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9000, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - let review_id = "review-current"; - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: review_id.to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - - let mut reservation = runtime - .reserve_cached_probe_fresh(&config, review_id, 300_000) - .expect("reserve cache") - .expect("fresh reviewed probe"); - assert_eq!(reservation.observed_at_unix_ms(), observed_at_unix_ms); - assert_eq!(reservation.result().companies[0].name, "Synthetic Company"); - assert!(runtime - .reserve_cached_probe_fresh(&config, review_id, 300_000) - .expect("second reserve") - .is_none()); - assert!(reservation.consume().expect("consume reservation")); - assert!(!reservation - .consume() - .expect("consuming an already consumed lease is inert")); - assert!(runtime - .reserve_cached_probe_fresh(&config, review_id, 300_000) - .expect("reserve consumed cache") - .is_none()); - } - - #[test] - fn stale_review_id_cannot_consume_or_reserve_a_newer_probe() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9002, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-b".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-a", 300_000) - .expect("reject stale review") - .is_none()); - let mut reservation = runtime - .reserve_cached_probe_fresh(&config, "review-b", 300_000) - .expect("reserve current review") - .expect("current review exists"); - assert!(reservation.release().expect("release current review")); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-b", 300_000) - .expect("retry current review") - .is_some()); - } - - #[test] - fn reviewed_probe_cache_rejects_future_expired_and_invalid_freshness() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9001, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - for observed_at_unix_ms in [ - chrono::Utc::now().timestamp_millis() + 1_000, - chrono::Utc::now().timestamp_millis() - 301_000, - ] { - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-expiry".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-expiry", 300_000) - .expect("reserve cache") - .is_none()); - } - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-expiry", 0) - .is_err()); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-expiry", 600_001) - .is_err()); - } - - #[test] - fn replacing_a_qualified_review_does_not_renew_its_freshness_origin() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9003, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let freshness_origin_unix_ms = chrono::Utc::now().timestamp_millis() - 299_000; - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-original".to_string(), - observed_at_unix_ms: freshness_origin_unix_ms, - freshness_origin_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - let mut reservation = runtime - .reserve_cached_probe_fresh(&config, "review-original", 300_000) - .expect("reserve original") - .expect("original remains barely fresh"); - assert!(reservation - .replace( - "review-qualified".to_string(), - chrono::Utc::now().timestamp_millis(), - synthetic_probe_result(), - ) - .expect("replace reservation")); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-qualified", 298_000) - .expect("check inherited freshness") - .is_none()); - } - - #[test] - fn ordinary_read_admission_and_review_reservation_are_mutually_exclusive() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9004, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-lease".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - - let read_lease = runtime - .begin_ordinary_read(&config) - .expect("admit ordinary read"); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-lease", 300_000) - .is_err()); - drop(read_lease); - - let reservation = runtime - .reserve_cached_probe_fresh(&config, "review-lease", 300_000) - .expect("reserve after read") - .expect("fresh review"); - assert!(runtime.begin_ordinary_read(&config).is_err()); - assert!(reservation.authorize(&runtime, &config).is_ok()); - assert!(reservation - .authorize( - &runtime, - &TallyConfig { - host: "127.0.0.2".to_string(), - port: 9004, - }, - ) - .is_err()); - } - - #[tokio::test] - async fn qualification_rejects_a_reservation_from_another_runtime_before_dispatch() { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind cross-runtime qualification server"); - let address = listener.local_addr().expect("qualification server address"); - let config = TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }; - let owner_runtime = TallyRuntime::default(); - let executing_runtime = TallyRuntime::default(); - let session = owner_runtime - .session(config.clone()) - .expect("owner runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-cross-runtime".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - drop(session); - let reservation = owner_runtime - .reserve_cached_probe_fresh(&config, "review-cross-runtime", 300_000) - .expect("reserve owner review") - .expect("fresh owner review"); - - let error = executing_runtime - .qualify_selected_ledgers( - config, - &reservation, - &verified_identity("Synthetic Company", "synthetic-guid"), - ) - .await - .expect_err("another runtime must not borrow the reservation"); - assert!(error - .to_string() - .contains("reviewed setup operation ownership changed")); - assert!( - tokio::time::timeout(std::time::Duration::from_millis(50), listener.accept(),) - .await - .is_err() - ); - } - - #[tokio::test] - async fn production_identity_bracket_rechecks_the_tuple_around_selected_ledger_qualification() { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind qualification server"); - let address = listener.local_addr().expect("qualification server address"); - let company_list = r#"
11
00000000-0000-4000-8000-00000000000110000120260401
"#; - let ledger_export = Fixture::NormalExport.body().into_owned(); - let server = tokio::spawn(async move { - let mut requests = Vec::new(); - for body in [ - company_list.to_string(), - company_list.to_string(), - ledger_export, - company_list.to_string(), - ] { - let (mut socket, _) = listener.accept().await.expect("accept reserved read"); - let request = read_http_request(&mut socket).await; - assert!(request.starts_with(b"POST /")); - requests.push(request); - socket - .write_all(&utf16_xml_response(body)) - .await - .expect("write reserved read response"); - } - requests - }); - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-qualified-tuple".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - drop(session); - let reservation = runtime - .reserve_cached_probe_fresh(&config, "review-qualified-tuple", 300_000) - .expect("reserve reviewed setup") - .expect("fresh review"); - - let ordinary_read = runtime - .fetch_companies(config.clone()) - .await - .expect_err("ordinary reads must remain blocked by the reservation"); - assert!(ordinary_read - .to_string() - .contains("reviewed setup operation is in progress")); - let companies = runtime - .fetch_companies_for_reservation(config.clone(), &reservation) - .await - .expect("reservation owner may recheck company tuple"); - assert_eq!(companies.len(), 1); - let identity = VerifiedCompanyIdentity::from_observed_companies( - "BRIDGE SYNTHETIC BOOK".to_string(), - "00000000-0000-4000-8000-000000000001".to_string(), - "100001".to_string(), - "20260401".to_string(), - &[TallyCompany { - name: "BRIDGE SYNTHETIC BOOK".to_string(), - guid: Some("00000000-0000-4000-8000-000000000001".to_string()), - company_number: Some("100001".to_string()), - books_from: Some("20260401".to_string()), - }], - ) - .expect("synthetic qualification identity is complete"); - let observation = runtime - .qualify_selected_ledgers(config, &reservation, &identity) - .await - .expect("qualification must run after the reserved tuple recheck"); - assert_eq!(observation.result_bucket, "non_empty_observed"); - let requests = server.await.expect("finish reserved qualification server"); - assert_eq!(requests.len(), 4); - let decoded = requests - .iter() - .map(|request| { - let body_start = request - .windows(4) - .position(|bytes| bytes == b"\r\n\r\n") - .expect("request has complete HTTP headers") - + 4; - bridge_tally_protocol::decode_tally_text_bytes_limited( - &request[body_start..], - request.len() - body_start, - ) - .expect("decode dispatched Tally request") - .text - }) - .collect::>(); - for (index, request) in [0, 1, 3].into_iter().map(|index| (index, &decoded[index])) { - assert!( - request.contains("Collection"), - "request {index}" - ); - assert!(request.contains("Company"), "request {index}"); - assert!(!request.contains(""), "request {index}"); - } - assert!(decoded[2].contains("BRIDGE SYNTHETIC BOOK")); - } - - #[test] - fn dropping_a_review_reservation_restores_the_same_fresh_review() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9005, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-drop".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - drop( - runtime - .reserve_cached_probe_fresh(&config, "review-drop", 300_000) - .expect("reserve review") - .expect("fresh review"), - ); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-drop", 300_000) - .expect("reserve after drop") - .is_some()); - } - - #[tokio::test] - async fn aborting_a_task_drops_and_releases_its_review_reservation() { - let runtime = Arc::new(TallyRuntime::default()); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9006, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-abort".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - let (held_tx, held_rx) = tokio::sync::oneshot::channel(); - let task_runtime = Arc::clone(&runtime); - let task_config = config.clone(); - let task = tokio::spawn(async move { - let _reservation = task_runtime - .reserve_cached_probe_fresh(&task_config, "review-abort", 300_000) - .expect("reserve review") - .expect("fresh review"); - held_tx.send(()).expect("announce held reservation"); - std::future::pending::<()>().await; - }); - held_rx.await.expect("reservation was held"); - task.abort(); - let _ = task.await; - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-abort", 300_000) - .expect("reserve after abort") - .is_some()); - } - - #[tokio::test] - async fn aborting_pending_qualification_releases_review_and_active_request() { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind pending qualification server"); - let address = listener.local_addr().expect("pending server address"); - let (accepted_tx, accepted_rx) = tokio::sync::oneshot::channel(); - let server = tokio::spawn(async move { - let (_socket, _) = listener.accept().await.expect("accept qualification"); - accepted_tx.send(()).expect("announce accepted request"); - std::future::pending::<()>().await; - }); - let runtime = Arc::new(TallyRuntime::default()); - let config = TallyConfig { - host: address.ip().to_string(), - port: address.port(), - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-pending".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - drop(session); - let task_runtime = Arc::clone(&runtime); - let task_config = config.clone(); - let task = tokio::spawn(async move { - let reservation = task_runtime - .reserve_cached_probe_fresh(&task_config, "review-pending", 300_000) - .expect("reserve pending review") - .expect("fresh pending review"); - let _ = task_runtime - .qualify_selected_ledgers( - task_config, - &reservation, - &verified_identity("Synthetic Company", "synthetic-guid"), - ) - .await; - }); - accepted_rx.await.expect("qualification reached server"); - task.abort(); - let _ = task.await; - server.abort(); - let snapshots = runtime.snapshots().expect("runtime snapshots after abort"); - assert_eq!(snapshots.len(), 1); - assert_eq!(snapshots[0].active_requests, 0); - assert!(snapshots[0].active_request_ids.is_empty()); - assert!(runtime - .reserve_cached_probe_fresh(&config, "review-pending", 300_000) - .expect("reserve after pending abort") - .is_some()); - } - - #[test] - fn stale_guard_cannot_release_or_consume_a_newer_reserved_review() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9007, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-old".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - let mut stale = runtime - .reserve_cached_probe_fresh(&config, "review-old", 300_000) - .expect("reserve old") - .expect("old review"); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-new".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: true, - }); - assert!(!stale.consume().expect("stale consume is inert")); - drop(stale); - let cache = session.cached_probe.read().expect("capability cache"); - let current = cache.as_ref().expect("new review remains"); - assert_eq!(current.review_id, "review-new"); - assert!(current.reserved); - } - - #[test] - fn stale_guard_cannot_replace_a_newer_reserved_review() { - let runtime = TallyRuntime::default(); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9008, - }; - let session = runtime.session(config.clone()).expect("runtime session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-old".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - let mut stale = runtime - .reserve_cached_probe_fresh(&config, "review-old", 300_000) - .expect("reserve old") - .expect("old review"); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-new".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: true, - }); - - assert!(!stale - .replace( - "review-illegal-replacement".to_string(), - observed_at_unix_ms, - synthetic_probe_result(), - ) - .expect("stale replace is inert")); - drop(stale); - let cache = session.cached_probe.read().expect("capability cache"); - let current = cache.as_ref().expect("new review remains"); - assert_eq!(current.review_id, "review-new"); - assert!(current.reserved); - } - - #[test] - fn held_review_reservation_prevents_endpoint_session_eviction() { - let runtime = TallyRuntime::default(); - let reserved_config = TallyConfig { - host: "127.0.0.1".to_string(), - port: 9200, - }; - let reserved_endpoint = EndpointKey::from_config(&reserved_config).unwrap(); - let session = runtime - .session(reserved_config.clone()) - .expect("reserved session"); - let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); - *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { - review_id: "review-capacity".to_string(), - observed_at_unix_ms, - freshness_origin_unix_ms: observed_at_unix_ms, - result: synthetic_probe_result(), - reserved: false, - }); - drop(session); - let _reservation = runtime - .reserve_cached_probe_fresh(&reserved_config, "review-capacity", 300_000) - .expect("reserve capacity review") - .expect("fresh capacity review"); - for host_suffix in 2..=MAX_ENDPOINT_SESSIONS { - runtime - .session(TallyConfig { - host: format!("127.0.0.{host_suffix}"), - port: 9200, - }) - .expect("fill endpoint capacity"); - } - runtime - .session(TallyConfig { - host: "127.0.0.254".to_string(), - port: 9200, - }) - .expect("evict one unreserved session"); - assert!(runtime - .sessions - .lock() - .expect("session registry") - .contains_key(&reserved_endpoint)); - } - - #[tokio::test] - async fn cancellation_registry_cancels_and_releases_requests() { - let runtime = Arc::new(TallyRuntime::default()); - let config = TallyConfig { - host: "localhost".to_string(), - port: 9100, - }; - let runtime_task = Arc::clone(&runtime); - let task = tokio::spawn(async move { - runtime_task - .execute( - config, - ReadOperation::OtherRead, - ReadRetryPolicy::SINGLE_ATTEMPT, - |_client| async { - tokio::time::sleep(std::time::Duration::from_secs(30)).await; - Ok::<_, anyhow::Error>(()) - }, - ) - .await - }); - tokio::task::yield_now().await; - let snapshot = runtime - .snapshots() - .expect("runtime snapshots") - .pop() - .expect("active session"); - assert_eq!(snapshot.active_requests, 1); - let session = runtime - .sessions - .lock() - .expect("sessions lock") - .values() - .next() - .expect("session") - .session - .clone(); - let request_id = session - .active_requests - .lock() - .expect("request lock") - .keys() - .next() - .expect("request ID") - .clone(); - assert!(runtime.cancel_request(&request_id).expect("cancel request")); - assert!(task.await.expect("request task").is_err()); - assert_eq!( - runtime.snapshots().expect("runtime snapshots")[0].active_requests, - 0 - ); - assert_eq!( - runtime.snapshots().expect("runtime snapshots")[0].consecutive_failures, - 0, - "operator cancellation must not degrade endpoint health" - ); - } - - #[test] - fn telemetry_preview_is_privacy_reduced_and_checksummed() { - let preview = TallyRuntime::default() - .telemetry_preview() - .expect("telemetry preview"); - assert_eq!(preview.schema, "bridge.tally.telemetry-preview/2"); - assert_eq!(preview.payload_sha256.len(), 64); - let preview_value: serde_json::Value = - serde_json::from_str(&preview.preview_json).expect("valid preview JSON"); - assert_eq!( - preview_value["privacy_profile"], - "fixed_dimensions_bucketed_values_v1" - ); - assert_eq!(preview_value["authenticity_claim"], "none"); - } -} +#[path = "runtime_tests.rs"] +mod tests; diff --git a/src-tauri/src/tally/runtime_tests.rs b/src-tauri/src/tally/runtime_tests.rs new file mode 100644 index 00000000..75657380 --- /dev/null +++ b/src-tauri/src/tally/runtime_tests.rs @@ -0,0 +1,1940 @@ +use super::*; +use crate::commands::VerifiedCompanyIdentity; +use crate::tally::TallyProduct; +use anyhow::Context; +use bridge_tally_core::CapabilityProfile; +use std::collections::BTreeMap; +use tally_protocol_simulator::Fixture; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +fn verified_identity(name: &str, guid: &str) -> VerifiedCompanyIdentity { + VerifiedCompanyIdentity::test_fixture(name, guid) +} + +fn captured_aarav_company_list_and_identity() -> (String, VerifiedCompanyIdentity) { + let bytes = include_bytes!("../../crates/bridge-tally-protocol/tests/fixtures/agent/native-licensed-release-companies.utf16le.xml"); + let xml = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( + bytes, + "text/xml; charset=utf-16", + bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, + bytes.len(), + ) + .expect("captured Company collection decodes") + .text; + let rows = parse_companies_from_collection(&xml).unwrap(); + let row = rows + .iter() + .find(|row| row.name == "Aarav Trading Company Demo") + .unwrap(); + let identity = VerifiedCompanyIdentity::from_observed_companies( + row.name.clone(), + row.guid.clone().unwrap(), + row.company_number.clone().unwrap(), + row.books_from.clone().unwrap(), + &rows, + ) + .unwrap(); + (xml, identity) +} + +#[test] +fn agent_company_list_evidence_hashes_the_encoded_utf16_response_bytes() { + let response = "
1
g-1120260401
".to_string(); + let encoded = bridge_tally_protocol::encode_tally_xml_request_utf16le(&response); + let expected_bytes = encoded.len(); + let expected_sha = Sha256::digest(&encoded) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let listed = agent_company_list_from_response(response, expected_bytes, expected_sha.clone()) + .expect("company list"); + assert_eq!(listed.response_bytes, expected_bytes); + assert_eq!(listed.response_sha256, expected_sha); + assert_eq!(listed.companies.len(), 1); +} + +#[test] +fn party_ledger_master_evidence_includes_currency_probe_and_source_responses() { + let source = |master_response_sha256: &str| PartyLedgerMasterSource { + company: "Synthetic Books".to_string(), + company_guid: "company-guid".to_string(), + currency_assertion: OutstandingsCurrencyAssertion::Inr, + currency_decimal_places: 2, + from: TallyDate::parse("20260401").expect("source from"), + to: TallyDate::parse("20260731").expect("source to"), + rows: Vec::new(), + request_sha256: "0".repeat(64), + master_response_sha256: master_response_sha256.to_string(), + balance_response_sha256: "b".repeat(64), + group_response_sha256: "c".repeat(64), + master_response_bytes: 11, + balance_response_bytes: 13, + group_response_bytes: 17, + groups: Vec::new(), + }; + let currency = RuntimeReadEvidence::paired("", sha256_hex(b"currency-response"), 19); + let baseline = TallyRuntime::party_ledger_master_source_evidence( + &source(&"a".repeat(64)), + currency.clone(), + ); + let changed = TallyRuntime::party_ledger_master_source_evidence( + &source(&"d".repeat(64)), + currency.clone(), + ); + let changed_currency = TallyRuntime::party_ledger_master_source_evidence( + &source(&"a".repeat(64)), + RuntimeReadEvidence::paired("", sha256_hex(b"changed-currency-response"), 23), + ); + let source_scope = "0".repeat(64); + let source_responses = + sha256_hex(format!("{}:{}:{}", "a".repeat(64), "b".repeat(64), "c".repeat(64)).as_bytes()); + assert_eq!( + baseline.request_sha256, + sha256_hex(format!("{}:{source_scope}", currency.request_sha256).as_bytes()) + ); + assert_eq!( + baseline.response_sha256, + sha256_hex(format!("{}:{source_responses}", currency.response_sha256).as_bytes()) + ); + assert_eq!(baseline.request_sha256, changed_currency.request_sha256); + assert_ne!(baseline.response_sha256, changed_currency.response_sha256); + assert_eq!(changed_currency.bytes, (11 + 13 + 17 + 23) * 2); + + assert_eq!(baseline.request_sha256, changed.request_sha256); + assert_ne!(baseline.response_sha256, changed.response_sha256); + assert_eq!(baseline.bytes, (11 + 13 + 17 + 19) * 2); +} + +#[test] +fn paired_evidence_commits_to_utf16le_request_body_not_utf8_source() { + let request = "₹ & Co"; + let mut encoded = vec![0xff, 0xfe]; + encoded.extend(request.encode_utf16().flat_map(u16::to_le_bytes)); + let evidence = RuntimeReadEvidence::paired(request, "r".repeat(64), 7); + assert_eq!(evidence.request_sha256, sha256_hex(&encoded)); + assert_ne!(evidence.request_sha256, sha256_hex(request.as_bytes())); + assert_eq!(evidence.bytes, 14); +} + +#[test] +fn outstandings_wire_evidence_changes_with_any_native_report_response() { + let receivable = RuntimeReadEvidence::paired("", sha256_hex(b"r1"), 3); + let payable = RuntimeReadEvidence::paired("", sha256_hex(b"p1"), 5); + let baseline = receivable.clone().combine(payable.clone()); + let changed = receivable.combine(RuntimeReadEvidence::paired( + "", + sha256_hex(b"p2"), + 5, + )); + + assert_eq!(baseline.bytes, 16, "both paired responses are counted"); + assert_ne!(baseline.response_sha256, changed.response_sha256); + assert_ne!(baseline.request_sha256, String::new()); +} + +fn utf16_xml_response(body: impl AsRef) -> Vec { + let body = bridge_tally_protocol::encode_tally_xml_request_utf16le(body.as_ref()); + utf16_xml_response_bytes(&body) +} + +fn utf16_xml_response_bytes(body: &[u8]) -> Vec { + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-16\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + [headers.as_bytes(), body].concat() +} + +fn utf8_status_response(body: impl AsRef) -> Vec { + let body = body.as_ref().as_bytes(); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + [headers.as_bytes(), body].concat() +} + +async fn read_http_request(socket: &mut tokio::net::TcpStream) -> Vec { + let mut request = Vec::new(); + let mut buffer = [0_u8; 4096]; + let (header_end, content_length) = loop { + let read = socket.read(&mut buffer).await.expect("read request"); + assert!(read > 0, "request ended before headers completed"); + request.extend_from_slice(&buffer[..read]); + let Some(header_end) = request.windows(4).position(|bytes| bytes == b"\r\n\r\n") else { + continue; + }; + let header = + std::str::from_utf8(&request[..header_end]).expect("request headers are valid UTF-8"); + let content_length = header + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then_some(value.trim()) + }) + .map(|value| { + value + .parse::() + .expect("request Content-Length is numeric") + }) + .unwrap_or(0); + break (header_end + 4, content_length); + }; + while request.len() < header_end + content_length { + let read = socket.read(&mut buffer).await.expect("read request body"); + assert!(read > 0, "request ended before declared body completed"); + request.extend_from_slice(&buffer[..read]); + } + request +} + +#[test] +fn ageing_anchor_serializes_as_an_explicit_wire_contract() { + assert_eq!( + serde_json::to_value(OutstandingsAgeingAnchor::DueDate).unwrap(), + serde_json::json!("due_date") + ); + assert_eq!( + serde_json::to_value(OutstandingsAgeingAnchor::BillDate).unwrap(), + serde_json::json!("bill_date") + ); +} + +#[test] +fn outstandings_read_strategy_serializes_as_an_explicit_wire_contract() { + assert_eq!( + serde_json::to_value(OutstandingsReadStrategy::NativeBills).unwrap(), + serde_json::json!("native_bills") + ); + assert_eq!( + serde_json::to_value(OutstandingsReadStrategy::VoucherScan).unwrap(), + serde_json::json!("voucher_scan") + ); +} + +#[test] +fn single_company_forex_ledger_capture_returns_a_typed_partial() { + const FOREX_LEDGER_CAPTURE: &[u8] = include_bytes!( + "../../crates/bridge-tally-protocol/tests/fixtures/ledgers_forex_composite_live.utf16le.xml" + ); + let ledger_body = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( + FOREX_LEDGER_CAPTURE, + "text/xml; charset=utf-16", + bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, + FOREX_LEDGER_CAPTURE.len(), + ) + .expect("captured forex ledger response decodes") + .text; + let admitted = admit_native_ledger_snapshot( + parse_native_ledger_snapshot(&ledger_body) + .map_err(anyhow::Error::from) + .context("stable native ledger snapshot"), + ) + .expect("a foreign-currency ledger is an in-band partial"); + + assert!(matches!( + admitted, + NativeLedgerSnapshotAdmission::Partial(OutstandingsLoadResult::Partial { reason, .. }) + if reason.reason_code == "company_foreign_currency_ledger_balance" + && reason.foreign_currency_ledger_name.as_deref() == Some("FX USD Debtor 02") + )); +} + +#[tokio::test] +async fn single_company_read_returns_the_forex_capture_partial() { + const EXTENT: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/agent/native-company-book-extents-with-number.utf8.xml" + ); + const FOREX_LEDGER_CAPTURE: &[u8] = include_bytes!( + "../../crates/bridge-tally-protocol/tests/fixtures/ledgers_forex_composite_live.utf16le.xml" + ); + const RECEIVABLE: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_receivable_aarav.xml" + ); + const PAYABLE: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_payable_aarav.xml" + ); + const STATUS: &str = "TallyPrime Server is Running"; + let (company_list, identity) = captured_aarav_company_list_and_identity(); + + let extent = EXTENT; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic outstandings server"); + let address = listener.local_addr().expect("synthetic server address"); + let server = tokio::spawn(async move { + let mut source_post_index = 0; + let mut company_post_index = 0; + for index in 0..30 { + let (mut socket, _) = + tokio::time::timeout(std::time::Duration::from_secs(2), listener.accept()) + .await + .expect("outstandings request timed out") + .expect("accept outstandings request"); + let request = read_http_request(&mut socket).await; + let response = if request.starts_with(b"GET /status") { + utf8_status_response(STATUS) + } else { + assert!(request.starts_with(b"POST /"), "request {index}"); + let body_start = request + .windows(4) + .position(|bytes| bytes == b"\r\n\r\n") + .expect("complete request headers") + + 4; + let xml = bridge_tally_protocol::decode_tally_text_bytes_limited( + &request[body_start..], + request.len() - body_start, + ) + .expect("decode request") + .text; + if xml.contains("BridgeCompanyExtent") + && !xml.contains("") + { + company_post_index += 1; + utf16_xml_response(&company_list) + } else { + let response = match source_post_index { + 0 | 1 | 10 | 11 => utf16_xml_response(extent), + 2 | 3 => utf16_xml_response(RECEIVABLE), + 6 | 7 => utf16_xml_response(PAYABLE), + _ => utf16_xml_response_bytes(FOREX_LEDGER_CAPTURE), + }; + source_post_index += 1; + response + } + }; + socket.write_all(&response).await.expect("write response"); + } + assert_eq!(company_post_index, 4); + assert_eq!(source_post_index, 12); + }); + + let result = TallyRuntime::default() + .fetch_outstandings( + TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }, + &identity, + TallyDate::parse("20260401").expect("captured book as-of"), + OutstandingsCurrencyAssertion::Inr, + OutstandingsAgeingAnchor::DueDate, + ) + .await + .expect("foreign-currency capture returns an in-band partial"); + + assert!(matches!( + result, + OutstandingsLoadResult::Partial { reason, .. } + if reason.reason_code == "company_foreign_currency_ledger_balance" + && reason.foreign_currency_ledger_name.as_deref() == Some("FX USD Debtor 02") + )); + server.await.expect("synthetic outstandings server task"); +} + +#[test] +fn validation_lab_future_bill_stays_unaged_in_open_bill_output() { + let receivable = parse_native_bill_rows( + include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_receivable_validation_lab.xml" + ), + &TallyDate::parse("20250401").expect("captured BooksFrom"), + &TallyDate::parse("20260817").expect("capture as-of"), + ) + .expect("captured validation-book rows parse"); + let rows = all_open_bill_rows( + &receivable, + &[], + OutstandingsAgeingAnchor::DueDate, + &TallyDate::parse("20260817").expect("capture as-of"), + ); + let future = rows + .iter() + .find(|row| row.reference == "ALPHA-FUTURE") + .expect("captured future-due bill remains present"); + assert_eq!(future.amount.as_str(), "22222.00"); + assert_eq!(future.age_days, None); + + let bill_date_rows = all_open_bill_rows( + &receivable, + &[], + OutstandingsAgeingAnchor::BillDate, + &TallyDate::parse("20260817").expect("capture as-of"), + ); + let bill_date_future = bill_date_rows + .iter() + .find(|row| row.reference == "ALPHA-FUTURE") + .expect("captured future-due bill remains present for bill-date ageing"); + assert!( + bill_date_future.age_days.is_some(), + "the selected bill-date basis must not reuse the future due date" + ); +} + +#[test] +fn settled_native_bill_rows_do_not_reach_statement_or_export_sources() { + let books_from = TallyDate::parse("20250401").expect("synthetic BooksFrom"); + let as_of = TallyDate::parse("20260817").expect("synthetic as-of"); + let parsed = parse_native_bill_rows( + "1-Aug-26SETTLED\ + Synthetic Customer0.00\ + 1-Aug-2616", + &books_from, + &as_of, + ) + .expect("synthetic settled row parses at the wire boundary"); + + assert_eq!(parsed.len(), 1, "the fixture proves the native row existed"); + assert!( + all_open_bill_rows(&parsed, &[], OutstandingsAgeingAnchor::DueDate, &as_of).is_empty(), + "a zero closing balance is not an open bill" + ); +} + +#[test] +fn captured_validation_lab_bytes_render_a_complete_working_paper_end_to_end() { + const COMPANY: &str = "Bridge Validation Lab"; + const COMPANY_GUID: &str = "c6afd306-00e1-4f51-802a-babe44daddd3"; + let books_from = TallyDate::parse("20250401").expect("captured BooksFrom"); + let as_of = TallyDate::parse("20260801").expect("captured as-of date"); + let receivable_xml = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_receivable_validation_lab.xml" + ); + let payable_xml = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/bills_payable_validation_lab.xml" + ); + let groups_xml = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/group_snapshot_validation_lab.xml" + ); + let ledgers_xml = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/native/ledger_snapshot_validation_lab.xml" + ); + + let receivable = parse_native_bill_rows(receivable_xml, &books_from, &as_of) + .expect("captured receivable rows parse"); + let payable = parse_native_bill_rows(payable_xml, &books_from, &as_of) + .expect("captured payable rows parse"); + // This captured Group body predates the request's response-bound + // compute. Add only that collection context for the parser contract; + // source-byte accounting below remains the exact captured response. + let groups_with_response_company_guid = groups_xml.replace( + "", + "c6afd306-00e1-4f51-802a-babe44daddd3", + ); + let groups = parse_native_group_snapshot(&groups_with_response_company_guid, COMPANY_GUID) + .expect("captured group identity and ancestry parse"); + let ledgers = + parse_native_ledger_snapshot(ledgers_xml).expect("captured ledger controls parse"); + let source_bytes = receivable_xml + .len() + .checked_add(payable_xml.len()) + .and_then(|value| value.checked_add(groups_xml.len())) + .and_then(|value| value.checked_add(ledgers_xml.len())) + .expect("captured source byte count fits usize"); + let computed = compute_native_outstandings( + COMPANY, + &receivable, + &payable, + NativeMasterSnapshot { + ledgers: &ledgers, + groups: NativeGroupSnapshot::Complete(&groups), + }, + NativeAgeingAnchor::DueDate, + &as_of, + source_bytes, + ) + .expect("captured native controls compute"); + assert_eq!( + native_crosscheck_partial_reason(&computed, &as_of), + None, + "captured overdue counters must positively prove the requested date" + ); + + let statement_open_bills = all_open_bill_rows( + &receivable, + &payable, + OutstandingsAgeingAnchor::DueDate, + &as_of, + ); + assert_eq!(statement_open_bills.len(), 6); + assert!(statement_open_bills.iter().all(|row| !row.amount.is_zero())); + let statement_unallocated_by_party = all_unallocated_parties(&computed.residuals); + let result = OutstandingsLoadResult::Complete { + report: Box::new(computed.report), + read_strategy: OutstandingsReadStrategy::NativeBills, + currency_assertion: OutstandingsCurrencyAssertion::Inr, + ageing_anchor: OutstandingsAgeingAnchor::DueDate, + synced_at_unix_ms: 1_777_000_000_000, + unallocated_total: Some(computed.residual_total), + statement_unallocated_by_party, + statement_open_bills, + }; + let source = crate::reports::outstandings_working_paper_store::source_from_complete_result( + &result, + COMPANY_GUID, + ) + .expect("captured source stays inside export budgets") + .expect("captured complete result substantiates a source"); + assert_eq!(source.source_bytes, 33_575); + assert_eq!(source.open_bills.len(), 6); + let paper = + crate::reports::outstandings_working_paper::build_outstandings_working_paper(source) + .expect("captured exact controls reconcile into a working paper"); + let workbook = + crate::reports::outstandings_working_paper_xlsx::render_outstandings_working_paper_xlsx( + &paper, + ) + .expect("captured working paper renders"); + assert!(workbook.len() > 200); + assert_eq!(&workbook[0..2], b"PK"); +} + +#[test] +fn raw_overdue_disagreement_withholds_native_complete() { + let rows = parse_native_bill_rows( + "1-Jul-26MISMATCH\ + Synthetic Customer-100.00\ + 1-Jul-2631", + &TallyDate::parse("20250401").expect("synthetic BooksFrom"), + &TallyDate::parse("20260817").expect("synthetic as-of"), + ) + .expect("raw bill parses"); + let computed = compute_native_outstandings( + "Synthetic Company", + &rows, + &[], + NativeMasterSnapshot { + ledgers: &[], + groups: NativeGroupSnapshot::LegacyFixtureWithoutGroups, + }, + NativeAgeingAnchor::DueDate, + &TallyDate::parse("20260817").expect("synthetic as-of"), + 0, + ) + .expect("arithmetic still computes for diagnostic comparison"); + assert_eq!( + computed.overdue_crosscheck, + NativeOverdueCrosscheck::Inconsistent + ); + assert_eq!( + native_crosscheck_partial_reason(&computed, &TallyDate::parse("20260817").unwrap()), + Some(OutstandingsPartialReason::code( + "native_overdue_crosscheck_mismatch" + )) + ); +} + +#[test] +fn zero_bill_rows_with_a_ledger_residual_withhold_native_complete() { + let groups = parse_native_group_snapshot( + r#"
1
+ 11111111-1111-1111-1111-111111111111-0000000111111111-1111-1111-1111-111111111111Primary +
"#, + "11111111-1111-1111-1111-111111111111", + ) + .expect("synthetic group snapshot parses"); + let ledgers = parse_native_ledger_snapshot( + r#"
1
+ Sundry Debtors-100.000No +
"#, + ) + .expect("synthetic ledger snapshot parses"); + let requested_as_of = TallyDate::parse("20260817").expect("synthetic as-of"); + let computed = compute_native_outstandings( + "Synthetic Company", + &[], + &[], + NativeMasterSnapshot { + ledgers: &ledgers, + groups: NativeGroupSnapshot::Complete(&groups), + }, + NativeAgeingAnchor::DueDate, + &requested_as_of, + 0, + ) + .expect("the residual is a partial-result diagnostic, not a parser failure"); + + assert_eq!( + computed.overdue_crosscheck, + NativeOverdueCrosscheck::UnconfirmedAsOfWithoutBillReferences + ); + assert_eq!( + native_crosscheck_partial_reason(&computed, &requested_as_of), + Some(OutstandingsPartialReason::code( + WarningCode::NativeOutstandingsAsOfUnconfirmedWithoutBillReferences.as_str(), + )) + ); +} + +#[test] +fn raw_empty_bill_references_preserve_each_amount_and_get_explicit_label() { + let raw_bills = "\ + 1-Jul-26Synthetic Customer\ + -40.001-Jul-2630\ + 2-Jul-26 \n\t Synthetic Customer\ + -60.002-Jul-2629\ + "; + let as_of = TallyDate::parse("20260731").expect("synthetic as-of"); + let parsed = parse_native_bill_rows( + raw_bills, + &TallyDate::parse("20250401").expect("synthetic BooksFrom"), + &as_of, + ) + .expect("paired empty BILLREF values remain parseable"); + let rows = all_open_bill_rows(&parsed, &[], OutstandingsAgeingAnchor::DueDate, &as_of); + + assert_eq!(rows.len(), 2, "empty identities must not collapse rows"); + let total = rows + .iter() + .try_fold(ExactDecimal::zero(), |sum, row| { + sum.checked_add(&row.amount) + }) + .expect("synthetic bill total remains exact"); + assert_eq!(total.as_str(), "100", "neither amount may be lost"); + assert_eq!( + rows.iter() + .map(|row| row.reference.as_str()) + .collect::>(), + vec!["No reference reported", "No reference reported"], + "client-facing rows must disclose the missing identity", + ); +} + +#[test] +fn party_master_currency_assertion_rejects_a_changed_company_extent() { + const EXTENT: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/unit_a_company_extent_live.xml" + ); + let read_extent_xml = EXTENT.replacen( + r#"bb8ad19e-6aef-4239-a917-87fec0c6215e"#, + r#"bb8ad19e-6aef-4239-a917-87fec0c6215e1"#, + 1, + ); + let changed_extent_xml = read_extent_xml.replace( + "20260401", + "20260402", + ); + let read_extent = bridge_tally_protocol::outstandings_shared::parse_company_book_extent( + &read_extent_xml, + "Aarav Trading Company Demo", + "bb8ad19e-6aef-4239-a917-87fec0c6215e", + ) + .expect("captured extent parses"); + let changed_extent = bridge_tally_protocol::outstandings_shared::parse_company_book_extent( + &changed_extent_xml, + "Aarav Trading Company Demo", + "bb8ad19e-6aef-4239-a917-87fec0c6215e", + ) + .expect("changed synthetic extent parses"); + let assertion = CompanyCurrencyRead { + currency: CompanyCurrency { + symbol: "₹".to_string(), + mailing_name: "INR".to_string(), + currency_count: 1, + decimal_places: 2, + is_inr: true, + }, + extent: read_extent.clone(), + evidence: RuntimeReadEvidence::empty(), + } + .bind_party_ledger_master_assertion(OutstandingsCurrencyAssertion::Inr); + + assert_eq!( + assertion + .require_opening_extent(&read_extent) + .expect("the observed extent releases INR"), + PartyLedgerMasterCurrency { + assertion: OutstandingsCurrencyAssertion::Inr, + decimal_places: 2, + } + ); + let error = assertion + .require_opening_extent(&changed_extent) + .expect_err("a changed extent must not release INR"); + assert!(matches!( + error.downcast_ref::(), + Some(PairedReadValidationError::CurrencyToMasterExtent) + )); +} + +#[tokio::test] +async fn detect_base_currency_rejects_book_drift_after_the_currency_read() { + const EXTENT: &str = include_str!( + "../../crates/bridge-tally-protocol/tests/fixtures/agent/native-company-book-extents-with-number.utf8.xml" + ); + const CURRENCY: &[u8] = include_bytes!( + "../../crates/bridge-tally-protocol/tests/fixtures/currency_inr_modern_live.utf16le.xml" + ); + const STATUS: &str = "TallyPrime Server is Running"; + let (company_list, identity) = captured_aarav_company_list_and_identity(); + + let currency = bridge_tally_protocol::decode_tally_xml_response_bytes_limited( + CURRENCY, + "text/xml; charset=utf-16", + bridge_tally_protocol::ExpectedTallyTextEncoding::Utf16Le, + CURRENCY.len(), + ) + .expect("captured currency response decodes") + .text; + + let opening_extent = EXTENT; + let closing_extent = opening_extent.replace( + "20260401", + "20260402", + ); + assert_ne!( + closing_extent, opening_extent, + "the drift mutation must apply" + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind synthetic currency server"); + let address = listener.local_addr().expect("synthetic server address"); + let server = tokio::spawn(async move { + let mut source_post_index = 0; + for index in 0..13 { + let (mut socket, _) = + tokio::time::timeout(std::time::Duration::from_secs(2), listener.accept()) + .await + .expect("currency request timed out") + .expect("accept currency request"); + let request = read_http_request(&mut socket).await; + let response = if request.starts_with(b"GET /status") { + utf8_status_response(STATUS) + } else { + assert!(request.starts_with(b"POST /"), "request {index}"); + let body_start = request + .windows(4) + .position(|bytes| bytes == b"\r\n\r\n") + .expect("complete request headers") + + 4; + let xml = bridge_tally_protocol::decode_tally_text_bytes_limited( + &request[body_start..], + request.len() - body_start, + ) + .expect("decode request") + .text; + if xml.contains("BridgeCompanyExtent") + && !xml.contains("") + { + utf16_xml_response(&company_list) + } else { + let response = match source_post_index { + 0 | 1 => utf16_xml_response(opening_extent), + 2 | 3 => utf16_xml_response(¤cy), + _ => utf16_xml_response(&closing_extent), + }; + source_post_index += 1; + response + } + }; + socket.write_all(&response).await.expect("write response"); + } + }); + + let runtime = TallyRuntime::default(); + let result = runtime + .detect_base_currency( + TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }, + &identity, + ) + .await; + + let error = result.expect_err("closing extent drift must reject the currency"); + assert!( + error.chain().any(|cause| matches!( + cause.downcast_ref::(), + Some(PairedReadValidationError::CurrencyExtent) + )), + "unexpected error: {error:#}" + ); + server.await.expect("synthetic currency server task"); +} + +fn synthetic_probe_result() -> TallyProbeResult { + TallyProbeResult { + connection: ConnectionStatus { + reachable: true, + compatible: false, + server_text: "Synthetic status".to_string(), + product: TallyProduct::Unknown, + error: None, + }, + companies: vec![TallyCompany { + name: "Synthetic Company".to_string(), + guid: Some("synthetic-guid".to_string()), + company_number: None, + books_from: None, + }], + profile: CapabilityProfile { + profile_version: 2, + product: "Unknown".to_string(), + release: None, + license_tier: None, + mode: None, + transports: BTreeMap::new(), + features: BTreeMap::new(), + packs: BTreeMap::new(), + }, + selected_read_scope: None, + passport_snapshot_id: None, + } +} + +#[test] +fn future_due_bill_from_raw_native_bytes_reaches_the_statement_source() { + let books_from = TallyDate::parse("20260401").expect("synthetic book start"); + let as_of = TallyDate::parse("20260731").expect("synthetic as-of date"); + let bills_xml = "\ + 1-Jul-26FUTURE-1Synthetic Party\ + -100.001-Aug-260\ + "; + let ledger_xml = "
1
"; + let receivable = parse_native_bill_rows(bills_xml, &books_from, &as_of) + .expect("a future due date in raw native bytes must parse"); + let ledgers = parse_native_ledger_snapshot(ledger_xml) + .expect("the synthetic raw ledger response must parse"); + let computed = compute_native_outstandings( + "Synthetic Company", + &receivable, + &[], + NativeMasterSnapshot { + ledgers: &ledgers, + groups: NativeGroupSnapshot::LegacyFixtureWithoutGroups, + }, + NativeAgeingAnchor::DueDate, + &as_of, + bills_xml.len() + ledger_xml.len(), + ) + .expect("a future-due bill must not abort the native computation"); + assert_eq!(computed.report.receivable_total.as_str(), "100"); + assert_eq!(computed.report.top_parties[0].oldest_bill_age_days, None); + assert_eq!(computed.report.ageing.days_0_30.as_str(), "100"); + assert_eq!(computed.report.ageing.days_31_60, ExactDecimal::zero()); + assert_eq!(computed.report.ageing.days_61_90, ExactDecimal::zero()); + assert_eq!(computed.report.ageing.days_90_plus, ExactDecimal::zero()); + assert_eq!(computed.report.open_receivable_bill_count, 1); + + let statement_rows = + all_open_bill_rows(&receivable, &[], OutstandingsAgeingAnchor::DueDate, &as_of); + assert_eq!( + statement_rows.len(), + 1, + "a future-due bill must remain available to the statement source" + ); + assert_eq!(statement_rows[0].amount.as_str(), "100.00"); + assert_eq!(statement_rows[0].age_days, None); + let statement = crate::reports::party_statement::build_party_statement( + "Synthetic Company", + as_of.as_str(), + "Synthetic Party", + &statement_rows, + &[], + ) + .expect("the future-due bill must build a party statement"); + assert_eq!(statement.bills.len(), 1); + assert_eq!(statement.bills[0].reference, "FUTURE-1"); + assert_eq!(statement.bills[0].age_days, None); + assert_eq!(statement.bills[0].bucket, None); + assert_eq!(statement.bill_total.as_str(), "100"); + + let destination = tempfile::tempdir().expect("synthetic destination"); + let approvals = + crate::reports::bulk_party_statement::PartyStatementDestinationApprovals::default(); + let approval_id = approvals + .issue(destination.path().to_path_buf()) + .expect("approve synthetic destination"); + let approved_destination = approvals + .consume(&approval_id, destination.path()) + .expect("consume synthetic approval"); + let bulk = crate::reports::bulk_party_statement::write_bulk_party_statements( + &approved_destination, + "Synthetic Company", + as_of.as_str(), + "xlsx", + &statement_rows, + &[], + |party_statement| { + crate::reports::party_statement_xlsx::render_party_statement_xlsx(party_statement) + .map_err(|error| error.to_string()) + }, + ) + .expect("a bulk run must write the future-due party statement"); + assert_eq!(bulk.written.len(), 1); + let workbook = std::fs::File::open(destination.path().join(&bulk.written[0].file_name)) + .expect("the bulk statement file exists"); + let mut archive = zip::ZipArchive::new(workbook).expect("bulk output is an XLSX archive"); + let mut workbook_text = String::new(); + for entry_name in ["xl/worksheets/sheet1.xml", "xl/sharedStrings.xml"] { + let mut entry = archive + .by_name(entry_name) + .expect("the XLSX statement entry exists"); + std::io::Read::read_to_string(&mut entry, &mut workbook_text) + .expect("the workbook XML is readable"); + } + assert!(workbook_text.contains("FUTURE-1")); + assert!(workbook_text.contains("Not due")); + assert!(workbook_text.contains("Unaged")); +} + +#[test] +fn education_snapshot_period_refusal_is_an_in_band_partial() { + let result = match admit_native_ledger_snapshot_period( + DateBoundaryProfile::EducationRestricted, + TallyDate::parse("20260401").expect("Education-valid book start"), + TallyDate::parse("20260415").expect("ordinary calendar as-of"), + ) { + NativeLedgerSnapshotPeriodAdmission::Period(_) => { + panic!("an Education-refused as-of must not construct a snapshot period") + } + NativeLedgerSnapshotPeriodAdmission::Partial(partial) => partial, + }; + assert!(matches!( + result, + OutstandingsLoadResult::Partial { reason, .. } + if reason.reason_code == "as_of_has_no_valid_window_boundary" + )); +} + +#[cfg(feature = "voucher-scan")] +#[test] +fn outstandings_read_failures_are_partial_and_deadlines_recommend_restart() { + assert_eq!( + outstandings_read_failure_reason(&anyhow::Error::new(TallyTransportError::RequestTimedOut)), + "tally_segment_deadline_restart_recommended" + ); + assert_eq!( + outstandings_read_failure_reason(&anyhow::Error::new(TallyTransportError::RequestFailed)), + "segment_request_failed" + ); + assert_eq!( + outstandings_read_failure_reason(&anyhow::Error::new(TallyTransportError::HttpStatus { + status: 500 + })), + "segment_http_status_failure" + ); + assert_eq!( + outstandings_read_failure_reason(&anyhow::anyhow!("connection refused")), + "segment_read_failed" + ); +} + +#[cfg(feature = "voucher-scan")] +#[tokio::test] +async fn outstandings_read_transport_failure_feeds_breaker_and_preserves_typed_partial() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9120, + }; + let result = runtime + .execute( + config, + ReadOperation::VoucherExport, + ReadRetryPolicy::SINGLE_ATTEMPT, + |_client| async { + Err::(anyhow::Error::new( + OutstandingsReadTransportFailure { + reason_code: "tally_segment_deadline_restart_recommended", + source: anyhow::Error::new(TallyTransportError::RequestTimedOut), + }, + )) + }, + ) + .await; + let caller_value = partial_after_outstandings_read_transport_failure(result) + .expect("outstandings read transport failures stay in-band for the caller"); + assert!(matches!( + caller_value, + OutstandingsLoadResult::Partial { reason, .. } + if reason.reason_code == "tally_segment_deadline_restart_recommended" + )); + assert_eq!( + runtime.snapshots().expect("runtime snapshot")[0].consecutive_failures, + 1, + "the breaker must observe the transport failure before UI mapping" + ); +} + +#[cfg(feature = "voucher-scan")] +#[tokio::test] +async fn verification_partial_stays_partial_without_feeding_breaker() { + let runtime = TallyRuntime::default(); + let result = runtime + .execute( + TallyConfig { + host: "localhost".to_string(), + port: 9121, + }, + ReadOperation::VoucherExport, + ReadRetryPolicy::SINGLE_ATTEMPT, + |_client| async { Ok::<_, anyhow::Error>(partial_result("paired_segment_mismatch")) }, + ) + .await; + let caller_value = partial_after_outstandings_read_transport_failure(result) + .expect("verification partial remains an in-band result"); + assert!(matches!( + caller_value, + OutstandingsLoadResult::Partial { reason, .. } + if reason.reason_code == "paired_segment_mismatch" + )); + assert_eq!( + runtime.snapshots().expect("runtime snapshot")[0].consecutive_failures, + 0, + "a verification failure reached a responder and must not poison endpoint health" + ); +} + +#[cfg(feature = "voucher-scan")] +#[test] +fn closing_coverage_drift_is_not_reported_as_uncovered_opening_bills() { + assert_eq!( + closing_coverage_partial_reason(false, true), + Some("ledger_master_identity_changed_during_scan") + ); + assert_eq!( + closing_coverage_partial_reason(true, false), + Some("ledger_opening_bills_not_covered") + ); + assert_eq!(closing_coverage_partial_reason(true, true), None); +} + +#[cfg(feature = "voucher-scan")] +#[test] +fn intra_pair_ledger_coverage_drift_is_an_in_band_partial() { + assert_eq!( + paired_coverage_partial_reason(&LedgerOpeningCoverageRead::Drifted), + Some("ledger_master_identity_changed_during_scan") + ); +} + +#[cfg(feature = "voucher-scan")] +#[tokio::test] +async fn witness_transport_failure_feeds_breaker_and_preserves_typed_partial() { + let error = fetch_empty_partition_witness( + VoucherAlterIdHighWater::parse("1").expect("positive high-water"), + || async { Err(anyhow::Error::new(TallyTransportError::RequestTimedOut)) }, + ) + .await + .expect_err("a witness transport failure must cross the runtime health boundary"); + assert!(error.downcast_ref::().is_some()); + + let runtime = TallyRuntime::default(); + let result = runtime + .execute( + TallyConfig { + host: "localhost".to_string(), + port: 9122, + }, + ReadOperation::VoucherExport, + ReadRetryPolicy::SINGLE_ATTEMPT, + |_client| async { + Err::(outstandings_read_transport_failure( + anyhow::Error::new(TallyTransportError::RequestTimedOut), + )) + }, + ) + .await; + let caller_value = partial_after_outstandings_read_transport_failure(result) + .expect("witness transport failure stays in-band only after health accounting"); + + assert!(matches!( + caller_value, + OutstandingsLoadResult::Partial { reason, .. } + if reason.reason_code == "tally_segment_deadline_restart_recommended" + )); + assert_eq!( + runtime.snapshots().expect("runtime snapshot")[0].consecutive_failures, + 1, + "the breaker must observe the witness transport failure" + ); +} + +#[cfg(feature = "voucher-scan")] +#[tokio::test] +async fn witness_non_transport_failure_remains_an_in_band_partial() { + let partial = fetch_empty_partition_witness( + VoucherAlterIdHighWater::parse("1").expect("positive high-water"), + || async { Err(anyhow::anyhow!("witness response was malformed")) }, + ) + .await + .expect("non-transport witness failure stays in-band") + .expect_err("a malformed witness cannot complete a non-empty book"); + + assert_eq!( + partial.reason_code, + "empty_date_witness_profile_unavailable" + ); +} + +#[cfg(feature = "voucher-scan")] +#[test] +fn outstandings_date_boundaries_follow_detected_mode_and_fallback_to_i12() { + let mut profile = synthetic_probe_result().profile; + profile.product = "TallyPrime Edit Log".to_string(); + profile.mode = Some("Education".to_string()); + assert_eq!( + select_date_boundary_profile(Some(&profile)), + DateBoundaryProfile::EducationRestricted + ); + + profile.mode = Some("Licensed".to_string()); + assert_eq!( + select_date_boundary_profile(Some(&profile)), + DateBoundaryProfile::ModeAgnostic + ); + assert_eq!( + select_date_boundary_profile(None), + DateBoundaryProfile::ModeAgnostic + ); + + profile.product = "Unknown".to_string(); + profile.mode = Some("Education".to_string()); + assert_eq!( + select_date_boundary_profile(Some(&profile)), + DateBoundaryProfile::ModeAgnostic, + "inconsistent or incomplete detection must rely on I12 rather than inventing compatibility evidence" + ); +} + +/// An uncalibrated segment width no longer refuses the read -- it selects +/// the native bills path, which needs no width. What must NOT change is +/// that a non-loopback endpoint is still refused. +/// +/// This test previously asserted `outstandings_segment_sizing_uncalibrated` +/// and, by using a non-loopback host, proved the refusal happened BEFORE +/// endpoint admission. That refusal was the defect: production has no +/// calibrated width by construction, so every shipped build returned it for +/// every company on every book and the screen could only ever say "No Tally +/// data was read". The reason code is gone with it. +/// +/// The loopback guard is unchanged and still fails closed; it is simply now +/// the first guard the native path reaches. That is the property worth +/// pinning, so this asserts it directly rather than inferring it from an +/// ordering that no longer exists. +#[tokio::test] +async fn uncalibrated_outstandings_takes_the_native_path_and_still_refuses_a_non_loopback_endpoint() +{ + let runtime = TallyRuntime::default(); + #[cfg(feature = "voucher-scan")] + assert!( + runtime.outstandings_segment_policy.is_none(), + "a default runtime must have no calibrated width -- that is what routes to the native path" + ); + + let error = runtime + .fetch_outstandings( + TallyConfig { + host: "not-a-loopback-endpoint".to_string(), + port: 9000, + }, + &verified_identity("Synthetic Company", "synthetic-guid"), + TallyDate::parse("20260731").unwrap(), + OutstandingsCurrencyAssertion::Inr, + OutstandingsAgeingAnchor::DueDate, + ) + .await + .expect_err("a non-loopback endpoint must never be contacted"); + assert!( + error.to_string().contains("non_loopback_forbidden"), + "loopback-only admission must still fail closed on the native path, got: {error}" + ); +} + +#[cfg(feature = "live-calibration-harness")] +#[tokio::test] +async fn calibrated_voucher_scan_withholds_totals_before_endpoint_admission_without_residual_coverage( +) { + let result = TallyRuntime::for_billwise_lab_reconciliation_exit_check() + .fetch_outstandings( + TallyConfig { + host: "not-a-loopback-endpoint".to_string(), + port: 9000, + }, + &verified_identity("Synthetic Company", "synthetic-guid"), + TallyDate::parse("20260731").unwrap(), + OutstandingsCurrencyAssertion::Inr, + OutstandingsAgeingAnchor::DueDate, + ) + .await + .expect("missing coverage is an in-band partial result"); + assert!(matches!( + result, + OutstandingsLoadResult::Partial { reason, .. } + if reason.reason_code == "unallocated_direct_postings_not_covered" + )); +} + +#[cfg(all(feature = "voucher-scan", not(feature = "live-calibration-harness")))] +#[test] +fn default_build_has_no_outstandings_width_admission() { + let runtime = TallyRuntime::default(); + assert!(runtime.outstandings_segment_policy.is_none()); + assert!(runtime.outstandings_boundary_profile_override.is_none()); +} + +#[cfg(feature = "live-calibration-harness")] +#[test] +fn billwise_lab_exit_harness_uses_only_education_valid_boundaries() { + let runtime = TallyRuntime::for_billwise_lab_reconciliation_exit_check(); + assert_eq!( + runtime.outstandings_boundary_profile_override, + Some(DateBoundaryProfile::EducationRestricted) + ); + let reporting_window = DateWindow::parse( + runtime + .outstandings_boundary_profile_override + .expect("exit profile is fixed"), + "20240401", + "20260702", + ) + .expect("accepted corpus extent uses Education-valid boundaries"); + let partitions = reporting_window + .narrow_partitions() + .expect("Education-valid corpus partitions without day-3 synthesis"); + assert!(partitions.iter().all(|partition| { + matches!(&partition.from().as_str()[6..8], "01" | "02" | "31") + && matches!(&partition.to().as_str()[6..8], "01" | "02" | "31") + })); + + let unprofiled = DateWindow::parse(DateBoundaryProfile::ModeAgnostic, "20240401", "20260702") + .unwrap() + .narrow_partitions() + .unwrap(); + assert!(unprofiled.iter().any(|partition| { + !matches!(&partition.from().as_str()[6..8], "01" | "02" | "31") + || !matches!(&partition.to().as_str()[6..8], "01" | "02" | "31") + })); +} + +#[test] +fn local_rejections_and_client_http_statuses_do_not_poison_endpoint_health() { + assert_eq!( + classify_failure(&anyhow::Error::new( + TallyRuntimeReadError::ApplicationResponseRejected, + )), + ReadFailureClass::Application + ); + for error in [ + TallyTransportError::RequestTooLarge { limit: 1024 }, + TallyTransportError::PolicyInvalid { code: "test" }, + TallyTransportError::EndpointInvalid { code: "test" }, + TallyTransportError::ClientInitializationFailed, + TallyTransportError::HttpStatus { status: 400 }, + ] { + assert_eq!( + classify_error(&anyhow::Error::new(error)), + HealthOutcome::ApplicationRejected + ); + } + for error in [ + TallyTransportError::ConnectionFailed, + TallyTransportError::RequestTimedOut, + TallyTransportError::HttpStatus { status: 503 }, + ] { + assert_eq!( + classify_error(&anyhow::Error::new(error)), + HealthOutcome::TransportFailure + ); + } +} + +#[test] +fn endpoint_identity_aliases_only_localhost_to_ipv4_loopback() { + let runtime = TallyRuntime::default(); + let localhost_identity = EndpointKey::from_config(&TallyConfig { + host: "localhost".to_string(), + port: 9000, + }) + .expect("localhost identity"); + let ipv4_identity = EndpointKey::from_config(&TallyConfig { + host: "127.0.0.1".to_string(), + port: 9000, + }) + .expect("IPv4 loopback identity"); + assert_eq!(localhost_identity, ipv4_identity); + let first = runtime + .session(TallyConfig { + host: "localhost".to_string(), + port: 9000, + }) + .expect("localhost session"); + let second = runtime + .session(TallyConfig { + host: "127.0.0.1".to_string(), + port: 9000, + }) + .expect("IPv4 loopback session"); + let third = runtime + .session(TallyConfig { + host: "::1".to_string(), + port: 9000, + }) + .expect("IPv6 loopback session"); + let fourth = runtime + .session(TallyConfig { + host: "127.0.0.2".to_string(), + port: 9000, + }) + .expect("alternate IPv4 loopback session"); + assert!(Arc::ptr_eq(&first, &second)); + assert!(!Arc::ptr_eq(&first, &third)); + assert!(!Arc::ptr_eq(&first, &fourth)); + assert!(!Arc::ptr_eq(&third, &fourth)); + let snapshots = runtime.snapshots().expect("runtime snapshots"); + assert_eq!(snapshots.len(), 3); + assert_eq!(snapshots[0].canonical_endpoint, "http://127.0.0.1:9000"); + assert_eq!(snapshots[1].canonical_endpoint, "http://127.0.0.2:9000"); + assert_eq!(snapshots[2].canonical_endpoint, "http://[::1]:9000"); +} + +#[test] +fn reviewed_probe_cache_preserves_observation_time_and_is_single_use() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9000, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + let review_id = "review-current"; + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: review_id.to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + + let mut reservation = runtime + .reserve_cached_probe_fresh(&config, review_id, 300_000) + .expect("reserve cache") + .expect("fresh reviewed probe"); + assert_eq!(reservation.observed_at_unix_ms(), observed_at_unix_ms); + assert_eq!(reservation.result().companies[0].name, "Synthetic Company"); + assert!(runtime + .reserve_cached_probe_fresh(&config, review_id, 300_000) + .expect("second reserve") + .is_none()); + assert!(reservation.consume().expect("consume reservation")); + assert!(!reservation + .consume() + .expect("consuming an already consumed lease is inert")); + assert!(runtime + .reserve_cached_probe_fresh(&config, review_id, 300_000) + .expect("reserve consumed cache") + .is_none()); +} + +#[test] +fn stale_review_id_cannot_consume_or_reserve_a_newer_probe() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9002, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-b".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-a", 300_000) + .expect("reject stale review") + .is_none()); + let mut reservation = runtime + .reserve_cached_probe_fresh(&config, "review-b", 300_000) + .expect("reserve current review") + .expect("current review exists"); + assert!(reservation.release().expect("release current review")); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-b", 300_000) + .expect("retry current review") + .is_some()); +} + +#[test] +fn reviewed_probe_cache_rejects_future_expired_and_invalid_freshness() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9001, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + for observed_at_unix_ms in [ + chrono::Utc::now().timestamp_millis() + 1_000, + chrono::Utc::now().timestamp_millis() - 301_000, + ] { + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-expiry".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-expiry", 300_000) + .expect("reserve cache") + .is_none()); + } + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-expiry", 0) + .is_err()); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-expiry", 600_001) + .is_err()); +} + +#[test] +fn replacing_a_qualified_review_does_not_renew_its_freshness_origin() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9003, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let freshness_origin_unix_ms = chrono::Utc::now().timestamp_millis() - 299_000; + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-original".to_string(), + observed_at_unix_ms: freshness_origin_unix_ms, + freshness_origin_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + let mut reservation = runtime + .reserve_cached_probe_fresh(&config, "review-original", 300_000) + .expect("reserve original") + .expect("original remains barely fresh"); + assert!(reservation + .replace( + "review-qualified".to_string(), + chrono::Utc::now().timestamp_millis(), + synthetic_probe_result(), + ) + .expect("replace reservation")); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-qualified", 298_000) + .expect("check inherited freshness") + .is_none()); +} + +#[test] +fn ordinary_read_admission_and_review_reservation_are_mutually_exclusive() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9004, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-lease".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + + let read_lease = runtime + .begin_ordinary_read(&config) + .expect("admit ordinary read"); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-lease", 300_000) + .is_err()); + drop(read_lease); + + let reservation = runtime + .reserve_cached_probe_fresh(&config, "review-lease", 300_000) + .expect("reserve after read") + .expect("fresh review"); + assert!(runtime.begin_ordinary_read(&config).is_err()); + assert!(reservation.authorize(&runtime, &config).is_ok()); + assert!(reservation + .authorize( + &runtime, + &TallyConfig { + host: "127.0.0.2".to_string(), + port: 9004, + }, + ) + .is_err()); +} + +#[tokio::test] +async fn qualification_rejects_a_reservation_from_another_runtime_before_dispatch() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind cross-runtime qualification server"); + let address = listener.local_addr().expect("qualification server address"); + let config = TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }; + let owner_runtime = TallyRuntime::default(); + let executing_runtime = TallyRuntime::default(); + let session = owner_runtime + .session(config.clone()) + .expect("owner runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-cross-runtime".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + drop(session); + let reservation = owner_runtime + .reserve_cached_probe_fresh(&config, "review-cross-runtime", 300_000) + .expect("reserve owner review") + .expect("fresh owner review"); + + let error = executing_runtime + .qualify_selected_ledgers( + config, + &reservation, + &verified_identity("Synthetic Company", "synthetic-guid"), + ) + .await + .expect_err("another runtime must not borrow the reservation"); + assert!(error + .to_string() + .contains("reviewed setup operation ownership changed")); + assert!( + tokio::time::timeout(std::time::Duration::from_millis(50), listener.accept(),) + .await + .is_err() + ); +} + +#[tokio::test] +async fn production_identity_bracket_rechecks_the_tuple_around_selected_ledger_qualification() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind qualification server"); + let address = listener.local_addr().expect("qualification server address"); + let company_list = r#"
11
00000000-0000-4000-8000-00000000000110000120260401
"#; + let ledger_export = Fixture::NormalExport.body().into_owned(); + let server = tokio::spawn(async move { + let mut requests = Vec::new(); + for body in [ + company_list.to_string(), + company_list.to_string(), + ledger_export, + company_list.to_string(), + ] { + let (mut socket, _) = listener.accept().await.expect("accept reserved read"); + let request = read_http_request(&mut socket).await; + assert!(request.starts_with(b"POST /")); + requests.push(request); + socket + .write_all(&utf16_xml_response(body)) + .await + .expect("write reserved read response"); + } + requests + }); + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-qualified-tuple".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + drop(session); + let reservation = runtime + .reserve_cached_probe_fresh(&config, "review-qualified-tuple", 300_000) + .expect("reserve reviewed setup") + .expect("fresh review"); + + let ordinary_read = runtime + .fetch_companies(config.clone()) + .await + .expect_err("ordinary reads must remain blocked by the reservation"); + assert!(ordinary_read + .to_string() + .contains("reviewed setup operation is in progress")); + let companies = runtime + .fetch_companies_for_reservation(config.clone(), &reservation) + .await + .expect("reservation owner may recheck company tuple"); + assert_eq!(companies.len(), 1); + let identity = VerifiedCompanyIdentity::from_observed_companies( + "BRIDGE SYNTHETIC BOOK".to_string(), + "00000000-0000-4000-8000-000000000001".to_string(), + "100001".to_string(), + "20260401".to_string(), + &[TallyCompany { + name: "BRIDGE SYNTHETIC BOOK".to_string(), + guid: Some("00000000-0000-4000-8000-000000000001".to_string()), + company_number: Some("100001".to_string()), + books_from: Some("20260401".to_string()), + }], + ) + .expect("synthetic qualification identity is complete"); + let observation = runtime + .qualify_selected_ledgers(config, &reservation, &identity) + .await + .expect("qualification must run after the reserved tuple recheck"); + assert_eq!(observation.result_bucket, "non_empty_observed"); + let requests = server.await.expect("finish reserved qualification server"); + assert_eq!(requests.len(), 4); + let decoded = requests + .iter() + .map(|request| { + let body_start = request + .windows(4) + .position(|bytes| bytes == b"\r\n\r\n") + .expect("request has complete HTTP headers") + + 4; + bridge_tally_protocol::decode_tally_text_bytes_limited( + &request[body_start..], + request.len() - body_start, + ) + .expect("decode dispatched Tally request") + .text + }) + .collect::>(); + for (index, request) in [0, 1, 3].into_iter().map(|index| (index, &decoded[index])) { + assert!( + request.contains("Collection"), + "request {index}" + ); + assert!(request.contains("Company"), "request {index}"); + assert!(!request.contains(""), "request {index}"); + } + assert!(decoded[2].contains("BRIDGE SYNTHETIC BOOK")); +} + +#[test] +fn dropping_a_review_reservation_restores_the_same_fresh_review() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9005, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-drop".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + drop( + runtime + .reserve_cached_probe_fresh(&config, "review-drop", 300_000) + .expect("reserve review") + .expect("fresh review"), + ); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-drop", 300_000) + .expect("reserve after drop") + .is_some()); +} + +#[tokio::test] +async fn aborting_a_task_drops_and_releases_its_review_reservation() { + let runtime = Arc::new(TallyRuntime::default()); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9006, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-abort".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + let (held_tx, held_rx) = tokio::sync::oneshot::channel(); + let task_runtime = Arc::clone(&runtime); + let task_config = config.clone(); + let task = tokio::spawn(async move { + let _reservation = task_runtime + .reserve_cached_probe_fresh(&task_config, "review-abort", 300_000) + .expect("reserve review") + .expect("fresh review"); + held_tx.send(()).expect("announce held reservation"); + std::future::pending::<()>().await; + }); + held_rx.await.expect("reservation was held"); + task.abort(); + let _ = task.await; + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-abort", 300_000) + .expect("reserve after abort") + .is_some()); +} + +#[tokio::test] +async fn aborting_pending_qualification_releases_review_and_active_request() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind pending qualification server"); + let address = listener.local_addr().expect("pending server address"); + let (accepted_tx, accepted_rx) = tokio::sync::oneshot::channel(); + let server = tokio::spawn(async move { + let (_socket, _) = listener.accept().await.expect("accept qualification"); + accepted_tx.send(()).expect("announce accepted request"); + std::future::pending::<()>().await; + }); + let runtime = Arc::new(TallyRuntime::default()); + let config = TallyConfig { + host: address.ip().to_string(), + port: address.port(), + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-pending".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + drop(session); + let task_runtime = Arc::clone(&runtime); + let task_config = config.clone(); + let task = tokio::spawn(async move { + let reservation = task_runtime + .reserve_cached_probe_fresh(&task_config, "review-pending", 300_000) + .expect("reserve pending review") + .expect("fresh pending review"); + let _ = task_runtime + .qualify_selected_ledgers( + task_config, + &reservation, + &verified_identity("Synthetic Company", "synthetic-guid"), + ) + .await; + }); + accepted_rx.await.expect("qualification reached server"); + task.abort(); + let _ = task.await; + server.abort(); + let snapshots = runtime.snapshots().expect("runtime snapshots after abort"); + assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots[0].active_requests, 0); + assert!(snapshots[0].active_request_ids.is_empty()); + assert!(runtime + .reserve_cached_probe_fresh(&config, "review-pending", 300_000) + .expect("reserve after pending abort") + .is_some()); +} + +#[test] +fn stale_guard_cannot_release_or_consume_a_newer_reserved_review() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9007, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-old".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + let mut stale = runtime + .reserve_cached_probe_fresh(&config, "review-old", 300_000) + .expect("reserve old") + .expect("old review"); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-new".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: true, + }); + assert!(!stale.consume().expect("stale consume is inert")); + drop(stale); + let cache = session.cached_probe.read().expect("capability cache"); + let current = cache.as_ref().expect("new review remains"); + assert_eq!(current.review_id, "review-new"); + assert!(current.reserved); +} + +#[test] +fn stale_guard_cannot_replace_a_newer_reserved_review() { + let runtime = TallyRuntime::default(); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9008, + }; + let session = runtime.session(config.clone()).expect("runtime session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-old".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + let mut stale = runtime + .reserve_cached_probe_fresh(&config, "review-old", 300_000) + .expect("reserve old") + .expect("old review"); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-new".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: true, + }); + + assert!(!stale + .replace( + "review-illegal-replacement".to_string(), + observed_at_unix_ms, + synthetic_probe_result(), + ) + .expect("stale replace is inert")); + drop(stale); + let cache = session.cached_probe.read().expect("capability cache"); + let current = cache.as_ref().expect("new review remains"); + assert_eq!(current.review_id, "review-new"); + assert!(current.reserved); +} + +#[test] +fn held_review_reservation_prevents_endpoint_session_eviction() { + let runtime = TallyRuntime::default(); + let reserved_config = TallyConfig { + host: "127.0.0.1".to_string(), + port: 9200, + }; + let reserved_endpoint = EndpointKey::from_config(&reserved_config).unwrap(); + let session = runtime + .session(reserved_config.clone()) + .expect("reserved session"); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis(); + *session.cached_probe.write().expect("capability cache") = Some(CachedProbe { + review_id: "review-capacity".to_string(), + observed_at_unix_ms, + freshness_origin_unix_ms: observed_at_unix_ms, + result: synthetic_probe_result(), + reserved: false, + }); + drop(session); + let _reservation = runtime + .reserve_cached_probe_fresh(&reserved_config, "review-capacity", 300_000) + .expect("reserve capacity review") + .expect("fresh capacity review"); + for host_suffix in 2..=MAX_ENDPOINT_SESSIONS { + runtime + .session(TallyConfig { + host: format!("127.0.0.{host_suffix}"), + port: 9200, + }) + .expect("fill endpoint capacity"); + } + runtime + .session(TallyConfig { + host: "127.0.0.254".to_string(), + port: 9200, + }) + .expect("evict one unreserved session"); + assert!(runtime + .sessions + .lock() + .expect("session registry") + .contains_key(&reserved_endpoint)); +} + +#[tokio::test] +async fn cancellation_registry_cancels_and_releases_requests() { + let runtime = Arc::new(TallyRuntime::default()); + let config = TallyConfig { + host: "localhost".to_string(), + port: 9100, + }; + let runtime_task = Arc::clone(&runtime); + let task = tokio::spawn(async move { + runtime_task + .execute( + config, + ReadOperation::OtherRead, + ReadRetryPolicy::SINGLE_ATTEMPT, + |_client| async { + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + Ok::<_, anyhow::Error>(()) + }, + ) + .await + }); + tokio::task::yield_now().await; + let snapshot = runtime + .snapshots() + .expect("runtime snapshots") + .pop() + .expect("active session"); + assert_eq!(snapshot.active_requests, 1); + let session = runtime + .sessions + .lock() + .expect("sessions lock") + .values() + .next() + .expect("session") + .session + .clone(); + let request_id = session + .active_requests + .lock() + .expect("request lock") + .keys() + .next() + .expect("request ID") + .clone(); + assert!(runtime.cancel_request(&request_id).expect("cancel request")); + assert!(task.await.expect("request task").is_err()); + assert_eq!( + runtime.snapshots().expect("runtime snapshots")[0].active_requests, + 0 + ); + assert_eq!( + runtime.snapshots().expect("runtime snapshots")[0].consecutive_failures, + 0, + "operator cancellation must not degrade endpoint health" + ); +} + +#[test] +fn telemetry_preview_is_privacy_reduced_and_checksummed() { + let preview = TallyRuntime::default() + .telemetry_preview() + .expect("telemetry preview"); + assert_eq!(preview.schema, "bridge.tally.telemetry-preview/2"); + assert_eq!(preview.payload_sha256.len(), 64); + let preview_value: serde_json::Value = + serde_json::from_str(&preview.preview_json).expect("valid preview JSON"); + assert_eq!( + preview_value["privacy_profile"], + "fixed_dimensions_bucketed_values_v1" + ); + assert_eq!(preview_value["authenticity_claim"], "none"); +} From 72afa576efa87d9f681abfc1eef27e3968edc5d8 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 16 Sep 2026 15:30:24 +0530 Subject: [PATCH 2/2] Point the outstandings evidence test at where the runtime's tests now live scripts/outstandings-evidence.test.mjs reads runtime.rs as text and asserted `assert_eq!(statement_open_bills.len(), 6);` in it -- a line of the inline test module the previous commit moved to runtime_tests.rs. CI's Frontend build failed on it. I had verified the Rust test lists, clippy and formatting but never ran the JS suite, which is where the breakage was. The production patterns it checks (the NativeBills and VoucherScan read strategies, and all_open_bill_rows consuming both sources) still read runtime.rs; only the Rust-test assertion reads runtime_tests.rs. Proven to still bite: changing the moved assertion's 6 to 7 fails this test, and the full frontend suite passes with it restored (239 tests). Co-Authored-By: Claude Opus 5 --- scripts/outstandings-evidence.test.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/outstandings-evidence.test.mjs b/scripts/outstandings-evidence.test.mjs index 8a45dbec..d46c5675 100644 --- a/scripts/outstandings-evidence.test.mjs +++ b/scripts/outstandings-evidence.test.mjs @@ -9,10 +9,12 @@ import { reportEvidenceDrawerEntry } from "../src/evidence-drawer-entry.ts"; import { readProvenance } from "../src/outstandings-provenance.ts"; test("native provenance labels its receivable-only count while native rows include both directions", async () => { - const [screen, panel, runtime] = await Promise.all([ + const [screen, panel, runtime, runtimeTests] = await Promise.all([ readFile(new URL("../src/OutstandingsScreen.tsx", import.meta.url), "utf8"), readFile(new URL("../src/OutstandingsEvidencePanel.tsx", import.meta.url), "utf8"), readFile(new URL("../src-tauri/src/tally/runtime.rs", import.meta.url), "utf8"), + // The runtime's unit tests live beside it, not inline. + readFile(new URL("../src-tauri/src/tally/runtime_tests.rs", import.meta.url), "utf8"), ]); assert.equal(readProvenance({ read_strategy: "native_bills", source_voucher_count: 0, open_receivable_bill_count: 1 }), "1 open receivable bill read from Tally"); @@ -28,7 +30,7 @@ test("native provenance labels its receivable-only count while native rows inclu /all_open_bill_rows\(\s*&receivable_rows,\s*&payable_rows,\s*ageing_anchor,\s*&as_of,?\s*\)/.test(runtime), "native statement rows must consume both receivable and payable sources", ); - assert.match(runtime, /assert_eq!\(statement_open_bills\.len\(\), 6\);/); + assert.match(runtimeTests, /assert_eq!\(statement_open_bills\.len\(\), 6\);/); assert.match(panel, /readProvenance\(evidence\.readProvenance\)/); assert.doesNotMatch(panel, /sourceVoucherCount/); });