Files
yvgude--lean-ctx/rust/tests/metrics_contract.rs
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

128 lines
4.6 KiB
Rust

//! Metrics-contract drift gate (GL #401).
//!
//! Customer Datadog/Grafana dashboards reference these metric names and label
//! keys — renaming one silently breaks every dashboard built on it. This test
//! freezes the `/metrics` surface (name, type, label keys; HELP text may
//! evolve) against `docs/reference/metrics-contract.json`.
//!
//! Intentional changes: `LEANCTX_UPDATE_METRICS_CONTRACT=1 cargo test --test
//! metrics_contract` regenerates the snapshot — justify the change in the MR
//! and treat removals/renames as breaking (additive entries are fine).
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct MetricSpec {
#[serde(rename = "type")]
metric_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
labels: Vec<String>,
}
fn contract_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../docs/reference/metrics-contract.json")
}
/// Parse the Prometheus text format into name → (type, sorted label keys).
fn parse_exposition(text: &str) -> BTreeMap<String, MetricSpec> {
let mut types: BTreeMap<String, String> = BTreeMap::new();
let mut labels: BTreeMap<String, Vec<String>> = BTreeMap::new();
for line in text.lines() {
if let Some(rest) = line.strip_prefix("# TYPE ") {
let mut parts = rest.split_whitespace();
if let (Some(name), Some(ty)) = (parts.next(), parts.next()) {
types.insert(name.to_string(), ty.to_string());
}
continue;
}
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
// Sample line: `name{k1="v",k2="v"} value` or `name value`.
let name_end = line.find(['{', ' ']).unwrap_or(line.len());
let name = &line[..name_end];
let mut keys: Vec<String> = Vec::new();
if let (Some(open), Some(close)) = (line.find('{'), line.rfind('}')) {
for pair in line[open + 1..close].split(',') {
if let Some(eq) = pair.find('=') {
keys.push(pair[..eq].trim().to_string());
}
}
}
keys.sort();
labels.entry(name.to_string()).or_insert(keys);
}
types
.into_iter()
.map(|(name, ty)| {
let l = labels.get(&name).cloned().unwrap_or_default();
(
name,
MetricSpec {
metric_type: ty,
labels: l,
},
)
})
.collect()
}
#[test]
fn metrics_surface_matches_committed_contract() {
let exposition = lean_ctx::core::telemetry::global_metrics().to_prometheus();
let current = parse_exposition(&exposition);
assert!(
!current.is_empty(),
"exposition parsed to zero metrics — parser or exporter broken"
);
let path = contract_path();
if std::env::var("LEANCTX_UPDATE_METRICS_CONTRACT").as_deref() == Ok("1") {
let json = serde_json::to_string_pretty(&current).expect("serialize contract");
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir reference dir");
std::fs::write(&path, json + "\n").expect("write contract snapshot");
eprintln!("metrics contract snapshot updated: {}", path.display());
return;
}
let committed_raw = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"missing {} ({e}) — generate it once with \
LEANCTX_UPDATE_METRICS_CONTRACT=1 cargo test --test metrics_contract",
path.display()
)
});
let committed: BTreeMap<String, MetricSpec> =
serde_json::from_str(&committed_raw).expect("contract JSON parses");
let mut problems: Vec<String> = Vec::new();
for (name, spec) in &committed {
match current.get(name) {
None => problems.push(format!("REMOVED metric `{name}` (breaking)")),
Some(cur) if cur != spec => problems.push(format!(
"CHANGED `{name}`: committed {spec:?} vs current {cur:?} (breaking)"
)),
_ => {}
}
}
for name in current.keys() {
if !committed.contains_key(name) {
problems.push(format!(
"NEW metric `{name}` — additive, but must be committed to the contract"
));
}
}
assert!(
problems.is_empty(),
"metrics contract drift — customer dashboards depend on these names:\n {}\n\
If intentional: LEANCTX_UPDATE_METRICS_CONTRACT=1 cargo test --test metrics_contract \
and justify in the MR.",
problems.join("\n ")
);
}