# Negative controls for the promtool suites in this directory.
#
# A unit-test suite that passes proves the rules behave as asserted. It does not
# prove the assertions are worth anything: a suite whose `rule_files` path does
# not resolve, or whose traces are all zero, passes just as green. Both of those
# happened while this suite was being written, and neither was visible from the
# exit code.
#
# So scripts/ci/promtool-rules.sh runs a second phase. For each mutation below
# it damages ONE value the rules depend on, re-runs every suite, and requires the
# run to FAIL. A mutation the suite does not notice is a hole in the suite, and
# CI reports it as such.
#
# FORMAT — three `|`-separated fields, no `|` inside any of them:
#
#   name | manifest path relative to the repo root | sed program
#
# The sed program is applied to the `spec:`-stripped copy in the mirrored tree,
# so its indentation is two spaces shallower than the manifest on disk. Anchor
# on `^[[:space:]]*` rather than a fixed indent. `#` in the first column is a
# comment; blank lines are ignored.
#
# The script also requires each sed program to CHANGE the file. A mutation that
# matches nothing is reported as a failure, not skipped — otherwise renaming a
# rule or rewording an expression silently turns its control into a no-op and
# the suite goes back to vouching for itself.
#
# `detected by` notes below are for the reader. The machine check is only "the
# suite must fail"; nothing parses them.

# The margin control, and the only reason case 9 is in the suite. The worst
# night in Prometheus' retention (2026-09-11, peak 141) holds the expression
# true for 12 minutes at a 1m evaluation step, so at `for: 12m` it fires. The
# seven reconstructed nights peak at 10m pending and would stay silent — without
# the raw worst-night case, `for: 20m` could be cut to 12m with the suite green.
# Detected by: case 9, the worst night on record.
backlog-for-12m|kubernetes/apps/monitoring-rules/prometheusrule-cnpg-backup.yaml|s/^\([[:space:]]*\)for: 20m$/\1for: 12m/

# The value REN-129 changed, in the direction it changed it from. At 30m the
# rule does not page until 35 minutes into a stall that started at t=5m, so the
# assertion at t=25m sees nothing. This is the control that stops the 20m being
# quietly walked back.
# Detected by: cases 10, 11, 13, 14 — every case that expects a page.
backlog-for-30m|kubernetes/apps/monitoring-rules/prometheusrule-cnpg-backup.yaml|s/^\([[:space:]]*\)for: 20m$/\1for: 30m/

# Drop the aggregation, keeping the selector. `role` flips on failover; a
# changed label is a new series to Prometheus and restarts the `for` timer, so
# a backlog that persists across a promotion pages 20m after the promotion
# instead of 20m after the backlog. Scoped to `expr:` lines because
# `max by (namespace, pod)` also appears in this manifest's comments, where
# rewriting it would mutate the documentation and nothing else.
# Detected by: case 13, the role flip.
no-max-by|kubernetes/apps/monitoring-rules/prometheusrule-cnpg-backup.yaml|/^[[:space:]]*expr:/s/max by (namespace, pod) (\(cnpg_collector_pg_wal_archive_status{value="ready"}\))/\1/

# Drop the `value="ready"` selector. The same metric name also carries
# `value="done"`, a monotonic counter in the hundreds of thousands, so the
# rule would fire permanently on every CNPG instance in the cluster. The
# measured-night cases carry that counter series in their input specifically so
# this is detectable rather than a silent no-op.
# Detected by: cases 1-9 and 12 — every case with a `done` series.
no-value-selector|kubernetes/apps/monitoring-rules/prometheusrule-cnpg-backup.yaml|/^[[:space:]]*expr:/s/{value="ready"}//

# Loosen the comparison. `> 5` is strict; `>= 5` pages
# on a backlog sitting at exactly five segments, which is 80 MiB and normal.
# Detected by: case 12, pinned at exactly 5.
backlog-ge-5|kubernetes/apps/monitoring-rules/prometheusrule-cnpg-backup.yaml|/^[[:space:]]*expr:/s/> 5$/>= 5/

# --- prometheusrule-external-secrets.yaml (REN-103) -------------------------
# Scoped to the ExternalSecretSyncFailing expression with /max by/ or /== 1/
# where it matters: the manifest's comments quote the rejected forms, and
# rewriting those would mutate the documentation and nothing else.

# The window. At 5m the failure that starts at t=10m is already firing at the
# t=15m assertion, which expects pending.
# Detected by: the eval_time 15m case.
eso-for-5m|kubernetes/apps/monitoring-rules/prometheusrule-external-secrets.yaml|s/^\([[:space:]]*\)for: 15m$/\1for: 5m/

