Skip to main content

nnrp_core/
cache.rs

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}