7f0ab9a635
One `logging_enabled` toggle (default off) gates everything (D25): - core/applog.py: rotating app.log (no-op unless enabled); setup() at GUI/CLI start. - core/diagstore.py: each diagnostic stored in DATA_DIR/diagnostics/<id>/ (capture, result.json, report.txt, scoped gamelogs, ai/ records of exactly what was sent to the model + which model + the reply). make_report() zips a diagnostic (+ app.log) into DATA_DIR/reports/. - diagnostic.finish()/analyze_crash() store when enabled; DiagnosticResult.dir. - GUI: Settings → Logging toggle; "Report" button on the diagnostic dialog; AI interactions recorded into the diagnostic dir on "Explain with AI". - CLI: `rigdoctor bundle` (report is taken by the M4 health report). - Tests for store/record_ai/make_report + applog gating; docs (D25, M15, Phase 8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.8 KiB
Python
101 lines
3.8 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.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")
|
|
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()
|