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_OP_VERSION,
490 ..valid.clone()
491 };
492 assert!(wrong_version.validate().is_err(), "v1 is rejected typed");
493 let empty_holder = KvLease {
494 holder_id: String::new(),
495 ..valid.clone()
496 };
497 assert!(empty_holder.validate().is_err());
498 let long_holder = KvLease {
499 holder_id: "h".repeat(MAX_HOLDER_ID_BYTES + 1),
500 ..valid.clone()
501 };
502 assert!(long_holder.validate().is_err());
503 let zero_ttl = KvLease {
504 lease_ttl_micros: 0,
505 ..valid
506 };
507 assert!(zero_ttl.validate().is_err());
508 }
509
510 #[test]
513 fn given_out_of_range_lease_ttls_when_validated_then_should_reject_both_ends() {
514 use crate::codes::KV_LEASE_OP_VERSION;
515 use crate::kv::{KvLease, KvLeaseRenew};
516 use crate::limits::{MAX_LEASE_TTL_MICROS, MIN_LEASE_TTL_MICROS};
517 let acquire = KvLease {
518 v: KV_LEASE_OP_VERSION,
519 namespace: "connectors.coordination".to_owned(),
520 key: b"source-owner".to_vec(),
521 lease_ttl_micros: MIN_LEASE_TTL_MICROS,
522 holder_id: "warden-node-1".to_owned(),
523 subject_user_id: None,
524 };
525 assert!(acquire.validate().is_ok(), "the floor itself is grantable");
526 assert!(
527 KvLease {
528 lease_ttl_micros: MAX_LEASE_TTL_MICROS,
529 ..acquire.clone()
530 }
531 .validate()
532 .is_ok(),
533 "the ceiling itself is grantable"
534 );
535 assert!(
536 KvLease {
537 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
538 ..acquire.clone()
539 }
540 .validate()
541 .is_err(),
542 "a lease too short to renew is rejected locally"
543 );
544 assert!(
545 KvLease {
546 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
547 ..acquire
548 }
549 .validate()
550 .is_err(),
551 "an unbounded ownership window is rejected locally"
552 );
553 let renew = KvLeaseRenew {
554 v: KV_LEASE_OP_VERSION,
555 namespace: "connectors.coordination".to_owned(),
556 key: b"source-owner".to_vec(),
557 holder_id: "warden-node-1".to_owned(),
558 subject_user_id: None,
559 lease_token: 7,
560 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
561 };
562 assert!(renew.validate().is_err(), "renewal shares the same range");
563 assert!(
564 KvLeaseRenew {
565 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
566 ..renew
567 }
568 .validate()
569 .is_err()
570 );
571 }
572
573 #[test]
574 fn given_renew_and_release_requests_when_validated_then_should_require_a_nonzero_token() {
575 use crate::codes::KV_LEASE_OP_VERSION;
576 use crate::kv::{KvLeaseRenew, KvRelease};
577 let renew = KvLeaseRenew {
578 v: KV_LEASE_OP_VERSION,
579 namespace: "connectors.coordination".to_owned(),
580 key: b"source-owner".to_vec(),
581 holder_id: "warden-node-1".to_owned(),
582 subject_user_id: None,
583 lease_token: 7,
584 lease_ttl_micros: 30_000_000,
585 };
586 assert!(renew.validate().is_ok());
587 let zero = KvLeaseRenew {
588 lease_token: 0,
589 ..renew
590 };
591 assert!(zero.validate().is_err());
592 let release = KvRelease {
593 v: KV_LEASE_OP_VERSION,
594 namespace: "connectors.coordination".to_owned(),
595 key: b"source-owner".to_vec(),
596 lease_token: 7,
597 holder_id: "warden-node-1".to_owned(),
598 };
599 assert!(release.validate().is_ok());
600 let zero = KvRelease {
601 lease_token: 0,
602 ..release
603 };
604 assert!(zero.validate().is_err());
605 }
606
607 #[test]
608 fn given_a_fenced_cas_when_validated_then_should_enforce_both_namespaces_and_the_token() {
609 use crate::codes::KV_LEASE_OP_VERSION;
610 use crate::kv::{CasExpect, KvCasFenced};
611 let valid = KvCasFenced {
612 v: KV_LEASE_OP_VERSION,
613 namespace: "connectors.state".to_owned(),
614 key: b"source_state/pg".to_vec(),
615 value: b"cursor".to_vec(),
616 expires_at_micros: None,
617 expect: CasExpect::Absent,
618 fence_namespace: "connectors.coordination".to_owned(),
619 fence_key: b"source-owner".to_vec(),
620 fence_token: 8,
621 };
622 assert!(valid.validate().is_ok());
623 let wrong_version = KvCasFenced {
624 v: KV_OP_VERSION,
625 ..valid.clone()
626 };
627 assert!(wrong_version.validate().is_err(), "v1 is rejected typed");
628 let bad_fence_namespace = KvCasFenced {
629 fence_namespace: "bad\nns".to_owned(),
630 ..valid.clone()
631 };
632 assert!(bad_fence_namespace.validate().is_err());
633 let zero_token = KvCasFenced {
634 fence_token: 0,
635 ..valid
636 };
637 assert!(zero_token.validate().is_err());
638 }
639
640 #[test]
641 fn given_an_oversized_key_when_validated_then_should_reject_and_a_valid_one_passes() {
642 let over = KvSet {
643 v: KV_OP_VERSION,
644 namespace: "ns".to_owned(),
645 key: vec![b'x'; MAX_KEY_BYTES + 1],
646 value: vec![1, 2, 3],
647 expires_at_micros: None,
648 };
649 assert!(over.validate().is_err());
650 let ok = KvSet {
651 v: KV_OP_VERSION,
652 namespace: "ns".to_owned(),
653 key: vec![b'x'; 8],
654 value: vec![1, 2, 3],
655 expires_at_micros: None,
656 };
657 assert!(ok.validate().is_ok());
658 }
659
660 fn node_with(labels: Vec<String>, attrs: Vec<(String, Value)>) -> GraphNode {
661 GraphNode {
662 id: crate::graph::NodeId::from_u128(1),
663 labels,
664 attrs,
665 embedding: None,
666 source: None,
667 }
668 }
669
670 #[test]
671 fn given_a_node_over_the_label_cap_when_validated_then_should_reject() {
672 let over = node_with(
673 (0..=MAX_GRAPH_NODE_LABELS)
674 .map(|index| format!("label-{index}"))
675 .collect(),
676 Vec::new(),
677 );
678 assert!(over.validate().is_err());
679 assert!(
680 node_with(vec!["Person".to_owned()], Vec::new())
681 .validate()
682 .is_ok()
683 );
684 }
685
686 #[test]
687 fn given_a_node_over_the_attribute_cap_when_validated_then_should_reject() {
688 let over = node_with(
689 Vec::new(),
690 (0..=MAX_METADATA_ENTRIES)
691 .map(|index| (format!("attr-{index}"), Value::from("x")))
692 .collect(),
693 );
694 assert!(over.validate().is_err());
695 }
696
697 #[test]
698 fn given_an_oversized_source_reference_when_validated_then_should_reject() {
699 let over = SourceRef::Kv {
700 namespace: "n".repeat(MAX_SOURCE_REF_BYTES),
701 key: "k".repeat(MAX_SOURCE_REF_BYTES),
702 };
703 assert!(over.validate().is_err());
704 assert!(
705 SourceRef::Memory {
706 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
707 }
708 .validate()
709 .is_ok()
710 );
711 }
712
713 #[test]
714 fn given_an_upsert_over_the_element_cap_when_validated_then_should_reject() {
715 let over = GraphUpsert {
716 v: 1,
717 graph: "knowledge".to_owned(),
718 nodes: (0..=MAX_GRAPH_RESULT_ELEMENTS)
719 .map(|_| node_with(Vec::new(), Vec::new()))
720 .collect(),
721 edges: Vec::new(),
722 };
723 assert!(over.validate().is_err());
724 let ok = GraphUpsert {
725 v: 1,
726 graph: "knowledge".to_owned(),
727 nodes: vec![node_with(vec!["Person".to_owned()], Vec::new())],
728 edges: Vec::new(),
729 };
730 assert!(ok.validate().is_ok());
731 }
732
733 #[test]
734 fn given_an_upsert_carrying_an_over_cap_node_when_validated_then_should_reject() {
735 let upsert = GraphUpsert {
736 v: 1,
737 graph: "knowledge".to_owned(),
738 nodes: vec![node_with(
739 (0..=MAX_GRAPH_NODE_LABELS)
740 .map(|index| format!("label-{index}"))
741 .collect(),
742 Vec::new(),
743 )],
744 edges: Vec::new(),
745 };
746 assert!(
747 upsert.validate().is_err(),
748 "an upsert must enforce its elements' caps, not just its own"
749 );
750 }
751
752 #[test]
753 fn given_an_oversized_text_query_when_validated_then_should_reject() {
754 let over = TextQuery {
755 field: None,
756 query: "q".repeat(MAX_TEXT_QUERY_BYTES + 1),
757 };
758 assert!(over.validate().is_err());
759 let ok = TextQuery {
760 field: None,
761 query: "checkout is slow".to_owned(),
762 };
763 assert!(ok.validate().is_ok());
764 }
765
766 #[test]
767 fn given_an_over_cap_vector_top_k_when_validated_then_should_reject() {
768 let over = VectorQuery {
769 field: "embedding".to_owned(),
770 embedding: vec![0.0; 4],
771 top_k: MAX_PAGE_SIZE as u32 + 1,
772 };
773 assert!(over.validate().is_err());
774 let ok = VectorQuery {
775 field: "embedding".to_owned(),
776 embedding: vec![0.0; 4],
777 top_k: 10,
778 };
779 assert!(ok.validate().is_ok());
780 }
781
782 #[test]
783 fn given_an_oversized_memory_body_when_validated_then_should_reject() {
784 let over = MemoryRecord::Item {
785 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
786 kind: "fact".to_owned(),
787 body: vec![0u8; MAX_MEMORY_BODY_BYTES + 1],
788 };
789 assert!(over.validate().is_err());
790 let ok = MemoryRecord::Item {
791 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
792 kind: "fact".to_owned(),
793 body: b"checkout is slow".to_vec(),
794 };
795 assert!(ok.validate().is_ok());
796 assert!(
797 MemoryRecord::Forget {
798 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
799 }
800 .validate()
801 .is_ok()
802 );
803 }
804
805 #[test]
806 fn given_oversized_client_metadata_when_validated_then_should_reject() {
807 let over = ClientMetadata {
808 client_id: 1,
809 user_id: None,
810 transport: 1,
811 address: "127.0.0.1:8090".to_owned(),
812 consumer_groups_count: 0,
813 metadata: Some(vec![0u8; MAX_CLIENT_METADATA + 1]),
814 };
815 assert!(over.validate().is_err());
816 let ok = ClientMetadata {
817 client_id: 1,
818 user_id: None,
819 transport: 1,
820 address: "127.0.0.1:8090".to_owned(),
821 consumer_groups_count: 0,
822 metadata: Some(b"card".to_vec()),
823 };
824 assert!(ok.validate().is_ok());
825 }
826}