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 if let MemoryRecord::Forget {
405 conversation: Some(conversation),
406 ..
407 }
408 | MemoryRecord::Feedback {
409 conversation: Some(conversation),
410 ..
411 } = self
412 {
413 conversation
414 .parse::<crate::agent::ConversationId>()
415 .map_err(|_| InvalidError::new("memory record conversation is not a valid id"))?;
416 }
417 Ok(())
418 }
419}
420
421impl Validate for ClientMetadata {
422 fn validate(&self) -> Result<(), InvalidError> {
423 let size = self.metadata.as_ref().map_or(0, Vec::len);
424 if size > MAX_CLIENT_METADATA {
425 return Err(InvalidError::new(format!(
426 "client metadata is {size}B, exceeds cap {MAX_CLIENT_METADATA}B"
427 )));
428 }
429 Ok(())
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::batch::BatchItem;
437 use crate::codes::{AGDX_KV_GET_CODE, BATCH_OP_VERSION, KV_OP_VERSION};
438
439 #[test]
440 fn given_a_batch_over_the_op_cap_when_validated_then_should_reject() {
441 let request = BatchRequest {
442 v: BATCH_OP_VERSION,
443 ops: (0..MAX_BATCH_OPS + 1)
444 .map(|_| BatchItem {
445 code: AGDX_KV_GET_CODE,
446 payload: Vec::new(),
447 })
448 .collect(),
449 };
450 assert!(request.validate().is_err());
451 }
452
453 #[test]
454 fn given_a_batch_nesting_a_batch_when_validated_then_should_reject() {
455 let request = BatchRequest {
456 v: BATCH_OP_VERSION,
457 ops: vec![BatchItem {
458 code: AGDX_BATCH_CODE,
459 payload: Vec::new(),
460 }],
461 };
462 assert!(request.validate().is_err());
463 }
464
465 #[test]
466 fn given_namespaces_when_validated_then_should_enforce_bounds() {
467 use crate::kv::validate_namespace;
468 use crate::limits::MAX_NAMESPACE_BYTES;
469 assert!(validate_namespace("default").is_ok());
470 assert!(validate_namespace("agent-abc/session").is_ok(), "hierarchy");
471 assert!(validate_namespace("").is_err(), "empty");
472 assert!(validate_namespace("bad\nns").is_err(), "control byte");
473 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES)).is_ok());
474 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES + 1)).is_err());
475 }
476
477 #[test]
478 fn given_graph_names_when_validated_then_should_enforce_bounds() {
479 use crate::graph::validate_graph_name;
480 use crate::limits::MAX_GRAPH_NAME_BYTES;
481 assert!(validate_graph_name("knowledge").is_ok());
482 assert!(validate_graph_name("").is_err(), "empty");
483 assert!(validate_graph_name("bad\tname").is_err(), "control byte");
484 assert!(validate_graph_name(&"g".repeat(MAX_GRAPH_NAME_BYTES + 1)).is_err());
485 }
486
487 #[test]
488 fn given_a_lease_request_when_validated_then_should_enforce_version_holder_and_ttl() {
489 use crate::codes::KV_LEASE_OP_VERSION;
490 use crate::kv::KvLease;
491 use crate::limits::MAX_HOLDER_ID_BYTES;
492 let valid = KvLease {
493 v: KV_LEASE_OP_VERSION,
494 namespace: "connectors.coordination".to_owned(),
495 key: b"source-owner".to_vec(),
496 lease_ttl_micros: 30_000_000,
497 holder_id: "warden-node-1".to_owned(),
498 subject_user_id: Some(42),
499 };
500 assert!(valid.validate().is_ok());
501 let wrong_version = KvLease {
502 v: KV_LEASE_OP_VERSION + 1,
503 ..valid.clone()
504 };
505 assert!(
506 wrong_version.validate().is_err(),
507 "an unsupported version is rejected typed"
508 );
509 let empty_holder = KvLease {
510 holder_id: String::new(),
511 ..valid.clone()
512 };
513 assert!(empty_holder.validate().is_err());
514 let long_holder = KvLease {
515 holder_id: "h".repeat(MAX_HOLDER_ID_BYTES + 1),
516 ..valid.clone()
517 };
518 assert!(long_holder.validate().is_err());
519 let zero_ttl = KvLease {
520 lease_ttl_micros: 0,
521 ..valid
522 };
523 assert!(zero_ttl.validate().is_err());
524 }
525
526 #[test]
529 fn given_out_of_range_lease_ttls_when_validated_then_should_reject_both_ends() {
530 use crate::codes::KV_LEASE_OP_VERSION;
531 use crate::kv::{KvLease, KvLeaseRenew};
532 use crate::limits::{MAX_LEASE_TTL_MICROS, MIN_LEASE_TTL_MICROS};
533 let acquire = KvLease {
534 v: KV_LEASE_OP_VERSION,
535 namespace: "connectors.coordination".to_owned(),
536 key: b"source-owner".to_vec(),
537 lease_ttl_micros: MIN_LEASE_TTL_MICROS,
538 holder_id: "warden-node-1".to_owned(),
539 subject_user_id: None,
540 };
541 assert!(acquire.validate().is_ok(), "the floor itself is grantable");
542 assert!(
543 KvLease {
544 lease_ttl_micros: MAX_LEASE_TTL_MICROS,
545 ..acquire.clone()
546 }
547 .validate()
548 .is_ok(),
549 "the ceiling itself is grantable"
550 );
551 assert!(
552 KvLease {
553 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
554 ..acquire.clone()
555 }
556 .validate()
557 .is_err(),
558 "a lease too short to renew is rejected locally"
559 );
560 assert!(
561 KvLease {
562 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
563 ..acquire
564 }
565 .validate()
566 .is_err(),
567 "an unbounded ownership window is rejected locally"
568 );
569 let renew = KvLeaseRenew {
570 v: KV_LEASE_OP_VERSION,
571 namespace: "connectors.coordination".to_owned(),
572 key: b"source-owner".to_vec(),
573 holder_id: "warden-node-1".to_owned(),
574 subject_user_id: None,
575 lease_token: 7,
576 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
577 };
578 assert!(renew.validate().is_err(), "renewal shares the same range");
579 assert!(
580 KvLeaseRenew {
581 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
582 ..renew
583 }
584 .validate()
585 .is_err()
586 );
587 }
588
589 #[test]
590 fn given_renew_and_release_requests_when_validated_then_should_require_a_nonzero_token() {
591 use crate::codes::KV_LEASE_OP_VERSION;
592 use crate::kv::{KvLeaseRenew, KvRelease};
593 let renew = KvLeaseRenew {
594 v: KV_LEASE_OP_VERSION,
595 namespace: "connectors.coordination".to_owned(),
596 key: b"source-owner".to_vec(),
597 holder_id: "warden-node-1".to_owned(),
598 subject_user_id: None,
599 lease_token: 7,
600 lease_ttl_micros: 30_000_000,
601 };
602 assert!(renew.validate().is_ok());
603 let zero = KvLeaseRenew {
604 lease_token: 0,
605 ..renew
606 };
607 assert!(zero.validate().is_err());
608 let release = KvRelease {
609 v: KV_LEASE_OP_VERSION,
610 namespace: "connectors.coordination".to_owned(),
611 key: b"source-owner".to_vec(),
612 lease_token: 7,
613 holder_id: "warden-node-1".to_owned(),
614 };
615 assert!(release.validate().is_ok());
616 let zero = KvRelease {
617 lease_token: 0,
618 ..release
619 };
620 assert!(zero.validate().is_err());
621 }
622
623 #[test]
624 fn given_a_fenced_cas_when_validated_then_should_enforce_both_namespaces_and_the_token() {
625 use crate::codes::KV_LEASE_OP_VERSION;
626 use crate::kv::{CasExpect, KvCasFenced};
627 let valid = KvCasFenced {
628 v: KV_LEASE_OP_VERSION,
629 namespace: "connectors.state".to_owned(),
630 key: b"source_state/pg".to_vec(),
631 value: b"cursor".to_vec(),
632 expires_at_micros: None,
633 expect: CasExpect::Absent,
634 fence_namespace: "connectors.coordination".to_owned(),
635 fence_key: b"source-owner".to_vec(),
636 fence_token: 8,
637 };
638 assert!(valid.validate().is_ok());
639 let wrong_version = KvCasFenced {
640 v: KV_LEASE_OP_VERSION + 1,
641 ..valid.clone()
642 };
643 assert!(
644 wrong_version.validate().is_err(),
645 "an unsupported version is rejected typed"
646 );
647 let bad_fence_namespace = KvCasFenced {
648 fence_namespace: "bad\nns".to_owned(),
649 ..valid.clone()
650 };
651 assert!(bad_fence_namespace.validate().is_err());
652 let zero_token = KvCasFenced {
653 fence_token: 0,
654 ..valid
655 };
656 assert!(zero_token.validate().is_err());
657 }
658
659 #[test]
660 fn given_an_oversized_key_when_validated_then_should_reject_and_a_valid_one_passes() {
661 let over = KvSet {
662 v: KV_OP_VERSION,
663 namespace: "ns".to_owned(),
664 session: None,
665 key: vec![b'x'; MAX_KEY_BYTES + 1],
666 value: vec![1, 2, 3],
667 expires_at_micros: None,
668 };
669 assert!(over.validate().is_err());
670 let ok = KvSet {
671 v: KV_OP_VERSION,
672 namespace: "ns".to_owned(),
673 session: None,
674 key: vec![b'x'; 8],
675 value: vec![1, 2, 3],
676 expires_at_micros: None,
677 };
678 assert!(ok.validate().is_ok());
679 }
680
681 fn node_with(labels: Vec<String>, attrs: Vec<(String, Value)>) -> GraphNode {
682 GraphNode {
683 producer: None,
684 id: crate::graph::NodeId::from_u128(1),
685 labels,
686 attrs,
687 embedding: None,
688 source: None,
689 }
690 }
691
692 #[test]
693 fn given_a_node_over_the_label_cap_when_validated_then_should_reject() {
694 let over = node_with(
695 (0..=MAX_GRAPH_NODE_LABELS)
696 .map(|index| format!("label-{index}"))
697 .collect(),
698 Vec::new(),
699 );
700 assert!(over.validate().is_err());
701 assert!(
702 node_with(vec!["Person".to_owned()], Vec::new())
703 .validate()
704 .is_ok()
705 );
706 }
707
708 #[test]
709 fn given_a_node_over_the_attribute_cap_when_validated_then_should_reject() {
710 let over = node_with(
711 Vec::new(),
712 (0..=MAX_METADATA_ENTRIES)
713 .map(|index| (format!("attr-{index}"), Value::from("x")))
714 .collect(),
715 );
716 assert!(over.validate().is_err());
717 }
718
719 #[test]
720 fn given_an_oversized_source_reference_when_validated_then_should_reject() {
721 let over = SourceRef::Kv {
722 namespace: "n".repeat(MAX_SOURCE_REF_BYTES),
723 key: "k".repeat(MAX_SOURCE_REF_BYTES),
724 };
725 assert!(over.validate().is_err());
726 assert!(
727 SourceRef::Memory {
728 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
729 }
730 .validate()
731 .is_ok()
732 );
733 }
734
735 #[test]
736 fn given_an_upsert_over_the_element_cap_when_validated_then_should_reject() {
737 let over = GraphUpsert {
738 v: 1,
739 graph: "knowledge".to_owned(),
740 session: None,
741 nodes: (0..=MAX_GRAPH_RESULT_ELEMENTS)
742 .map(|_| node_with(Vec::new(), Vec::new()))
743 .collect(),
744 edges: Vec::new(),
745 };
746 assert!(over.validate().is_err());
747 let ok = GraphUpsert {
748 v: 1,
749 graph: "knowledge".to_owned(),
750 session: None,
751 nodes: vec![node_with(vec!["Person".to_owned()], Vec::new())],
752 edges: Vec::new(),
753 };
754 assert!(ok.validate().is_ok());
755 }
756
757 #[test]
758 fn given_an_upsert_carrying_an_over_cap_node_when_validated_then_should_reject() {
759 let upsert = GraphUpsert {
760 v: 1,
761 graph: "knowledge".to_owned(),
762 session: None,
763 nodes: vec![node_with(
764 (0..=MAX_GRAPH_NODE_LABELS)
765 .map(|index| format!("label-{index}"))
766 .collect(),
767 Vec::new(),
768 )],
769 edges: Vec::new(),
770 };
771 assert!(
772 upsert.validate().is_err(),
773 "an upsert must enforce its elements' caps, not just its own"
774 );
775 }
776
777 #[test]
778 fn given_an_oversized_text_query_when_validated_then_should_reject() {
779 let over = TextQuery {
780 field: None,
781 query: "q".repeat(MAX_TEXT_QUERY_BYTES + 1),
782 };
783 assert!(over.validate().is_err());
784 let ok = TextQuery {
785 field: None,
786 query: "auth is slow".to_owned(),
787 };
788 assert!(ok.validate().is_ok());
789 }
790
791 #[test]
792 fn given_an_over_cap_vector_top_k_when_validated_then_should_reject() {
793 let over = VectorQuery {
794 field: "embedding".to_owned(),
795 embedding: vec![0.0; 4],
796 top_k: MAX_PAGE_SIZE as u32 + 1,
797 };
798 assert!(over.validate().is_err());
799 let ok = VectorQuery {
800 field: "embedding".to_owned(),
801 embedding: vec![0.0; 4],
802 top_k: 10,
803 };
804 assert!(ok.validate().is_ok());
805 }
806
807 #[test]
808 fn given_an_oversized_memory_body_when_validated_then_should_reject() {
809 let over = MemoryRecord::Item {
810 origin: None,
811 producer: None,
812 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
813 kind: "fact".to_owned(),
814 body: vec![0u8; MAX_MEMORY_BODY_BYTES + 1],
815 };
816 assert!(over.validate().is_err());
817 let ok = MemoryRecord::Item {
818 origin: None,
819 producer: None,
820 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
821 kind: "fact".to_owned(),
822 body: b"auth is slow".to_vec(),
823 };
824 assert!(ok.validate().is_ok());
825 assert!(
826 MemoryRecord::Forget {
827 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
828 conversation: None,
829 }
830 .validate()
831 .is_ok()
832 );
833 assert!(
834 MemoryRecord::Forget {
835 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
836 conversation: Some("not an id".to_owned()),
837 }
838 .validate()
839 .is_err()
840 );
841 }
842
843 #[test]
844 fn given_oversized_client_metadata_when_validated_then_should_reject() {
845 let over = ClientMetadata {
846 client_id: 1,
847 user_id: None,
848 transport: 1,
849 address: "127.0.0.1:8090".to_owned(),
850 consumer_groups_count: 0,
851 metadata: Some(vec![0u8; MAX_CLIENT_METADATA + 1]),
852 };
853 assert!(over.validate().is_err());
854 let ok = ClientMetadata {
855 client_id: 1,
856 user_id: None,
857 transport: 1,
858 address: "127.0.0.1:8090".to_owned(),
859 consumer_groups_count: 0,
860 metadata: Some(b"card".to_vec()),
861 };
862 assert!(ok.validate().is_ok());
863 }
864}