1use crate::{
2 NnrpError, CACHE_ERROR_DEPENDENCY_INVALID, CACHE_ERROR_LEASE_EXPIRED, CACHE_ERROR_MISS,
3 CACHE_ERROR_SCHEMA_MISMATCH, CACHE_ERROR_VERSION_MISMATCH,
4};
5
6pub const CACHE_PUT_METADATA_LEN: usize = 40;
7pub const CACHE_ACK_METADATA_LEN: usize = 40;
8pub const CACHE_INVALIDATE_METADATA_LEN: usize = 32;
9pub const CACHE_PUT_FLAGS_KNOWN_MASK: u32 = 0x0000_0003;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
12#[repr(u32)]
13pub enum CacheObjectKind {
14 CameraBlock = 0x0001,
15 TileIndexBlock = 0x0002,
16 TensorSectionTable = 0x0003,
17 CodecTable = 0x0004,
18 ReusableResultObject = 0x0005,
19 PayloadLayoutTemplate = 0x0006,
20 PromptSegment = 0x0007,
21 ToolSchema = 0x0008,
22 StructuredEventSchema = 0x0009,
23}
24
25impl CacheObjectKind {
26 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
27 match value {
28 0x0001 => Ok(Self::CameraBlock),
29 0x0002 => Ok(Self::TileIndexBlock),
30 0x0003 => Ok(Self::TensorSectionTable),
31 0x0004 => Ok(Self::CodecTable),
32 0x0005 => Ok(Self::ReusableResultObject),
33 0x0006 => Ok(Self::PayloadLayoutTemplate),
34 0x0007 => Ok(Self::PromptSegment),
35 0x0008 => Ok(Self::ToolSchema),
36 0x0009 => Ok(Self::StructuredEventSchema),
37 _ => Err(NnrpError::UnknownEnumValue {
38 enum_name: "cache_object_kind",
39 value: value as u64,
40 }),
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[repr(u32)]
47pub enum CacheAckStatus {
48 Accepted = 0,
49 Rejected = 1,
50 Replaced = 2,
51}
52
53impl CacheAckStatus {
54 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
55 match value {
56 0 => Ok(Self::Accepted),
57 1 => Ok(Self::Rejected),
58 2 => Ok(Self::Replaced),
59 _ => Err(NnrpError::UnknownEnumValue {
60 enum_name: "cache_ack_status",
61 value: value as u64,
62 }),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68#[repr(u32)]
69pub enum CacheInvalidateScope {
70 WholeSession = 0,
71 Namespace = 1,
72 ObjectKind = 2,
73 ObjectKey = 3,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
77pub struct CacheObjectId {
78 pub cache_namespace: u32,
79 pub cache_key_hi: u64,
80 pub cache_key_lo: u64,
81 pub object_kind: CacheObjectKind,
82}
83
84impl CacheObjectId {
85 pub fn from_put(metadata: &CachePutMetadata) -> Self {
86 Self {
87 cache_namespace: metadata.cache_namespace,
88 cache_key_hi: metadata.cache_key_hi,
89 cache_key_lo: metadata.cache_key_lo,
90 object_kind: metadata.object_kind,
91 }
92 }
93
94 pub fn matches_invalidate(&self, metadata: &CacheInvalidateMetadata) -> bool {
95 match metadata.invalidate_scope {
96 CacheInvalidateScope::WholeSession => true,
97 CacheInvalidateScope::Namespace => self.cache_namespace == metadata.cache_namespace,
98 CacheInvalidateScope::ObjectKind => {
99 self.cache_namespace == metadata.cache_namespace
100 && self.object_kind as u64 == metadata.cache_key_hi
101 }
102 CacheInvalidateScope::ObjectKey => {
103 self.cache_namespace == metadata.cache_namespace
104 && self.cache_key_hi == metadata.cache_key_hi
105 && self.cache_key_lo == metadata.cache_key_lo
106 }
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112#[repr(u8)]
113pub enum CacheLeaseOwnerScope {
114 Connection = 0,
115 Session = 1,
116 Operation = 2,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct CacheLease {
121 pub object_id: CacheObjectId,
122 pub object_version: u64,
123 pub lease_id: u64,
124 pub owner_scope: CacheLeaseOwnerScope,
125 pub owner_id: u64,
126 pub granted_at_ms: u64,
127 pub ttl_ms: u32,
128}
129
130impl CacheLease {
131 pub fn expires_at_ms(&self) -> u64 {
132 self.granted_at_ms.saturating_add(self.ttl_ms as u64)
133 }
134
135 pub fn is_expired_at(&self, now_ms: u64) -> bool {
136 now_ms >= self.expires_at_ms()
137 }
138
139 pub fn validate_live_at(&self, now_ms: u64) -> Result<(), CacheValidationFailure> {
140 if self.is_expired_at(now_ms) {
141 return Err(CacheValidationFailure::LeaseExpired);
142 }
143
144 Ok(())
145 }
146
147 pub fn validate_version(&self, expected_version: u64) -> Result<(), CacheValidationFailure> {
148 if self.object_version != expected_version {
149 return Err(CacheValidationFailure::VersionMismatch);
150 }
151
152 Ok(())
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct CacheDependency {
158 pub object_id: CacheObjectId,
159 pub required_version: u64,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct CacheDependencyState {
164 pub object_id: CacheObjectId,
165 pub current_version: u64,
166 pub invalidated: bool,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum CacheValidationFailure {
171 Miss,
172 LeaseExpired,
173 VersionMismatch,
174 DependencyInvalid,
175 SchemaMismatch,
176}
177
178impl CacheValidationFailure {
179 pub fn error_code(self) -> u32 {
180 match self {
181 Self::Miss => CACHE_ERROR_MISS,
182 Self::LeaseExpired => CACHE_ERROR_LEASE_EXPIRED,
183 Self::VersionMismatch => CACHE_ERROR_VERSION_MISMATCH,
184 Self::DependencyInvalid => CACHE_ERROR_DEPENDENCY_INVALID,
185 Self::SchemaMismatch => CACHE_ERROR_SCHEMA_MISMATCH,
186 }
187 }
188}
189
190pub fn validate_cache_dependencies(
191 dependencies: &[CacheDependency],
192 states: &[CacheDependencyState],
193) -> Result<(), CacheValidationFailure> {
194 for dependency in dependencies {
195 let state = states
196 .iter()
197 .find(|state| state.object_id == dependency.object_id)
198 .ok_or(CacheValidationFailure::DependencyInvalid)?;
199
200 if state.invalidated || state.current_version != dependency.required_version {
201 return Err(CacheValidationFailure::DependencyInvalid);
202 }
203 }
204
205 Ok(())
206}
207
208impl CacheInvalidateScope {
209 pub fn try_from_u32(value: u32) -> Result<Self, NnrpError> {
210 match value {
211 0 => Ok(Self::WholeSession),
212 1 => Ok(Self::Namespace),
213 2 => Ok(Self::ObjectKind),
214 3 => Ok(Self::ObjectKey),
215 _ => Err(NnrpError::UnknownEnumValue {
216 enum_name: "cache_invalidate_scope",
217 value: value as u64,
218 }),
219 }
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub struct CachePutMetadata {
225 pub cache_namespace: u32,
226 pub cache_key_hi: u64,
227 pub cache_key_lo: u64,
228 pub object_kind: CacheObjectKind,
229 pub ttl_ms: u32,
230 pub object_bytes: u32,
231 pub codec_bitmap: u32,
232 pub flags: u32,
233}
234
235impl CachePutMetadata {
236 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
237 require_len(source, CACHE_PUT_METADATA_LEN)?;
238 let flags = read_u32(source, 36);
239 validate_mask_u32(flags, CACHE_PUT_FLAGS_KNOWN_MASK)?;
240
241 Ok(Self {
242 cache_namespace: read_u32(source, 0),
243 cache_key_hi: read_u64(source, 8),
244 cache_key_lo: read_u64(source, 16),
245 object_kind: CacheObjectKind::try_from_u32(read_u32(source, 4))?,
246 ttl_ms: read_u32(source, 24),
247 object_bytes: read_u32(source, 28),
248 codec_bitmap: read_u32(source, 32),
249 flags,
250 })
251 }
252
253 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
254 require_destination_len(destination, CACHE_PUT_METADATA_LEN)?;
255 validate_mask_u32(self.flags, CACHE_PUT_FLAGS_KNOWN_MASK)?;
256
257 write_u32(destination, 0, self.cache_namespace);
258 write_u32(destination, 4, self.object_kind as u32);
259 write_u64(destination, 8, self.cache_key_hi);
260 write_u64(destination, 16, self.cache_key_lo);
261 write_u32(destination, 24, self.ttl_ms);
262 write_u32(destination, 28, self.object_bytes);
263 write_u32(destination, 32, self.codec_bitmap);
264 write_u32(destination, 36, self.flags);
265 Ok(())
266 }
267
268 pub fn to_bytes(&self) -> Result<[u8; CACHE_PUT_METADATA_LEN], NnrpError> {
269 let mut bytes = [0u8; CACHE_PUT_METADATA_LEN];
270 self.write(&mut bytes)?;
271 Ok(bytes)
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub struct CacheAckMetadata {
277 pub cache_namespace: u32,
278 pub cache_key_hi: u64,
279 pub cache_key_lo: u64,
280 pub status: CacheAckStatus,
281 pub accepted_ttl_ms: u32,
282 pub max_object_bytes: u32,
283 pub detail_code: u32,
284}
285
286impl CacheAckMetadata {
287 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
288 require_len(source, CACHE_ACK_METADATA_LEN)?;
289 validate_zero_u32("cache_ack.reserved", read_u32(source, 36))?;
290 Ok(Self {
291 cache_namespace: read_u32(source, 0),
292 cache_key_hi: read_u64(source, 8),
293 cache_key_lo: read_u64(source, 16),
294 status: CacheAckStatus::try_from_u32(read_u32(source, 4))?,
295 accepted_ttl_ms: read_u32(source, 24),
296 max_object_bytes: read_u32(source, 28),
297 detail_code: read_u32(source, 32),
298 })
299 }
300
301 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
302 require_destination_len(destination, CACHE_ACK_METADATA_LEN)?;
303 destination[..CACHE_ACK_METADATA_LEN].fill(0);
304 write_u32(destination, 0, self.cache_namespace);
305 write_u32(destination, 4, self.status as u32);
306 write_u64(destination, 8, self.cache_key_hi);
307 write_u64(destination, 16, self.cache_key_lo);
308 write_u32(destination, 24, self.accepted_ttl_ms);
309 write_u32(destination, 28, self.max_object_bytes);
310 write_u32(destination, 32, self.detail_code);
311 Ok(())
312 }
313
314 pub fn to_bytes(&self) -> Result<[u8; CACHE_ACK_METADATA_LEN], NnrpError> {
315 let mut bytes = [0u8; CACHE_ACK_METADATA_LEN];
316 self.write(&mut bytes)?;
317 Ok(bytes)
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub struct CacheInvalidateMetadata {
323 pub invalidate_scope: CacheInvalidateScope,
324 pub cache_namespace: u32,
325 pub cache_key_hi: u64,
326 pub cache_key_lo: u64,
327 pub reason_code: u32,
328}
329
330impl CacheInvalidateMetadata {
331 pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
332 require_len(source, CACHE_INVALIDATE_METADATA_LEN)?;
333 validate_zero_u32("cache_invalidate.reserved", read_u32(source, 28))?;
334 let metadata = Self {
335 invalidate_scope: CacheInvalidateScope::try_from_u32(read_u32(source, 0))?,
336 cache_namespace: read_u32(source, 4),
337 cache_key_hi: read_u64(source, 8),
338 cache_key_lo: read_u64(source, 16),
339 reason_code: read_u32(source, 24),
340 };
341 metadata.validate_scope_fields()?;
342 Ok(metadata)
343 }
344
345 pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
346 require_destination_len(destination, CACHE_INVALIDATE_METADATA_LEN)?;
347 self.validate_scope_fields()?;
348 destination[..CACHE_INVALIDATE_METADATA_LEN].fill(0);
349 write_u32(destination, 0, self.invalidate_scope as u32);
350 write_u32(destination, 4, self.cache_namespace);
351 write_u64(destination, 8, self.cache_key_hi);
352 write_u64(destination, 16, self.cache_key_lo);
353 write_u32(destination, 24, self.reason_code);
354 Ok(())
355 }
356
357 pub fn to_bytes(&self) -> Result<[u8; CACHE_INVALIDATE_METADATA_LEN], NnrpError> {
358 let mut bytes = [0u8; CACHE_INVALIDATE_METADATA_LEN];
359 self.write(&mut bytes)?;
360 Ok(bytes)
361 }
362
363 fn validate_scope_fields(&self) -> Result<(), NnrpError> {
364 let valid = match self.invalidate_scope {
365 CacheInvalidateScope::WholeSession => {
366 self.cache_namespace == 0 && self.cache_key_hi == 0 && self.cache_key_lo == 0
367 }
368 CacheInvalidateScope::Namespace => self.cache_key_hi == 0 && self.cache_key_lo == 0,
369 CacheInvalidateScope::ObjectKind => {
370 self.cache_key_hi <= u32::MAX as u64 && self.cache_key_lo == 0
371 }
372 CacheInvalidateScope::ObjectKey => true,
373 };
374 if !valid {
375 return Err(NnrpError::InvalidProtocolCombination {
376 rule: "cache invalidate identity fields must match invalidate_scope",
377 });
378 }
379 Ok(())
380 }
381}
382
383fn require_len(source: &[u8], expected: usize) -> Result<(), NnrpError> {
384 if source.len() < expected {
385 return Err(NnrpError::SourceTooShort {
386 expected,
387 actual: source.len(),
388 });
389 }
390 Ok(())
391}
392
393fn require_destination_len(destination: &[u8], expected: usize) -> Result<(), NnrpError> {
394 if destination.len() < expected {
395 return Err(NnrpError::DestinationTooShort {
396 expected,
397 actual: destination.len(),
398 });
399 }
400 Ok(())
401}
402
403fn validate_mask_u32(value: u32, allowed: u32) -> Result<(), NnrpError> {
404 if value & !allowed != 0 {
405 return Err(NnrpError::ReservedBitsSet {
406 value: value as u64,
407 allowed: allowed as u64,
408 });
409 }
410 Ok(())
411}
412
413fn validate_zero_u32(field: &'static str, value: u32) -> Result<(), NnrpError> {
414 if value != 0 {
415 return Err(NnrpError::NonZeroReservedField { field });
416 }
417 Ok(())
418}
419
420fn read_u32(source: &[u8], offset: usize) -> u32 {
421 u32::from_le_bytes(source[offset..offset + 4].try_into().expect("slice length"))
422}
423
424fn read_u64(source: &[u8], offset: usize) -> u64 {
425 u64::from_le_bytes(source[offset..offset + 8].try_into().expect("slice length"))
426}
427
428fn write_u32(destination: &mut [u8], offset: usize, value: u32) {
429 destination[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
430}
431
432fn write_u64(destination: &mut [u8], offset: usize, value: u64) {
433 destination[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn cache_metadata_round_trips_python_golden_vectors() {
442 let put_bytes = hex_to_bytes(
443 "0100000001000000887766554433221100ffeeddccbbaa99983a0000000800000300000003000000",
444 );
445 let put = CachePutMetadata::parse(&put_bytes).unwrap();
446 assert_eq!(put.cache_namespace, 1);
447 assert_eq!(put.cache_key_hi, 0x1122_3344_5566_7788);
448 assert_eq!(put.cache_key_lo, 0x99aa_bbcc_ddee_ff00);
449 assert_eq!(put.object_kind, CacheObjectKind::CameraBlock);
450 assert_eq!(put.ttl_ms, 15_000);
451 assert_eq!(put.object_bytes, 2048);
452 assert_eq!(put.flags, 3);
453 assert_eq!(put.to_bytes().unwrap().as_slice(), put_bytes.as_slice());
454
455 let ack_bytes = hex_to_bytes(
456 "0100000000000000887766554433221100ffeeddccbbaa99983a0000002000000000000000000000",
457 );
458 let ack = CacheAckMetadata::parse(&ack_bytes).unwrap();
459 assert_eq!(ack.status, CacheAckStatus::Accepted);
460 assert_eq!(ack.max_object_bytes, 8192);
461 assert_eq!(ack.to_bytes().unwrap().as_slice(), ack_bytes.as_slice());
462
463 let invalidate_bytes =
464 hex_to_bytes("0300000001000000887766554433221100ffeeddccbbaa990200000000000000");
465 let invalidate = CacheInvalidateMetadata::parse(&invalidate_bytes).unwrap();
466 assert_eq!(invalidate.invalidate_scope, CacheInvalidateScope::ObjectKey);
467 assert_eq!(invalidate.cache_namespace, 1);
468 assert_eq!(invalidate.cache_key_hi, 0x1122_3344_5566_7788);
469 assert_eq!(invalidate.cache_key_lo, 0x99aa_bbcc_ddee_ff00);
470 assert_eq!(
471 invalidate.to_bytes().unwrap().as_slice(),
472 invalidate_bytes.as_slice()
473 );
474 }
475
476 #[test]
477 fn cache_metadata_rejects_unknown_assignments_and_flags() {
478 for value in 1..=9 {
479 assert!(CacheObjectKind::try_from_u32(value).is_ok());
480 }
481 for value in 0..=2 {
482 assert!(CacheAckStatus::try_from_u32(value).is_ok());
483 }
484 for value in 0..=3 {
485 assert!(CacheInvalidateScope::try_from_u32(value).is_ok());
486 }
487
488 assert_eq!(
489 CacheObjectKind::try_from_u32(0xffff),
490 Err(NnrpError::UnknownEnumValue {
491 enum_name: "cache_object_kind",
492 value: 0xffff
493 })
494 );
495 assert_eq!(
496 CacheInvalidateScope::try_from_u32(0xff),
497 Err(NnrpError::UnknownEnumValue {
498 enum_name: "cache_invalidate_scope",
499 value: 0xff
500 })
501 );
502
503 let mut put_bytes = [0u8; CACHE_PUT_METADATA_LEN];
504 write_u32(&mut put_bytes, 4, CacheObjectKind::CameraBlock as u32);
505 write_u32(&mut put_bytes, 36, 0x4);
506 assert_eq!(
507 CachePutMetadata::parse(&put_bytes),
508 Err(NnrpError::ReservedBitsSet {
509 value: 0x4,
510 allowed: CACHE_PUT_FLAGS_KNOWN_MASK as u64
511 })
512 );
513
514 assert_eq!(
515 CacheAckStatus::try_from_u32(99),
516 Err(NnrpError::UnknownEnumValue {
517 enum_name: "cache_ack_status",
518 value: 99
519 })
520 );
521 assert_eq!(
522 CachePutMetadata::parse(&[0u8; CACHE_PUT_METADATA_LEN - 1]),
523 Err(NnrpError::SourceTooShort {
524 expected: CACHE_PUT_METADATA_LEN,
525 actual: CACHE_PUT_METADATA_LEN - 1
526 })
527 );
528 let put = CachePutMetadata {
529 cache_namespace: 1,
530 cache_key_hi: 2,
531 cache_key_lo: 3,
532 object_kind: CacheObjectKind::CameraBlock,
533 ttl_ms: 4,
534 object_bytes: 5,
535 codec_bitmap: 6,
536 flags: 0,
537 };
538 assert_eq!(
539 put.write(&mut [0u8; CACHE_PUT_METADATA_LEN - 1]),
540 Err(NnrpError::DestinationTooShort {
541 expected: CACHE_PUT_METADATA_LEN,
542 actual: CACHE_PUT_METADATA_LEN - 1
543 })
544 );
545
546 let mut ack_bytes = [0u8; CACHE_ACK_METADATA_LEN];
547 write_u32(&mut ack_bytes, 36, 1);
548 assert_eq!(
549 CacheAckMetadata::parse(&ack_bytes),
550 Err(NnrpError::NonZeroReservedField {
551 field: "cache_ack.reserved"
552 })
553 );
554
555 let invalid_scope_fields = [
556 CacheInvalidateMetadata {
557 invalidate_scope: CacheInvalidateScope::WholeSession,
558 cache_namespace: 1,
559 cache_key_hi: 0,
560 cache_key_lo: 0,
561 reason_code: 0,
562 },
563 CacheInvalidateMetadata {
564 invalidate_scope: CacheInvalidateScope::Namespace,
565 cache_namespace: 1,
566 cache_key_hi: 1,
567 cache_key_lo: 0,
568 reason_code: 0,
569 },
570 CacheInvalidateMetadata {
571 invalidate_scope: CacheInvalidateScope::ObjectKind,
572 cache_namespace: 1,
573 cache_key_hi: u32::MAX as u64 + 1,
574 cache_key_lo: 0,
575 reason_code: 0,
576 },
577 CacheInvalidateMetadata {
578 invalidate_scope: CacheInvalidateScope::ObjectKind,
579 cache_namespace: 1,
580 cache_key_hi: CacheObjectKind::CameraBlock as u64,
581 cache_key_lo: 1,
582 reason_code: 0,
583 },
584 ];
585 for metadata in invalid_scope_fields {
586 assert_eq!(
587 metadata.to_bytes(),
588 Err(NnrpError::InvalidProtocolCombination {
589 rule: "cache invalidate identity fields must match invalidate_scope"
590 })
591 );
592 }
593
594 let mut invalidate_bytes = [0u8; CACHE_INVALIDATE_METADATA_LEN];
595 write_u32(
596 &mut invalidate_bytes,
597 0,
598 CacheInvalidateScope::WholeSession as u32,
599 );
600 write_u32(&mut invalidate_bytes, 28, 1);
601 assert_eq!(
602 CacheInvalidateMetadata::parse(&invalidate_bytes),
603 Err(NnrpError::NonZeroReservedField {
604 field: "cache_invalidate.reserved"
605 })
606 );
607 }
608
609 #[test]
610 fn cache_lease_exports_stable_validation_failures() {
611 let object_id = CacheObjectId {
612 cache_namespace: 1,
613 cache_key_hi: 2,
614 cache_key_lo: 3,
615 object_kind: CacheObjectKind::PromptSegment,
616 };
617 let lease = CacheLease {
618 object_id,
619 object_version: 7,
620 lease_id: 99,
621 owner_scope: CacheLeaseOwnerScope::Session,
622 owner_id: 42,
623 granted_at_ms: 1_000,
624 ttl_ms: 500,
625 };
626
627 assert_eq!(lease.expires_at_ms(), 1_500);
628 assert_eq!(lease.validate_live_at(1_499), Ok(()));
629 assert_eq!(
630 lease.validate_live_at(1_500),
631 Err(CacheValidationFailure::LeaseExpired)
632 );
633 assert_eq!(lease.validate_version(7), Ok(()));
634 assert_eq!(
635 lease.validate_version(8),
636 Err(CacheValidationFailure::VersionMismatch)
637 );
638 assert_eq!(
639 CacheValidationFailure::LeaseExpired.error_code(),
640 CACHE_ERROR_LEASE_EXPIRED
641 );
642 assert_eq!(
643 CacheValidationFailure::SchemaMismatch.error_code(),
644 CACHE_ERROR_SCHEMA_MISMATCH
645 );
646 }
647
648 #[test]
649 fn cache_dependencies_validate_versions_and_invalidations() {
650 let object_id = CacheObjectId {
651 cache_namespace: 1,
652 cache_key_hi: 2,
653 cache_key_lo: 3,
654 object_kind: CacheObjectKind::PromptSegment,
655 };
656 let dependencies = [CacheDependency {
657 object_id,
658 required_version: 7,
659 }];
660 let states = [CacheDependencyState {
661 object_id,
662 current_version: 7,
663 invalidated: false,
664 }];
665
666 assert_eq!(validate_cache_dependencies(&dependencies, &states), Ok(()));
667
668 let wrong_version = [CacheDependencyState {
669 current_version: 8,
670 ..states[0]
671 }];
672 assert_eq!(
673 validate_cache_dependencies(&dependencies, &wrong_version),
674 Err(CacheValidationFailure::DependencyInvalid)
675 );
676
677 let invalidated = [CacheDependencyState {
678 invalidated: true,
679 ..states[0]
680 }];
681 assert_eq!(
682 validate_cache_dependencies(&dependencies, &invalidated),
683 Err(CacheValidationFailure::DependencyInvalid)
684 );
685 assert_eq!(
686 validate_cache_dependencies(&dependencies, &[]),
687 Err(CacheValidationFailure::DependencyInvalid)
688 );
689 }
690
691 #[test]
692 fn cache_object_id_consumes_invalidate_scopes() {
693 let put = CachePutMetadata {
694 cache_namespace: 7,
695 cache_key_hi: 8,
696 cache_key_lo: 9,
697 object_kind: CacheObjectKind::ToolSchema,
698 ttl_ms: 100,
699 object_bytes: 64,
700 codec_bitmap: 0,
701 flags: 0,
702 };
703 let object_id = CacheObjectId::from_put(&put);
704
705 assert!(object_id.matches_invalidate(&CacheInvalidateMetadata {
706 invalidate_scope: CacheInvalidateScope::WholeSession,
707 cache_namespace: 0,
708 cache_key_hi: 0,
709 cache_key_lo: 0,
710 reason_code: 0,
711 }));
712 assert!(object_id.matches_invalidate(&CacheInvalidateMetadata {
713 invalidate_scope: CacheInvalidateScope::Namespace,
714 cache_namespace: 7,
715 cache_key_hi: 0,
716 cache_key_lo: 0,
717 reason_code: 0,
718 }));
719 assert!(object_id.matches_invalidate(&CacheInvalidateMetadata {
720 invalidate_scope: CacheInvalidateScope::ObjectKind,
721 cache_namespace: 7,
722 cache_key_hi: CacheObjectKind::ToolSchema as u64,
723 cache_key_lo: 0,
724 reason_code: 0,
725 }));
726 assert!(object_id.matches_invalidate(&CacheInvalidateMetadata {
727 invalidate_scope: CacheInvalidateScope::ObjectKey,
728 cache_namespace: 7,
729 cache_key_hi: 8,
730 cache_key_lo: 9,
731 reason_code: 0,
732 }));
733 assert!(!object_id.matches_invalidate(&CacheInvalidateMetadata {
734 invalidate_scope: CacheInvalidateScope::ObjectKey,
735 cache_namespace: 7,
736 cache_key_hi: 8,
737 cache_key_lo: 10,
738 reason_code: 0,
739 }));
740 }
741
742 fn hex_to_bytes(hex: &str) -> Vec<u8> {
743 assert_eq!(hex.len() % 2, 0);
744 (0..hex.len())
745 .step_by(2)
746 .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).unwrap())
747 .collect()
748 }
749}