# REN-100's aggregation. `sum` over the two-pod rollout case is 2, fails
# `== 1`, and loki-basic-auth never fires.
# Detected by: the eval_time 30m case, loki-basic-auth.
eso-sum-not-max|kubernetes/apps/monitoring-rules/prometheusrule-external-secrets.yaml|s/^\([[:space:]]*\)max by (exported_namespace, name) ($/\1sum by (exported_namespace, name) (/

# The label Argus found shadowed on #994. Grouping by `namespace` names the
# controller's namespace on every alert.
# Detected by: the eval_time 30m case, exp_labels and the rendered summary.
eso-by-namespace|kubernetes/apps/monitoring-rules/prometheusrule-external-secrets.yaml|s/^\([[:space:]]*\)max by (exported_namespace, name) ($/\1max by (namespace, name) (/

# Drop `== 1`. ESO writes the inactive Ready polarity at 0, so every healthy
# ExternalSecret matches the bare selector and fires.
# Detected by: the eval_time 30m case, uptime-robot-api-key.
eso-no-eq-1|kubernetes/apps/monitoring-rules/prometheusrule-external-secrets.yaml|s/^\([[:space:]]*\)) == 1$/\1)/

# Drop the condition selector. The Deleted tombstone is status="False" at 1
# forever, so it pages until the controller restarts.
# Detected by: the eval_time 30m case, wiki-index-basic-auth.
eso-any-condition|kubernetes/apps/monitoring-rules/prometheusrule-external-secrets.yaml|s/^\([[:space:]]*\)externalsecret_status_condition{condition="Ready",status="False"}$/\1externalsecret_status_condition{status="False"}/

# prometheusrule-home-assistant.yaml (REN-245). Detected by the matching
# `eval_time: 14m` / `15m` pairs in home-assistant_test.yaml.
ha-down-for-10m|kubernetes/apps/monitoring-rules/prometheusrule-home-assistant.yaml|/alert: HomeAssistantDown/,/for:/s/for: 5m/for: 10m/
# Drops the label normalisation: the alert changes identity when the target
# comes back failing, and the t=25m assertion loses it.
ha-down-no-max|kubernetes/apps/monitoring-rules/prometheusrule-home-assistant.yaml|s/max by (job, namespace) (up{job="home-assistant", namespace="home-assistant"}) == 0/up{job="home-assistant", namespace="home-assistant"} == 0/
# The instance floor pinned to spec.instances. At 3 the healthy two-instance
# cluster pages. Detected by the switchover case, which must stay silent.
ha-recorder-count-3|kubernetes/apps/monitoring-rules/prometheusrule-home-assistant.yaml|s/== 1) < 2$/== 1) < 3/
# The loose regex that lets a Ready bootstrap Job pod mask a down broker.
ha-mosquitto-loose-regex|kubernetes/apps/monitoring-rules/prometheusrule-home-assistant.yaml|s/pod=~"mosquitto-\[a-z0-9\]{6,10}-\[a-z0-9\]{5}"/pod=~"mosquitto-.*"/
# Drops the bridge-sensor branch. Detected by the coordinator-offline case.
ha-z2m-no-bridge|kubernetes/apps/monitoring-rules/prometheusrule-home-assistant.yaml|s/bridge_connection_state"}) == 0$/bridge_connection_state"}) == 2/

# --- prometheusrule-ceph.yaml (REN-93) --------------------------------------
# The canary and the critical rule it guards share the `{type="replicated"}`
# join text, so these scope by the canary's `absent(` ... `)` block: inside it
# for the canary, `!` for everything else. The annotation quotes the selector
# with escaped quotes and so never matches.

# The window, shortened. At 15m the canary pages on a mgr-failover-length gap.
# Detected by: case F, the eval_time 75m silence.
ceph-blind-for-15m|kubernetes/apps/monitoring-rules/prometheusrule-ceph.yaml|s/^\([[:space:]]*\)for: 1h$/\1for: 15m/

# The window, lengthened. At 2h a real blindness that started at t=30m is
# still not reported at 95m.
# Detected by: case F, the eval_time 95m page.
ceph-blind-for-2h|kubernetes/apps/monitoring-rules/prometheusrule-ceph.yaml|s/^\([[:space:]]*\)for: 1h$/\1for: 2h/

# Drop the value filter from the canary only. It then watches a join the
# critical rule does not use, and stays quiet through a renamed `type` label
# or value while the critical rule is dark.
# Detected by: cases C and D.
ceph-blind-any-type|kubernetes/apps/monitoring-rules/prometheusrule-ceph.yaml|/^[[:space:]]*absent($/,/^[[:space:]]*)$/s/{type="replicated"}//

