Skip to main content

codex_wrapper/
version.rs

1//! Version parsing and comparison utilities.
2//!
3//! Beyond parsing, this module declares the range of `codex-cli` versions this
4//! wrapper is tested against, and classifies an installed binary against it
5//! via [`CliVersionStatus`].
6//!
7//! The range is not an assertion of intent. Both bounds are exercised by the
8//! contract check in `tests/contract.rs`, which runs against each of them in
9//! CI and fails if any flag or config key the builders emit has stopped being
10//! accepted. Bumping these constants means adding that version to the CI
11//! matrix and fixing whatever drift the check reports.
12
13pub use crate::types::{CliVersion, CliVersionStatus, VersionParseError};
14
15/// Lowest `codex-cli` version this wrapper is tested against.
16///
17/// Older versions are not merely untested: 0.145.0 removed
18/// `--ask-for-approval` and `--search` from the exec family in favor of
19/// config keys (#53), so the arguments this wrapper emits are not accepted by
20/// earlier releases.
21pub const TESTED_CLI_VERSION_MIN: CliVersion = CliVersion {
22    major: 0,
23    minor: 145,
24    patch: 0,
25};
26
27/// Highest `codex-cli` version this wrapper is tested against.
28pub const TESTED_CLI_VERSION_MAX: CliVersion = CliVersion {
29    major: 0,
30    minor: 147,
31    patch: 0,
32};
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    #[test]
39    fn tested_range_is_ordered() {
40        assert!(
41            TESTED_CLI_VERSION_MIN <= TESTED_CLI_VERSION_MAX,
42            "tested range is inverted: {TESTED_CLI_VERSION_MIN}..={TESTED_CLI_VERSION_MAX}"
43        );
44    }
45
46    /// The declared range is only meaningful if CI actually runs the contract
47    /// check against both ends of it. Bumping one without the other would
48    /// leave the crate claiming coverage it does not have, which is the exact
49    /// failure mode this range exists to prevent.
50    #[test]
51    fn tested_range_matches_the_ci_contract_matrix() {
52        let ci = concat!(
53            env!("CARGO_MANIFEST_DIR"),
54            "/../../.github/workflows/ci.yml"
55        );
56        let Ok(contents) = std::fs::read_to_string(ci) else {
57            // Not in a repo checkout (vendored or packaged source). Nothing to
58            // cross-check against.
59            return;
60        };
61
62        let matrix = contents
63            .lines()
64            .find_map(|line| line.trim().strip_prefix("codex: ["))
65            .expect("ci.yml should declare a `codex:` matrix for the contract job")
66            .trim_end_matches(']')
67            .split(',')
68            .map(|v| v.trim().trim_matches('"').to_string())
69            .collect::<Vec<_>>();
70
71        for bound in [TESTED_CLI_VERSION_MIN, TESTED_CLI_VERSION_MAX] {
72            assert!(
73                matrix.contains(&bound.to_string()),
74                "`{bound}` is a declared bound of the tested range but is not in \
75                 ci.yml's contract matrix {matrix:?}; the range would claim \
76                 coverage CI does not provide"
77            );
78        }
79    }
80
81    #[test]
82    fn status_within_classifies_each_case() {
83        let min = CliVersion::new(0, 145, 0);
84        let max = CliVersion::new(0, 146, 0);
85
86        assert_eq!(min.status_within(&min, &max), CliVersionStatus::Tested);
87        assert_eq!(max.status_within(&min, &max), CliVersionStatus::Tested);
88        assert_eq!(
89            CliVersion::new(0, 145, 7).status_within(&min, &max),
90            CliVersionStatus::Tested
91        );
92
93        assert_eq!(
94            CliVersion::new(0, 144, 9).status_within(&min, &max),
95            CliVersionStatus::OlderThanMinimum {
96                found: CliVersion::new(0, 144, 9),
97                minimum: min,
98            }
99        );
100        assert_eq!(
101            CliVersion::new(0, 147, 0).status_within(&min, &max),
102            CliVersionStatus::NewerUntested {
103                found: CliVersion::new(0, 147, 0),
104                tested_max: max,
105            }
106        );
107    }
108
109    #[test]
110    fn is_tested_is_true_only_for_tested() {
111        let min = CliVersion::new(0, 145, 0);
112        let max = CliVersion::new(0, 146, 0);
113        assert!(min.status_within(&min, &max).is_tested());
114        assert!(
115            !CliVersion::new(0, 1, 0)
116                .status_within(&min, &max)
117                .is_tested()
118        );
119        assert!(
120            !CliVersion::new(9, 0, 0)
121                .status_within(&min, &max)
122                .is_tested()
123        );
124    }
125}