Files
brain-of-reese/tests/unit/test_core_memory.py
T
ducoterra c054b47df5
Build and Push Containers / build-and-push-app (push) Successful in 1m47s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 187_sync_peak_memory_line
**Phase 187 — final verification pass: all criteria met, repo 100% green.**

Verified (all four tasks already in `complete/`)
- `app/core/memory.py`: `rss_mb` / `rss_peak_mb` / `cgroup_peak_mb` / `fmt_mb`, pure stdlib, never-raise, **100%** coverage, 24 branch tests.
- Lifecycle lines append-only: `sync: started … rss_mb=`, all three terminals `… rss_mb= rss_peak_mb=` (failed = sanitized one-liner beside the surviving `logger.exception`), `import: summary … rss_mb=`; prefixes byte-identical.
- `GET /api/sync/status`: additive `rss_mb`/`rss_peak_mb` (`int|null`), keyed off the ONE `_sample_rss` (run start, phase transitions, forced terminals, 5 s-throttled hook), `peak >= current` invariant, anonymous 403.
- `sources.js` `fmtSyncPeakClause`: ` · peak <N> MB` trailing the counts line / "Sync cancelled", nothing when null, no new control/live region.

Gates (exact commands)
- `uv run pytest --cov=app --cov-report=term-missing` → **4570 passed**, TOTAL **99%**, `app/api/sync.py` **100%**, `app/core/memory.py` **100%**
- `uv run ruff check .` → clean · `uv run pyright` → **0 errors**
- `uv run pytest tests/e2e/test_sync_peak_memory_visible.py -v --no-cov` → **5 passed in isolation** (ran twice)
- All 27 touched E2E families re-run **in isolation**, all green (progress_box 8, cancel 3, status_freshness 4, summary_visibility 3, sync_button 3, session_reset 4, git_freeze 2, git_log_stream_dates 5, PDF/image suites, …)

Completion criteria: every checkbox ✅ except the commit + phase-dir move, which belong to the harness.

Documentation
- README reviewed in full (pages, commands, config tables cross-checked against `app/config.py` — 85 env rows, all defaults match; phase-187 ops section, greps, process-vs-cgroup caveat): consistent, no changes needed.
- `AGENTS.md`: **fixed one pre-existing inconsistency** — rule 6 claimed the no-CDN test covers "the index page"; it actually parametrizes 10 pages (all but `/doc-edit.html`).

Notable
- Two failures early in the pass were self-inflicted by running the E2E module concurrently with the full suite on the shared Postgres (TRUNCATE deadlock + one order-dependent `test_overview` flake); both vanish under the documented serial isolation — no code defect, no change made.
- **PLAN §9 still needs the owner's reconciliation** (phase 187 adds the sync memory fields + two status keys; PLAN is never agent-edited — AGENTS rule 10 carries the spec). `TODO.md` holds an unrelated owner note, left untouched.

Next pending phase: `188_chunks_hnsw_index`.
2026-10-16 15:22:51 -04:00

286 lines
10 KiB
Python

