| 1 | use super::*; |
| 2 | |
| 3 | const FIXTURES: &str = include_str!("fixtures/cases.json"); |
| 4 | |
| 5 | #[derive(Debug, Deserialize)] |
| 6 | #[serde(rename_all = "camelCase")] |
| 7 | struct FixtureFile { |
| 8 | cases: Vec<FixtureCase>, |
| 9 | } |
| 10 | |
| 11 | #[derive(Debug, Deserialize)] |
| 12 | #[serde(rename_all = "camelCase")] |
| 13 | struct FixtureCase { |
| 14 | id: String, |
| 15 | expect: String, |
| 16 | #[serde(default)] |
| 17 | code: Option<String>, |
| 18 | #[serde(default)] |
| 19 | accepted_seconds: Option<u64>, |
| 20 | #[serde(default)] |
| 21 | standard_equivalent_seconds: Option<u64>, |
| 22 | #[serde(default)] |
| 23 | admission: Option<ComputerAdmissionRequest>, |
| 24 | #[serde(default)] |
| 25 | observation: Option<ProviderObservation>, |
| 26 | #[serde(default)] |
| 27 | restated_observation: Option<ProviderObservation>, |
| 28 | #[serde(default)] |
| 29 | intervals: Vec<FixtureInterval>, |
| 30 | #[serde(default)] |
| 31 | profile_input: Option<String>, |
| 32 | #[serde(default)] |
| 33 | profile_id: Option<String>, |
| 34 | #[serde(default)] |
| 35 | multiplier: Option<u32>, |
| 36 | } |
| 37 | |
| 38 | #[derive(Debug, Deserialize)] |
| 39 | #[serde(rename_all = "camelCase")] |
| 40 | struct FixtureInterval { |
| 41 | admission: ComputerAdmissionRequest, |
| 42 | observation: ProviderObservation, |
| 43 | } |
| 44 | |
| 45 | fn fixtures() -> Vec<FixtureCase> { |
| 46 | serde_json::from_str::<FixtureFile>(FIXTURES) |
| 47 | .expect("computer meter fixtures must parse") |
| 48 | .cases |
| 49 | } |
| 50 | |
| 51 | fn issue_case(case: &FixtureCase) -> Result<ComputerMeterReceipt, ComputerMeterError> { |
| 52 | let admission = bind_computer_admission( |
| 53 | case.admission |
| 54 | .clone() |
| 55 | .expect("receipt fixture requires admission"), |
| 56 | )?; |
| 57 | issue_computer_meter_receipt( |
| 58 | &admission, |
| 59 | case.observation |
| 60 | .clone() |
| 61 | .expect("receipt fixture requires observation"), |
| 62 | ) |
| 63 | } |
| 64 | |
| 65 | #[test] |
| 66 | fn fixtures_cover_provider_accepted_active_seconds_only() { |
| 67 | let cases = fixtures(); |
| 68 | assert!( |
| 69 | cases.len() >= 12, |
| 70 | "fixture pack must cover accept, idle, queued, stopped, teardown, mismatch, concurrent, historic, correction" |
| 71 | ); |
| 72 | |
| 73 | for case in &cases { |
| 74 | match case.expect.as_str() { |
| 75 | "receipt" => { |
| 76 | let receipt = issue_case(case).unwrap_or_else(|error| { |
| 77 | panic!("{} should mint a receipt, got {}", case.id, error.code()) |
| 78 | }); |
| 79 | assert_eq!(receipt.schema_id, COMPUTER_METER_RECEIPT_SCHEMA); |
| 80 | assert_eq!(receipt.kind, COMPUTER_METER_RECEIPT_KIND); |
| 81 | assert_eq!(receipt.accepted_seconds, case.accepted_seconds.unwrap()); |
| 82 | assert_eq!( |
| 83 | receipt.standard_equivalent_seconds, |
| 84 | case.standard_equivalent_seconds.unwrap() |
| 85 | ); |
| 86 | assert_eq!( |
| 87 | receipt.standard_equivalent_seconds, |
| 88 | receipt.accepted_seconds * u64::from(receipt.multiplier) |
| 89 | ); |
| 90 | let admission = bind_computer_admission(case.admission.clone().unwrap()).unwrap(); |
| 91 | assert_eq!(receipt.admission_id, admission.admission_id); |
| 92 | assert_eq!(receipt.account_id, admission.account_id); |
| 93 | assert_eq!(receipt.profile_id, admission.profile_id); |
| 94 | assert_eq!(receipt.multiplier, admission.multiplier); |
| 95 | assert_eq!(receipt.provider_allocation.cpu, admission.cpu); |
| 96 | assert_eq!(receipt.provider_allocation.memory_gib, admission.memory_gib); |
| 97 | assert_eq!(receipt.provider_allocation.disk_gib, admission.disk_gib); |
| 98 | assert!(receipt.provider_allocation.accepted); |
| 99 | assert_eq!(receipt.meter_revision, COMPUTER_METER_REVISION); |
| 100 | assert_eq!(receipt.catalog_revision, COMPUTER_CATALOG_REVISION); |
| 101 | assert_ne!(receipt.binding_digest, admission.binding_digest); |
| 102 | } |
| 103 | "error" => { |
| 104 | let error = issue_case(case).expect_err(&case.id); |
| 105 | assert_eq!(error.code(), case.code.as_deref().unwrap(), "{}", case.id); |
| 106 | } |
| 107 | "sum" => { |
| 108 | let receipts: Vec<_> = case |
| 109 | .intervals |
| 110 | .iter() |
| 111 | .map(|interval| { |
| 112 | let admission = bind_computer_admission(interval.admission.clone()) |
| 113 | .unwrap_or_else(|error| panic!("{}: {}", case.id, error)); |
| 114 | issue_computer_meter_receipt(&admission, interval.observation.clone()) |
| 115 | .unwrap_or_else(|error| panic!("{}: {}", case.id, error)) |
| 116 | }) |
| 117 | .collect(); |
| 118 | assert_eq!( |
| 119 | sum_standard_equivalent_seconds(&receipts), |
| 120 | case.standard_equivalent_seconds.unwrap() |
| 121 | ); |
| 122 | } |
| 123 | "decode" => { |
| 124 | let profile = decode_computer_profile(case.profile_input.as_deref().unwrap()) |
| 125 | .unwrap_or_else(|error| panic!("{}: {}", case.id, error)); |
| 126 | assert_eq!(profile.id.as_str(), case.profile_id.as_deref().unwrap()); |
| 127 | assert_eq!(profile.multiplier, case.multiplier.unwrap()); |
| 128 | } |
| 129 | "admit_error" => { |
| 130 | let error = admit_computer_profile(case.profile_input.as_deref().unwrap()) |
| 131 | .expect_err(&case.id); |
| 132 | assert_eq!(error.code(), case.code.as_deref().unwrap()); |
| 133 | } |
| 134 | "correction" => { |
| 135 | let admission = bind_computer_admission(case.admission.clone().unwrap()).unwrap(); |
| 136 | let original = |
| 137 | issue_computer_meter_receipt(&admission, case.observation.clone().unwrap()) |
| 138 | .unwrap(); |
| 139 | let original_clone = original.clone(); |
| 140 | let correction = correct_computer_meter_receipt( |
| 141 | &original, |
| 142 | &admission, |
| 143 | case.restated_observation.clone().unwrap(), |
| 144 | ) |
| 145 | .unwrap(); |
| 146 | assert_eq!( |
| 147 | original, original_clone, |
| 148 | "corrections must not mutate the original receipt" |
| 149 | ); |
| 150 | assert_eq!( |
| 151 | correction.correction_of.as_deref(), |
| 152 | Some(original.receipt_id.as_str()) |
| 153 | ); |
| 154 | assert!(correction.lineage.contains(&original.receipt_id)); |
| 155 | assert_ne!(correction.receipt_id, original.receipt_id); |
| 156 | assert_eq!(correction.accepted_seconds, case.accepted_seconds.unwrap()); |
| 157 | assert_eq!( |
| 158 | correction.standard_equivalent_seconds, |
| 159 | case.standard_equivalent_seconds.unwrap() |
| 160 | ); |
| 161 | assert_eq!(correction.account_id, original.account_id); |
| 162 | assert_eq!(correction.admission_id, original.admission_id); |
| 163 | assert_eq!(correction.profile_id, original.profile_id); |
| 164 | } |
| 165 | other => panic!("unknown fixture expect `{other}`"), |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | #[test] |
| 171 | fn exact_replay_is_idempotent_and_a_mutated_binding_conflicts() { |
| 172 | let case = fixtures() |
| 173 | .into_iter() |
| 174 | .find(|case| case.id == "accepted-standard-8") |
| 175 | .unwrap(); |
| 176 | let first = issue_case(&case).unwrap(); |
| 177 | let replay = issue_case(&case).unwrap(); |
| 178 | assert_eq!(first.receipt_id, replay.receipt_id); |
| 179 | assert_eq!(first.binding_digest, replay.binding_digest); |
| 180 | assert_computer_meter_receipt_replay(&first, &replay).unwrap(); |
| 181 | |
| 182 | let mut mutated = replay.clone(); |
| 183 | mutated.accepted_seconds = first.accepted_seconds + 1; |
| 184 | mutated.standard_equivalent_seconds = mutated.accepted_seconds; |
| 185 | assert_eq!( |
| 186 | assert_computer_meter_receipt_replay(&first, &mutated) |
| 187 | .unwrap_err() |
| 188 | .code(), |
| 189 | "computer_meter_receipt_replay_conflict" |
| 190 | ); |
| 191 | } |
| 192 | |
| 193 | #[test] |
| 194 | fn admission_binds_profile_multiplier_account_and_expiry_before_dispatch() { |
| 195 | let admission = bind_computer_admission(ComputerAdmissionRequest { |
| 196 | admission_id: "adm_bind".to_string(), |
| 197 | account_id: "acct_demo".to_string(), |
| 198 | computer_id: "cmp_bind".to_string(), |
| 199 | run_id: "run_bind".to_string(), |
| 200 | provider: "daytona".to_string(), |
| 201 | profile_id: "standard-16".to_string(), |
| 202 | funding_authority: "coding_membership_included".to_string(), |
| 203 | quote_id: "quote_bind".to_string(), |
| 204 | expires_at: "2026-08-31T18:00:00.000Z".to_string(), |
| 205 | meter_revision: String::new(), |
| 206 | catalog_revision: String::new(), |
| 207 | }) |
| 208 | .unwrap(); |
| 209 | assert_eq!(admission.schema_id, COMPUTER_ADMISSION_SCHEMA); |
| 210 | assert_eq!(admission.profile_id, ComputerProfileId::Standard16); |
| 211 | assert_eq!(admission.cpu, 4); |
| 212 | assert_eq!(admission.memory_gib, 16); |
| 213 | assert_eq!(admission.disk_gib, 16); |
| 214 | assert_eq!(admission.multiplier, 2); |
| 215 | assert_eq!(admission.account_id, "acct_demo"); |
| 216 | assert_eq!(admission.meter_revision, COMPUTER_METER_REVISION); |
| 217 | assert!(!admission.binding_digest.is_empty()); |
| 218 | } |
| 219 | |
| 220 | #[test] |
| 221 | fn one_standard_16_matches_two_standard_8_over_the_same_ten_minutes() { |
| 222 | let cases = fixtures(); |
| 223 | let two_eights = cases |
| 224 | .iter() |
| 225 | .find(|case| case.id == "concurrent-two-standard-8") |
| 226 | .unwrap(); |
| 227 | let sixteen = cases |
| 228 | .iter() |
| 229 | .find(|case| case.id == "accepted-standard-16") |
| 230 | .unwrap(); |
| 231 | let eight_receipts: Vec<_> = two_eights |
| 232 | .intervals |
| 233 | .iter() |
| 234 | .map(|interval| { |
| 235 | let admission = bind_computer_admission(interval.admission.clone()).unwrap(); |
| 236 | issue_computer_meter_receipt(&admission, interval.observation.clone()).unwrap() |
| 237 | }) |
| 238 | .collect(); |
| 239 | let sixteen_receipt = issue_case(sixteen).unwrap(); |
| 240 | assert_eq!( |
| 241 | sum_standard_equivalent_seconds(&eight_receipts), |
| 242 | sixteen_receipt.standard_equivalent_seconds |
| 243 | ); |
| 244 | assert_eq!(sum_standard_equivalent_seconds(&eight_receipts), 1200); |
| 245 | } |
| 246 |