//! Process exit codes the `mcptest` CLI hands back to the OS.
//!
//! Why this exists (WOR-1317): CI pipelines branch on these numbers, so
//! they are a contract, not an implementation detail. Every handler
//! should name a constant from this module instead of returning a bare
//! literal, and the table in `docs/exit-codes.md` mirrors this file.
//! The values are pinned by tests below; changing one is a breaking
//! change to every pipeline that consumes mcptest.

/// `0`: the command completed and everything it checked passed (or
/// there was nothing to check, for example an empty suite).
pub const EXIT_OK: i32 = 0;

/// `1`: the command completed but the result is a failure: at least one
/// test, check, gate, or probe failed, or a run-level error (an
/// unreachable server, an unstartable watcher) prevented a passing
/// result. Also the catch-all for `anyhow` errors at the CLI boundary.
pub const EXIT_FAILURE: i32 = 1;

/// `2`: the invocation itself is invalid: conflicting or malformed
/// flags, an invalid `--filter` expression, a config file that fails
/// schema validation, or a CI-incompatible suite (a focused suite, a
/// suite refused by `--deny-lints`). Nothing was executed.
pub const EXIT_INVALID_INVOCATION: i32 = 2;

/// `3`: `--wait-for-ready` exhausted its budget before every URL server
/// accepted connections. Distinct from [`EXIT_FAILURE`] so CI can retry
/// a slow-starting deployment instead of failing the build.
pub const EXIT_NOT_READY: i32 = 3;

/// `5`: the command refused to proceed by policy: `--update-snapshots`
/// or `--update-cassettes` under `CI=true` without
/// `--allow-update-in-ci`, or an eval run whose `--max-cost` budget
/// tripped. Distinct from [`EXIT_INVALID_INVOCATION`] because the
/// invocation is well-formed; a policy gate said no.
pub const EXIT_REFUSED: i32 = 5;

/// `6`: `mcptest coverage --threshold` ran cleanly but the measured
/// tool coverage is below the requested floor.
pub const EXIT_COVERAGE_FLOOR: i32 = 6;

/// `7`: `mcptest run` selected zero tests (empty suite, or a filter,
/// shard, or `--last-failed` pass that matched nothing) and
/// `--pass-with-no-tests` was not set. Distinct from [`EXIT_FAILURE`] so
/// CI can tell "tests ran and failed" from "nothing ran at all", which
/// is usually a misconfigured selection rather than a regression.
pub const EXIT_NO_TESTS_SELECTED: i32 = 7;

/// `130`: the run was interrupted by SIGINT (Ctrl-C). The value follows
/// the shell convention of `128 + signal number` (SIGINT is 2), so a
/// CI step or shell wrapper sees the same code it would for any other
/// SIGINT-terminated process. Distinct from [`EXIT_FAILURE`] so a
/// deliberate Ctrl-C is never mistaken for a test regression. The handler
/// writes whatever partial results exist before returning this (WOR-1434).
pub const EXIT_INTERRUPTED: i32 = 130;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exit_codes_are_pinned_to_the_documented_contract() {
        // docs/exit-codes.md mirrors these values; CI pipelines branch
        // on them. A change here is a breaking change.
        assert_eq!(EXIT_OK, 0);
        assert_eq!(EXIT_FAILURE, 1);
        assert_eq!(EXIT_INVALID_INVOCATION, 2);
        assert_eq!(EXIT_NOT_READY, 3);
        assert_eq!(EXIT_REFUSED, 5);
        assert_eq!(EXIT_COVERAGE_FLOOR, 6);
        assert_eq!(EXIT_NO_TESTS_SELECTED, 7);
        // WOR-1434: 128 + SIGINT(2). Shells and CI wrappers already map
        // Ctrl-C to this number; pinning it keeps the run path aligned.
        assert_eq!(EXIT_INTERRUPTED, 130);
    }
}