# Invert the canary: the bare join instead of its absence. It then fires on
# every healthy cluster and on every real redundancy loss, i.e. it masks the
# alert it exists to protect.
# Detected by: cases A and B.
ceph-blind-inverted|kubernetes/apps/monitoring-rules/prometheusrule-ceph.yaml|s/^\([[:space:]]*\)absent($/\1(/

# REN-88's gate, removed from the critical rule. Erasure-coded and renamed
# pools then match the critical rule again. Proves this suite loads and asserts
# the critical rule, not only the canary.
# Detected by: cases C, D and E, which assert the critical rule is silent.
ceph-critical-any-type|kubernetes/apps/monitoring-rules/prometheusrule-ceph.yaml|/^[[:space:]]*absent($/,/^[[:space:]]*)$/!s/{type="replicated"}//

# prometheusrule-prometheus-retention.yaml (REN-107). Detected by the cases in
# prometheus-retention_test.yaml.
retention-no-time-clause|kubernetes/apps/monitoring-rules/prometheusrule-prometheus-retention.yaml|s/< 0.9 \* min(prometheus_tsdb_retention_limit_seconds/< 100 * min(prometheus_tsdb_retention_limit_seconds/
retention-no-size-clause|kubernetes/apps/monitoring-rules/prometheusrule-prometheus-retention.yaml|s/^\([[:space:]]*\)> 0.8$/\1> 0/
retention-ratio-0.9|kubernetes/apps/monitoring-rules/prometheusrule-prometheus-retention.yaml|s/^\([[:space:]]*\)> 0.8$/\1> 0.9/
retention-lookback-1d|kubernetes/apps/monitoring-rules/prometheusrule-prometheus-retention.yaml|s/)\[7d:1h\]$/)[1d:1h]/
retention-cap-at-peak-time|kubernetes/apps/monitoring-rules/prometheusrule-prometheus-retention.yaml|s/\/ max(prometheus_tsdb_retention_limit_bytes{job="kube-prometheus-stack-prometheus"})$/\/ min_over_time(max(prometheus_tsdb_retention_limit_bytes{job="kube-prometheus-stack-prometheus"})[7d:1h])/

# prometheusrule-disk-wear.yaml (REN-419): the EPHEMERAL exclusion on the two
# wear alerts. Detected by the cases in disk-wear_test.yaml.
#
# Gate inverted on the projection: the EPHEMERAL drive alone fires.
# Detected by: case A.
disk-wear-projection-gate-inverted|kubernetes/apps/monitoring-rules/prometheusrule-disk-wear.yaml|/alert: NvmeDriveWearProjectedExhaustion/,/for: 6h/s/unless on (node, device)/and on (node, device)/
# Gate inverted on the critical rule.
# Detected by: case B.
disk-wear-critical-gate-inverted|kubernetes/apps/monitoring-rules/prometheusrule-disk-wear.yaml|/alert: NvmeDriveWearCritical/,/for: 1h/s/unless on (node, device)/and on (node, device)/
# Matched on device name only: k8s-1's /var on nvme1 then silences nvme1 on
# every node.
# Detected by: case B, k8s-2 nvme1.
disk-wear-critical-device-only|kubernetes/apps/monitoring-rules/prometheusrule-disk-wear.yaml|/alert: NvmeDriveWearCritical/,/for: 1h/s/unless on (node, device)/unless on (device)/
# Partition suffix not stripped: /dev/nvme1n1p1 no longer maps to nvme1 and the
# exclusion matches nothing.
# Detected by: cases A and B.
disk-wear-ephemeral-keeps-partition|kubernetes/apps/monitoring-rules/prometheusrule-disk-wear.yaml|s/n\[0-9\]+(p\[0-9\]+)?"/n[0-9]+"/

# prometheusrule-gitea.yaml (REN-583). Detected by the 24m/25m pair and the
# exactly-25% case in gitea_test.yaml.
gitea-for-10m|kubernetes/apps/monitoring-rules/prometheusrule-gitea.yaml|/alert: GiteaDataVolumeLow/,/for:/s/for: 15m/for: 10m/
gitea-floor-le|kubernetes/apps/monitoring-rules/prometheusrule-gitea.yaml|s/^\([[:space:]]*\)< 0\.25$/\1<= 0.25/

# prometheusrule-uptime-kuma.yaml / blackbox kuma exclusion. Detected by the
# cases in uptime-kuma_test.yaml.
kuma-slow-no-positive-filter|kubernetes/apps/monitoring-rules/prometheusrule-uptime-kuma.yaml|s/(monitor_response_time > 0)/(monitor_response_time)/
kuma-slow-threshold-2500|kubernetes/apps/monitoring-rules/prometheusrule-uptime-kuma.yaml|s/\[15m:1m\])) > 1500$/[15m:1m])) > 2500/
kuma-slow-threshold-900|kubernetes/apps/monitoring-rules/prometheusrule-uptime-kuma.yaml|s/\[15m:1m\])) > 1500$/[15m:1m])) > 900/
kuma-slow-for-30m|kubernetes/apps/monitoring-rules/prometheusrule-uptime-kuma.yaml|/alert: UptimeKumaSlowResponse/,/for:/s/for: 15m/for: 30m/
kuma-down-for-20m|kubernetes/apps/monitoring-rules/prometheusrule-uptime-kuma.yaml|/alert: UptimeKumaUnreachable/,/for:/s/for: 10m/for: 20m/
kuma-blackbox-exclusion-dropped|kubernetes/apps/monitoring-rules/prometheusrule-blackbox.yaml|s/kuma\\\\\.renner/kumx\\\\.renner/
