1use std::collections::HashSet;
2
3use crate::error::{Diagnostics, Error};
4use crate::ir::{Node, NodeId, SymbolicFlowGraph};
5use crate::phase::{Phase, PhaseNodeKind, Transition, transition};
6
7pub fn validate(graph: &SymbolicFlowGraph) -> Result<(), Error> {
15 validate_collecting(graph).into_result(()).map_err(Error::from)
16}
17
18#[must_use]
24pub fn validate_collecting(graph: &SymbolicFlowGraph) -> Diagnostics {
25 let mut d = Diagnostics::new();
26 check_id_ranges(graph, &mut d);
27 if d.is_empty() {
32 check_fetch_edges(graph, &mut d);
33 check_acyclic(graph, &mut d);
34 check_phases_collecting(graph, &mut d);
35 }
36 d
37}
38
39fn check_id_ranges(graph: &SymbolicFlowGraph, d: &mut Diagnostics) {
40 let n_nodes = u32::try_from(graph.nodes.len()).unwrap_or(u32::MAX);
41 let n_preds = u32::try_from(graph.predicates.len()).unwrap_or(u32::MAX);
42 let n_mws = u32::try_from(graph.middlewares.len()).unwrap_or(u32::MAX);
43 let n_fetches = u32::try_from(graph.fetches.len()).unwrap_or(u32::MAX);
44 let n_terms = u32::try_from(graph.terminators.len()).unwrap_or(u32::MAX);
45
46 for (idx, node) in graph.nodes.iter().enumerate() {
47 match node {
48 Node::Check { predicate, on_match, on_miss, .. } => {
49 if predicate.get() >= n_preds {
50 d.push(Error::compile(format!("node {idx}: dangling PredicateId({})", predicate.get())));
51 }
52 if on_match.get() >= n_nodes {
53 d.push(Error::compile(format!("node {idx}.on_match dangling")));
54 }
55 if on_miss.get() >= n_nodes {
56 d.push(Error::compile(format!("node {idx}.on_miss dangling")));
57 }
58 }
59 Node::Middleware { id, next, on_error, .. } => {
60 if id.get() >= n_mws {
61 d.push(Error::compile(format!("node {idx}: dangling MiddlewareId({})", id.get())));
62 }
63 if next.get() >= n_nodes {
64 d.push(Error::compile(format!("node {idx}.next dangling")));
65 }
66 if let Some(e) = on_error
67 && e.get() >= n_nodes
68 {
69 d.push(Error::compile(format!("node {idx}.on_error dangling")));
70 }
71 }
72 Node::Fetch { id, next_response, next_tunnel, .. } => {
73 if id.get() >= n_fetches {
74 d.push(Error::compile(format!("node {idx}: dangling FetchId({})", id.get())));
75 }
76 if let Some(r) = next_response
77 && r.get() >= n_nodes
78 {
79 d.push(Error::compile(format!("node {idx}.next_response dangling")));
80 }
81 if let Some(t) = next_tunnel
82 && t.get() >= n_nodes
83 {
84 d.push(Error::compile(format!("node {idx}.next_tunnel dangling")));
85 }
86 }
87 Node::Upgrade { next } => {
88 if next.get() >= n_nodes {
89 d.push(Error::compile(format!("node {idx}.next dangling")));
90 }
91 }
92 Node::Terminate(t) => {
93 if t.get() >= n_terms {
94 d.push(Error::compile(format!("node {idx}: dangling TerminatorId({})", t.get())));
95 }
96 }
97 }
98 }
99}
100
101fn check_fetch_edges(graph: &SymbolicFlowGraph, d: &mut Diagnostics) {
102 use crate::fetch::FetchKind::{
103 AcmeChallenge, HttpProxy, HttpSynthesize, L4Forward, WebSocketUpgrade,
104 };
105 for (idx, node) in graph.nodes.iter().enumerate() {
106 let Node::Fetch { id, next_response, next_tunnel, .. } = node else {
107 continue;
108 };
109 let kind = graph[*id].kind;
110 match kind {
111 HttpProxy | HttpSynthesize | AcmeChallenge => {
112 if next_response.is_none() {
113 d.push(Error::compile(format!("node {idx}: {kind:?} requires next_response")));
114 }
115 if next_tunnel.is_some() {
116 d.push(Error::compile(format!("node {idx}: {kind:?} must not have next_tunnel")));
117 }
118 }
119 L4Forward => {
120 if next_tunnel.is_none() {
121 d.push(Error::compile(format!("node {idx}: L4Forward requires next_tunnel")));
122 }
123 if next_response.is_some() {
124 d.push(Error::compile(format!("node {idx}: L4Forward must not have next_response")));
125 }
126 }
127 WebSocketUpgrade => {
128 if next_response.is_none() || next_tunnel.is_none() {
129 d.push(Error::compile(format!(
130 "node {idx}: WebSocketUpgrade requires both next_response and next_tunnel"
131 )));
132 }
133 }
134 }
135 }
136}
137
138fn check_acyclic(graph: &SymbolicFlowGraph, d: &mut Diagnostics) {
139 #[derive(Copy, Clone)]
140 enum Color {
141 White,
142 Gray,
143 Black,
144 }
145 let mut color: Vec<Color> = (0..graph.nodes.len()).map(|_| Color::White).collect();
146
147 let mut reported: HashSet<usize> = HashSet::new();
148 for start in 0..graph.nodes.len() {
149 if !matches!(color[start], Color::White) {
150 continue;
151 }
152 let mut stack: Vec<(usize, usize)> = vec![(start, 0)];
153 color[start] = Color::Gray;
154 while let Some(&(node_idx, child_idx)) = stack.last() {
155 let succs = successors(&graph.nodes[node_idx]);
156 if child_idx < succs.len() {
157 let next = succs[child_idx].get() as usize;
158 stack.last_mut().expect("non-empty").1 += 1;
159 match color[next] {
160 Color::White => {
161 color[next] = Color::Gray;
162 stack.push((next, 0));
163 }
164 Color::Gray => {
165 if reported.insert(next) {
170 d.push(Error::compile(format!("cycle in graph at node {next}")));
171 }
172 }
173 Color::Black => {}
174 }
175 } else {
176 color[node_idx] = Color::Black;
177 stack.pop();
178 }
179 }
180 }
181}
182
183fn successors(node: &Node) -> Vec<NodeId> {
184 match node {
185 Node::Check { on_match, on_miss, .. } => vec![*on_match, *on_miss],
186 Node::Middleware { next, on_error, .. } => {
187 let mut v = vec![*next];
188 if let Some(e) = on_error {
189 v.push(*e);
190 }
191 v
192 }
193 Node::Fetch { next_response, next_tunnel, .. } => {
194 let mut v = Vec::new();
195 if let Some(r) = next_response {
196 v.push(*r);
197 }
198 if let Some(t) = next_tunnel {
199 v.push(*t);
200 }
201 v
202 }
203 Node::Upgrade { next } => vec![*next],
204 Node::Terminate(_) => Vec::new(),
205 }
206}
207
208fn node_kind_for_phase(graph: &SymbolicFlowGraph, node: &Node) -> PhaseNodeKind {
209 match node {
210 Node::Check { .. } => PhaseNodeKind::Check,
211 Node::Middleware { id, .. } => PhaseNodeKind::Middleware(graph[*id].kind),
212 Node::Fetch { id, .. } => PhaseNodeKind::Fetch(graph[*id].kind),
213 Node::Upgrade { .. } => PhaseNodeKind::Upgrade,
214 Node::Terminate(t) => PhaseNodeKind::Terminate(graph[*t]),
215 }
216}
217
218pub fn check_phases(graph: &SymbolicFlowGraph) -> Result<(), Error> {
230 let mut d = Diagnostics::new();
231 check_phases_collecting(graph, &mut d);
232 d.into_result(()).map_err(Error::from)
233}
234
235fn check_phases_collecting(graph: &SymbolicFlowGraph, d: &mut Diagnostics) {
236 let mut seen: HashSet<(NodeId, Phase)> = HashSet::new();
237 for &entry in graph.entries.values() {
238 if let Err(e) = visit_phase(graph, entry, Phase::L4Raw, &mut seen) {
239 d.push(e);
240 }
241 }
242 for &synth in graph.meta.short_circuit_response_entry.values() {
247 if let Err(e) = visit_phase(graph, synth, Phase::L7Response, &mut seen) {
248 d.push(e);
249 }
250 }
251}
252
253fn visit_phase(
254 graph: &SymbolicFlowGraph,
255 id: NodeId,
256 phase: Phase,
257 seen: &mut HashSet<(NodeId, Phase)>,
258) -> Result<(), Error> {
259 if !seen.insert((id, phase)) {
260 return Ok(());
261 }
262 let node = &graph[id];
263 let kind = node_kind_for_phase(graph, node);
264 let t = transition(kind, phase).map_err(|e| {
265 Error::compile(format!(
266 "phase mismatch at NodeId({}): expected one of {:?}, got {:?}",
267 id.get(),
268 e.expected,
269 e.got,
270 ))
271 })?;
272 match (t, node) {
273 (Transition::Terminal, _) => Ok(()),
274 (Transition::PassThrough, _) => {
275 for succ in successors(node) {
276 visit_phase(graph, succ, phase, seen)?;
277 }
278 Ok(())
279 }
280 (Transition::Into(next_phase), _) => {
281 for succ in successors(node) {
282 visit_phase(graph, succ, next_phase, seen)?;
283 }
284 Ok(())
285 }
286 (
287 Transition::BiOutcome { response, tunnel },
288 Node::Fetch { next_response, next_tunnel, .. },
289 ) => {
290 if let Some(r) = next_response {
291 visit_phase(graph, *r, response, seen)?;
292 }
293 if let Some(t) = next_tunnel {
294 visit_phase(graph, *t, tunnel, seen)?;
295 }
296 Ok(())
297 }
298 (Transition::BiOutcome { .. }, _) => {
299 Err(Error::compile("BiOutcome transition on non-Fetch node".to_string()))
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use std::collections::HashMap;
307 use std::path::PathBuf;
308 use std::time::SystemTime;
309
310 use super::*;
311 use crate::fetch::{FetchKind, SymbolicFetchRef, Terminator};
312 use crate::ir::{BodySide, FetchId, FlowGraphMeta, PredicateId, TerminatorId};
313
314 fn empty_meta() -> FlowGraphMeta {
315 FlowGraphMeta {
316 version_hash: [0; 32],
317 compiled_at: SystemTime::UNIX_EPOCH,
318 source_files: vec![PathBuf::new()],
319 feature_set: &[],
320 short_circuit_response_entry: std::collections::BTreeMap::new(),
321 listener_tls: std::collections::BTreeMap::new(),
322 listener_kinds: std::collections::BTreeMap::new(),
323 listener_transports: std::collections::BTreeMap::new(),
324 annotations: Vec::new(),
325 }
326 }
327
328 #[test]
329 fn validate_collecting_accumulates_every_dangling_check_edge_in_one_pass() {
330 let graph = SymbolicFlowGraph {
334 nodes: vec![
335 Node::Check {
336 predicate: PredicateId::new(0),
337 on_match: NodeId::new(50),
338 on_miss: NodeId::new(51),
339 collect_body_before: None,
340 body_limit: 0,
341 },
342 Node::Check {
343 predicate: PredicateId::new(0),
344 on_match: NodeId::new(52),
345 on_miss: NodeId::new(53),
346 collect_body_before: None,
347 body_limit: 0,
348 },
349 ],
350 predicates: vec![dummy_predicate()],
351 middlewares: vec![],
352 fetches: vec![],
353 terminators: vec![],
354 entries: HashMap::new(),
355 meta: empty_meta(),
356 };
357 let d = validate_collecting(&graph);
358 assert_eq!(d.len(), 4, "expected one error per dangling edge: {d}");
359 let dump = d.to_string();
360 assert!(dump.contains("on_match dangling"), "{dump}");
361 assert!(dump.contains("on_miss dangling"), "{dump}");
362 }
363
364 #[test]
365 fn dangling_terminator_id_in_terminate_node_rejected() {
366 let graph = SymbolicFlowGraph {
367 nodes: vec![Node::Terminate(TerminatorId::new(0))],
368 predicates: vec![],
369 middlewares: vec![],
370 fetches: vec![],
371 terminators: vec![],
372 entries: HashMap::new(),
373 meta: empty_meta(),
374 };
375 let err = validate(&graph).expect_err("must error");
376 assert!(err.to_string().contains("dangling TerminatorId"));
377 }
378
379 #[test]
380 fn dangling_node_id_in_fetch_edge_rejected() {
381 let graph = SymbolicFlowGraph {
382 nodes: vec![Node::Fetch {
383 id: FetchId::new(0),
384 next_response: Some(NodeId::new(99)),
385 next_tunnel: None,
386 collect_body_before: None,
387 body_limit: 0,
388 }],
389 predicates: vec![],
390 middlewares: vec![],
391 fetches: vec![SymbolicFetchRef {
392 kind: FetchKind::HttpProxy,
393 args: serde_json::Value::Null,
394 retry_buffer_required: false,
395 allow_zero_rtt: None,
396 }],
397 terminators: vec![],
398 entries: HashMap::new(),
399 meta: empty_meta(),
400 };
401 let err = validate(&graph).expect_err("must error");
402 assert!(err.to_string().contains("next_response dangling"));
403 }
404
405 #[test]
406 fn http_fetch_without_next_response_rejected() {
407 let term = Node::Terminate(TerminatorId::new(0));
408 let graph = SymbolicFlowGraph {
409 nodes: vec![
410 term,
411 Node::Fetch {
412 id: FetchId::new(0),
413 next_response: None,
414 next_tunnel: None,
415 collect_body_before: None,
416 body_limit: 0,
417 },
418 ],
419 predicates: vec![],
420 middlewares: vec![],
421 fetches: vec![SymbolicFetchRef {
422 kind: FetchKind::HttpProxy,
423 args: serde_json::Value::Null,
424 retry_buffer_required: false,
425 allow_zero_rtt: None,
426 }],
427 terminators: vec![Terminator::WriteHttpResponse],
428 entries: HashMap::new(),
429 meta: empty_meta(),
430 };
431 let err = validate(&graph).expect_err("must error");
432 assert!(err.to_string().contains("requires next_response"));
433 }
434
435 #[test]
436 fn l4_forward_with_next_response_rejected() {
437 let graph = SymbolicFlowGraph {
438 nodes: vec![
439 Node::Terminate(TerminatorId::new(0)),
440 Node::Fetch {
441 id: FetchId::new(0),
442 next_response: Some(NodeId::new(0)),
443 next_tunnel: Some(NodeId::new(0)),
444 collect_body_before: None,
445 body_limit: 0,
446 },
447 ],
448 predicates: vec![],
449 middlewares: vec![],
450 fetches: vec![SymbolicFetchRef {
451 kind: FetchKind::L4Forward,
452 args: serde_json::Value::Null,
453 retry_buffer_required: false,
454 allow_zero_rtt: None,
455 }],
456 terminators: vec![Terminator::ByteTunnel],
457 entries: HashMap::new(),
458 meta: empty_meta(),
459 };
460 let err = validate(&graph).expect_err("must error");
461 assert!(err.to_string().contains("L4Forward must not have next_response"));
462 }
463
464 #[test]
465 fn cyclic_graph_is_rejected() {
466 let graph = SymbolicFlowGraph {
468 nodes: vec![
469 Node::Check {
470 predicate: PredicateId::new(0),
471 on_match: NodeId::new(1),
472 on_miss: NodeId::new(1),
473 collect_body_before: None,
474 body_limit: 0,
475 },
476 Node::Check {
477 predicate: PredicateId::new(0),
478 on_match: NodeId::new(0),
479 on_miss: NodeId::new(0),
480 collect_body_before: None,
481 body_limit: 0,
482 },
483 ],
484 predicates: vec![dummy_predicate()],
485 middlewares: vec![],
486 fetches: vec![],
487 terminators: vec![],
488 entries: HashMap::new(),
489 meta: empty_meta(),
490 };
491 let err = validate(&graph).expect_err("must error");
492 assert!(err.to_string().contains("cycle"));
493 }
494
495 #[test]
496 fn phase_check_rejects_write_http_response_reached_in_wrong_phase() {
497 let tid = TerminatorId::new(0);
501 let graph = SymbolicFlowGraph {
502 nodes: vec![Node::Terminate(tid), Node::Upgrade { next: NodeId::new(0) }],
503 predicates: vec![],
504 middlewares: vec![],
505 fetches: vec![],
506 terminators: vec![Terminator::WriteHttpResponse],
507 entries: {
508 let mut m = HashMap::new();
509 m.insert("127.0.0.1:443".parse().expect("parse"), NodeId::new(1));
510 m
511 },
512 meta: empty_meta(),
513 };
514 let err = check_phases(&graph).expect_err("must error");
515 assert!(err.to_string().contains("phase mismatch"));
516 }
517
518 #[test]
519 fn phase_check_rejects_short_circuit_synth_with_wrong_terminator() {
520 let bad_tid = TerminatorId::new(0);
527 let mut meta = empty_meta();
528 meta.short_circuit_response_entry.insert(NodeId::new(1), NodeId::new(0));
529 let graph = SymbolicFlowGraph {
530 nodes: vec![Node::Terminate(bad_tid), Node::Upgrade { next: NodeId::new(0) }],
531 predicates: vec![],
532 middlewares: vec![],
533 fetches: vec![],
534 terminators: vec![Terminator::ByteTunnel],
535 entries: HashMap::new(),
537 meta,
538 };
539 let err = check_phases(&graph).expect_err("must error on bad synth phase");
540 assert!(err.to_string().contains("phase mismatch"), "{err}");
541 }
542
543 fn dummy_predicate() -> crate::predicate::PredicateInst {
544 use crate::predicate::{CompiledOperator, CompiledValue, FieldPath, PredicateInst};
545 PredicateInst {
546 path: FieldPath::TlsSni,
547 op: CompiledOperator::Equals(CompiledValue::Str(std::sync::Arc::from("x"))),
548 }
549 }
550
551 use crate::middleware::{MiddlewareKind, SymbolicMiddlewareRef};
558
559 fn http_fetch_ref() -> SymbolicFetchRef {
560 SymbolicFetchRef {
561 kind: FetchKind::HttpProxy,
562 args: serde_json::Value::Null,
563 retry_buffer_required: false,
564 allow_zero_rtt: None,
565 }
566 }
567
568 fn ws_fetch_ref() -> SymbolicFetchRef {
569 SymbolicFetchRef {
570 kind: FetchKind::WebSocketUpgrade,
571 args: serde_json::Value::Null,
572 retry_buffer_required: false,
573 allow_zero_rtt: None,
574 }
575 }
576
577 fn l4_fetch_ref() -> SymbolicFetchRef {
578 SymbolicFetchRef {
579 kind: FetchKind::L4Forward,
580 args: serde_json::Value::Null,
581 retry_buffer_required: false,
582 allow_zero_rtt: None,
583 }
584 }
585
586 fn dummy_middleware_ref() -> SymbolicMiddlewareRef {
587 SymbolicMiddlewareRef {
588 name: std::sync::Arc::from("noop"),
589 args: serde_json::Value::Null,
590 kind: MiddlewareKind::L4Peek,
591 stateless: true,
592 needs_body: false,
593 on_error: None,
594 }
595 }
596
597 fn assert_err_contains(graph: &SymbolicFlowGraph, needle: &str) {
598 let err = validate(graph).expect_err("must error");
599 let msg = err.to_string();
600 assert!(msg.contains(needle), "expected {needle:?} in error, got: {msg}");
601 }
602
603 #[test]
604 fn validate_rejects_dangling_predicate_id_in_check() {
605 let graph = SymbolicFlowGraph {
606 nodes: vec![Node::Check {
607 predicate: PredicateId::new(7),
608 on_match: NodeId::new(0),
609 on_miss: NodeId::new(0),
610 collect_body_before: None,
611 body_limit: 0,
612 }],
613 predicates: vec![],
614 middlewares: vec![],
615 fetches: vec![],
616 terminators: vec![],
617 entries: HashMap::new(),
618 meta: empty_meta(),
619 };
620 assert_err_contains(&graph, "dangling PredicateId");
621 }
622
623 #[test]
624 fn validate_rejects_dangling_on_match_in_check() {
625 let graph = SymbolicFlowGraph {
626 nodes: vec![Node::Check {
627 predicate: PredicateId::new(0),
628 on_match: NodeId::new(42),
629 on_miss: NodeId::new(0),
630 collect_body_before: None,
631 body_limit: 0,
632 }],
633 predicates: vec![dummy_predicate()],
634 middlewares: vec![],
635 fetches: vec![],
636 terminators: vec![],
637 entries: HashMap::new(),
638 meta: empty_meta(),
639 };
640 assert_err_contains(&graph, "on_match dangling");
641 }
642
643 #[test]
644 fn validate_rejects_dangling_on_miss_in_check() {
645 let graph = SymbolicFlowGraph {
646 nodes: vec![Node::Check {
647 predicate: PredicateId::new(0),
648 on_match: NodeId::new(0),
649 on_miss: NodeId::new(42),
650 collect_body_before: None,
651 body_limit: 0,
652 }],
653 predicates: vec![dummy_predicate()],
654 middlewares: vec![],
655 fetches: vec![],
656 terminators: vec![],
657 entries: HashMap::new(),
658 meta: empty_meta(),
659 };
660 assert_err_contains(&graph, "on_miss dangling");
661 }
662
663 #[test]
664 fn validate_rejects_dangling_middleware_id() {
665 let graph = SymbolicFlowGraph {
666 nodes: vec![Node::Middleware {
667 id: crate::ir::MiddlewareId::new(7),
668 next: NodeId::new(0),
669 on_error: None,
670 collect_body_before: None,
671 body_limit: 0,
672 }],
673 predicates: vec![],
674 middlewares: vec![],
675 fetches: vec![],
676 terminators: vec![],
677 entries: HashMap::new(),
678 meta: empty_meta(),
679 };
680 assert_err_contains(&graph, "dangling MiddlewareId");
681 }
682
683 #[test]
684 fn validate_rejects_dangling_next_in_middleware() {
685 let graph = SymbolicFlowGraph {
686 nodes: vec![Node::Middleware {
687 id: crate::ir::MiddlewareId::new(0),
688 next: NodeId::new(42),
689 on_error: None,
690 collect_body_before: None,
691 body_limit: 0,
692 }],
693 predicates: vec![],
694 middlewares: vec![dummy_middleware_ref()],
695 fetches: vec![],
696 terminators: vec![],
697 entries: HashMap::new(),
698 meta: empty_meta(),
699 };
700 assert_err_contains(&graph, "next dangling");
701 }
702
703 #[test]
704 fn validate_rejects_dangling_on_error_in_middleware() {
705 let graph = SymbolicFlowGraph {
706 nodes: vec![Node::Middleware {
707 id: crate::ir::MiddlewareId::new(0),
708 next: NodeId::new(0),
709 on_error: Some(NodeId::new(42)),
710 collect_body_before: None,
711 body_limit: 0,
712 }],
713 predicates: vec![],
714 middlewares: vec![dummy_middleware_ref()],
715 fetches: vec![],
716 terminators: vec![],
717 entries: HashMap::new(),
718 meta: empty_meta(),
719 };
720 assert_err_contains(&graph, "on_error dangling");
721 }
722
723 #[test]
724 fn validate_rejects_dangling_fetch_id() {
725 let graph = SymbolicFlowGraph {
726 nodes: vec![Node::Fetch {
727 id: FetchId::new(7),
728 next_response: Some(NodeId::new(0)),
729 next_tunnel: None,
730 collect_body_before: None,
731 body_limit: 0,
732 }],
733 predicates: vec![],
734 middlewares: vec![],
735 fetches: vec![],
736 terminators: vec![],
737 entries: HashMap::new(),
738 meta: empty_meta(),
739 };
740 assert_err_contains(&graph, "dangling FetchId");
741 }
742
743 #[test]
744 fn validate_rejects_dangling_next_tunnel() {
745 let graph = SymbolicFlowGraph {
746 nodes: vec![Node::Fetch {
747 id: FetchId::new(0),
748 next_response: None,
749 next_tunnel: Some(NodeId::new(42)),
750 collect_body_before: None,
751 body_limit: 0,
752 }],
753 predicates: vec![],
754 middlewares: vec![],
755 fetches: vec![l4_fetch_ref()],
756 terminators: vec![],
757 entries: HashMap::new(),
758 meta: empty_meta(),
759 };
760 assert_err_contains(&graph, "next_tunnel dangling");
761 }
762
763 #[test]
764 fn validate_rejects_dangling_next_in_upgrade() {
765 let graph = SymbolicFlowGraph {
766 nodes: vec![Node::Upgrade { next: NodeId::new(42) }],
767 predicates: vec![],
768 middlewares: vec![],
769 fetches: vec![],
770 terminators: vec![],
771 entries: HashMap::new(),
772 meta: empty_meta(),
773 };
774 assert_err_contains(&graph, "next dangling");
775 }
776
777 #[test]
778 fn validate_rejects_http_fetch_with_next_tunnel() {
779 let graph = SymbolicFlowGraph {
781 nodes: vec![
782 Node::Terminate(TerminatorId::new(0)),
783 Node::Fetch {
784 id: FetchId::new(0),
785 next_response: Some(NodeId::new(0)),
786 next_tunnel: Some(NodeId::new(0)),
787 collect_body_before: None,
788 body_limit: 0,
789 },
790 ],
791 predicates: vec![],
792 middlewares: vec![],
793 fetches: vec![http_fetch_ref()],
794 terminators: vec![Terminator::WriteHttpResponse],
795 entries: HashMap::new(),
796 meta: empty_meta(),
797 };
798 assert_err_contains(&graph, "must not have next_tunnel");
799 }
800
801 #[test]
802 fn validate_rejects_l4_forward_without_next_tunnel() {
803 let graph = SymbolicFlowGraph {
804 nodes: vec![Node::Fetch {
805 id: FetchId::new(0),
806 next_response: None,
807 next_tunnel: None,
808 collect_body_before: None,
809 body_limit: 0,
810 }],
811 predicates: vec![],
812 middlewares: vec![],
813 fetches: vec![l4_fetch_ref()],
814 terminators: vec![],
815 entries: HashMap::new(),
816 meta: empty_meta(),
817 };
818 assert_err_contains(&graph, "L4Forward requires next_tunnel");
819 }
820
821 #[test]
822 fn validate_rejects_websocket_upgrade_missing_branch() {
823 let graph = SymbolicFlowGraph {
825 nodes: vec![
826 Node::Terminate(TerminatorId::new(0)),
827 Node::Fetch {
828 id: FetchId::new(0),
829 next_response: None,
830 next_tunnel: Some(NodeId::new(0)),
831 collect_body_before: None,
832 body_limit: 0,
833 },
834 ],
835 predicates: vec![],
836 middlewares: vec![],
837 fetches: vec![ws_fetch_ref()],
838 terminators: vec![Terminator::WriteHttpResponse],
839 entries: HashMap::new(),
840 meta: empty_meta(),
841 };
842 assert_err_contains(&graph, "WebSocketUpgrade requires both");
843 }
844
845 #[test]
846 fn validate_rejects_bi_outcome_transition_on_non_fetch_node() {
847 let bad_tid = TerminatorId::new(0);
860 let mut meta = empty_meta();
861 meta.short_circuit_response_entry.insert(NodeId::new(1), NodeId::new(0));
862 let graph = SymbolicFlowGraph {
863 nodes: vec![Node::Terminate(bad_tid), Node::Upgrade { next: NodeId::new(0) }],
864 predicates: vec![],
865 middlewares: vec![],
866 fetches: vec![],
867 terminators: vec![Terminator::ByteTunnel],
868 entries: HashMap::new(),
869 meta,
870 };
871 assert!(check_phases(&graph).is_err());
872 }
873
874 const _: BodySide = BodySide::Request;
877}