"""Unit tests: ``app.core.memory`` — process RSS helpers (phase 187, task 01).
Every branch of the four helpers, with the filesystem stubbed through the
module's path constants (``PROC_STATUS_PATH`` / ``CGROUP_PEAK_PATH`` →
``tmp_path`` files, so ``open`` is never mocked globally) and ``resource``
stubbed through ``memory._resource`` (the guarded-import binding):
* the REAL ``/proc/self/status`` read on a Linux runner — ``rss_mb()`` is a
positive int and provably comes from the ``VmRSS`` parse, not the
fallback (the fallback is pinned to a sentinel that never surfaces);
* the kB→MB floor arithmetic (1500 kB → 1 MB);
* missing file, unparseable value, value-less ``VmRSS:`` line, and a file
without any ``VmRSS`` line — each falls back to ``rss_peak_mb()``;
* the non-Linux path: ``/proc`` open raises AND ``getrusage`` is patched →
the proxy value; both unavailable → ``None``;
* ``rss_peak_mb`` arithmetic, the no-``resource`` ``None``, and the
never-raise wrapper when ``getrusage`` itself raises;
* ``cgroup_peak_mb`` present (bytes→MB), absent, garbage, empty → the
latter three are ``None``;
* ``fmt_mb(None) == "?"``, ``fmt_mb(0) == "0"`` (0 is a value, not
missing), plain ints as digits;
* the purity contract: stdlib imports only — no ``app.`` import — and the
docstring carries the "Pure stdlib" line the phase completion criteria
require.
"""
from __future__ import annotations
import ast
import importlib
import inspect
import sys
from types import SimpleNamespace
from typing import Any
import pytest
import app.core.memory as memory
#: Stub whose ``getrusage`` answers with a fixed ``ru_maxrss`` (kB on
#: Linux) so fallback assertions are deterministic.
def _fake_resource(maxrss_kb: int) -> Any:
return SimpleNamespace(
RUSAGE_SELF=0,
getrusage=lambda who: SimpleNamespace(ru_maxrss=maxrss_kb),
)
#: Stub whose ``getrusage`` raises — the never-raise contract probe.
def _raising_resource(exc: Exception) -> Any:
def getrusage(who: int) -> Any:
raise exc
return SimpleNamespace(RUSAGE_SELF=0, getrusage=getrusage)
# ---------------------------------------------------------------------------
# rss_mb — the real /proc read (Linux) and the stubbed failure modes
# ---------------------------------------------------------------------------
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="needs /proc/self/status")
def test_rss_mb_reads_real_vm_rss_on_linux(monkeypatch: pytest.MonkeyPatch) -> None:
# Pin the FALLBACK to a negative sentinel: a positive int can therefore
# only come from the real VmRSS parse, not from getrusage.
monkeypatch.setattr(memory, "rss_peak_mb", lambda: -1)
value = memory.rss_mb()
assert value is not None
assert isinstance(value, int)
assert value > 0
def test_rss_mb_parses_vm_rss_kb_flooring_to_mb(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
status = tmp_path / "status"
status.write_text("Name:\tpython\nVmRSS:\t 1500 kB\nVmSize:\t999999 kB\n")
monkeypatch.setattr(memory, "PROC_STATUS_PATH", str(status))
monkeypatch.setattr(memory, "_resource", _fake_resource(999_999))
assert memory.rss_mb() == 1 # 1500 kB // 1024 — integer floor, not round
def test_rss_mb_missing_file_falls_back_to_getrusage(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
monkeypatch.setattr(memory, "PROC_STATUS_PATH", str(tmp_path / "nope"))
monkeypatch.setattr(memory, "_resource", _fake_resource(9216)) # 9 MB
assert memory.rss_mb() == 9
def test_rss_mb_unparseable_value_falls_back(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
status = tmp_path / "status"
status.write_text("VmRSS:\tnot-a-number kB\n")
monkeypatch.setattr(memory, "PROC_STATUS_PATH", str(status))
monkeypatch.setattr(memory, "_resource", _fake_resource(2048))
assert memory.rss_mb() == 2
def test_rss_mb_value_less_line_falls_back(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
# "VmRSS:" with no value: the split()[1] raises IndexError internally,
# the suppress turns it into the rss_peak_mb() proxy.
status = tmp_path / "status"
status.write_text("VmRSS:\n")
monkeypatch.setattr(memory, "PROC_STATUS_PATH", str(status))
monkeypatch.setattr(memory, "_resource", _fake_resource(3072))
assert memory.rss_mb() == 3
def test_rss_mb_file_without_vm_rss_line_falls_back(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
status = tmp_path / "status"
status.write_text("Name:\tpython\nVmSize:\t999999 kB\nVmHWM:\t888888 kB\n")
monkeypatch.setattr(memory, "PROC_STATUS_PATH", str(status))
monkeypatch.setattr(memory, "_resource", _fake_resource(1024))
assert memory.rss_mb() == 1
def test_rss_mb_returns_none_when_proc_and_resource_both_unavailable(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
monkeypatch.setattr(memory, "PROC_STATUS_PATH", str(tmp_path / "nope"))
monkeypatch.setattr(memory, "_resource", None)
assert memory.rss_mb() is None
# ---------------------------------------------------------------------------
# rss_peak_mb — arithmetic, the guarded import, the never-raise wrapper
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("maxrss_kb", "expected_mb"),
[(1024, 1), (1023, 0), (1500, 1), (4_294_967_296, 4_194_304)],
)
def test_rss_peak_mb_arithmetic(
monkeypatch: pytest.MonkeyPatch, maxrss_kb: int, expected_mb: int
) -> None:
monkeypatch.setattr(memory, "_resource", _fake_resource(maxrss_kb))
assert memory.rss_peak_mb() == expected_mb
def test_rss_peak_mb_none_without_resource_module(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(memory, "_resource", None)
assert memory.rss_peak_mb() is None
def test_guarded_import_degrades_to_none_when_resource_is_missing(
tmp_path: Any,
) -> None:
# The guarded-import branch itself: a platform WITHOUT the Unix-only
# `resource` module must still import this module, and every helper
# must answer None (no /proc, no proxy). `sys.modules[name] = None`
# makes `import resource` raise ImportError, so re-executing the
# module exercises the except arm for real (no pragma).
saved = sys.modules.get("resource")
sys.modules["resource"] = None # type: ignore[assignment]
try:
importlib.reload(memory)
assert memory._resource is None
assert memory.rss_peak_mb() is None
memory.PROC_STATUS_PATH = str(tmp_path / "nope") # reset by reload
assert memory.rss_mb() is None
finally:
if saved is not None:
sys.modules["resource"] = saved
else: # pragma: no cover - non-Unix runners never had it
sys.modules.pop("resource", None)
importlib.reload(memory) # restore the real bindings for later tests
def test_module_reloads_with_real_resource_after_the_missing_module_probe() -> None:
# Sibling pin: the finally-reload above restored the module, so the
# guarded import took the happy arm again (order-independent: this
# only passes if _resource is the live module on this platform).
if not sys.platform.startswith("linux"):
pytest.skip("resource exists on Linux")
assert memory._resource is not None
def test_rss_peak_mb_never_raises_when_getrusage_raises(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(memory, "_resource", _raising_resource(OSError("boom")))
assert memory.rss_peak_mb() is None
def test_rss_peak_mb_real_call_on_linux_yields_positive_int() -> None:
if not sys.platform.startswith("linux"):
pytest.skip("resource.getrusage is the Linux deploy path")
value = memory.rss_peak_mb()
assert value is not None
assert isinstance(value, int)
assert value > 0
# ---------------------------------------------------------------------------
# cgroup_peak_mb — present / absent / garbage / empty
# ---------------------------------------------------------------------------
def test_cgroup_peak_mb_present_converts_bytes_to_mb(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
# 2,306,867,200 bytes == 2200 MiB — the incident box's ~2.16 GB of
# cgroup page-cache-inflated "peak" (module docstring caveat).
peak = tmp_path / "memory.peak"
peak.write_text("2306867200\n")
monkeypatch.setattr(memory, "CGROUP_PEAK_PATH", str(peak))
assert memory.cgroup_peak_mb() == 2200
def test_cgroup_peak_mb_absent_is_none(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
monkeypatch.setattr(memory, "CGROUP_PEAK_PATH", str(tmp_path / "memory.peak"))
assert memory.cgroup_peak_mb() is None
@pytest.mark.parametrize("body", ["max", "", " \n", "1048576 kB"])
def test_cgroup_peak_mb_garbage_is_none(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any, body: str
) -> None:
peak = tmp_path / "memory.peak"
peak.write_text(body)
monkeypatch.setattr(memory, "CGROUP_PEAK_PATH", str(peak))
assert memory.cgroup_peak_mb() is None
# ---------------------------------------------------------------------------
# fmt_mb — the "?" rendering
# ---------------------------------------------------------------------------
def test_fmt_mb_renders_none_as_question_mark_and_values_as_digits() -> None:
assert memory.fmt_mb(None) == "?"
assert memory.fmt_mb(0) == "0" # 0 is a VALUE, not missing
assert memory.fmt_mb(512) == "512"
# ---------------------------------------------------------------------------
# Purity contract (completion criterion: stdlib only, no app. imports)
# ---------------------------------------------------------------------------
def test_module_imports_stdlib_only_and_declares_pure_stdlib() -> None:
tree = ast.parse(inspect.getsource(memory))
imported: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imported.update(alias.name.split(".")[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom):
if node.level: # relative import — still inside the app package
imported.add("app")
elif node.module:
imported.add(node.module.split(".")[0])
assert "app" not in imported
# __future__, contextlib, types are static; `resource` is the guarded
# Unix-only stdlib import — everything must stay standard library.
assert imported <= {"__future__", "contextlib", "resource", "types"}, imported
assert "Pure stdlib" in (memory.__doc__ or "")