Skip to main content

eggress_testkit/
manifest.rs

1//! Manifest validation for pproxy feature parity tracking.
2//!
3//! Parses `tests/compat/pproxy_manifest.toml` and validates structural
4//! invariants that prevent regressions in the evidence index.
5
6use std::collections::HashSet;
7use std::fmt;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11
12use serde::Deserialize;
13use thiserror::Error;
14
15/// Pinned pproxy version that manifest metadata must reference.
16pub const PINNED_PPROXY_VERSION: &str = "2.7.9";
17
18/// Allowed `category` values for manifest entries.
19///
20/// Keep this list in sync with the categories used in
21/// `tests/compat/pproxy_manifest.toml`. Adding a new category requires
22/// updating both this enum and any docs that enumerate parity categories.
23pub const ALLOWED_CATEGORIES: &[&str] = &[
24    "protocol",
25    "udp",
26    "routing",
27    "security",
28    "cli",
29    "uri",
30    "transport",
31    "platform",
32    "system_proxy",
33    "python",
34    "python-api",
35    "packaging",
36    "performance",
37    "inbound_tcp",
38    "upstream_tcp",
39];
40
41// ---------------------------------------------------------------------------
42// Data model
43// ---------------------------------------------------------------------------
44
45/// Top-level metadata section of the manifest.
46#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
47pub struct ManifestMeta {
48    pub pproxy_version: String,
49    pub manifest_version: String,
50}
51
52/// A single feature entry in the manifest.
53#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
54pub struct FullManifestEntry {
55    pub id: String,
56    pub category: String,
57    pub pproxy_version: String,
58    pub egress_status: String,
59    pub evidence_level: String,
60    #[serde(default)]
61    pub tests: Vec<String>,
62    #[serde(default)]
63    pub divergence: String,
64    #[serde(default)]
65    pub external_dependency: Option<String>,
66}
67
68/// The complete manifest structure.
69#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
70pub struct FullManifest {
71    pub meta: ManifestMeta,
72    pub features: Vec<FullManifestEntry>,
73}
74
75// ---------------------------------------------------------------------------
76// Enums
77// ---------------------------------------------------------------------------
78
79/// Represents the egress implementation status for a feature.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub enum EgressStatus {
82    Compatible,
83    Supported,
84    Partial,
85    IntentionalNonParity,
86    Experimental,
87    Unsupported,
88}
89
90impl FromStr for EgressStatus {
91    type Err = ValidationError;
92
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        match s {
95            "compatible" => Ok(Self::Compatible),
96            "supported" => Ok(Self::Supported),
97            "partial" => Ok(Self::Partial),
98            "intentional_non_parity" => Ok(Self::IntentionalNonParity),
99            "experimental" => Ok(Self::Experimental),
100            "unsupported" => Ok(Self::Unsupported),
101            other => Err(ValidationError::InvalidEgressStatus {
102                value: other.to_string(),
103            }),
104        }
105    }
106}
107
108impl fmt::Display for EgressStatus {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::Compatible => write!(f, "compatible"),
112            Self::Supported => write!(f, "supported"),
113            Self::Partial => write!(f, "partial"),
114            Self::IntentionalNonParity => write!(f, "intentional_non_parity"),
115            Self::Experimental => write!(f, "experimental"),
116            Self::Unsupported => write!(f, "unsupported"),
117        }
118    }
119}
120
121/// Represents the evidence level for a feature claim.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum EvidenceLevel {
124    Unimplemented,
125    ImplementedSynthetic,
126    ImplementedDifferential,
127    ImplementedInterop,
128    Compatible,
129    IntentionalNonParity,
130}
131
132impl FromStr for EvidenceLevel {
133    type Err = ValidationError;
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        match s {
137            "unimplemented" => Ok(Self::Unimplemented),
138            "implemented_synthetic" => Ok(Self::ImplementedSynthetic),
139            "implemented_differential" => Ok(Self::ImplementedDifferential),
140            "implemented_interop" => Ok(Self::ImplementedInterop),
141            "compatible" => Ok(Self::Compatible),
142            "intentional_non_parity" => Ok(Self::IntentionalNonParity),
143            other => Err(ValidationError::InvalidEvidenceLevel {
144                value: other.to_string(),
145            }),
146        }
147    }
148}
149
150impl fmt::Display for EvidenceLevel {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        match self {
153            Self::Unimplemented => write!(f, "unimplemented"),
154            Self::ImplementedSynthetic => write!(f, "implemented_synthetic"),
155            Self::ImplementedDifferential => write!(f, "implemented_differential"),
156            Self::ImplementedInterop => write!(f, "implemented_interop"),
157            Self::Compatible => write!(f, "compatible"),
158            Self::IntentionalNonParity => write!(f, "intentional_non_parity"),
159        }
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Errors
165// ---------------------------------------------------------------------------
166
167/// A single validation error with context.
168#[derive(Debug, Clone, Error, PartialEq, Eq)]
169pub enum ValidationError {
170    #[error("TOML parse error: {message}")]
171    TomlParse { message: String },
172
173    #[error("file I/O error: {message}")]
174    Io { message: String },
175
176    #[error("invalid egress_status value: \"{value}\"")]
177    InvalidEgressStatus { value: String },
178
179    #[error("invalid evidence_level value: \"{value}\"")]
180    InvalidEvidenceLevel { value: String },
181
182    #[error(
183        "feature \"{id}\" has egress_status=\"compatible\" but evidence_level=\"{evidence}\" (must be \"compatible\")"
184    )]
185    CompatibleStatusRequiresCompatibleEvidence { id: String, evidence: String },
186
187    #[error(
188        "feature \"{id}\" has evidence_level=\"compatible\" but no test names (at least one required)"
189    )]
190    CompatibleEvidenceRequiresTests { id: String },
191
192    #[error(
193        "feature \"{id}\" has evidence_level=\"implemented_synthetic\" with egress_status=\"compatible\" (not allowed)"
194    )]
195    SyntheticCannotPairWithCompatible { id: String },
196
197    #[error("feature \"{id}\" has egress_status=\"intentional_non_parity\" but empty divergence")]
198    IntentionalNonParityRequiresDivergence { id: String },
199
200    #[error("duplicate feature id: \"{id}\"")]
201    DuplicateFeatureId { id: String },
202
203    #[error(
204        "meta.pproxy_version=\"{actual}\" does not match expected pinned version \"{expected}\""
205    )]
206    PproxyVersionMismatch { actual: String, expected: String },
207
208    #[error(
209        "feature \"{id}\" has evidence_level=\"compatible\" with differential test names but no external_dependency (expected \"pproxy==2.7.9\")"
210    )]
211    CompatibleDifferentialMissingExternalDependency { id: String },
212
213    #[error(
214        "feature \"{id}\" has evidence_level=\"implemented_interop\" but no external_dependency and no divergence explaining the interop suite"
215    )]
216    InteropMissingExternalDependencyOrDivergence { id: String },
217
218    #[error(
219        "feature \"{id}\" has no external_dependency but evidence_level=\"{evidence}\" (expected external_dependency for non-synthetic, non-intentional-non-parity evidence)"
220    )]
221    MissingExternalDependency { id: String, evidence: String },
222
223    #[error("feature \"{id}\" has unknown category \"{category}\" (must be one of: {allowed:?})")]
224    InvalidCategory {
225        id: String,
226        category: String,
227        allowed: Vec<String>,
228    },
229
230    #[error(
231        "feature \"{id}\" has egress_status=\"intentional_non_parity\" but evidence_level=\"{evidence}\" (must be \"intentional_non_parity\" or \"implemented_synthetic\")"
232    )]
233    IntentionalNonParityEvidenceMismatch { id: String, evidence: String },
234
235    #[error(
236        "feature \"{id}\" has egress_status=\"unsupported\" with empty divergence (expected rationale explaining why not implemented)"
237    )]
238    UnsupportedRequiresDivergence { id: String },
239
240    #[error(
241        "feature \"{id}\" has egress_status=\"experimental\" with empty divergence (expected rationale describing the experiment)"
242    )]
243    ExperimentalRequiresDivergence { id: String },
244
245    #[error(
246        "feature \"{id}\" has category=\"platform\" but divergence does not mention a platform constraint (e.g. \"Linux only\", \"Unix only\")"
247    )]
248    PlatformMissingConstraint { id: String },
249
250    #[error(
251        "feature \"{id}\" tests reference a file path (\"{test}\") rather than a test function or group alias"
252    )]
253    TestReferenceIsFilePath { id: String, test: String },
254
255    #[error(
256        "feature \"{id}\" tests reference a CI workflow (\"{test}\") rather than a test function or group alias"
257    )]
258    TestReferenceIsCIWorkflow { id: String, test: String },
259}
260
261/// A collection of validation errors and warnings.
262///
263/// Only errors (in `errors`) cause `validate_manifest` to return `Err`.
264/// Warnings (in `warnings`) are informational and never cause failure.
265#[derive(Debug, Clone, Error, PartialEq, Eq)]
266#[error("{errors:#?}")]
267pub struct ValidationErrors {
268    pub errors: Vec<ValidationError>,
269    pub warnings: Vec<ValidationError>,
270}
271
272impl ValidationErrors {
273    /// Create an empty collection.
274    pub fn new() -> Self {
275        Self {
276            errors: Vec::new(),
277            warnings: Vec::new(),
278        }
279    }
280
281    /// Add a hard error to the collection.
282    pub fn push(&mut self, err: ValidationError) {
283        self.errors.push(err);
284    }
285
286    /// Add a non-fatal warning.
287    pub fn warn(&mut self, warning: ValidationError) {
288        self.warnings.push(warning);
289    }
290
291    /// Returns `true` if no hard errors were recorded (warnings are ignored).
292    pub fn is_empty(&self) -> bool {
293        self.errors.is_empty()
294    }
295
296    /// Number of hard errors.
297    pub fn len(&self) -> usize {
298        self.errors.len()
299    }
300}
301
302impl Default for ValidationErrors {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308// ---------------------------------------------------------------------------
309// Validation
310// ---------------------------------------------------------------------------
311
312/// Validate a manifest parsed from TOML.
313///
314/// Returns `Ok(())` when all invariants hold, or `Err(ValidationErrors)`
315/// listing every violation found.
316/// recorded but do **not** cause a failure.
317pub fn validate_manifest(manifest: &FullManifest) -> Result<(), ValidationErrors> {
318    let mut errs = ValidationErrors::new();
319
320    // 1. meta.pproxy_version must match pinned version
321    if manifest.meta.pproxy_version != PINNED_PPROXY_VERSION {
322        errs.push(ValidationError::PproxyVersionMismatch {
323            actual: manifest.meta.pproxy_version.clone(),
324            expected: PINNED_PPROXY_VERSION.to_string(),
325        });
326    }
327
328    // 2. Collect IDs and check for duplicates
329    let mut seen_ids = HashSet::new();
330    for feature in &manifest.features {
331        if !seen_ids.insert(feature.id.clone()) {
332            errs.push(ValidationError::DuplicateFeatureId {
333                id: feature.id.clone(),
334            });
335        }
336    }
337
338    // 4. Per-feature validations
339    for feature in &manifest.features {
340        // Parse enums (validates allowed values)
341        let status = EgressStatus::from_str(&feature.egress_status);
342        let evidence = EvidenceLevel::from_str(&feature.evidence_level);
343
344        if let Err(ref e) = status {
345            errs.push(e.clone());
346        }
347        if let Err(ref e) = evidence {
348            errs.push(e.clone());
349        }
350
351        // Remaining cross-field checks require valid enum values
352        let status = match status {
353            Ok(s) => s,
354            Err(_) => continue,
355        };
356        let evidence = match evidence {
357            Ok(e) => e,
358            Err(_) => continue,
359        };
360
361        // compatible status → evidence must also be compatible
362        if status == EgressStatus::Compatible && evidence != EvidenceLevel::Compatible {
363            errs.push(
364                ValidationError::CompatibleStatusRequiresCompatibleEvidence {
365                    id: feature.id.clone(),
366                    evidence: feature.evidence_level.clone(),
367                },
368            );
369        }
370
371        // compatible evidence → at least one non-empty test name required
372        if evidence == EvidenceLevel::Compatible
373            && feature.tests.iter().all(|t| t.trim().is_empty())
374        {
375            errs.push(ValidationError::CompatibleEvidenceRequiresTests {
376                id: feature.id.clone(),
377            });
378        }
379
380        // implemented_synthetic cannot pair with compatible status
381        if evidence == EvidenceLevel::ImplementedSynthetic && status == EgressStatus::Compatible {
382            errs.push(ValidationError::SyntheticCannotPairWithCompatible {
383                id: feature.id.clone(),
384            });
385        }
386
387        // intentional_non_parity requires non-empty divergence
388        if status == EgressStatus::IntentionalNonParity && feature.divergence.trim().is_empty() {
389            errs.push(ValidationError::IntentionalNonParityRequiresDivergence {
390                id: feature.id.clone(),
391            });
392        }
393
394        // compatible evidence with differential_ test names requires external_dependency
395        if evidence == EvidenceLevel::Compatible {
396            let has_differential_test =
397                feature.tests.iter().any(|t| t.starts_with("differential_"));
398            if has_differential_test && feature.external_dependency.is_none() {
399                errs.push(
400                    ValidationError::CompatibleDifferentialMissingExternalDependency {
401                        id: feature.id.clone(),
402                    },
403                );
404            }
405        }
406
407        // implemented_interop requires external_dependency or divergence explaining interop
408        if evidence == EvidenceLevel::ImplementedInterop
409            && feature.external_dependency.is_none()
410            && feature.divergence.trim().is_empty()
411        {
412            errs.push(
413                ValidationError::InteropMissingExternalDependencyOrDivergence {
414                    id: feature.id.clone(),
415                },
416            );
417        }
418
419        // non-synthetic, non-intentional-non-parity evidence should have external_dependency
420        // (soft rule: only for compatible and implemented_differential)
421        if matches!(
422            evidence,
423            EvidenceLevel::Compatible | EvidenceLevel::ImplementedDifferential
424        ) && feature.external_dependency.is_none()
425            && feature.tests.iter().any(|t| t.starts_with("differential_"))
426        {
427            errs.push(ValidationError::MissingExternalDependency {
428                id: feature.id.clone(),
429                evidence: feature.evidence_level.clone(),
430            });
431        }
432
433        // category must be from the allowed list
434        if !ALLOWED_CATEGORIES.contains(&feature.category.as_str()) {
435            errs.push(ValidationError::InvalidCategory {
436                id: feature.id.clone(),
437                category: feature.category.clone(),
438                allowed: ALLOWED_CATEGORIES.iter().map(|s| s.to_string()).collect(),
439            });
440        }
441
442        // intentional_non_parity status must pair with intentional_non_parity or
443        // implemented_synthetic evidence (not "unimplemented" which means absent)
444        if status == EgressStatus::IntentionalNonParity
445            && !matches!(
446                evidence,
447                EvidenceLevel::IntentionalNonParity | EvidenceLevel::ImplementedSynthetic
448            )
449        {
450            errs.push(ValidationError::IntentionalNonParityEvidenceMismatch {
451                id: feature.id.clone(),
452                evidence: feature.evidence_level.clone(),
453            });
454        }
455
456        // unsupported requires a divergence explaining the omission
457        if status == EgressStatus::Unsupported && feature.divergence.trim().is_empty() {
458            errs.push(ValidationError::UnsupportedRequiresDivergence {
459                id: feature.id.clone(),
460            });
461        }
462
463        // experimental requires a divergence describing the experiment
464        if status == EgressStatus::Experimental && feature.divergence.trim().is_empty() {
465            errs.push(ValidationError::ExperimentalRequiresDivergence {
466                id: feature.id.clone(),
467            });
468        }
469
470        // platform category must mention a platform constraint in divergence
471        if feature.category == "platform" {
472            let d = feature.divergence.to_lowercase();
473            let has_platform_keyword = [
474                "linux", "macos", "windows", "freebsd", "unix", "solaris", "bsd", "android", "ios",
475            ]
476            .iter()
477            .any(|kw| d.contains(kw));
478            if !has_platform_keyword {
479                errs.push(ValidationError::PlatformMissingConstraint {
480                    id: feature.id.clone(),
481                });
482            }
483        }
484
485        // tests must not reference bare file paths or CI workflow files.
486        // Acceptable forms:
487        //   - group alias (e.g. "cli_tests", "integration_tests") — checked elsewhere
488        //   - file::test_name reference (e.g. "test_foo.py::test_bar")
489        //   - file::TestClassName reference (e.g. "test_foo.py::TestBar")
490        //   - bare test function name (e.g. "test_foo")
491        // Unacceptable:
492        //   - bare file paths like "crates/.../foo.rs" with no test function
493        //   - CI workflow references like ".github/workflows/ci.yml::cargo-deny"
494        for test in &feature.tests {
495            if test.starts_with(".github/workflows/") || test.starts_with(".github\\workflows\\") {
496                errs.push(ValidationError::TestReferenceIsCIWorkflow {
497                    id: feature.id.clone(),
498                    test: test.clone(),
499                });
500                continue;
501            }
502            if !test.contains("::") {
503                continue;
504            }
505            // file::needle form: require the needle to look like a test identifier
506            let (_, needle) = match test.split_once("::") {
507                Some(parts) if !parts.1.is_empty() => parts,
508                _ => continue,
509            };
510            let first_char = needle.chars().next();
511            let looks_like_test_id = match first_char {
512                Some(c) if c.is_ascii_alphabetic() || c == '_' => needle
513                    .chars()
514                    .all(|c| c.is_ascii_alphanumeric() || c == '_'),
515                _ => false,
516            };
517            if !looks_like_test_id {
518                errs.push(ValidationError::TestReferenceIsFilePath {
519                    id: feature.id.clone(),
520                    test: test.clone(),
521                });
522            }
523        }
524    }
525
526    if errs.is_empty() {
527        Ok(())
528    } else {
529        Err(errs)
530    }
531}
532
533/// Parse and validate a manifest from a filesystem path.
534pub fn validate_manifest_file(path: &Path) -> Result<FullManifest, ValidationErrors> {
535    let content = fs::read_to_string(path).map_err(|e| {
536        let mut errs = ValidationErrors::new();
537        errs.push(ValidationError::Io {
538            message: format!("failed to read {}: {}", path.display(), e),
539        });
540        errs
541    })?;
542
543    let manifest: FullManifest = toml::from_str(&content).map_err(|e| {
544        let mut errs = ValidationErrors::new();
545        errs.push(ValidationError::TomlParse {
546            message: e.to_string(),
547        });
548        errs
549    })?;
550
551    validate_manifest(&manifest)?;
552    Ok(manifest)
553}
554
555// ---------------------------------------------------------------------------
556// Helpers
557// ---------------------------------------------------------------------------
558
559/// Locate the pproxy manifest file relative to the workspace root.
560///
561/// Searches upward from `start` looking for `tests/compat/pproxy_manifest.toml`,
562/// then falls back to `CARGO_MANIFEST_DIR`-relative paths.
563pub fn find_manifest_path() -> Option<PathBuf> {
564    // Try CARGO_MANIFEST_DIR → ../../tests/compat/pproxy_manifest.toml
565    if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
566        let candidate =
567            PathBuf::from(&manifest_dir).join("../../tests/compat/pproxy_manifest.toml");
568        if candidate.exists() {
569            return Some(candidate);
570        }
571    }
572
573    // Try walking up from current directory
574    let cwd = std::env::current_dir().ok()?;
575    let mut dir = cwd.as_path();
576    loop {
577        let candidate = dir.join("tests/compat/pproxy_manifest.toml");
578        if candidate.exists() {
579            return Some(candidate);
580        }
581        dir = dir.parent()?;
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    fn make_manifest(meta: ManifestMeta, features: Vec<FullManifestEntry>) -> FullManifest {
590        FullManifest { meta, features }
591    }
592
593    fn default_meta() -> ManifestMeta {
594        ManifestMeta {
595            pproxy_version: PINNED_PPROXY_VERSION.to_string(),
596            manifest_version: "1".to_string(),
597        }
598    }
599
600    fn compatible_feature(id: &str) -> FullManifestEntry {
601        FullManifestEntry {
602            id: id.to_string(),
603            category: "protocol".to_string(),
604            pproxy_version: PINNED_PPROXY_VERSION.to_string(),
605            egress_status: "compatible".to_string(),
606            evidence_level: "compatible".to_string(),
607            tests: vec!["test_a".to_string()],
608            divergence: "some divergence".to_string(),
609            external_dependency: None,
610        }
611    }
612
613    #[test]
614    fn valid_manifest_passes() {
615        let manifest = make_manifest(
616            default_meta(),
617            vec![
618                compatible_feature("feat_a"),
619                FullManifestEntry {
620                    id: "feat_b".to_string(),
621                    category: "udp".to_string(),
622                    pproxy_version: PINNED_PPROXY_VERSION.to_string(),
623                    egress_status: "supported".to_string(),
624                    evidence_level: "implemented_synthetic".to_string(),
625                    tests: vec!["unit_tests".to_string()],
626                    divergence: "different entry points".to_string(),
627                    external_dependency: None,
628                },
629            ],
630        );
631        let result = validate_manifest(&manifest);
632        assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
633    }
634
635    #[test]
636    fn compatible_status_with_synthetic_evidence_fails() {
637        let manifest = make_manifest(
638            default_meta(),
639            vec![FullManifestEntry {
640                id: "bad_feat".to_string(),
641                category: "protocol".to_string(),
642                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
643                egress_status: "compatible".to_string(),
644                evidence_level: "implemented_synthetic".to_string(),
645                tests: vec!["some_test".to_string()],
646                divergence: "n/a".to_string(),
647                external_dependency: None,
648            }],
649        );
650        let errs = validate_manifest(&manifest).unwrap_err();
651        assert!(
652            errs.errors
653                .iter()
654                .any(|e| matches!(e, ValidationError::CompatibleStatusRequiresCompatibleEvidence { id, .. } if id == "bad_feat")),
655            "expected CompatibleStatusRequiresCompatibleEvidence"
656        );
657        assert!(
658            errs.errors
659                .iter()
660                .any(|e| matches!(e, ValidationError::SyntheticCannotPairWithCompatible { id, .. } if id == "bad_feat")),
661            "expected SyntheticCannotPairWithCompatible"
662        );
663    }
664
665    #[test]
666    fn duplicate_ids_fail() {
667        let manifest = make_manifest(
668            default_meta(),
669            vec![compatible_feature("dup"), compatible_feature("dup")],
670        );
671        let errs = validate_manifest(&manifest).unwrap_err();
672        assert!(
673            errs.errors.iter().any(
674                |e| matches!(e, ValidationError::DuplicateFeatureId { id, .. } if id == "dup")
675            ),
676            "expected DuplicateFeatureId"
677        );
678    }
679
680    #[test]
681    fn compatible_evidence_with_empty_tests_fails() {
682        let manifest = make_manifest(
683            default_meta(),
684            vec![FullManifestEntry {
685                id: "no_tests".to_string(),
686                category: "protocol".to_string(),
687                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
688                egress_status: "compatible".to_string(),
689                evidence_level: "compatible".to_string(),
690                tests: vec![],
691                divergence: "n/a".to_string(),
692                external_dependency: None,
693            }],
694        );
695        let errs = validate_manifest(&manifest).unwrap_err();
696        assert!(
697            errs.errors
698                .iter()
699                .any(|e| matches!(e, ValidationError::CompatibleEvidenceRequiresTests { id, .. } if id == "no_tests")),
700            "expected CompatibleEvidenceRequiresTests"
701        );
702    }
703
704    #[test]
705    fn compatible_evidence_with_whitespace_only_tests_fails() {
706        let manifest = make_manifest(
707            default_meta(),
708            vec![FullManifestEntry {
709                id: "blank_tests".to_string(),
710                category: "protocol".to_string(),
711                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
712                egress_status: "compatible".to_string(),
713                evidence_level: "compatible".to_string(),
714                tests: vec!["   ".to_string(), "".to_string()],
715                divergence: "n/a".to_string(),
716                external_dependency: None,
717            }],
718        );
719        let errs = validate_manifest(&manifest).unwrap_err();
720        assert!(
721            errs.errors
722                .iter()
723                .any(|e| matches!(e, ValidationError::CompatibleEvidenceRequiresTests { id, .. } if id == "blank_tests")),
724            "expected CompatibleEvidenceRequiresTests for whitespace-only tests"
725        );
726    }
727
728    #[test]
729    fn intentional_non_parity_without_divergence_fails() {
730        let manifest = make_manifest(
731            default_meta(),
732            vec![FullManifestEntry {
733                id: "no_div".to_string(),
734                category: "cli".to_string(),
735                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
736                egress_status: "intentional_non_parity".to_string(),
737                evidence_level: "intentional_non_parity".to_string(),
738                tests: vec![],
739                divergence: String::new(),
740                external_dependency: None,
741            }],
742        );
743        let errs = validate_manifest(&manifest).unwrap_err();
744        assert!(
745            errs.errors
746                .iter()
747                .any(|e| matches!(e, ValidationError::IntentionalNonParityRequiresDivergence { id, .. } if id == "no_div")),
748            "expected IntentionalNonParityRequiresDivergence"
749        );
750    }
751
752    #[test]
753    fn intentional_non_parity_with_whitespace_divergence_fails() {
754        let manifest = make_manifest(
755            default_meta(),
756            vec![FullManifestEntry {
757                id: "ws_div".to_string(),
758                category: "cli".to_string(),
759                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
760                egress_status: "intentional_non_parity".to_string(),
761                evidence_level: "intentional_non_parity".to_string(),
762                tests: vec![],
763                divergence: "   ".to_string(),
764                external_dependency: None,
765            }],
766        );
767        let errs = validate_manifest(&manifest).unwrap_err();
768        assert!(
769            errs.errors
770                .iter()
771                .any(|e| matches!(e, ValidationError::IntentionalNonParityRequiresDivergence { id, .. } if id == "ws_div")),
772            "expected IntentionalNonParityRequiresDivergence for whitespace divergence"
773        );
774    }
775
776    #[test]
777    fn invalid_egress_status_fails() {
778        let manifest = make_manifest(
779            default_meta(),
780            vec![FullManifestEntry {
781                id: "bad_status".to_string(),
782                category: "protocol".to_string(),
783                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
784                egress_status: "bogus".to_string(),
785                evidence_level: "compatible".to_string(),
786                tests: vec!["test".to_string()],
787                divergence: "n/a".to_string(),
788                external_dependency: None,
789            }],
790        );
791        let errs = validate_manifest(&manifest).unwrap_err();
792        assert!(
793            errs.errors
794                .iter()
795                .any(|e| matches!(e, ValidationError::InvalidEgressStatus { value, .. } if value == "bogus")),
796            "expected InvalidEgressStatus"
797        );
798    }
799
800    #[test]
801    fn invalid_evidence_level_fails() {
802        let manifest = make_manifest(
803            default_meta(),
804            vec![FullManifestEntry {
805                id: "bad_evidence".to_string(),
806                category: "protocol".to_string(),
807                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
808                egress_status: "supported".to_string(),
809                evidence_level: "not_real".to_string(),
810                tests: vec!["test".to_string()],
811                divergence: "n/a".to_string(),
812                external_dependency: None,
813            }],
814        );
815        let errs = validate_manifest(&manifest).unwrap_err();
816        assert!(
817            errs.errors
818                .iter()
819                .any(|e| matches!(e, ValidationError::InvalidEvidenceLevel { value, .. } if value == "not_real")),
820            "expected InvalidEvidenceLevel"
821        );
822    }
823
824    #[test]
825    fn pproxy_version_mismatch_fails() {
826        let mut meta = default_meta();
827        meta.pproxy_version = "1.2.3".to_string();
828        let manifest = make_manifest(meta, vec![compatible_feature("f")]);
829        let errs = validate_manifest(&manifest).unwrap_err();
830        assert!(
831            errs.errors
832                .iter()
833                .any(|e| matches!(e, ValidationError::PproxyVersionMismatch { .. })),
834            "expected PproxyVersionMismatch"
835        );
836    }
837
838    #[test]
839    fn compatible_evidence_without_tests_all_variants_fail() {
840        // Tests with only empty/whitespace entries
841        let manifest = make_manifest(
842            default_meta(),
843            vec![FullManifestEntry {
844                id: "mixed_blanks".to_string(),
845                category: "protocol".to_string(),
846                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
847                egress_status: "compatible".to_string(),
848                evidence_level: "compatible".to_string(),
849                tests: vec!["".to_string(), "  ".to_string(), "\t".to_string()],
850                divergence: "n/a".to_string(),
851                external_dependency: None,
852            }],
853        );
854        let errs = validate_manifest(&manifest).unwrap_err();
855        assert!(!errs.is_empty());
856        assert!(errs
857            .errors
858            .iter()
859            .any(|e| matches!(e, ValidationError::CompatibleEvidenceRequiresTests { .. })));
860    }
861
862    #[test]
863    fn multiple_errors_collected() {
864        let manifest = make_manifest(
865            ManifestMeta {
866                pproxy_version: "0.0.1".to_string(),
867                manifest_version: "1".to_string(),
868            },
869            vec![
870                FullManifestEntry {
871                    id: "dup".to_string(),
872                    category: "protocol".to_string(),
873                    pproxy_version: PINNED_PPROXY_VERSION.to_string(),
874                    egress_status: "bogus".to_string(),
875                    evidence_level: "also_bogus".to_string(),
876                    tests: vec![],
877                    divergence: String::new(),
878                    external_dependency: None,
879                },
880                FullManifestEntry {
881                    id: "dup".to_string(),
882                    category: "protocol".to_string(),
883                    pproxy_version: PINNED_PPROXY_VERSION.to_string(),
884                    egress_status: "intentional_non_parity".to_string(),
885                    evidence_level: "intentional_non_parity".to_string(),
886                    tests: vec![],
887                    divergence: String::new(),
888                    external_dependency: None,
889                },
890            ],
891        );
892        let errs = validate_manifest(&manifest).unwrap_err();
893        assert!(
894            errs.len() >= 4,
895            "expected at least 4 errors, got {}",
896            errs.len()
897        );
898        assert!(errs
899            .errors
900            .iter()
901            .any(|e| matches!(e, ValidationError::PproxyVersionMismatch { .. })));
902        assert!(errs
903            .errors
904            .iter()
905            .any(|e| matches!(e, ValidationError::InvalidEgressStatus { .. })));
906        assert!(errs
907            .errors
908            .iter()
909            .any(|e| matches!(e, ValidationError::InvalidEvidenceLevel { .. })));
910        assert!(errs
911            .errors
912            .iter()
913            .any(|e| matches!(e, ValidationError::DuplicateFeatureId { .. })));
914        assert!(errs.errors.iter().any(|e| matches!(
915            e,
916            ValidationError::IntentionalNonParityRequiresDivergence { .. }
917        )));
918    }
919
920    #[test]
921    fn intentional_non_parity_with_divergence_passes() {
922        let manifest = make_manifest(
923            default_meta(),
924            vec![FullManifestEntry {
925                id: "ok_innp".to_string(),
926                category: "cli".to_string(),
927                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
928                egress_status: "intentional_non_parity".to_string(),
929                evidence_level: "intentional_non_parity".to_string(),
930                tests: vec![],
931                divergence: "Deliberate design choice".to_string(),
932                external_dependency: None,
933            }],
934        );
935        let result = validate_manifest(&manifest);
936        assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
937    }
938
939    #[test]
940    fn supported_with_synthetic_passes() {
941        let manifest = make_manifest(
942            default_meta(),
943            vec![FullManifestEntry {
944                id: "ok_sup".to_string(),
945                category: "protocol".to_string(),
946                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
947                egress_status: "supported".to_string(),
948                evidence_level: "implemented_synthetic".to_string(),
949                tests: vec!["unit_tests".to_string()],
950                divergence: "n/a".to_string(),
951                external_dependency: None,
952            }],
953        );
954        let result = validate_manifest(&manifest);
955        assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
956    }
957
958    #[test]
959    fn toml_parse_error() {
960        let bad_toml = "this is not [valid toml {{{{";
961        let manifest: Result<FullManifest, _> = toml::from_str(bad_toml);
962        assert!(manifest.is_err());
963    }
964
965    #[test]
966    fn from_str_roundtrip_egress_status() {
967        for variant in &[
968            "compatible",
969            "supported",
970            "partial",
971            "intentional_non_parity",
972            "experimental",
973            "unsupported",
974        ] {
975            let status = EgressStatus::from_str(variant).unwrap();
976            assert_eq!(status.to_string(), *variant);
977        }
978    }
979
980    #[test]
981    fn from_str_roundtrip_evidence_level() {
982        for variant in &[
983            "unimplemented",
984            "implemented_synthetic",
985            "implemented_differential",
986            "implemented_interop",
987            "compatible",
988            "intentional_non_parity",
989        ] {
990            let level = EvidenceLevel::from_str(variant).unwrap();
991            assert_eq!(level.to_string(), *variant);
992        }
993    }
994
995    #[test]
996    fn from_str_invalid_egress_status() {
997        let result = EgressStatus::from_str("nope");
998        assert!(result.is_err());
999        match result.unwrap_err() {
1000            ValidationError::InvalidEgressStatus { value } => assert_eq!(value, "nope"),
1001            other => panic!("unexpected error: {:?}", other),
1002        }
1003    }
1004
1005    #[test]
1006    fn from_str_invalid_evidence_level() {
1007        let result = EvidenceLevel::from_str("nope");
1008        assert!(result.is_err());
1009        match result.unwrap_err() {
1010            ValidationError::InvalidEvidenceLevel { value } => assert_eq!(value, "nope"),
1011            other => panic!("unexpected error: {:?}", other),
1012        }
1013    }
1014
1015    #[test]
1016    fn validation_errors_collection() {
1017        let mut errs = ValidationErrors::new();
1018        assert!(errs.is_empty());
1019        assert_eq!(errs.len(), 0);
1020
1021        errs.push(ValidationError::DuplicateFeatureId {
1022            id: "a".to_string(),
1023        });
1024        errs.push(ValidationError::DuplicateFeatureId {
1025            id: "b".to_string(),
1026        });
1027        assert!(!errs.is_empty());
1028        assert_eq!(errs.len(), 2);
1029    }
1030
1031    #[test]
1032    fn validate_manifest_file_missing_path() {
1033        let path = Path::new("/nonexistent/path/manifest.toml");
1034        let result = validate_manifest_file(path);
1035        assert!(result.is_err());
1036        let errs = result.unwrap_err();
1037        assert!(errs
1038            .errors
1039            .iter()
1040            .any(|e| matches!(e, ValidationError::Io { .. })));
1041    }
1042
1043    #[test]
1044    fn compatible_differential_without_external_dependency_fails() {
1045        let manifest = make_manifest(
1046            default_meta(),
1047            vec![FullManifestEntry {
1048                id: "no_dep".to_string(),
1049                category: "protocol".to_string(),
1050                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
1051                egress_status: "compatible".to_string(),
1052                evidence_level: "compatible".to_string(),
1053                tests: vec!["differential_something".to_string()],
1054                divergence: "some divergence".to_string(),
1055                external_dependency: None,
1056            }],
1057        );
1058        let errs = validate_manifest(&manifest).unwrap_err();
1059        assert!(
1060            errs.errors.iter().any(|e| matches!(
1061                e,
1062                ValidationError::CompatibleDifferentialMissingExternalDependency { id, .. }
1063                    if id == "no_dep"
1064            )),
1065            "expected CompatibleDifferentialMissingExternalDependency"
1066        );
1067    }
1068
1069    #[test]
1070    fn compatible_differential_with_external_dependency_passes() {
1071        let manifest = make_manifest(
1072            default_meta(),
1073            vec![FullManifestEntry {
1074                id: "has_dep".to_string(),
1075                category: "protocol".to_string(),
1076                pproxy_version: PINNED_PPROXY_VERSION.to_string(),
1077                egress_status: "compatible".to_string(),
1078                evidence_level: "compatible".to_string(),
1079                tests: vec!["differential_something".to_string()],
1080                divergence: "some divergence".to_string(),
1081                external_dependency: Some("pproxy==2.7.9".to_string()),
1082            }],
1083        );
1084        let result = validate_manifest(&manifest);
1085        assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1086    }
1087
1088    #[test]
1089    fn validate_real_manifest() {
1090        let path = match find_manifest_path() {
1091            Some(p) => p,
1092            None => {
1093                eprintln!("manifest file not found, skipping");
1094                return;
1095            }
1096        };
1097        eprintln!("Validating manifest at: {}", path.display());
1098        match validate_manifest_file(&path) {
1099            Ok(manifest) => {
1100                eprintln!(
1101                    "Manifest OK: {} features, meta.pproxy_version={}",
1102                    manifest.features.len(),
1103                    manifest.meta.pproxy_version
1104                );
1105            }
1106            Err(errs) => {
1107                eprintln!("Manifest validation FAILED with {} errors:", errs.len());
1108                for (i, err) in errs.errors.iter().enumerate() {
1109                    eprintln!("  ERROR {}: {}", i + 1, err);
1110                }
1111                for (i, warn) in errs.warnings.iter().enumerate() {
1112                    eprintln!("  WARNING {}: {}", i + 1, warn);
1113                }
1114                panic!(
1115                    "manifest validation failed with {} errors (see above)",
1116                    errs.len()
1117                );
1118            }
1119        }
1120    }
1121
1122    #[test]
1123    fn manifest_test_names_exist() {
1124        const GROUP_ALIASES: &[&str] = &[
1125            "integration_tests",
1126            "unit_tests",
1127            "cli_tests",
1128            "scheduler_runtime_tests",
1129            "udp_tests",
1130            "udp_upstream_tests",
1131            "tls_tests",
1132            "shadowsocks_tcp_tests",
1133            "shadowsocks_udp_tests",
1134            "pproxy_compat_tests",
1135            "pproxy_cli_tests",
1136            "pproxy_redaction_tests",
1137            "wheel_tests",
1138            "reload_tests",
1139            "interoperability_shadowsocks_tcp",
1140            "implicit_in_echo_tests",
1141            "test_pproxy_compat.py",
1142            "test_pproxy_redaction.py",
1143            "test_pproxy_concurrency.py",
1144            "test_server_lifecycle.py",
1145            "test_pproxy_oracle.py",
1146            // Phase 36 group aliases: represent evidence that is verified by an
1147            // external command rather than an in-tree test function.
1148            "deny_audit_gate",
1149            "python_wheel_ci_workflow",
1150        ];
1151
1152        let manifest_path = match find_manifest_path() {
1153            Some(p) => p,
1154            None => {
1155                eprintln!("manifest not found, skipping");
1156                return;
1157            }
1158        };
1159        let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1160
1161        let workspace_root = manifest_path
1162            .parent()
1163            .and_then(|p| p.parent())
1164            .and_then(|p| p.parent())
1165            .expect("should have workspace root");
1166
1167        let mut source_files = Vec::new();
1168        let crates_dir = workspace_root.join("crates");
1169        if crates_dir.exists() {
1170            for entry in walk_dir_recursive(&crates_dir) {
1171                if entry.extension().is_some_and(|e| e == "rs" || e == "py") {
1172                    source_files.push(entry);
1173                }
1174            }
1175        }
1176        let python_dir = workspace_root.join("python");
1177        if python_dir.exists() {
1178            for entry in walk_dir_recursive(&python_dir) {
1179                if entry.extension().is_some_and(|e| e == "py") {
1180                    source_files.push(entry);
1181                }
1182            }
1183        }
1184        let workflows_dir = workspace_root.join(".github").join("workflows");
1185        if workflows_dir.exists() {
1186            for entry in walk_dir_recursive(&workflows_dir) {
1187                if entry.extension().is_some_and(|e| e == "yml" || e == "yaml") {
1188                    source_files.push(entry);
1189                }
1190            }
1191        }
1192
1193        let mut missing = Vec::new();
1194        for feature in &manifest.features {
1195            for test_name in &feature.tests {
1196                if GROUP_ALIASES.contains(&test_name.as_str()) {
1197                    continue;
1198                }
1199                // Accept three reference shapes:
1200                //   1. file.py::needle       — search in `file.py` for `needle`
1201                //   2. path/file.py::needle  — search in matched file for `needle`
1202                //   3. bare token            — match if any source file's stem,
1203                //                              path, or body contains the token
1204                let (file_filter, needle) = match test_name.split_once("::") {
1205                    Some((file_part, fn_part)) if !fn_part.is_empty() => {
1206                        (Some(file_part.to_string()), fn_part.to_string())
1207                    }
1208                    _ => (None, test_name.clone()),
1209                };
1210                let file_filter_stem = file_filter.as_deref().map(|p| {
1211                    std::path::Path::new(p)
1212                        .file_stem()
1213                        .map(|s| s.to_string_lossy().to_string())
1214                        .unwrap_or_else(|| p.to_string())
1215                });
1216                let found = source_files.iter().any(|path| {
1217                    if let Some(ref stem) = file_filter_stem {
1218                        // Scope to a specific file by stem.
1219                        let path_stem = path.file_stem().map(|s| s.to_string_lossy().to_string());
1220                        if path_stem.as_deref() != Some(stem.as_str()) {
1221                            return false;
1222                        }
1223                        // Search the body of the matched file for the needle.
1224                        return std::fs::read_to_string(path)
1225                            .map(|content| content.contains(needle.as_str()))
1226                            .unwrap_or(false);
1227                    }
1228                    // Bare token: accept if stem or full path or body matches.
1229                    let path_str = path.to_string_lossy().replace('\\', "/");
1230                    let path_stem = path
1231                        .file_stem()
1232                        .map(|s| s.to_string_lossy().to_string())
1233                        .unwrap_or_default();
1234                    if path_stem == needle || path_str.contains(needle.as_str()) {
1235                        return true;
1236                    }
1237                    std::fs::read_to_string(path)
1238                        .map(|content| content.contains(needle.as_str()))
1239                        .unwrap_or(false)
1240                });
1241                if !found {
1242                    missing.push((feature.id.clone(), test_name.clone()));
1243                }
1244            }
1245        }
1246
1247        if !missing.is_empty() {
1248            eprintln!("Manifest references test names not found in codebase:");
1249            for (feat, test) in &missing {
1250                eprintln!("  feature \"{}\" references \"{}\"", feat, test);
1251            }
1252            panic!(
1253                "{} manifest test name(s) not found in codebase",
1254                missing.len()
1255            );
1256        }
1257    }
1258
1259    fn walk_dir_recursive(dir: &std::path::Path) -> Vec<PathBuf> {
1260        let mut results = Vec::new();
1261        if let Ok(entries) = std::fs::read_dir(dir) {
1262            for entry in entries.flatten() {
1263                let path = entry.path();
1264                if path.is_dir() {
1265                    results.extend(walk_dir_recursive(&path));
1266                } else {
1267                    results.push(path);
1268                }
1269            }
1270        }
1271        results
1272    }
1273
1274    #[test]
1275    fn compatibility_evidence_doc_matches_manifest() {
1276        let manifest_path = match find_manifest_path() {
1277            Some(p) => p,
1278            None => {
1279                eprintln!("manifest not found, skipping");
1280                return;
1281            }
1282        };
1283        let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1284
1285        let workspace_root = manifest_path
1286            .parent()
1287            .and_then(|p| p.parent())
1288            .and_then(|p| p.parent())
1289            .expect("should have workspace root");
1290
1291        let evidence_doc_path = workspace_root.join("docs/COMPATIBILITY_EVIDENCE.md");
1292        if !evidence_doc_path.exists() {
1293            eprintln!("COMPATIBILITY_EVIDENCE.md not found, skipping");
1294            return;
1295        }
1296        let evidence_doc =
1297            fs::read_to_string(&evidence_doc_path).expect("should be able to read evidence doc");
1298
1299        // Every feature with egress_status="compatible" must appear in the evidence doc
1300        // as "Compatible" (not just "Supported" or something else).
1301        let compatible_features: Vec<&FullManifestEntry> = manifest
1302            .features
1303            .iter()
1304            .filter(|f| f.egress_status == "compatible")
1305            .collect();
1306
1307        let mut missing = Vec::new();
1308        for feature in &compatible_features {
1309            // The evidence doc uses backtick-quoted feature IDs in table rows.
1310            // Check that the feature ID appears with a Compatible tier marker nearby.
1311            if !evidence_doc.contains(&format!("`{}`", feature.id)) {
1312                missing.push(feature.id.clone());
1313            }
1314        }
1315
1316        if !missing.is_empty() {
1317            eprintln!(
1318                "The following compatible features are not listed in docs/COMPATIBILITY_EVIDENCE.md:"
1319            );
1320            for id in &missing {
1321                eprintln!("  - {}", id);
1322            }
1323            panic!(
1324                "{} compatible feature(s) missing from COMPATIBILITY_EVIDENCE.md",
1325                missing.len()
1326            );
1327        }
1328    }
1329
1330    #[test]
1331    fn readme_pproxy_compatible_claims_match_manifest() {
1332        let manifest_path = match find_manifest_path() {
1333            Some(p) => p,
1334            None => {
1335                eprintln!("manifest not found, skipping");
1336                return;
1337            }
1338        };
1339        let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1340
1341        let workspace_root = manifest_path
1342            .parent()
1343            .and_then(|p| p.parent())
1344            .and_then(|p| p.parent())
1345            .expect("should have workspace root");
1346
1347        let readme_path = workspace_root.join("README.md");
1348        if !readme_path.exists() {
1349            eprintln!("README.md not found, skipping");
1350            return;
1351        }
1352        let readme = fs::read_to_string(&readme_path).expect("should be able to read README.md");
1353
1354        // Build a set of manifest feature IDs that are egress_status="compatible"
1355        let manifest_compatible: HashSet<String> = manifest
1356            .features
1357            .iter()
1358            .filter(|f| f.egress_status == "compatible")
1359            .map(|f| f.id.clone())
1360            .collect();
1361
1362        // Look for lines in README that claim "pproxy-compatible" for a specific feature.
1363        let mut overclaims = Vec::new();
1364        for line in readme.lines() {
1365            if line.contains("pproxy-compatible") || line.contains("pproxy compatible") {
1366                // Look for backtick-quoted feature IDs on the line
1367                let mut remaining = line;
1368                while let Some(start) = remaining.find('`') {
1369                    let rest = &remaining[start + 1..];
1370                    if let Some(end) = rest.find('`') {
1371                        let candidate = &rest[..end];
1372                        if candidate.contains('_')
1373                            && !candidate.starts_with("EGRESS_REQUIRE")
1374                            && !candidate.starts_with("cargo")
1375                            && candidate.len() > 3
1376                            && candidate.len() < 80
1377                            && !manifest_compatible.contains(candidate)
1378                            && manifest.features.iter().any(|f| f.id == *candidate)
1379                        {
1380                            overclaims.push(candidate.to_string());
1381                        }
1382                        remaining = &rest[end + 1..];
1383                    } else {
1384                        break;
1385                    }
1386                }
1387            }
1388        }
1389
1390        if !overclaims.is_empty() {
1391            eprintln!("README claims pproxy-compatible for features not marked compatible in the manifest:");
1392            for id in &overclaims {
1393                let entry = manifest.features.iter().find(|f| f.id == *id).unwrap();
1394                eprintln!(
1395                    "  - `{}`: manifest egress_status=\"{}\"",
1396                    id, entry.egress_status
1397                );
1398            }
1399            panic!(
1400                "{} feature(s) overclaimed as pproxy-compatible in README",
1401                overclaims.len()
1402            );
1403        }
1404    }
1405
1406    #[test]
1407    fn parity_matrix_compatible_claims_match_manifest() {
1408        let manifest_path = match find_manifest_path() {
1409            Some(p) => p,
1410            None => {
1411                eprintln!("manifest not found, skipping");
1412                return;
1413            }
1414        };
1415        let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1416
1417        let workspace_root = manifest_path
1418            .parent()
1419            .and_then(|p| p.parent())
1420            .and_then(|p| p.parent())
1421            .expect("should have workspace root");
1422
1423        let matrix_path = workspace_root.join("docs/PARITY_MATRIX.md");
1424        if !matrix_path.exists() {
1425            eprintln!("PARITY_MATRIX.md not found, skipping");
1426            return;
1427        }
1428        let matrix =
1429            fs::read_to_string(&matrix_path).expect("should be able to read PARITY_MATRIX.md");
1430
1431        // Build a set of manifest feature IDs that are egress_status="compatible"
1432        let manifest_compatible: HashSet<String> = manifest
1433            .features
1434            .iter()
1435            .filter(|f| f.egress_status == "compatible")
1436            .map(|f| f.id.clone())
1437            .collect();
1438
1439        // Check that every "Compatible" row in PARITY_MATRIX.md references a feature
1440        // that is actually compatible in the manifest.
1441        let mut overclaims = Vec::new();
1442        for line in matrix.lines() {
1443            if line.contains("| Compatible |") || line.contains("|Compatible|") {
1444                let mut remaining = line;
1445                while let Some(start) = remaining.find('`') {
1446                    let rest = &remaining[start + 1..];
1447                    if let Some(end) = rest.find('`') {
1448                        let candidate = &rest[..end];
1449                        if candidate.contains('_')
1450                            && !candidate.starts_with("EGRESS_REQUIRE")
1451                            && candidate.len() > 3
1452                            && candidate.len() < 80
1453                            && !manifest_compatible.contains(candidate)
1454                            && manifest.features.iter().any(|f| f.id == *candidate)
1455                        {
1456                            overclaims.push(candidate.to_string());
1457                        }
1458                        remaining = &rest[end + 1..];
1459                    } else {
1460                        break;
1461                    }
1462                }
1463            }
1464        }
1465
1466        if !overclaims.is_empty() {
1467            eprintln!("PARITY_MATRIX.md claims Compatible for features not marked compatible in the manifest:");
1468            for id in &overclaims {
1469                let entry = manifest.features.iter().find(|f| f.id == *id).unwrap();
1470                eprintln!(
1471                    "  - `{}`: manifest egress_status=\"{}\"",
1472                    id, entry.egress_status
1473                );
1474            }
1475            panic!(
1476                "{} feature(s) overclaimed as Compatible in PARITY_MATRIX.md",
1477                overclaims.len()
1478            );
1479        }
1480    }
1481}