Skip to main content

lean_ctx/core/
pipeline.rs

1//! # Context Pipeline
2//!
3//! The pipeline defines the processing stages that content flows through
4//! between raw input and the compressed output delivered to the LLM.
5//!
6//! ## Pipeline Flow
7//!
8//! ```text
9//! Input → Intent → Relevance → Compression → Translation → Delivery
10//! ```
11//!
12//! - **Input**: Raw file content / shell output enters the pipeline
13//! - **Intent**: Task-conditioned filtering — what is relevant to the current goal?
14//! - **Relevance**: Graph/heatmap-based prioritization of content sections
15//! - **Compression**: AST signatures, entropy filtering, delta encoding
16//! - **Translation**: Token shorthand (TDD), symbol replacement
17//! - **Delivery**: LITM positioning, CRP formatting, final output assembly
18//!
19//! Each layer can be enabled/disabled per profile (see `core::profiles`).
20//! `PipelineStats` aggregates per-layer metrics across all runs for observability.
21
22use std::collections::HashMap;
23
24/// Identifies a stage in the compression pipeline.
25///
26/// Layers execute in the order defined by [`LayerKind::all`]:
27/// Input → Intent → Relevance → Compression → Translation → Delivery.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
29pub enum LayerKind {
30    Input,
31    Autonomy,
32    Intent,
33    Relevance,
34    Compression,
35    Translation,
36    Delivery,
37}
38
39impl LayerKind {
40    /// Returns the canonical string label for this layer.
41    pub fn as_str(&self) -> &'static str {
42        match self {
43            Self::Input => "input",
44            Self::Autonomy => "autonomy",
45            Self::Intent => "intent",
46            Self::Relevance => "relevance",
47            Self::Compression => "compression",
48            Self::Translation => "translation",
49            Self::Delivery => "delivery",
50        }
51    }
52
53    /// Returns all layer kinds in pipeline execution order.
54    pub fn all() -> &'static [LayerKind] {
55        &[
56            Self::Input,
57            Self::Autonomy,
58            Self::Intent,
59            Self::Relevance,
60            Self::Compression,
61            Self::Translation,
62            Self::Delivery,
63        ]
64    }
65}
66
67impl std::fmt::Display for LayerKind {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "{}", self.as_str())
70    }
71}
72
73impl std::str::FromStr for LayerKind {
74    type Err = String;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        match s.to_ascii_lowercase().as_str() {
78            "input" => Ok(Self::Input),
79            "autonomy" => Ok(Self::Autonomy),
80            "intent" => Ok(Self::Intent),
81            "relevance" => Ok(Self::Relevance),
82            "compression" => Ok(Self::Compression),
83            "translation" => Ok(Self::Translation),
84            "delivery" => Ok(Self::Delivery),
85            _ => Err(format!(
86                "unknown pipeline layer '{s}'; expected one of: input, autonomy, intent, relevance, compression, translation, delivery"
87            )),
88        }
89    }
90}
91
92/// Content and metadata passed into a pipeline layer for processing.
93#[derive(Debug, Clone)]
94pub struct LayerInput {
95    pub content: String,
96    pub tokens: usize,
97    pub metadata: HashMap<String, String>,
98}
99
100/// Result produced by a pipeline layer after processing.
101#[derive(Debug, Clone)]
102pub struct LayerOutput {
103    pub content: String,
104    pub tokens: usize,
105    pub metadata: HashMap<String, String>,
106}
107
108/// Performance metrics for a single layer execution: tokens in/out, timing, ratio.
109#[derive(Debug, Clone)]
110pub struct LayerMetrics {
111    pub layer: LayerKind,
112    pub input_tokens: usize,
113    pub output_tokens: usize,
114    pub duration_us: u64,
115    pub compression_ratio: f64,
116}
117
118impl LayerMetrics {
119    pub fn new(
120        layer: LayerKind,
121        input_tokens: usize,
122        output_tokens: usize,
123        duration_us: u64,
124    ) -> Self {
125        let ratio = if input_tokens == 0 {
126            1.0
127        } else {
128            output_tokens as f64 / input_tokens as f64
129        };
130        Self {
131            layer,
132            input_tokens,
133            output_tokens,
134            duration_us,
135            compression_ratio: ratio,
136        }
137    }
138}
139
140/// A single processing stage in the compression pipeline.
141pub trait Layer {
142    fn kind(&self) -> LayerKind;
143    fn process(&self, input: LayerInput) -> LayerOutput;
144}
145
146/// Returns whether a given layer is enabled according to a profile's pipeline config.
147pub fn is_layer_enabled(kind: LayerKind, cfg: &crate::core::profiles::PipelineConfig) -> bool {
148    match kind {
149        LayerKind::Input | LayerKind::Autonomy | LayerKind::Delivery => true,
150        LayerKind::Intent => cfg.intent_effective(),
151        LayerKind::Relevance => cfg.relevance_effective(),
152        LayerKind::Compression => cfg.compression_effective(),
153        LayerKind::Translation => cfg.translation_effective(),
154    }
155}
156
157/// A chain of processing layers that content flows through sequentially.
158pub struct Pipeline {
159    layers: Vec<Box<dyn Layer>>,
160}
161
162impl Pipeline {
163    /// Creates an empty pipeline with no layers.
164    pub fn new() -> Self {
165        Self { layers: Vec::new() }
166    }
167
168    /// Appends a processing layer to the pipeline (builder pattern).
169    pub fn add_layer(mut self, layer: Box<dyn Layer>) -> Self {
170        self.layers.push(layer);
171        self
172    }
173
174    /// Appends a layer only if the profile's pipeline config allows it.
175    pub fn add_layer_if_enabled(
176        self,
177        layer: Box<dyn Layer>,
178        cfg: &crate::core::profiles::PipelineConfig,
179    ) -> Self {
180        if is_layer_enabled(layer.kind(), cfg) {
181            self.add_layer(layer)
182        } else {
183            self
184        }
185    }
186
187    /// Runs all layers in sequence, collecting per-layer metrics.
188    pub fn execute(&self, input: LayerInput) -> (LayerOutput, Vec<LayerMetrics>) {
189        let mut current = input;
190        let mut metrics = Vec::new();
191
192        for layer in &self.layers {
193            let start = std::time::Instant::now();
194            let input_tokens = current.tokens;
195            let output = layer.process(current);
196            let duration = start.elapsed().as_micros() as u64;
197
198            metrics.push(LayerMetrics::new(
199                layer.kind(),
200                input_tokens,
201                output.tokens,
202                duration,
203            ));
204
205            current = LayerInput {
206                content: output.content,
207                tokens: output.tokens,
208                metadata: output.metadata,
209            };
210        }
211
212        let final_output = LayerOutput {
213            content: current.content,
214            tokens: current.tokens,
215            metadata: current.metadata,
216        };
217
218        (final_output, metrics)
219    }
220
221    /// Formats pipeline metrics as a human-readable summary with per-layer and total stats.
222    pub fn format_metrics(metrics: &[LayerMetrics]) -> String {
223        let mut out = String::from("Pipeline Metrics:\n");
224        let mut total_saved = 0usize;
225        for m in metrics {
226            let saved = m.input_tokens.saturating_sub(m.output_tokens);
227            total_saved += saved;
228            out.push_str(&format!(
229                "  {} : {} -> {} tok ({:.0}%, {:.1}ms)\n",
230                m.layer,
231                m.input_tokens,
232                m.output_tokens,
233                m.compression_ratio * 100.0,
234                m.duration_us as f64 / 1000.0,
235            ));
236        }
237        if let (Some(first), Some(last)) = (metrics.first(), metrics.last()) {
238            let total_ratio = if first.input_tokens == 0 {
239                1.0
240            } else {
241                last.output_tokens as f64 / first.input_tokens as f64
242            };
243            out.push_str(&format!(
244                "  TOTAL: {} -> {} tok ({:.0}%, saved {})\n",
245                first.input_tokens,
246                last.output_tokens,
247                total_ratio * 100.0,
248                total_saved,
249            ));
250        }
251        out
252    }
253}
254
255/// Persistent aggregated statistics across all pipeline runs.
256#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
257pub struct PipelineStats {
258    pub runs: usize,
259    pub per_layer: HashMap<LayerKind, AggregatedMetrics>,
260}
261
262/// Cumulative token counts and timing for a single pipeline layer across all runs.
263#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
264pub struct AggregatedMetrics {
265    pub total_input_tokens: usize,
266    pub total_output_tokens: usize,
267    pub total_duration_us: u64,
268    pub count: usize,
269}
270
271impl AggregatedMetrics {
272    /// Returns the average compression ratio (output/input) across all runs.
273    pub fn avg_ratio(&self) -> f64 {
274        if self.total_input_tokens == 0 {
275            return 1.0;
276        }
277        self.total_output_tokens as f64 / self.total_input_tokens as f64
278    }
279
280    /// Returns the average duration per invocation in milliseconds.
281    pub fn avg_duration_ms(&self) -> f64 {
282        if self.count == 0 {
283            return 0.0;
284        }
285        self.total_duration_us as f64 / self.count as f64 / 1000.0
286    }
287}
288
289impl PipelineStats {
290    /// Creates empty pipeline stats with zero runs.
291    pub fn new() -> Self {
292        Self {
293            runs: 0,
294            per_layer: HashMap::new(),
295        }
296    }
297
298    /// Records a batch of layer metrics from a single pipeline execution.
299    pub fn record(&mut self, metrics: &[LayerMetrics]) {
300        self.runs += 1;
301        for m in metrics {
302            let agg = self.per_layer.entry(m.layer).or_default();
303            agg.total_input_tokens += m.input_tokens;
304            agg.total_output_tokens += m.output_tokens;
305            agg.total_duration_us += m.duration_us;
306            agg.count += 1;
307        }
308    }
309
310    /// Records metrics for a single layer execution.
311    pub fn record_single(
312        &mut self,
313        layer: LayerKind,
314        input_tokens: usize,
315        output_tokens: usize,
316        duration: std::time::Duration,
317    ) {
318        self.runs += 1;
319        let agg = self.per_layer.entry(layer).or_default();
320        agg.total_input_tokens += input_tokens;
321        agg.total_output_tokens += output_tokens;
322        agg.total_duration_us += duration.as_micros() as u64;
323        agg.count += 1;
324    }
325
326    /// Returns the total tokens saved across all pipeline layers.
327    pub fn total_tokens_saved(&self) -> usize {
328        self.per_layer
329            .values()
330            .map(|a| a.total_input_tokens.saturating_sub(a.total_output_tokens))
331            .sum()
332    }
333
334    /// Persists pipeline stats to the state dir's `pipeline_stats.json`.
335    pub fn save(&self) {
336        if let Ok(dir) = crate::core::paths::state_dir() {
337            let path = dir.join("pipeline_stats.json");
338            if let Ok(json) = serde_json::to_string(self) {
339                let _ = std::fs::write(path, json);
340            }
341        }
342    }
343
344    /// Loads pipeline stats from disk, returning defaults if absent.
345    pub fn load() -> Self {
346        crate::core::paths::state_dir()
347            .ok()
348            .map(|d| d.join("pipeline_stats.json"))
349            .and_then(|p| std::fs::read_to_string(p).ok())
350            .and_then(|s| serde_json::from_str(&s).ok())
351            .unwrap_or_default()
352    }
353
354    /// Formats a human-readable summary of per-layer stats and total savings.
355    pub fn format_summary(&self) -> String {
356        let mut out = format!("Pipeline Stats ({} runs):\n", self.runs);
357        for kind in LayerKind::all() {
358            if let Some(agg) = self.per_layer.get(kind) {
359                out.push_str(&format!(
360                    "  {}: avg {:.0}% ratio, {:.1}ms, {} invocations\n",
361                    kind,
362                    agg.avg_ratio() * 100.0,
363                    agg.avg_duration_ms(),
364                    agg.count,
365                ));
366            }
367        }
368        out.push_str(&format!("  SAVED: {} tokens\n", self.total_tokens_saved()));
369        out
370    }
371}
372
373impl Default for Pipeline {
374    fn default() -> Self {
375        Self::new()
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    /// GH #408 (XDG-3): `pipeline_stats.json` is runtime STATE and must persist
384    /// to `state_dir()`, never the data dir. With distinct category overrides the
385    /// file MUST land under STATE and be absent from DATA — this is the only kind
386    /// of test that catches a write/read category mismatch, since the shared test
387    /// sandbox collapses all categories onto one dir.
388    #[test]
389    fn pipeline_stats_persist_to_state_dir_not_data_dir() {
390        let _lock = crate::core::data_dir::test_env_lock();
391        let state = tempfile::tempdir().unwrap();
392        let data = tempfile::tempdir().unwrap();
393        crate::test_env::set_var("LEAN_CTX_STATE_DIR", state.path());
394        crate::test_env::set_var("LEAN_CTX_DATA_DIR", data.path());
395
396        PipelineStats::default().save();
397
398        let in_state = state.path().join("pipeline_stats.json").exists();
399        let in_data = data.path().join("pipeline_stats.json").exists();
400
401        crate::test_env::remove_var("LEAN_CTX_STATE_DIR");
402        crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
403
404        assert!(in_state, "pipeline_stats.json must be written to state_dir");
405        assert!(
406            !in_data,
407            "pipeline_stats.json must NOT land in the data dir"
408        );
409    }
410
411    struct PassthroughLayer {
412        kind: LayerKind,
413    }
414
415    impl Layer for PassthroughLayer {
416        fn kind(&self) -> LayerKind {
417            self.kind
418        }
419
420        fn process(&self, input: LayerInput) -> LayerOutput {
421            LayerOutput {
422                content: input.content,
423                tokens: input.tokens,
424                metadata: input.metadata,
425            }
426        }
427    }
428
429    struct CompressionLayer {
430        ratio: f64,
431    }
432
433    impl Layer for CompressionLayer {
434        fn kind(&self) -> LayerKind {
435            LayerKind::Compression
436        }
437
438        fn process(&self, input: LayerInput) -> LayerOutput {
439            let new_tokens = (input.tokens as f64 * self.ratio) as usize;
440            let truncated = if input.content.len() > new_tokens * 4 {
441                input.content[..new_tokens * 4].to_string()
442            } else {
443                input.content
444            };
445            LayerOutput {
446                content: truncated,
447                tokens: new_tokens,
448                metadata: input.metadata,
449            }
450        }
451    }
452
453    #[test]
454    fn layer_kind_all_ordered() {
455        let all = LayerKind::all();
456        assert_eq!(all.len(), 7);
457        assert_eq!(all[0], LayerKind::Input);
458        assert_eq!(all[1], LayerKind::Autonomy);
459        assert_eq!(all[6], LayerKind::Delivery);
460    }
461
462    #[test]
463    fn passthrough_preserves_content() {
464        let layer = PassthroughLayer {
465            kind: LayerKind::Input,
466        };
467        let input = LayerInput {
468            content: "hello world".to_string(),
469            tokens: 2,
470            metadata: HashMap::new(),
471        };
472        let output = layer.process(input);
473        assert_eq!(output.content, "hello world");
474        assert_eq!(output.tokens, 2);
475    }
476
477    #[test]
478    fn compression_layer_reduces() {
479        let layer = CompressionLayer { ratio: 0.5 };
480        let input = LayerInput {
481            content: "a ".repeat(100),
482            tokens: 100,
483            metadata: HashMap::new(),
484        };
485        let output = layer.process(input);
486        assert_eq!(output.tokens, 50);
487    }
488
489    #[test]
490    fn pipeline_chains_layers() {
491        let pipeline = Pipeline::new()
492            .add_layer(Box::new(PassthroughLayer {
493                kind: LayerKind::Input,
494            }))
495            .add_layer(Box::new(CompressionLayer { ratio: 0.5 }))
496            .add_layer(Box::new(PassthroughLayer {
497                kind: LayerKind::Delivery,
498            }));
499
500        let input = LayerInput {
501            content: "a ".repeat(100),
502            tokens: 100,
503            metadata: HashMap::new(),
504        };
505        let (output, metrics) = pipeline.execute(input);
506        assert_eq!(output.tokens, 50);
507        assert_eq!(metrics.len(), 3);
508        assert_eq!(metrics[0].layer, LayerKind::Input);
509        assert_eq!(metrics[1].layer, LayerKind::Compression);
510        assert_eq!(metrics[2].layer, LayerKind::Delivery);
511    }
512
513    #[test]
514    fn metrics_new_calculates_ratio() {
515        let m = LayerMetrics::new(LayerKind::Compression, 100, 50, 1000);
516        assert!((m.compression_ratio - 0.5).abs() < f64::EPSILON);
517    }
518
519    #[test]
520    fn metrics_format_readable() {
521        let metrics = vec![
522            LayerMetrics::new(LayerKind::Input, 1000, 1000, 100),
523            LayerMetrics::new(LayerKind::Compression, 1000, 300, 5000),
524            LayerMetrics::new(LayerKind::Delivery, 300, 300, 50),
525        ];
526        let formatted = Pipeline::format_metrics(&metrics);
527        assert!(formatted.contains("input"));
528        assert!(formatted.contains("compression"));
529        assert!(formatted.contains("delivery"));
530        assert!(formatted.contains("TOTAL"));
531    }
532
533    #[test]
534    fn empty_pipeline_passes_through() {
535        let pipeline = Pipeline::new();
536        let input = LayerInput {
537            content: "test".to_string(),
538            tokens: 1,
539            metadata: HashMap::new(),
540        };
541        let (output, metrics) = pipeline.execute(input);
542        assert_eq!(output.content, "test");
543        assert!(metrics.is_empty());
544    }
545
546    #[test]
547    fn pipeline_stats_record_and_summarize() {
548        let mut stats = PipelineStats::default();
549        let metrics = vec![
550            LayerMetrics::new(LayerKind::Input, 1000, 1000, 100),
551            LayerMetrics::new(LayerKind::Compression, 1000, 300, 5000),
552            LayerMetrics::new(LayerKind::Delivery, 300, 300, 50),
553        ];
554        stats.record(&metrics);
555        stats.record(&metrics);
556
557        assert_eq!(stats.runs, 2);
558        assert_eq!(stats.total_tokens_saved(), 1400);
559
560        let agg = stats.per_layer.get(&LayerKind::Compression).unwrap();
561        assert_eq!(agg.count, 2);
562        assert_eq!(agg.total_input_tokens, 2000);
563        assert_eq!(agg.total_output_tokens, 600);
564
565        let summary = stats.format_summary();
566        assert!(summary.contains("2 runs"));
567        assert!(summary.contains("SAVED: 1400"));
568    }
569
570    #[test]
571    fn aggregated_metrics_avg() {
572        let agg = AggregatedMetrics {
573            total_input_tokens: 1000,
574            total_output_tokens: 500,
575            total_duration_us: 10000,
576            count: 2,
577        };
578        assert!((agg.avg_ratio() - 0.5).abs() < f64::EPSILON);
579        assert!((agg.avg_duration_ms() - 5.0).abs() < f64::EPSILON);
580    }
581
582    #[test]
583    fn layer_kind_from_str_valid() {
584        assert_eq!("input".parse::<LayerKind>().unwrap(), LayerKind::Input);
585        assert_eq!("Intent".parse::<LayerKind>().unwrap(), LayerKind::Intent);
586        assert_eq!(
587            "COMPRESSION".parse::<LayerKind>().unwrap(),
588            LayerKind::Compression
589        );
590        assert_eq!(
591            "delivery".parse::<LayerKind>().unwrap(),
592            LayerKind::Delivery
593        );
594    }
595
596    #[test]
597    fn layer_kind_from_str_invalid() {
598        let err = "unknown".parse::<LayerKind>().unwrap_err();
599        assert!(err.contains("unknown pipeline layer"));
600        assert!(err.contains("input, autonomy, intent"));
601    }
602
603    #[test]
604    fn layer_kind_roundtrip_str() {
605        for kind in LayerKind::all() {
606            let s = kind.as_str();
607            let parsed: LayerKind = s.parse().unwrap();
608            assert_eq!(*kind, parsed);
609        }
610    }
611
612    #[test]
613    fn pipeline_stats_record_single() {
614        let mut stats = PipelineStats::new();
615        stats.record_single(
616            LayerKind::Compression,
617            1000,
618            300,
619            std::time::Duration::from_millis(5),
620        );
621        assert_eq!(stats.runs, 1);
622        let agg = stats.per_layer.get(&LayerKind::Compression).unwrap();
623        assert_eq!(agg.total_input_tokens, 1000);
624        assert_eq!(agg.total_output_tokens, 300);
625        assert_eq!(agg.count, 1);
626    }
627
628    #[test]
629    fn pipeline_full_flow_integration() {
630        let pipeline = Pipeline::new()
631            .add_layer(Box::new(PassthroughLayer {
632                kind: LayerKind::Input,
633            }))
634            .add_layer(Box::new(PassthroughLayer {
635                kind: LayerKind::Autonomy,
636            }))
637            .add_layer(Box::new(PassthroughLayer {
638                kind: LayerKind::Intent,
639            }))
640            .add_layer(Box::new(PassthroughLayer {
641                kind: LayerKind::Relevance,
642            }))
643            .add_layer(Box::new(CompressionLayer { ratio: 0.3 }))
644            .add_layer(Box::new(PassthroughLayer {
645                kind: LayerKind::Translation,
646            }))
647            .add_layer(Box::new(PassthroughLayer {
648                kind: LayerKind::Delivery,
649            }));
650
651        let input = LayerInput {
652            content: "x ".repeat(500),
653            tokens: 500,
654            metadata: HashMap::new(),
655        };
656        let (output, metrics) = pipeline.execute(input);
657
658        assert_eq!(metrics.len(), 7, "all layers should produce metrics");
659        assert_eq!(output.tokens, 150, "compression at 0.3 ratio");
660
661        for (i, kind) in LayerKind::all().iter().enumerate() {
662            assert_eq!(metrics[i].layer, *kind, "layer order must match");
663        }
664
665        let mut stats = PipelineStats::new();
666        stats.record(&metrics);
667        assert_eq!(stats.runs, 1);
668        assert_eq!(stats.total_tokens_saved(), 350);
669
670        let formatted = Pipeline::format_metrics(&metrics);
671        assert!(formatted.contains("TOTAL"));
672        assert!(formatted.contains("500"));
673    }
674
675    #[test]
676    fn is_layer_enabled_respects_config() {
677        let cfg = crate::core::profiles::PipelineConfig {
678            intent: Some(false),
679            relevance: Some(false),
680            compression: Some(true),
681            translation: Some(true),
682        };
683
684        assert!(is_layer_enabled(LayerKind::Input, &cfg));
685        assert!(!is_layer_enabled(LayerKind::Intent, &cfg));
686        assert!(!is_layer_enabled(LayerKind::Relevance, &cfg));
687        assert!(is_layer_enabled(LayerKind::Compression, &cfg));
688        assert!(is_layer_enabled(LayerKind::Translation, &cfg));
689        assert!(is_layer_enabled(LayerKind::Delivery, &cfg));
690    }
691
692    #[test]
693    fn add_layer_if_enabled_skips_disabled() {
694        let cfg = crate::core::profiles::PipelineConfig {
695            intent: Some(false),
696            relevance: Some(true),
697            compression: Some(true),
698            translation: Some(true),
699        };
700
701        let pipeline = Pipeline::new()
702            .add_layer_if_enabled(
703                Box::new(PassthroughLayer {
704                    kind: LayerKind::Input,
705                }),
706                &cfg,
707            )
708            .add_layer_if_enabled(
709                Box::new(PassthroughLayer {
710                    kind: LayerKind::Intent,
711                }),
712                &cfg,
713            )
714            .add_layer_if_enabled(Box::new(CompressionLayer { ratio: 0.5 }), &cfg)
715            .add_layer_if_enabled(
716                Box::new(PassthroughLayer {
717                    kind: LayerKind::Delivery,
718                }),
719                &cfg,
720            );
721
722        let input = LayerInput {
723            content: "x ".repeat(100),
724            tokens: 100,
725            metadata: HashMap::new(),
726        };
727        let (output, metrics) = pipeline.execute(input);
728
729        assert_eq!(
730            metrics.len(),
731            3,
732            "Intent layer should be skipped, leaving Input + Compression + Delivery"
733        );
734        assert_eq!(metrics[0].layer, LayerKind::Input);
735        assert_eq!(metrics[1].layer, LayerKind::Compression);
736        assert_eq!(metrics[2].layer, LayerKind::Delivery);
737        assert_eq!(output.tokens, 50);
738    }
739}