test_triage_dump.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """Tests for the opt-in triage I/O dump (TriageDumper)."""
  2. import json
  3. import re
  4. from datetime import datetime
  5. from stt_daemon.assistant.triage_dump import TriageDumper
  6. TS = 1759073916.19
  7. def _only_file(directory):
  8. files = list(directory.rglob("*.json"))
  9. assert len(files) == 1
  10. return files[0]
  11. def test_enabled_writes_one_shaped_file(tmp_path):
  12. dumper = TriageDumper(True, directory=str(tmp_path))
  13. dumper.begin(
  14. TS,
  15. laya={"request": {"state": "s"}, "response": {"ok": True}, "error": None},
  16. llm={"request": {"url": "u", "body": {}}, "response": {"c": 1}, "error": None},
  17. )
  18. dumper.finalize(TS)
  19. path = _only_file(tmp_path)
  20. assert re.fullmatch(r"\d{6}-\d{3}\.json", path.name)
  21. expected_day = datetime.fromtimestamp(TS).strftime("%Y-%m-%d")
  22. assert path.parent.name == expected_day
  23. data = json.loads(path.read_text(encoding="utf-8"))
  24. assert data["ts"] == TS
  25. assert isinstance(data["iso"], str) and data["iso"]
  26. assert data["laya"] == {
  27. "request": {"state": "s"},
  28. "response": {"ok": True},
  29. "error": None,
  30. }
  31. assert data["llm"] == {
  32. "request": {"url": "u", "body": {}},
  33. "response": {"c": 1},
  34. "error": None,
  35. }
  36. def test_same_millisecond_collision_keeps_first_shape(tmp_path):
  37. """Same-millisecond records all land; only repeats get a `-N` suffix."""
  38. dumper = TriageDumper(True, directory=str(tmp_path))
  39. for n in (1, 2, 3):
  40. dumper.begin(TS, laya={"request": {}, "response": {"n": n}, "error": None})
  41. dumper.finalize(TS)
  42. dt = datetime.fromtimestamp(TS)
  43. base = dt.strftime("%H%M%S") + "-" + dt.strftime("%f")[:3]
  44. files = {path.name: path for path in tmp_path.rglob("*.json")}
  45. assert set(files) == {f"{base}.json", f"{base}-1.json", f"{base}-2.json"}
  46. assert re.fullmatch(r"\d{6}-\d{3}\.json", f"{base}.json")
  47. first = json.loads(files[f"{base}.json"].read_text(encoding="utf-8"))
  48. second = json.loads(files[f"{base}-1.json"].read_text(encoding="utf-8"))
  49. third = json.loads(files[f"{base}-2.json"].read_text(encoding="utf-8"))
  50. assert first["laya"]["response"] == {"n": 1}
  51. assert second["laya"]["response"] == {"n": 2}
  52. assert third["laya"]["response"] == {"n": 3}
  53. def test_disabled_writes_nothing(tmp_path):
  54. dumper = TriageDumper(False, directory=str(tmp_path))
  55. assert dumper.enabled is False
  56. dumper.begin(TS, laya={"request": {}, "response": {}, "error": None})
  57. dumper.record(TS, llm={"request": {}, "response": {}, "error": None})
  58. dumper.finalize(TS)
  59. dumper.clear()
  60. assert list(tmp_path.rglob("*")) == []
  61. def test_record_merges_without_dropping_earlier_parts(tmp_path):
  62. dumper = TriageDumper(True, directory=str(tmp_path))
  63. dumper.begin(
  64. TS, laya={"request": {"state": "s"}, "response": None, "error": None}
  65. )
  66. dumper.record(TS, llm={"request": {"url": "u"}, "response": None, "error": None})
  67. dumper.record(TS, laya={"response": {"ok": True}, "error": None})
  68. dumper.finalize(TS)
  69. data = json.loads(_only_file(tmp_path).read_text(encoding="utf-8"))
  70. assert data["laya"]["request"] == {"state": "s"}
  71. assert data["laya"]["response"] == {"ok": True}
  72. assert data["llm"]["request"] == {"url": "u"}
  73. def test_llm_response_phase_keeps_request(tmp_path):
  74. """The response-phase record must not wipe the captured request."""
  75. dumper = TriageDumper(True, directory=str(tmp_path))
  76. dumper.begin(TS, laya={"request": {}, "response": {}, "error": None})
  77. dumper.record(
  78. TS,
  79. llm={"request": {"url": "u", "body": {"m": 1}}, "response": None, "error": None},
  80. )
  81. dumper.record(TS, llm={"request": None, "response": {"c": 1}, "error": None})
  82. dumper.finalize(TS)
  83. data = json.loads(_only_file(tmp_path).read_text(encoding="utf-8"))
  84. assert data["llm"]["request"] == {"url": "u", "body": {"m": 1}}
  85. assert data["llm"]["response"] == {"c": 1}
  86. assert data["llm"]["error"] is None
  87. def test_llm_duration_ms_survives_response_merge(tmp_path):
  88. """The response-phase record carries duration_ms alongside the payload."""
  89. dumper = TriageDumper(True, directory=str(tmp_path))
  90. dumper.begin(TS, laya={"request": {}, "response": {}, "error": None})
  91. dumper.record(
  92. TS, llm={"request": {"url": "u"}, "response": None, "error": None}
  93. )
  94. dumper.record(
  95. TS,
  96. llm={
  97. "request": None,
  98. "response": {"c": 1},
  99. "error": None,
  100. "duration_ms": 812.4,
  101. },
  102. )
  103. dumper.finalize(TS)
  104. data = json.loads(_only_file(tmp_path).read_text(encoding="utf-8"))
  105. assert data["llm"]["duration_ms"] == 812.4
  106. assert data["llm"]["request"] == {"url": "u"}
  107. def test_finalize_unknown_ts_is_noop(tmp_path):
  108. dumper = TriageDumper(True, directory=str(tmp_path))
  109. dumper.finalize(1234567890.0)
  110. assert list(tmp_path.rglob("*")) == []
  111. def test_clear_drops_pending(tmp_path):
  112. dumper = TriageDumper(True, directory=str(tmp_path))
  113. dumper.begin(TS, laya={"request": {}, "response": {}, "error": None})
  114. dumper.clear()
  115. dumper.finalize(TS)
  116. assert list(tmp_path.rglob("*")) == []
  117. def test_write_failure_is_swallowed(tmp_path):
  118. blocker = tmp_path / "blocker"
  119. blocker.write_text("not a directory")
  120. dumper = TriageDumper(True, directory=str(blocker / "sub"))
  121. # Must not raise even though no directory can be created here.
  122. dumper.begin(TS, laya={"request": {}, "response": {}, "error": None})
  123. dumper.finalize(TS)
  124. assert blocker.is_file()
  125. def test_discard_drops_pending_without_writing(tmp_path):
  126. dumper = TriageDumper(True, directory=str(tmp_path))
  127. dumper.begin(TS, laya={"request": {}, "response": {}, "error": None})
  128. dumper.discard(TS)
  129. dumper.finalize(TS)
  130. assert list(tmp_path.rglob("*")) == []
  131. def test_discard_unknown_and_disabled_is_noop(tmp_path):
  132. dumper = TriageDumper(True, directory=str(tmp_path))
  133. dumper.discard(1234567890.0)
  134. assert list(tmp_path.rglob("*")) == []
  135. disabled = TriageDumper(False, directory=str(tmp_path))
  136. disabled.discard(TS)
  137. disabled.finalize(TS)
  138. assert list(tmp_path.rglob("*")) == []