4bd51a40c3
Expand diagnostic/report collection (all stored per-diagnostic, in the Report zip; logs also fed to the AI on "Explain"): - syslogs: nvidia-smi -q snapshot (driver/throttle/clocks/power/temps/PCIe/ECC/ retired pages) + display-server log auto-detected — Xorg.0.log on X11, or the compositor user-journal slice (gnome-shell/kwin/sway/gamescope) on Wayland. - diagstore: include the full M5 inventory (inventory.txt + .json) — invaluable for larger/shared debugging. inventory.collect() degrades gracefully (no root prompt). Best-effort throughout. - Tests for nvidia/display + inventory in store; docs (M15/SPEC). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
4.2 KiB
Python
105 lines
4.2 KiB
Python
"""Tests for M15 per-diagnostic storage + Report bundles + app logging."""
|
|
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
from rigdoctor.core import applog, diagstore
|
|
|
|
|
|
@dataclass
|
|
class FakeSummary:
|
|
start: float = 1.0
|
|
end: float = 2.0
|
|
samples: int = 3
|
|
events: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class FakeFinding:
|
|
severity: str = "ok"
|
|
category: str = "GPU"
|
|
title: str = "Looks fine"
|
|
detail: str = "no issues"
|
|
|
|
|
|
@dataclass
|
|
class FakeResult:
|
|
game: str = "Path of Exile 2"
|
|
summary: FakeSummary = field(default_factory=FakeSummary)
|
|
findings: list = field(default_factory=lambda: [FakeFinding()])
|
|
dir: str | None = None
|
|
|
|
|
|
class StoreTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = Path(tempfile.mkdtemp())
|
|
|
|
def test_disabled_returns_none(self):
|
|
with mock.patch.object(diagstore, "enabled", return_value=False):
|
|
self.assertIsNone(diagstore.store(FakeResult()))
|
|
|
|
def test_store_writes_artifacts(self):
|
|
with mock.patch.object(diagstore, "enabled", return_value=True), \
|
|
mock.patch("rigdoctor.render.render_summary", return_value="SUMMARY-TEXT"), \
|
|
mock.patch("rigdoctor.core.gamelogs.collect", return_value="LOG-TEXT"), \
|
|
mock.patch("rigdoctor.core.syslogs.collect", return_value="SYS-LOG"), \
|
|
mock.patch("rigdoctor.core.inventory.collect", return_value=[]), \
|
|
mock.patch.object(diagstore.config, "DIAGNOSTICS_DIR", self.tmp / "diagnostics"):
|
|
directory = diagstore.store(FakeResult())
|
|
self.assertTrue((directory / "result.json").exists())
|
|
self.assertTrue((directory / "report.txt").exists())
|
|
self.assertEqual((directory / "gamelogs.txt").read_text(), "LOG-TEXT")
|
|
self.assertEqual((directory / "syslogs.txt").read_text(), "SYS-LOG")
|
|
self.assertTrue((directory / "inventory.txt").exists()) # inventory included for debugging
|
|
data = json.loads((directory / "result.json").read_text())
|
|
self.assertEqual(data["game"], "Path of Exile 2")
|
|
self.assertEqual(len(data["findings"]), 1)
|
|
|
|
def test_record_ai_then_report_includes_ai_and_applog(self):
|
|
diag = self.tmp / "20260522-poe2"
|
|
diag.mkdir()
|
|
diagstore.record_ai(diag, provider="claude", model="claude-opus-4-7",
|
|
system="SYS", prompt="EXACT DATA SENT", response="THE REPLY")
|
|
ai_files = list((diag / "ai").glob("explain-*.json"))
|
|
self.assertTrue(ai_files)
|
|
record = json.loads(ai_files[0].read_text())
|
|
self.assertEqual(record["model"], "claude-opus-4-7")
|
|
self.assertEqual(record["data_sent_to_model"], "EXACT DATA SENT")
|
|
self.assertEqual(record["model_reply"], "THE REPLY")
|
|
|
|
app_log = self.tmp / "app.log"
|
|
app_log.write_text("app log line")
|
|
with mock.patch.object(diagstore.config, "REPORTS_DIR", self.tmp / "reports"), \
|
|
mock.patch.object(diagstore.config, "APP_LOG", app_log):
|
|
out = diagstore.make_report(diag)
|
|
self.assertTrue(out.exists())
|
|
with zipfile.ZipFile(out) as zf:
|
|
names = zf.namelist()
|
|
self.assertTrue(any(n.endswith("app.log") for n in names))
|
|
self.assertTrue(any("/ai/explain-" in n for n in names))
|
|
|
|
|
|
class AppLogTests(unittest.TestCase):
|
|
def test_disabled_is_noop(self):
|
|
with mock.patch.object(applog.config, "load_config", return_value={"logging_enabled": False}):
|
|
self.assertFalse(applog.setup(force=True))
|
|
|
|
def test_enabled_writes_file(self):
|
|
tmp = Path(tempfile.mkdtemp())
|
|
with mock.patch.object(applog.config, "load_config", return_value={"logging_enabled": True}), \
|
|
mock.patch.object(applog.config, "STATE_DIR", tmp), \
|
|
mock.patch.object(applog.config, "APP_LOG", tmp / "app.log"):
|
|
self.assertTrue(applog.setup(force=True))
|
|
applog.get_logger("test").info("hello world")
|
|
applog.setup(force=True) # cleanup path: re-run detaches/reattaches cleanly
|
|
self.assertTrue((tmp / "app.log").exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|