1use crate::batch::BatchRequest;
2use crate::clients::ClientMetadata;
3use crate::codes::AGDX_BATCH_CODE;
4use crate::codes::KV_LEASE_OP_VERSION;
5use crate::error::InvalidError;
6use crate::graph::{GraphEdge, GraphNode, GraphQuery, GraphUpsert, SourceRef};
7use crate::kv::{KvCasFenced, KvLease, KvLeaseRenew, KvRelease, KvScan, KvSet};
8use crate::limits::{
9 MAX_BATCH_OPS, MAX_CLIENT_METADATA, MAX_FRAME_BYTES, MAX_GRAPH_NODE_LABELS,
10 MAX_GRAPH_RESULT_ELEMENTS, MAX_GRAPH_TRAVERSE_DEPTH, MAX_HOLDER_ID_BYTES, MAX_KEY_BYTES,
11 MAX_LEASE_TTL_MICROS, MAX_MEMORY_BODY_BYTES, MAX_METADATA_ENTRIES, MAX_METADATA_KEY_BYTES,
12 MAX_PAGE_SIZE, MAX_QUERY_NAME_BYTES, MAX_SCAN_LIMIT, MAX_SOURCE_REF_BYTES,
13 MAX_TEXT_QUERY_BYTES, MAX_VALUE_BYTES, MAX_VECTOR_DIMENSIONS, MIN_LEASE_TTL_MICROS,
14};
15use crate::memory::MemoryRecord;
16use crate::query::{TextQuery, Value, VectorQuery};
17
18pub trait Validate {
24 fn validate(&self) -> Result<(), InvalidError>;
26}
27
28pub(crate) fn validate_safelisted_name(
34 label: &str,
35 value: &str,
36 cap: usize,
37) -> Result<(), InvalidError> {
38 if value.is_empty() {
39 return Err(InvalidError::new(format!("{label} must not be empty")));
40 }
41 if value.len() > cap {
42 return Err(InvalidError::new(format!(
43 "{label} is {}B, exceeds cap {cap}B",
44 value.len()
45 )));
46 }
47 if let Some(bad) = value
48 .bytes()
49 .find(|byte| !matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
50 {
51 return Err(InvalidError::new(format!(
52 "{label} has a disallowed byte {bad:#04x}: allowed are ASCII letters, digits, '-', '_', '.'"
53 )));
54 }
55 Ok(())
56}
57
58impl Validate for BatchRequest {
59 fn validate(&self) -> Result<(), InvalidError> {
60 if self.ops.len() > MAX_BATCH_OPS {
61 return Err(InvalidError::new(format!(
62 "batch has {} ops, exceeds cap {MAX_BATCH_OPS}",
63 self.ops.len()
64 )));
65 }
66 let mut total = 0usize;
67 for item in &self.ops {
68 if item.code == AGDX_BATCH_CODE {
71 return Err(InvalidError::new(
72 "a batch may not contain a batch op".to_owned(),
73 ));
74 }
75 if item.payload.len() > MAX_VALUE_BYTES {
76 return Err(InvalidError::new(format!(
77 "batch op payload is {}B, exceeds cap {MAX_VALUE_BYTES}B",
78 item.payload.len()
79 )));
80 }
81 total = total.saturating_add(item.payload.len());
82 }
83 if total > MAX_FRAME_BYTES {
84 return Err(InvalidError::new(format!(
85 "batch total payload is {total}B, exceeds cap {MAX_FRAME_BYTES}B"
86 )));
87 }
88 Ok(())
89 }
90}
91
92impl Validate for KvSet {
93 fn validate(&self) -> Result<(), InvalidError> {
94 crate::kv::validate_namespace(&self.namespace)?;
95 if self.key.is_empty() {
96 return Err(InvalidError::new("key-value key is empty".to_owned()));
97 }
98 if self.key.len() > MAX_KEY_BYTES {
99 return Err(InvalidError::new(format!(
100 "key is {}B, exceeds cap {MAX_KEY_BYTES}B",
101 self.key.len()
102 )));
103 }
104 if self.value.len() > MAX_VALUE_BYTES {
105 return Err(InvalidError::new(format!(
106 "value is {}B, exceeds cap {MAX_VALUE_BYTES}B",
107 self.value.len()
108 )));
109 }
110 Ok(())
111 }
112}
113
114impl Validate for KvScan {
115 fn validate(&self) -> Result<(), InvalidError> {
116 crate::kv::validate_namespace(&self.namespace)?;
117 if self.limit > MAX_SCAN_LIMIT {
118 return Err(InvalidError::new(format!(
119 "scan limit {} exceeds cap {MAX_SCAN_LIMIT}",
120 self.limit
121 )));
122 }
123 Ok(())
124 }
125}
126
127fn validate_lease_shape(
131 v: u32,
132 namespace: &str,
133 key: &[u8],
134 holder_id: &str,
135) -> Result<(), InvalidError> {
136 if v != KV_LEASE_OP_VERSION {
137 return Err(InvalidError::new(format!(
138 "unsupported fenced-lease op version (expected {KV_LEASE_OP_VERSION}, got {v})"
139 )));
140 }
141 crate::kv::validate_namespace(namespace)?;
142 validate_kv_key("lease key", key)?;
143 if holder_id.is_empty() {
144 return Err(InvalidError::new("lease holder id must not be empty"));
145 }
146 if holder_id.len() > MAX_HOLDER_ID_BYTES {
147 return Err(InvalidError::new(format!(
148 "lease holder id is {}B, exceeds cap {MAX_HOLDER_ID_BYTES}B",
149 holder_id.len()
150 )));
151 }
152 Ok(())
153}
154
155fn validate_lease_ttl(ttl_micros: u64) -> Result<(), InvalidError> {
162 if !(MIN_LEASE_TTL_MICROS..=MAX_LEASE_TTL_MICROS).contains(&ttl_micros) {
163 return Err(InvalidError::new(format!(
164 "lease ttl is {ttl_micros}\u{b5}s, outside the supported range \
165 {MIN_LEASE_TTL_MICROS}..={MAX_LEASE_TTL_MICROS}\u{b5}s"
166 )));
167 }
168 Ok(())
169}
170
171fn validate_kv_key(label: &str, key: &[u8]) -> Result<(), InvalidError> {
172 if key.is_empty() {
173 return Err(InvalidError::new(format!("{label} is empty")));
174 }
175 if key.len() > MAX_KEY_BYTES {
176 return Err(InvalidError::new(format!(
177 "{label} is {}B, exceeds cap {MAX_KEY_BYTES}B",
178 key.len()
179 )));
180 }
181 Ok(())
182}
183
184impl Validate for KvLease {
185 fn validate(&self) -> Result<(), InvalidError> {
186 validate_lease_shape(self.v, &self.namespace, &self.key, &self.holder_id)?;
187 validate_lease_ttl(self.lease_ttl_micros)?;
188 Ok(())
189 }
190}
191
192impl Validate for KvLeaseRenew {
193 fn validate(&self) -> Result<(), InvalidError> {
194 validate_lease_shape(self.v, &self.namespace, &self.key, &self.holder_id)?;
195 validate_lease_ttl(self.lease_ttl_micros)?;
196 if self.lease_token == 0 {
197 return Err(InvalidError::new("lease token must not be zero"));
198 }
199 Ok(())
200 }
201}
202
203impl Validate for KvRelease {
204 fn validate(&self) -> Result<(), InvalidError> {
205 validate_lease_shape(self.v, &self.namespace, &self.key, &self.holder_id)?;
206 if self.lease_token == 0 {
207 return Err(InvalidError::new("lease token must not be zero"));
208 }
209 Ok(())
210 }
211}
212
213impl Validate for KvCasFenced {
214 fn validate(&self) -> Result<(), InvalidError> {
215 if self.v != KV_LEASE_OP_VERSION {
216 return Err(InvalidError::new(format!(
217 "unsupported fenced-lease op version (expected {KV_LEASE_OP_VERSION}, got {})",
218 self.v
219 )));
220 }
221 crate::kv::validate_namespace(&self.namespace)?;
222 crate::kv::validate_namespace(&self.fence_namespace)?;
223 validate_kv_key("key-value key", &self.key)?;
224 validate_kv_key("fence key", &self.fence_key)?;
225 if self.value.len() > MAX_VALUE_BYTES {
226 return Err(InvalidError::new(format!(
227 "value is {}B, exceeds cap {MAX_VALUE_BYTES}B",
228 self.value.len()
229 )));
230 }
231 if self.fence_token == 0 {
233 return Err(InvalidError::new("fence token must not be zero"));
234 }
235 Ok(())
236 }
237}
238
239impl Validate for GraphQuery {
240 fn validate(&self) -> Result<(), InvalidError> {
241 crate::graph::validate_graph_name(&self.graph)?;
242 if self.traverse.len() > MAX_GRAPH_TRAVERSE_DEPTH as usize {
243 return Err(InvalidError::new(format!(
244 "graph traversal depth {} exceeds cap {MAX_GRAPH_TRAVERSE_DEPTH}",
245 self.traverse.len()
246 )));
247 }
248 if self.limit > MAX_GRAPH_RESULT_ELEMENTS {
249 return Err(InvalidError::new(format!(
250 "graph result limit {} exceeds cap {MAX_GRAPH_RESULT_ELEMENTS}",
251 self.limit
252 )));
253 }
254 Ok(())
255 }
256}
257
258impl Validate for SourceRef {
259 fn validate(&self) -> Result<(), InvalidError> {
260 let size = match self {
261 SourceRef::Message { conversation, .. } => conversation.as_ref().map_or(0, String::len),
264 SourceRef::Kv { namespace, key } => namespace.len() + key.len(),
265 SourceRef::Memory { id } => id.len(),
266 };
267 if size > MAX_SOURCE_REF_BYTES {
268 return Err(InvalidError::new(format!(
269 "source reference is {size}B, exceeds cap {MAX_SOURCE_REF_BYTES}B"
270 )));
271 }
272 Ok(())
273 }
274}
275
276fn validate_attrs(label: &str, attrs: &[(String, Value)]) -> Result<(), InvalidError> {
278 if attrs.len() > MAX_METADATA_ENTRIES {
279 return Err(InvalidError::new(format!(
280 "{label} carries {} attributes, exceeds cap {MAX_METADATA_ENTRIES}",
281 attrs.len()
282 )));
283 }
284 for (key, _) in attrs {
285 if key.len() > MAX_METADATA_KEY_BYTES {
286 return Err(InvalidError::new(format!(
287 "{label} attribute name is {}B, exceeds cap {MAX_METADATA_KEY_BYTES}B",
288 key.len()
289 )));
290 }
291 }
292 Ok(())
293}
294
295impl Validate for GraphNode {
296 fn validate(&self) -> Result<(), InvalidError> {
297 if self.labels.len() > MAX_GRAPH_NODE_LABELS {
298 return Err(InvalidError::new(format!(
299 "graph node carries {} labels, exceeds cap {MAX_GRAPH_NODE_LABELS}",
300 self.labels.len()
301 )));
302 }
303 validate_attrs("graph node", &self.attrs)?;
304 if let Some(source) = &self.source {
305 source.validate()?;
306 }
307 Ok(())
308 }
309}
310
311impl Validate for GraphEdge {
312 fn validate(&self) -> Result<(), InvalidError> {
313 validate_attrs("graph edge", &self.attrs)?;
314 if let Some(source) = &self.source {
315 source.validate()?;
316 }
317 Ok(())
318 }
319}
320
321impl Validate for GraphUpsert {
322 fn validate(&self) -> Result<(), InvalidError> {
323 crate::graph::validate_graph_name(&self.graph)?;
324 let elements = self.nodes.len() + self.edges.len();
325 if elements > MAX_GRAPH_RESULT_ELEMENTS {
326 return Err(InvalidError::new(format!(
327 "graph upsert carries {elements} elements, exceeds cap {MAX_GRAPH_RESULT_ELEMENTS}"
328 )));
329 }
330 for node in &self.nodes {
331 node.validate()?;
332 }
333 for edge in &self.edges {
334 edge.validate()?;
335 }
336 Ok(())
337 }
338}
339
340impl Validate for TextQuery {
341 fn validate(&self) -> Result<(), InvalidError> {
342 if self.query.trim().is_empty() || self.query.len() > MAX_TEXT_QUERY_BYTES {
343 return Err(InvalidError::new(format!(
344 "text query is {}B, expected 1..={MAX_TEXT_QUERY_BYTES}B",
345 self.query.len()
346 )));
347 }
348 if self.query.chars().any(char::is_control) {
349 return Err(InvalidError::new("text query contains a control character"));
350 }
351 if let Some(field) = &self.field
352 && (field.is_empty()
353 || field.len() > MAX_QUERY_NAME_BYTES
354 || field.chars().any(char::is_control))
355 {
356 return Err(InvalidError::new("text query field is invalid"));
357 }
358 Ok(())
359 }
360}
361
362impl Validate for VectorQuery {
363 fn validate(&self) -> Result<(), InvalidError> {
364 if self.field.is_empty()
365 || self.field.len() > MAX_QUERY_NAME_BYTES
366 || self.field.chars().any(char::is_control)
367 {
368 return Err(InvalidError::new("vector field is invalid"));
369 }
370 if self.embedding.is_empty() || self.embedding.len() > MAX_VECTOR_DIMENSIONS {
371 return Err(InvalidError::new(format!(
372 "vector dimensions must be in 1..={MAX_VECTOR_DIMENSIONS}"
373 )));
374 }
375 if self
376 .embedding
377 .iter()
378 .any(|value| !value.is_finite() || (*value == 0.0 && value.is_sign_negative()))
379 {
380 return Err(InvalidError::new(
381 "vector values must be finite canonical floats",
382 ));
383 }
384 if self.top_k == 0 || self.top_k > MAX_PAGE_SIZE as u32 {
385 return Err(InvalidError::new(format!(
386 "vector top_k {} is outside 1..={MAX_PAGE_SIZE}",
387 self.top_k
388 )));
389 }
390 Ok(())
391 }
392}
393
394impl Validate for MemoryRecord {
395 fn validate(&self) -> Result<(), InvalidError> {
396 if let MemoryRecord::Item { body, .. } = self
397 && body.len() > MAX_MEMORY_BODY_BYTES
398 {
399 return Err(InvalidError::new(format!(
400 "memory body is {}B, exceeds cap {MAX_MEMORY_BODY_BYTES}B",
401 body.len()
402 )));
403 }
404 Ok(())
405 }
406}
407
408impl Validate for ClientMetadata {
409 fn validate(&self) -> Result<(), InvalidError> {
410 let size = self.metadata.as_ref().map_or(0, Vec::len);
411 if size > MAX_CLIENT_METADATA {
412 return Err(InvalidError::new(format!(
413 "client metadata is {size}B, exceeds cap {MAX_CLIENT_METADATA}B"
414 )));
415 }
416 Ok(())
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::batch::BatchItem;
424 use crate::codes::{AGDX_KV_GET_CODE, BATCH_OP_VERSION, KV_OP_VERSION};
425
426 #[test]
427 fn given_a_batch_over_the_op_cap_when_validated_then_should_reject() {
428 let request = BatchRequest {
429 v: BATCH_OP_VERSION,
430 ops: (0..MAX_BATCH_OPS + 1)
431 .map(|_| BatchItem {
432 code: AGDX_KV_GET_CODE,
433 payload: Vec::new(),
434 })
435 .collect(),
436 };
437 assert!(request.validate().is_err());
438 }
439
440 #[test]
441 fn given_a_batch_nesting_a_batch_when_validated_then_should_reject() {
442 let request = BatchRequest {
443 v: BATCH_OP_VERSION,
444 ops: vec![BatchItem {
445 code: AGDX_BATCH_CODE,
446 payload: Vec::new(),
447 }],
448 };
449 assert!(request.validate().is_err());
450 }
451
452 #[test]
453 fn given_namespaces_when_validated_then_should_enforce_bounds() {
454 use crate::kv::validate_namespace;
455 use crate::limits::MAX_NAMESPACE_BYTES;
456 assert!(validate_namespace("default").is_ok());
457 assert!(validate_namespace("agent-abc/session").is_ok(), "hierarchy");
458 assert!(validate_namespace("").is_err(), "empty");
459 assert!(validate_namespace("bad\nns").is_err(), "control byte");
460 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES)).is_ok());
461 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES + 1)).is_err());
462 }
463
464 #[test]
465 fn given_graph_names_when_validated_then_should_enforce_bounds() {
466 use crate::graph::validate_graph_name;
467 use crate::limits::MAX_GRAPH_NAME_BYTES;
468 assert!(validate_graph_name("knowledge").is_ok());
469 assert!(validate_graph_name("").is_err(), "empty");
470 assert!(validate_graph_name("bad\tname").is_err(), "control byte");
471 assert!(validate_graph_name(&"g".repeat(MAX_GRAPH_NAME_BYTES + 1)).is_err());
472 }
473
474 #[test]
475 fn given_a_lease_request_when_validated_then_should_enforce_version_holder_and_ttl() {
476 use crate::codes::KV_LEASE_OP_VERSION;
477 use crate::kv::KvLease;
478 use crate::limits::MAX_HOLDER_ID_BYTES;
479 let valid = KvLease {
480 v: KV_LEASE_OP_VERSION,
481 namespace: "connectors.coordination".to_owned(),
482 key: b"source-owner".to_vec(),
483 lease_ttl_micros: 30_000_000,
484 holder_id: "warden-node-1".to_owned(),
485 subject_user_id: Some(42),
486 };
487 assert!(valid.validate().is_ok());
488 let wrong_version = KvLease {
489 v: KV_LEASE_OP_VERSION + 1,
490 ..valid.clone()
491 };
492 assert!(
493 wrong_version.validate().is_err(),
494 "an unsupported version is rejected typed"
495 );
496 let empty_holder = KvLease {
497 holder_id: String::new(),
498 ..valid.clone()
499 };
500 assert!(empty_holder.validate().is_err());
501 let long_holder = KvLease {
502 holder_id: "h".repeat(MAX_HOLDER_ID_BYTES + 1),
503 ..valid.clone()
504 };
505 assert!(long_holder.validate().is_err());
506 let zero_ttl = KvLease {
507 lease_ttl_micros: 0,
508 ..valid
509 };
510 assert!(zero_ttl.validate().is_err());
511 }
512
513 #[test]
516 fn given_out_of_range_lease_ttls_when_validated_then_should_reject_both_ends() {
517 use crate::codes::KV_LEASE_OP_VERSION;
518 use crate::kv::{KvLease, KvLeaseRenew};
519 use crate::limits::{MAX_LEASE_TTL_MICROS, MIN_LEASE_TTL_MICROS};
520 let acquire = KvLease {
521 v: KV_LEASE_OP_VERSION,
522 namespace: "connectors.coordination".to_owned(),
523 key: b"source-owner".to_vec(),
524 lease_ttl_micros: MIN_LEASE_TTL_MICROS,
525 holder_id: "warden-node-1".to_owned(),
526 subject_user_id: None,
527 };
528 assert!(acquire.validate().is_ok(), "the floor itself is grantable");
529 assert!(
530 KvLease {
531 lease_ttl_micros: MAX_LEASE_TTL_MICROS,
532 ..acquire.clone()
533 }
534 .validate()
535 .is_ok(),
536 "the ceiling itself is grantable"
537 );
538 assert!(
539 KvLease {
540 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
541 ..acquire.clone()
542 }
543 .validate()
544 .is_err(),
545 "a lease too short to renew is rejected locally"
546 );
547 assert!(
548 KvLease {
549 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
550 ..acquire
551 }
552 .validate()
553 .is_err(),
554 "an unbounded ownership window is rejected locally"
555 );
556 let renew = KvLeaseRenew {
557 v: KV_LEASE_OP_VERSION,
558 namespace: "connectors.coordination".to_owned(),
559 key: b"source-owner".to_vec(),
560 holder_id: "warden-node-1".to_owned(),
561 subject_user_id: None,
562 lease_token: 7,
563 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
564 };
565 assert!(renew.validate().is_err(), "renewal shares the same range");
566 assert!(
567 KvLeaseRenew {
568 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
569 ..renew
570 }
571 .validate()
572 .is_err()
573 );
574 }
575
576 #[test]
577 fn given_renew_and_release_requests_when_validated_then_should_require_a_nonzero_token() {
578 use crate::codes::KV_LEASE_OP_VERSION;
579 use crate::kv::{KvLeaseRenew, KvRelease};
580 let renew = KvLeaseRenew {
581 v: KV_LEASE_OP_VERSION,
582 namespace: "connectors.coordination".to_owned(),
583 key: b"source-owner".to_vec(),
584 holder_id: "warden-node-1".to_owned(),
585 subject_user_id: None,
586 lease_token: 7,
587 lease_ttl_micros: 30_000_000,
588 };
589 assert!(renew.validate().is_ok());
590 let zero = KvLeaseRenew {
591 lease_token: 0,
592 ..renew
593 };
594 assert!(zero.validate().is_err());
595 let release = KvRelease {
596 v: KV_LEASE_OP_VERSION,
597 namespace: "connectors.coordination".to_owned(),
598 key: b"source-owner".to_vec(),
599 lease_token: 7,
600 holder_id: "warden-node-1".to_owned(),
601 };
602 assert!(release.validate().is_ok());
603 let zero = KvRelease {
604 lease_token: 0,
605 ..release
606 };
607 assert!(zero.validate().is_err());
608 }
609
610 #[test]
611 fn given_a_fenced_cas_when_validated_then_should_enforce_both_namespaces_and_the_token() {
612 use crate::codes::KV_LEASE_OP_VERSION;
613 use crate::kv::{CasExpect, KvCasFenced};
614 let valid = KvCasFenced {
615 v: KV_LEASE_OP_VERSION,
616 namespace: "connectors.state".to_owned(),
617 key: b"source_state/pg".to_vec(),
618 value: b"cursor".to_vec(),
619 expires_at_micros: None,
620 expect: CasExpect::Absent,
621 fence_namespace: "connectors.coordination".to_owned(),
622 fence_key: b"source-owner".to_vec(),
623 fence_token: 8,
624 };
625 assert!(valid.validate().is_ok());
626 let wrong_version = KvCasFenced {
627 v: KV_LEASE_OP_VERSION + 1,
628 ..valid.clone()
629 };
630 assert!(
631 wrong_version.validate().is_err(),
632 "an unsupported version is rejected typed"
633 );
634 let bad_fence_namespace = KvCasFenced {
635 fence_namespace: "bad\nns".to_owned(),
636 ..valid.clone()
637 };
638 assert!(bad_fence_namespace.validate().is_err());
639 let zero_token = KvCasFenced {
640 fence_token: 0,
641 ..valid
642 };
643 assert!(zero_token.validate().is_err());
644 }
645
646 #[test]
647 fn given_an_oversized_key_when_validated_then_should_reject_and_a_valid_one_passes() {
648 let over = KvSet {
649 v: KV_OP_VERSION,
650 namespace: "ns".to_owned(),
651 key: vec![b'x'; MAX_KEY_BYTES + 1],
652 value: vec![1, 2, 3],
653 expires_at_micros: None,
654 };
655 assert!(over.validate().is_err());
656 let ok = KvSet {
657 v: KV_OP_VERSION,
658 namespace: "ns".to_owned(),
659 key: vec![b'x'; 8],
660 value: vec![1, 2, 3],
661 expires_at_micros: None,
662 };
663 assert!(ok.validate().is_ok());
664 }
665
666 fn node_with(labels: Vec<String>, attrs: Vec<(String, Value)>) -> GraphNode {
667 GraphNode {
668 id: crate::graph::NodeId::from_u128(1),
669 labels,
670 attrs,
671 embedding: None,
672 source: None,
673 }
674 }
675
676 #[test]
677 fn given_a_node_over_the_label_cap_when_validated_then_should_reject() {
678 let over = node_with(
679 (0..=MAX_GRAPH_NODE_LABELS)
680 .map(|index| format!("label-{index}"))
681 .collect(),
682 Vec::new(),
683 );
684 assert!(over.validate().is_err());
685 assert!(
686 node_with(vec!["Person".to_owned()], Vec::new())
687 .validate()
688 .is_ok()
689 );
690 }
691
692 #[test]
693 fn given_a_node_over_the_attribute_cap_when_validated_then_should_reject() {
694 let over = node_with(
695 Vec::new(),
696 (0..=MAX_METADATA_ENTRIES)
697 .map(|index| (format!("attr-{index}"), Value::from("x")))
698 .collect(),
699 );
700 assert!(over.validate().is_err());
701 }
702
703 #[test]
704 fn given_an_oversized_source_reference_when_validated_then_should_reject() {
705 let over = SourceRef::Kv {
706 namespace: "n".repeat(MAX_SOURCE_REF_BYTES),
707 key: "k".repeat(MAX_SOURCE_REF_BYTES),
708 };
709 assert!(over.validate().is_err());
710 assert!(
711 SourceRef::Memory {
712 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
713 }
714 .validate()
715 .is_ok()
716 );
717 }
718
719 #[test]
720 fn given_an_upsert_over_the_element_cap_when_validated_then_should_reject() {
721 let over = GraphUpsert {
722 v: 1,
723 graph: "knowledge".to_owned(),
724 nodes: (0..=MAX_GRAPH_RESULT_ELEMENTS)
725 .map(|_| node_with(Vec::new(), Vec::new()))
726 .collect(),
727 edges: Vec::new(),
728 };
729 assert!(over.validate().is_err());
730 let ok = GraphUpsert {
731 v: 1,
732 graph: "knowledge".to_owned(),
733 nodes: vec![node_with(vec!["Person".to_owned()], Vec::new())],
734 edges: Vec::new(),
735 };
736 assert!(ok.validate().is_ok());
737 }
738
739 #[test]
740 fn given_an_upsert_carrying_an_over_cap_node_when_validated_then_should_reject() {
741 let upsert = GraphUpsert {
742 v: 1,
743 graph: "knowledge".to_owned(),
744 nodes: vec![node_with(
745 (0..=MAX_GRAPH_NODE_LABELS)
746 .map(|index| format!("label-{index}"))
747 .collect(),
748 Vec::new(),
749 )],
750 edges: Vec::new(),
751 };
752 assert!(
753 upsert.validate().is_err(),
754 "an upsert must enforce its elements' caps, not just its own"
755 );
756 }
757
758 #[test]
759 fn given_an_oversized_text_query_when_validated_then_should_reject() {
760 let over = TextQuery {
761 field: None,
762 query: "q".repeat(MAX_TEXT_QUERY_BYTES + 1),
763 };
764 assert!(over.validate().is_err());
765 let ok = TextQuery {
766 field: None,
767 query: "auth is slow".to_owned(),
768 };
769 assert!(ok.validate().is_ok());
770 }
771
772 #[test]
773 fn given_an_over_cap_vector_top_k_when_validated_then_should_reject() {
774 let over = VectorQuery {
775 field: "embedding".to_owned(),
776 embedding: vec![0.0; 4],
777 top_k: MAX_PAGE_SIZE as u32 + 1,
778 };
779 assert!(over.validate().is_err());
780 let ok = VectorQuery {
781 field: "embedding".to_owned(),
782 embedding: vec![0.0; 4],
783 top_k: 10,
784 };
785 assert!(ok.validate().is_ok());
786 }
787
788 #[test]
789 fn given_an_oversized_memory_body_when_validated_then_should_reject() {
790 let over = MemoryRecord::Item {
791 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
792 kind: "fact".to_owned(),
793 body: vec![0u8; MAX_MEMORY_BODY_BYTES + 1],
794 };
795 assert!(over.validate().is_err());
796 let ok = MemoryRecord::Item {
797 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
798 kind: "fact".to_owned(),
799 body: b"auth is slow".to_vec(),
800 };
801 assert!(ok.validate().is_ok());
802 assert!(
803 MemoryRecord::Forget {
804 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
805 }
806 .validate()
807 .is_ok()
808 );
809 }
810
811 #[test]
812 fn given_oversized_client_metadata_when_validated_then_should_reject() {
813 let over = ClientMetadata {
814 client_id: 1,
815 user_id: None,
816 transport: 1,
817 address: "127.0.0.1:8090".to_owned(),
818 consumer_groups_count: 0,
819 metadata: Some(vec![0u8; MAX_CLIENT_METADATA + 1]),
820 };
821 assert!(over.validate().is_err());
822 let ok = ClientMetadata {
823 client_id: 1,
824 user_id: None,
825 transport: 1,
826 address: "127.0.0.1:8090".to_owned(),
827 consumer_groups_count: 0,
828 metadata: Some(b"card".to_vec()),
829 };
830 assert!(ok.validate().is_ok());
831 }
832}