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/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/); }); 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"); +}