Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions fixos/diagnostics/quick_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

HISTORY_SCHEMA = "fixos-quick-history-v1"
MAX_HISTORY_ITEMS = 192
SAFE_DISK_FREE_BYTES = 50 * 1024**3


@dataclass(frozen=True)
Expand Down Expand Up @@ -797,16 +798,30 @@ def _alerts(
swap_warning = thresholds["swap_usage_warning"]
cpu_warning = thresholds["cpu_usage_warning"]
if disk["percent"] >= disk_critical:
alerts.append(
{
"resource": "disk",
"severity": "critical",
"message": (
"Dysk systemowy przekroczył próg krytyczny "
f"{disk_critical:.0f}%."
),
}
)
free_bytes = disk.get("free_bytes", 0)
if free_bytes >= SAFE_DISK_FREE_BYTES:
free_gb = free_bytes / 1024**3
alerts.append(
{
"resource": "disk",
"severity": "warning",
"message": (
"Dysk systemowy przekroczył próg krytyczny "
f"{disk_critical:.0f}%, ale pozostało {free_gb:.1f} GB wolnego."
),
}
)
else:
alerts.append(
{
"resource": "disk",
"severity": "critical",
"message": (
"Dysk systemowy przekroczył próg krytyczny "
f"{disk_critical:.0f}%."
),
}
)
elif disk["percent"] >= disk_warning:
alerts.append(
{
Expand Down
24 changes: 24 additions & 0 deletions project/ticket-109/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Ticket 109: fix(quick): suppress false-positive critical disk alert when absolute free space exceeds safe threshold on multi-TB filesystems

- **ID**: ticket-109
- **Owner**: agent:agy
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-26

## Goal and scope

On large filesystems (e.g. 2 TB NVMe where 5% reserved blocks + 93-95% usage still leaves > 100 GB of available disk space), `fixos/diagnostics/quick_snapshot.py` (`_alerts`) triggers a `CRITICAL` disk alert purely based on `disk['percent'] >= disk_critical` without accounting for large absolute free space.

Downgrade `critical` disk alerts to `warning` in `_alerts` when `disk['free_bytes'] >= 50 * 1024**3` (50 GiB), and include the human-readable free GB in the alert message so operators on multi-TB drives get accurate severity classification. Add unit tests covering alert severity on both small and multi-TB filesystems.

## Acceptance criteria

- [x] AC-01: Scope is approved by a human owner (GitHub issue semcod/fixos#133 / PLF-20296).
- [x] AC-02: `_alerts` in `fixos/diagnostics/quick_snapshot.py` downgrades critical disk alert to warning when `free_bytes >= 50 * 1024**3`, formatting free space in GB in the message.
- [x] AC-03: Unit tests in `tests/unit/test_quick_snapshot.py` verify alert severity and message on small and multi-TB filesystems.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
81 changes: 81 additions & 0 deletions project/ticket-109/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-109",
"summary": "fix(quick): suppress false-positive critical disk alert when absolute free space exceeds safe threshold on multi-TB filesystems",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P2",
"origin": "requested"
},
"allowedPaths": [
"project/ticket-109/**",
"TODO.md",
"project/TICKETS.md",
"fixos/diagnostics/quick_snapshot.py",
"tests/unit/test_quick_snapshot.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [
"python"
],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "f787fa4192b39fffd79aa781469dff405f640406",
"targetBranch": "main",
"outcome": "In fixos/diagnostics/quick_snapshot.py (_alerts), downgrade critical disk alerts to warning when disk['free_bytes'] >= 50 * 1024**3 (50 GiB) and include human-readable free GB in the alert message. Verified by unit tests in tests/unit/test_quick_snapshot.py covering small and multi-TB filesystems.",
"nonGoals": [
"Do not modify full fixos analysis or service scanner disk logic",
"Do not modify percentage threshold configuration profiles"
],
"complexity": "S",
"estimatedMinutes": 30,
"budgets": {
"maxImplementationFiles": 5,
"maxAffectedComponents": 2,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "In _alerts, check disk free_bytes against SAFE_DISK_FREE_BYTES (50 GiB) before emitting critical disk alert. When free space exceeds safe threshold, emit warning severity with formatted free GB.",
"components": [
{
"name": "quick-snapshot",
"paths": [
"fixos/diagnostics/quick_snapshot.py"
]
},
{
"name": "tests",
"paths": [
"tests/unit/test_quick_snapshot.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Revert ticket-109 commit."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"pytest tests/unit/test_quick_snapshot.py -q"
],
"evidence": "project/ticket-109/README.md"
}
]
}
}
120 changes: 120 additions & 0 deletions tests/unit/test_quick_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,123 @@ def test_timed_out_cache_shows_last_complete_size():
assert items[0]["last_complete_at"] == "t1"
assert items[0]["complete"] is False
assert "last_complete_at" not in items[1]


def test_disk_critical_alert_downgrades_to_warning_on_multi_tb_filesystem():
resources = _resources()
total_bytes = 2 * 1024**4 # 2 TiB
free_bytes = 100 * 1024**3 # 100 GiB (>= 50 GiB threshold)
resources["disk"] = {
"mount": "/",
"total_bytes": total_bytes,
"used_bytes": total_bytes - free_bytes,
"free_bytes": free_bytes,
"percent": 95.1,
}
thresholds = quick_snapshot._profile_thresholds("developer")

alerts = quick_snapshot._alerts(
resources,
{"escalating": []},
thresholds,
)

disk_alerts = [a for a in alerts if a["resource"] == "disk"]
assert len(disk_alerts) == 1
alert = disk_alerts[0]
assert alert["severity"] == "warning"
assert "100.0 GB" in alert["message"]
assert "Dysk systemowy przekroczył próg krytyczny" in alert["message"]


def test_disk_critical_alert_remains_critical_on_small_filesystem():
resources = _resources()
total_bytes = 100 * 1024**3 # 100 GiB
free_bytes = 4 * 1024**3 # 4 GiB (< 50 GiB threshold)
resources["disk"] = {
"mount": "/",
"total_bytes": total_bytes,
"used_bytes": total_bytes - free_bytes,
"free_bytes": free_bytes,
"percent": 96.0,
}
thresholds = quick_snapshot._profile_thresholds("developer")

alerts = quick_snapshot._alerts(
resources,
{"escalating": []},
thresholds,
)

disk_alerts = [a for a in alerts if a["resource"] == "disk"]
assert len(disk_alerts) == 1
alert = disk_alerts[0]
assert alert["severity"] == "critical"
assert alert["message"] == "Dysk systemowy przekroczył próg krytyczny 95%."


def test_disk_critical_alert_safe_free_space_boundary():
thresholds = {
"disk_usage_critical": 95,
"disk_usage_warning": 85,
"memory_usage_warning": 80,
"memory_usage_critical": 90,
"swap_usage_warning": 75,
"cpu_usage_warning": 90,
}

# Exactly 50 GiB free -> downgraded to warning
resources_at_boundary = _resources()
resources_at_boundary["disk"] = {
"mount": "/",
"total_bytes": 1000 * 1024**3,
"used_bytes": 950 * 1024**3,
"free_bytes": 50 * 1024**3,
"percent": 95.0,
}
alerts_at = quick_snapshot._alerts(
resources_at_boundary, {"escalating": []}, thresholds
)
disk_at = next(a for a in alerts_at if a["resource"] == "disk")
assert disk_at["severity"] == "warning"
assert "50.0 GB" in disk_at["message"]

# Just below 50 GiB free -> critical
resources_below = _resources()
resources_below["disk"] = {
"mount": "/",
"total_bytes": 1000 * 1024**3,
"used_bytes": 950 * 1024**3 + 1,
"free_bytes": 50 * 1024**3 - 1,
"percent": 95.0,
}
alerts_below = quick_snapshot._alerts(
resources_below, {"escalating": []}, thresholds
)
disk_below = next(a for a in alerts_below if a["resource"] == "disk")
assert disk_below["severity"] == "critical"


def test_disk_critical_alert_on_multi_tb_filesystem_when_nearly_exhausted():
resources = _resources()
total_bytes = 4 * 1024**4 # 4 TiB
free_bytes = 20 * 1024**3 # 20 GiB (< 50 GiB threshold)
resources["disk"] = {
"mount": "/",
"total_bytes": total_bytes,
"used_bytes": total_bytes - free_bytes,
"free_bytes": free_bytes,
"percent": 99.5,
}
thresholds = quick_snapshot._profile_thresholds("developer")

alerts = quick_snapshot._alerts(
resources,
{"escalating": []},
thresholds,
)

disk_alerts = [a for a in alerts if a["resource"] == "disk"]
assert len(disk_alerts) == 1
assert disk_alerts[0]["severity"] == "critical"

Loading