upravy projektu a skillu

This commit is contained in:
lachtan
2026-09-02 10:36:37 +02:00
parent 0fa619bbbe
commit f77cc2dcfe
19 changed files with 3875 additions and 52 deletions

View File

@@ -0,0 +1,381 @@
"""Tests for reflect_apply.py — the gate that stands between a finding and a real edit.
These are the guarantees the user asked for: nothing is applied without an explicit,
per-finding approval, an ambiguous patch is refused rather than guessed at, and every
applied change is one revertable commit touching one file.
"""
import json
import subprocess
import sys
from datetime import datetime
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import reflect_apply
NOW = datetime(2026, 9, 8, 10, 30)
TARGET_REL = "skills/demo/SKILL.md"
ORIGINAL = "# Demo\n\nIf the fetch fails, try again.\n\nDone.\n"
def _record(**overrides):
record = {
"id": "f7a2",
"status": "open",
"created": "2026-09-08",
"pattern": "retry-without-diagnosis",
"severity": "medium",
"diagnosis": "Repeats a call without diagnosing it.",
"evidence": [{"session": "websocket_abc"}],
"occurrences": 7,
"sessions_affected": 4,
"proposal": "Add a hard STOP gate.",
"patch": {
"file": TARGET_REL,
"old_text": "If the fetch fails, try again.",
"new_text": "If the fetch fails, STOP and diagnose.",
},
}
record.update(overrides)
return record
@pytest.fixture
def workspace(tmp_path):
"""A miniature workspace that is a real git repo, like the server's."""
target = tmp_path / TARGET_REL
target.parent.mkdir(parents=True)
target.write_text(ORIGINAL, encoding="utf-8")
(tmp_path / "reflect").mkdir()
(tmp_path / "reflect" / "findings.jsonl").write_text(json.dumps(_record()) + "\n", encoding="utf-8")
for args in (
["init", "-q"],
["config", "user.email", "test@example.com"],
["config", "user.name", "test"],
["add", "-A"],
["commit", "-q", "-m", "init"],
):
subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True)
return tmp_path
def _write_findings(workspace, *records):
path = workspace / "reflect" / "findings.jsonl"
path.write_text("\n".join(json.dumps(r) for r in records) + "\n", encoding="utf-8")
def _findings(workspace):
lines = (workspace / "reflect" / "findings.jsonl").read_text(encoding="utf-8").splitlines()
return [json.loads(line) for line in lines if line.strip()]
def _git(workspace, *args):
return subprocess.run(["git", *args], cwd=workspace, capture_output=True, text=True, check=True).stdout.strip()
class TestRefusals:
def test_missing_original_text_is_refused(self, workspace):
_write_findings(workspace, _record(patch={**_record()["patch"], "old_text": "text that is not there"}))
with pytest.raises(reflect_apply.ApplyError, match="no longer in"):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL
def test_ambiguous_original_text_is_refused(self, workspace):
"""Two matches means the intended location is a guess — refuse, never guess."""
(workspace / TARGET_REL).write_text(ORIGINAL + "If the fetch fails, try again.\n", encoding="utf-8")
before = (workspace / TARGET_REL).read_text(encoding="utf-8")
with pytest.raises(reflect_apply.ApplyError, match="occurs 2"):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == before
def test_a_finding_without_a_patch_is_refused(self, workspace):
record = _record()
del record["patch"]
_write_findings(workspace, record)
with pytest.raises(reflect_apply.ApplyError, match="no patch"):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
@pytest.mark.parametrize("status", ["watch", "applied", "rejected"])
def test_only_open_findings_can_be_applied(self, workspace, status):
"""`watch` findings were never shown to the user, so they were never approved."""
_write_findings(workspace, _record(status=status))
with pytest.raises(reflect_apply.ApplyError):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
def test_unknown_id_is_refused(self, workspace):
with pytest.raises(reflect_apply.ApplyError, match="no finding"):
reflect_apply.apply_finding(workspace, "nope", None, NOW)
def test_path_outside_the_workspace_is_refused(self, workspace):
_write_findings(workspace, _record(patch={**_record()["patch"], "file": "../../../etc/passwd"}))
with pytest.raises(reflect_apply.ApplyError):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
def test_refusal_leaves_the_store_untouched(self, workspace):
_write_findings(workspace, _record(patch={**_record()["patch"], "old_text": "absent"}))
with pytest.raises(reflect_apply.ApplyError):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert _findings(workspace)[0]["status"] == "open"
class TestApply:
def test_patch_is_applied_and_committed(self, workspace):
message = reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert "STOP and diagnose" in (workspace / TARGET_REL).read_text(encoding="utf-8")
assert "commit" in message
assert "retry-without-diagnosis (f7a2)" in _git(workspace, "log", "-1", "--pretty=%s")
def test_commit_contains_only_the_patched_file(self, workspace):
"""Dream and other skills leave unrelated work in progress — it must not be swept in."""
(workspace / "unrelated.md").write_text("someone else's work\n", encoding="utf-8")
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert _git(workspace, "show", "--name-only", "--pretty=", "HEAD") == TARGET_REL
assert "unrelated.md" in _git(workspace, "status", "--porcelain")
def test_prior_edits_to_the_same_file_are_checkpointed_first(self, workspace):
"""The patch commit must be the patch alone, so the revert is exact."""
target = workspace / TARGET_REL
target.write_text(ORIGINAL + "\nhand-written note\n", encoding="utf-8")
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
subjects = _git(workspace, "log", "-2", "--pretty=%s").splitlines()
assert subjects[1] == "reflect: checkpoint before f7a2"
assert "hand-written note" in target.read_text(encoding="utf-8")
def test_revert_restores_the_original(self, workspace):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
sha = _findings(workspace)[0]["applied"]["sha"]
subprocess.run(["git", "revert", "--no-edit", sha], cwd=workspace, check=True, capture_output=True)
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL
def test_store_records_status_sha_and_file(self, workspace):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
record = _findings(workspace)[0]
assert record["status"] == "applied"
assert record["applied"]["file"] == TARGET_REL
assert record["applied"]["sha"]
def test_other_records_survive_byte_identical(self, workspace):
other = _record(id="f0002", pattern="other-thing", status="watch")
_write_findings(workspace, _record(), other)
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert _findings(workspace)[1] == other
def test_audit_line_is_appended(self, workspace):
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
log = (workspace / "log" / "reflect.log").read_text(encoding="utf-8")
assert "APPLIED f7a2 [retry-without-diagnosis]" in log
def test_user_edited_replacement_is_used_and_recorded(self, workspace):
reflect_apply.apply_finding(workspace, "f7a2", "STOP. Read the status code first.", NOW)
assert "Read the status code first." in (workspace / TARGET_REL).read_text(encoding="utf-8")
assert _findings(workspace)[0]["applied"]["edited_by_user"] is True
class TestCheckAndReject:
def test_check_reports_success_without_touching_anything(self, workspace):
exit_code = reflect_apply.main(["--id", "f7a2", "--check", "--workspace", str(workspace)])
assert exit_code == 0
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL
assert _findings(workspace)[0]["status"] == "open"
def test_check_prints_the_diff_the_skill_shows(self, workspace, capsys):
"""The skill presents whatever this prints, so the diff has to come from here."""
reflect_apply.main(["--id", "f7a2", "--check", "--workspace", str(workspace)])
printed = capsys.readouterr().out
assert "-If the fetch fails, try again." in printed
assert "+If the fetch fails, STOP and diagnose." in printed
def test_check_fails_on_a_stale_patch(self, workspace):
_write_findings(workspace, _record(patch={**_record()["patch"], "old_text": "absent"}))
assert reflect_apply.main(["--id", "f7a2", "--check", "--workspace", str(workspace)]) == 2
def test_failed_commit_restores_the_file(self, workspace, monkeypatch, capsys):
"""Exit code 2 tells the skill nothing happened — so nothing may be left behind."""
real_git = reflect_apply._git
def failing_git(ws, *args):
if args[0] == "commit" and "checkpoint" not in " ".join(args):
raise subprocess.CalledProcessError(1, ["git", *args], stderr="empty ident name")
return real_git(ws, *args)
monkeypatch.setattr(reflect_apply, "_git", failing_git)
exit_code = reflect_apply.main(["--id", "f7a2", "--workspace", str(workspace)])
assert exit_code == 2
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL
assert _findings(workspace)[0]["status"] == "open"
assert _git(workspace, "status", "--porcelain") == ""
def test_reject_closes_the_finding_without_editing(self, workspace):
reflect_apply.reject_finding(workspace, "f7a2", "false positive", NOW)
assert _findings(workspace)[0]["status"] == "rejected"
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL
assert "REJECTED f7a2" in (workspace / "log" / "reflect.log").read_text(encoding="utf-8")
def test_cli_refusal_exits_nonzero(self, workspace):
assert reflect_apply.main(["--id", "unknown", "--workspace", str(workspace)]) == 2
class TestSetPatch:
"""A patch drafted during the review is filed by the script — the agent never touches the store.
Before this existed the agent had to edit findings.jsonl with an ad-hoc script to get a patch
in, which put an LLM inside the audit trail and left unapplicable patches behind on a miss.
"""
def _patch_file(self, tmp_path, **overrides):
patch = {"file": TARGET_REL, "old_text": "Done.", "new_text": "Done, and diagnosed."}
patch.update(overrides)
path = tmp_path / "patch.json"
path.write_text(json.dumps(patch), encoding="utf-8")
return path
def _set(self, workspace, path, finding_id="f7a2"):
return reflect_apply.main(["--id", finding_id, "--set-patch", str(path), "--workspace", str(workspace)])
def _without_patch(self, workspace):
record = _record()
del record["patch"]
_write_findings(workspace, record)
return record
def test_a_patch_that_does_not_apply_never_reaches_the_store(self, workspace, tmp_path):
"""Verification runs before the write, so a failed attempt leaves nothing behind."""
self._without_patch(workspace)
assert self._set(workspace, self._patch_file(tmp_path, old_text="TEXT THAT IS NOT THERE")) == 2
assert "patch" not in _findings(workspace)[0]
def test_an_ambiguous_patch_never_reaches_the_store(self, workspace, tmp_path):
(workspace / TARGET_REL).write_text(ORIGINAL + "Done.\n", encoding="utf-8")
self._without_patch(workspace)
assert self._set(workspace, self._patch_file(tmp_path)) == 2
assert "patch" not in _findings(workspace)[0]
def test_a_valid_patch_is_filed_with_its_provenance(self, workspace, tmp_path):
self._without_patch(workspace)
assert self._set(workspace, self._patch_file(tmp_path)) == 0
record = _findings(workspace)[0]
assert record["patch"]["new_text"] == "Done, and diagnosed."
assert record["patch_drafted_at"], "drafted during the review, not proposed by the analysis"
assert record["status"] == "open", "drafting a patch decides nothing"
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL, "nothing is applied yet"
assert "DRAFTED f7a2 [retry-without-diagnosis]" in (workspace / "log" / "reflect.log").read_text("utf-8")
def test_the_diff_comes_back_in_the_same_call(self, workspace, tmp_path, capsys):
"""Two calls instead of six: draft and show. Asking --check afterwards would be a third."""
self._without_patch(workspace)
self._set(workspace, self._patch_file(tmp_path))
printed = capsys.readouterr().out
assert "-Done." in printed
assert "+Done, and diagnosed." in printed
def test_a_wrong_first_draft_can_be_replaced(self, workspace, tmp_path):
"""The guard is the `open` status, not the absence of a patch — first drafts get it wrong."""
assert self._set(workspace, self._patch_file(tmp_path)) == 0
assert _findings(workspace)[0]["patch"]["old_text"] == "Done."
@pytest.mark.parametrize("status", ["watch", "applied", "rejected"])
def test_only_open_findings_can_be_drafted_for(self, workspace, tmp_path, status):
_write_findings(workspace, _record(status=status))
assert self._set(workspace, self._patch_file(tmp_path)) == 2
def test_a_patch_outside_the_workspace_is_refused(self, workspace, tmp_path):
self._without_patch(workspace)
assert self._set(workspace, self._patch_file(tmp_path, file="../../../etc/passwd")) == 2
assert "patch" not in _findings(workspace)[0]
@pytest.mark.parametrize("body", ['{"file": "a.md"}', '{"file": 1, "old_text": "a", "new_text": "b"}', "not json"])
def test_a_malformed_patch_file_is_refused(self, workspace, tmp_path, body):
self._without_patch(workspace)
path = tmp_path / "patch.json"
path.write_text(body, encoding="utf-8")
assert self._set(workspace, path) == 2
assert "patch" not in _findings(workspace)[0]
@pytest.mark.parametrize("other", [["--check"], ["--skip"], ["--reject", "--reason", "no"]])
def test_set_patch_excludes_the_other_decisions(self, workspace, tmp_path, other):
path = self._patch_file(tmp_path)
with pytest.raises(SystemExit):
reflect_apply.main(["--id", "f7a2", "--set-patch", str(path), *other, "--workspace", str(workspace)])
def test_a_drafted_patch_can_then_be_applied(self, workspace, tmp_path):
self._without_patch(workspace)
self._set(workspace, self._patch_file(tmp_path))
reflect_apply.apply_finding(workspace, "f7a2", None, NOW)
assert "Done, and diagnosed." in (workspace / TARGET_REL).read_text(encoding="utf-8")
class TestDecisionRecord:
"""A decision the audit cannot reconstruct is not recorded — the reason and the skips too."""
def _log(self, workspace) -> str:
return (workspace / "log" / "reflect.log").read_text(encoding="utf-8")
def test_reject_needs_a_reason(self, workspace):
with pytest.raises(SystemExit):
reflect_apply.main(["--id", "f7a2", "--reject", "--workspace", str(workspace)])
assert _findings(workspace)[0]["status"] == "open"
def test_a_blank_reason_does_not_count(self, workspace):
with pytest.raises(SystemExit):
reflect_apply.main(["--id", "f7a2", "--reject", "--reason", " ", "--workspace", str(workspace)])
assert _findings(workspace)[0]["status"] == "open"
def test_a_reason_without_reject_is_refused(self, workspace):
with pytest.raises(SystemExit):
reflect_apply.main(["--id", "f7a2", "--reason", "because", "--workspace", str(workspace)])
def test_the_reason_lands_in_the_record_and_the_log(self, workspace):
exit_code = reflect_apply.main(
["--id", "f7a2", "--reject", "--reason", "false positive", "--workspace", str(workspace)]
)
assert exit_code == 0
rejected = _findings(workspace)[0]["rejected"]
assert rejected["reason"] == "false positive"
assert rejected["at"]
assert "false positive" in self._log(workspace)
def test_skip_counts_up_and_decides_nothing(self, workspace):
for expected in (1, 2, 3):
assert reflect_apply.main(["--id", "f7a2", "--skip", "--workspace", str(workspace)]) == 0
record = _findings(workspace)[0]
assert record["skipped"]["count"] == expected
assert record["status"] == "open", "a skip is a deferral, not a decision"
assert (workspace / TARGET_REL).read_text(encoding="utf-8") == ORIGINAL
assert "SKIPPED f7a2 [retry-without-diagnosis] ×3" in self._log(workspace)
def test_an_edited_patch_keeps_the_model_proposal(self, workspace, tmp_path):
"""Overwriting `patch` lost the only record of what was proposed versus approved."""
replacement = tmp_path / "new.txt"
replacement.write_text("If the fetch fails, ask the user.", encoding="utf-8")
exit_code = reflect_apply.main(
["--id", "f7a2", "--new-text-file", str(replacement), "--workspace", str(workspace)]
)
assert exit_code == 0
record = _findings(workspace)[0]
assert record["patch"]["new_text"] == "If the fetch fails, STOP and diagnose.", "the model's proposal"
assert record["applied"]["new_text"] == "If the fetch fails, ask the user.", "what the user approved"
assert record["applied"]["edited_by_user"] is True
assert "ask the user" in (workspace / TARGET_REL).read_text(encoding="utf-8")
assert "APPLIED-EDITED f7a2" in self._log(workspace)
def test_a_pre_migration_record_still_works(self, workspace):
"""Findings decided before the reason existed carry a flat `rejected_at` — leave them be."""
_write_findings(workspace, _record(id="fold", status="rejected", rejected_at="2026-09-01 10:55"), _record())
assert reflect_apply.main(["--id", "f7a2", "--skip", "--workspace", str(workspace)]) == 0
assert _findings(workspace)[0]["rejected_at"] == "2026-09-01 10:55"
def test_skip_refuses_a_finding_that_is_already_decided(self, workspace):
_write_findings(workspace, _record(status="applied"))
assert reflect_apply.main(["--id", "f7a2", "--skip", "--workspace", str(workspace)]) == 2
def test_skip_and_reject_are_mutually_exclusive(self, workspace):
with pytest.raises(SystemExit):
reflect_apply.main(["--id", "f7a2", "--skip", "--reject", "--reason", "x", "--workspace", str(workspace)])

View File

@@ -0,0 +1,701 @@
"""Tests for reflect_auto.py — answer validation, batch persistence and the notification threshold."""
import asyncio
import json
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import reflect_auto
TODAY = "2026-09-08"
WHEN = datetime(2026, 9, 8)
def _stats(**overrides):
"""The per-run statistics dict that feeds both the report and state.json."""
stats = {"at": "2026-09-08 03:30", "sessions": 4, "batches": 1, "open": 0, "watch": 0, "repeat_per_100": 0.0}
stats.update(overrides)
return stats
def _raw_finding(**overrides):
finding = {
"pattern": "retry-without-diagnosis",
"severity": "medium",
"diagnosis": "After an error the same call is repeated with identical arguments.",
"evidence": [{"session": "websocket_abc", "when": "2026-07-11", "excerpt": "web_fetch → ERROR ×4"}],
"occurrences": 1,
"sessions_affected": 1,
"proposal": "Add a hard STOP gate.",
}
finding.update(overrides)
return finding
def _answer(*findings) -> str:
return "```json\n" + json.dumps({"findings": list(findings)}) + "\n```"
def _parsed(answer: str, session_count: int = 10) -> list[dict]:
"""Only the findings — notes about what was salvaged are asserted where they matter."""
return reflect_auto.parse_findings(answer, session_count)[0]
def _filed(**overrides):
"""A record as it would already sit in findings.jsonl."""
record = {
"id": "f0001",
"status": "watch",
"created": "2026-09-01",
"pattern": "retry-without-diagnosis",
"severity": "medium",
"diagnosis": "After an error the same call is repeated.",
"evidence": [{"session": "websocket_old"}],
"occurrences": 1,
"sessions_affected": 1,
"proposal": "Add a hard STOP gate.",
}
record.update(overrides)
return record
class TestParseFindings:
def test_accepts_a_well_formed_answer(self):
parsed = _parsed(_answer(_raw_finding()))
assert parsed[0]["pattern"] == "retry-without-diagnosis"
assert parsed[0]["patch"] is None
def test_accepts_an_empty_findings_list(self):
assert _parsed(_answer()) == []
def test_accepts_bare_json_without_a_fence(self):
assert _parsed(json.dumps({"findings": []})) == []
def test_ignores_narration_around_the_block(self):
answer = "Here are the findings:\n" + _answer(_raw_finding()) + "\nDone."
assert len(_parsed(answer)) == 1
def test_rejects_an_answer_without_json(self):
with pytest.raises(reflect_auto.FindingsError):
reflect_auto.parse_findings("I found three problems but I am not sending JSON.", 10)
def test_unescaped_quote_error_points_at_the_offending_text(self):
"""The real failure seen on the first live run: a Czech „…" closing with ASCII ".
A bare "no parseable json" message gives the retry nothing to work with, so the
error must name the line, the column and the surrounding text.
"""
broken = '```json\n{"findings": [{"pattern": "x", "diagnosis": "runtime said („blocked") and then"}]}\n```'
with pytest.raises(reflect_auto.FindingsError) as raised:
reflect_auto.parse_findings(broken, 10)
message = str(raised.value)
assert "line 1 column" in message
assert "blocked" in message
assert "unescaped double quote" in message
def test_error_is_located_inside_the_block_not_the_whole_reply(self):
"""Column numbers measured across the fence would be meaningless to the model."""
broken = 'Here is the result:\n\n```json\n{"findings": [{"diagnosis": "a "b" c"}]}\n```'
with pytest.raises(reflect_auto.FindingsError) as raised:
reflect_auto.parse_findings(broken, 10)
assert "line 1 column" in str(raised.value)
def test_last_block_wins_when_the_model_shows_its_work(self):
first = json.dumps({"findings": [_raw_finding(pattern="draft-version")]})
second = json.dumps({"findings": [_raw_finding(pattern="final-version")]})
answer = f"First draft:\n```json\n{first}\n```\nCorrected:\n```json\n{second}\n```"
assert _parsed(answer)[0]["pattern"] == "final-version"
@pytest.mark.parametrize(
"override",
[
{"pattern": "Retry Without Diagnosis"},
{"pattern": "ab"},
{"severity": "critical"},
{"diagnosis": ""},
{"occurrences": 0},
{"occurrences": "sedm"},
{"evidence": []},
{"evidence": [{"when": "2026-07-11"}]},
],
)
def test_malformed_finding_is_dropped_and_the_others_survive(self, override):
"""Re-asking costs a ~420k token turn — one bad record must not throw the batch away."""
parsed, problems = reflect_auto.parse_findings(
_answer(_raw_finding(**override), _raw_finding(pattern="something-else")), 10
)
assert [finding["pattern"] for finding in parsed] == ["something-else"]
assert any("dropped" in problem for problem in problems)
def test_an_answer_with_nothing_usable_still_raises(self):
"""Nothing salvageable means the turn was wasted — that is worth one retry."""
with pytest.raises(reflect_auto.FindingsError):
_parsed(_answer(_raw_finding(severity="critical")))
def test_unknown_fields_are_ignored_not_fatal(self):
parsed, problems = reflect_auto.parse_findings(_answer(_raw_finding(confidence=0.9)), 10)
assert "confidence" not in parsed[0]
assert any("confidence" in problem for problem in problems)
def test_extra_findings_are_trimmed_not_the_batch(self):
many = [_raw_finding(pattern=f"pattern-{index}") for index in range(reflect_auto.MAX_FINDINGS_PER_BATCH + 1)]
parsed, problems = reflect_auto.parse_findings(_answer(*many), 10)
assert len(parsed) == reflect_auto.MAX_FINDINGS_PER_BATCH
assert any("kept the first" in problem for problem in problems)
def test_overlong_diagnosis_is_truncated(self):
parsed = _parsed(_answer(_raw_finding(diagnosis="x" * (reflect_auto.MAX_DIAGNOSIS_CHARS + 50))))
assert len(parsed[0]["diagnosis"]) == reflect_auto.MAX_DIAGNOSIS_CHARS
assert parsed[0]["diagnosis"].endswith("")
class TestParsePatch:
def _patch(self, **overrides):
patch = {"file": "skills/note/SKILL.md", "old_text": "try again", "new_text": "STOP and diagnose"}
patch.update(overrides)
return patch
def test_accepts_a_complete_patch(self):
parsed = _parsed(_answer(_raw_finding(patch=self._patch())))
assert parsed[0]["patch"]["file"] == "skills/note/SKILL.md"
@pytest.mark.parametrize(
"override",
[
{"old_text": ""},
{"new_text": "try again"},
{"file": "/etc/passwd"},
{"file": "../../../etc/passwd"},
],
)
def test_unsafe_or_empty_patch_is_dropped_but_the_finding_survives(self, override):
"""The diagnosis and the proposal are still worth reviewing without a patch."""
parsed, problems = reflect_auto.parse_findings(_answer(_raw_finding(patch=self._patch(**override))), 10)
assert len(parsed) == 1
assert parsed[0]["patch"] is None
assert any("kept the finding without it" in problem for problem in problems)
def test_incomplete_patch_is_dropped_but_the_finding_survives(self):
parsed = _parsed(_answer(_raw_finding(patch={"file": "a.md", "old_text": "x"})))
assert parsed[0]["patch"] is None
class TestCounts:
"""The counts drive the threshold and the ranking, and nothing but this checks them."""
def test_more_sessions_than_occurrences_is_impossible(self):
"""Seen live: a filed finding claimed 4 occurrences across 5 sessions."""
parsed, problems = reflect_auto.parse_findings(_answer(_raw_finding(occurrences=4, sessions_affected=5)), 10)
assert parsed[0]["sessions_affected"] == 4
assert any("clamped" in problem for problem in problems)
def test_more_sessions_than_the_slice_held_is_impossible(self):
parsed, _ = reflect_auto.parse_findings(_answer(_raw_finding(occurrences=9, sessions_affected=6)), 3)
assert parsed[0]["sessions_affected"] == 3
def test_a_coherent_count_is_left_alone(self):
parsed, problems = reflect_auto.parse_findings(_answer(_raw_finding(occurrences=7, sessions_affected=4)), 10)
assert (parsed[0]["occurrences"], parsed[0]["sessions_affected"]) == (7, 4)
assert problems == []
class TestEvidenceFolding:
"""`occurrences` sums across runs, so the examples behind it have to survive the fold."""
def _evidence(self, session: str) -> list[dict]:
return [{"session": session, "when": "2026-09-02", "excerpt": "web_fetch → ERROR"}]
def test_the_fold_keeps_the_older_evidence(self):
existing = [_filed(evidence=self._evidence("websocket_old"))]
raw = _raw_finding(evidence=self._evidence("websocket_new"))
merged = reflect_auto.merge_findings(existing, _parsed(_answer(raw)), TODAY)
sessions = [item["session"] for item in merged[0].evidence]
assert sessions == ["websocket_new", "websocket_old"], "newest first, older behind it"
def test_the_same_evidence_twice_is_kept_once(self):
existing = [_filed(evidence=self._evidence("websocket_same"))]
raw = _raw_finding(evidence=self._evidence("websocket_same"))
merged = reflect_auto.merge_findings(existing, _parsed(_answer(raw)), TODAY)
assert len(merged[0].evidence) == 1
def test_the_evidence_list_is_capped(self):
older = [{"session": f"websocket_{i}", "excerpt": str(i)} for i in range(10)]
raw = _raw_finding(evidence=self._evidence("websocket_new"))
merged = reflect_auto.merge_findings([_filed(evidence=older)], _parsed(_answer(raw)), TODAY)
assert len(merged[0].evidence) == reflect_auto.MAX_EVIDENCE
def test_a_regression_does_not_mix_evidence_from_before_the_fix(self):
existing = [_filed(status="applied", evidence=self._evidence("websocket_old"))]
raw = _raw_finding(evidence=self._evidence("websocket_new"))
merged = reflect_auto.merge_findings(existing, _parsed(_answer(raw)), TODAY)
assert [item["session"] for item in merged[0].evidence] == ["websocket_new"]
class TestThreshold:
def test_first_sighting_stays_silent(self):
"""A single occurrence is noise, not a pattern — it must not reach Telegram."""
merged = reflect_auto.merge_findings([], _parsed(_answer(_raw_finding())), TODAY)
assert merged[0].status == reflect_auto.STATUS_WATCH
def test_repeat_within_one_batch_opens_immediately(self):
raw = _raw_finding(occurrences=7, sessions_affected=4)
merged = reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)
assert merged[0].status == reflect_auto.STATUS_OPEN
def test_many_occurrences_in_one_session_stay_silent(self):
"""One session looping seven times is still one session — not yet a habit."""
raw = _raw_finding(occurrences=7, sessions_affected=1)
merged = reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)
assert merged[0].status == reflect_auto.STATUS_WATCH
def test_second_sighting_promotes_a_watched_pattern(self):
merged = reflect_auto.merge_findings([_filed()], _parsed(_answer(_raw_finding())), TODAY)
assert merged[0].status == reflect_auto.STATUS_OPEN
assert merged[0].occurrences == 2
assert merged[0].sessions_affected == 2
def test_promotion_records_the_superseded_record(self):
merged = reflect_auto.merge_findings([_filed()], _parsed(_answer(_raw_finding())), TODAY)
assert merged[0].history == ("2026-09-01:f0001",)
def test_reappearing_after_apply_is_a_regression(self):
"""A fixed pattern coming back must open at once, however few the occurrences."""
merged = reflect_auto.merge_findings([_filed(status="applied")], _parsed(_answer(_raw_finding())), TODAY)
assert merged[0].status == reflect_auto.STATUS_OPEN
assert merged[0].regression_of == "f0001"
assert merged[0].occurrences == 1
def test_evidence_from_before_the_fix_is_not_a_regression(self):
"""The window reaches back over sessions that predate the fix; they say nothing about it."""
applied = _filed(status="applied", applied={"at": "2026-09-01 10:00", "sha": "abc", "file": "SOUL.md"})
raw = _raw_finding(occurrences=9, sessions_affected=5, evidence=[{"session": "ws", "when": "2026-08-31"}])
merged = reflect_auto.merge_findings([applied], _parsed(_answer(raw)), TODAY)
assert merged[0].regression_of is None
assert merged[0].status == reflect_auto.STATUS_WATCH, "a finding about the past must not open"
def test_evidence_from_after_the_fix_is_a_regression(self):
applied = _filed(status="applied", applied={"at": "2026-09-01 10:00", "sha": "abc", "file": "SOUL.md"})
raw = _raw_finding(evidence=[{"session": "ws", "when": "2026-09-02 13:53"}])
merged = reflect_auto.merge_findings([applied], _parsed(_answer(raw)), TODAY)
assert merged[0].regression_of == "f0001"
assert merged[0].status == reflect_auto.STATUS_OPEN
def test_rejected_pattern_never_opens_again(self):
"""Rejection is a decision, not a deferral — a repeat must not start nagging again."""
raw = _raw_finding(occurrences=9, sessions_affected=5)
merged = reflect_auto.merge_findings([_filed(status="rejected")], _parsed(_answer(raw)), TODAY)
assert merged[0].status == reflect_auto.STATUS_WATCH
def test_rejection_survives_the_run_after_next(self):
"""The watch record filed after a rejection is newer — it must not supersede the decision."""
store = [_filed(status="rejected")]
raw = _raw_finding(occurrences=9, sessions_affected=5)
first = reflect_auto.merge_findings(store, _parsed(_answer(raw)), "2026-09-02")
second = reflect_auto.merge_findings(reflect_auto.supersede(store, first), _parsed(_answer(raw)), "2026-09-03")
assert second[0].status == reflect_auto.STATUS_WATCH
def test_rejected_pattern_does_not_inherit_counts(self):
merged = reflect_auto.merge_findings([_filed(status="rejected")], _parsed(_answer(_raw_finding())), TODAY)
assert merged[0].occurrences == 1
def test_regression_flag_survives_a_second_sighting(self):
"""A pattern that came back after a fix stays flagged until it is dealt with."""
applied = [_filed(status="applied")]
first = reflect_auto.merge_findings(applied, _parsed(_answer(_raw_finding())), "2026-09-02")
second = reflect_auto.merge_findings(
reflect_auto.supersede(applied, first), _parsed(_answer(_raw_finding())), "2026-09-03"
)
assert second[0].regression_of == "f0001"
def test_ids_are_unique_against_existing_records(self):
merged = reflect_auto.merge_findings([_filed()], _parsed(_answer(_raw_finding(pattern="other-thing"))), TODAY)
assert merged[0].id != "f0001"
class TestLastSeen:
"""`created` is when a record was filed; only the evidence says when the pattern last occurred."""
def _merge(self, *evidence: dict) -> reflect_auto.Finding:
raw = _raw_finding(evidence=list(evidence))
return reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)[0]
def test_the_newest_evidence_date_wins(self):
finding = self._merge(
{"session": "ws_a", "when": "2026-08-20"},
{"session": "ws_b", "when": "2026-08-31 13:53"},
{"session": "ws_c", "when": "2026-08-25"},
)
assert finding.last_seen == "2026-08-31", "mixed shapes, only the leading date counts"
def test_undated_evidence_falls_back_to_created(self):
finding = self._merge({"session": "ws_a", "excerpt": "no when at all"})
assert finding.last_seen == TODAY
def test_non_date_when_is_ignored(self):
finding = self._merge({"session": "ws_a", "when": "yesterday"}, {"session": "ws_b", "when": "turn 4"})
assert finding.last_seen == TODAY
def test_folded_evidence_from_an_earlier_run_counts_too(self):
existing = [_filed(evidence=[{"session": "ws_old", "when": "2026-09-05"}])]
raw = _raw_finding(evidence=[{"session": "ws_new", "when": "2026-08-01"}])
merged = reflect_auto.merge_findings(existing, _parsed(_answer(raw)), TODAY)
assert merged[0].last_seen == "2026-09-05"
def test_last_seen_is_stored_for_the_review_to_sort_by(self):
assert self._merge({"session": "ws_a", "when": "2026-08-20"}).to_json()["last_seen"] == "2026-08-20"
class TestSupersede:
def test_folded_record_is_replaced_not_duplicated(self):
existing = [_filed()]
merged = reflect_auto.merge_findings(existing, _parsed(_answer(_raw_finding())), TODAY)
result = reflect_auto.supersede(existing, merged)
assert len(result) == 1
assert result[0]["status"] == reflect_auto.STATUS_OPEN
def test_unrelated_records_survive(self):
existing = [_filed(id="f0001", pattern="other-thing", status="applied")]
merged = reflect_auto.merge_findings(existing, _parsed(_answer(_raw_finding())), TODAY)
result = reflect_auto.supersede(existing, merged)
assert {record["pattern"] for record in result} == {"other-thing", "retry-without-diagnosis"}
def test_applied_history_is_kept_for_regression_tracking(self):
existing = [_filed(status="applied")]
merged = reflect_auto.merge_findings(existing, _parsed(_answer(_raw_finding())), TODAY)
result = reflect_auto.supersede(existing, merged)
assert len(result) == 2
class TestRendering:
def test_known_patterns_feed_the_id_vocabulary_back(self):
text = reflect_auto._known_patterns([_filed()])
assert "`retry-without-diagnosis`" in text
assert "[watch]" in text
def test_known_patterns_handles_an_empty_store(self):
assert "none yet" in reflect_auto._known_patterns([])
def test_report_marks_regressions(self):
merged = reflect_auto.merge_findings([_filed(status="applied")], _parsed(_answer(_raw_finding())), TODAY)
report = reflect_auto.render_report(merged, _stats(), None, WHEN)
assert "REGRESSION" in report
def test_report_renders_a_patch_as_a_diff(self):
raw = _raw_finding(patch={"file": "a.md", "old_text": "try again", "new_text": "STOP"})
merged = reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)
report = reflect_auto.render_report(merged, _stats(), None, WHEN)
assert "```diff" in report
assert "- try again" in report
assert "+ STOP" in report
def test_report_shows_how_old_a_finding_is(self):
"""`occurrences` is cumulative, so the count alone cannot say whether this is still live."""
merged = reflect_auto.merge_findings([_filed()], _parsed(_answer(_raw_finding())), TODAY)
report = reflect_auto.render_report(merged, _stats(), None, WHEN)
assert "first seen 2026-09-01, last seen 2026-07-11" in report, "the evidence date, not the refile date"
def test_a_brand_new_finding_reports_one_date(self):
raw = _raw_finding(evidence=[{"session": "websocket_abc", "excerpt": "no date here"}])
merged = reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)
report = reflect_auto.render_report(merged, _stats(), None, WHEN)
assert f"first seen {TODAY}, last seen {TODAY}" in report
def test_report_marks_a_finding_the_window_no_longer_reaches(self):
"""Nothing in the window backs it any more, so no future run will refresh it either."""
raw = _raw_finding(occurrences=7, sessions_affected=4)
merged = reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)
report = reflect_auto.render_report(merged, _stats(window_from="2026-08-18T00:00:00"), None, WHEN)
assert "STALE" in report
def test_a_finding_inside_the_window_is_not_stale(self):
raw = _raw_finding(evidence=[{"session": "websocket_abc", "when": "2026-09-01", "excerpt": "x"}])
merged = reflect_auto.merge_findings([], _parsed(_answer(raw)), TODAY)
report = reflect_auto.render_report(merged, _stats(window_from="2026-08-18T00:00:00"), None, WHEN)
assert "STALE" not in report
def test_empty_report_says_so(self):
report = reflect_auto.render_report([], _stats(), None, WHEN)
assert "Nothing to report" in report
def test_report_shows_the_known_pattern_rate(self):
"""The one number proving the skill pays off — without it results/ is dead weight."""
report = reflect_auto.render_report([], _stats(repeat_per_100=3.2), 5.1, WHEN)
assert "Known patterns: 3.2 occurrences / 100 sessions (previous run 5.1)." in report
def test_first_run_has_no_trend_to_show(self):
assert "(minule" not in reflect_auto.render_report([], _stats(), None, WHEN)
class TestPromptContract:
def test_prompt_forbids_writing(self):
"""The read-only instruction is one of the two guards on the analysis run."""
prompt = reflect_auto.build_prompt("digest", "none", 3)
assert "Write nothing" in prompt
def test_prompt_carries_digest_and_known_patterns(self):
prompt = reflect_auto.build_prompt("SESSION-DIGEST-HERE", "PATTERN-LIST-HERE", 3)
assert "SESSION-DIGEST-HERE" in prompt
assert "PATTERN-LIST-HERE" in prompt
def test_prompt_caps_file_reads(self):
"""Every extra tool iteration re-prefills the whole digest — the cap is what keeps a batch cheap."""
assert "Read at most 2 files" in reflect_auto.build_prompt("digest", "none", 3)
class FakeBot:
"""Stands in for Nanobot: hands out canned replies and records what it was asked."""
def __init__(self, replies: list[Any]):
self._replies = list(replies)
self.calls: list[tuple[str, str]] = []
async def run(self, message: str, *, session_key: str):
self.calls.append((message, session_key))
reply = self._replies.pop(0) if len(self._replies) > 1 else self._replies[0]
if isinstance(reply, Exception):
raise reply
return reply
def _reply(content: str = "", stop_reason: str | None = None, error: str | None = None) -> SimpleNamespace:
return SimpleNamespace(content=content, stop_reason=stop_reason, error=error)
def _llm_failure() -> SimpleNamespace:
"""What nanobot hands back when the provider gives up: the error text as the reply."""
return _reply("Error calling LLM: timed out after 300s", stop_reason="error", error="timed out after 300s")
def _session_records(started: str, turns: int = 3) -> list[dict]:
"""A conversation long enough to clear reflect_distill.MIN_MESSAGES."""
records: list[dict] = [{"_type": "metadata", "key": "websocket:x", "created_at": started}]
for i in range(turns):
records.append({"role": "user", "content": f"dotaz {i}"})
records.append({"role": "assistant", "content": f"answer {i}"})
return records
EARLIER = "2026-07-11T14:02:03"
LATER = "2026-08-20T09:15:00"
@pytest.fixture
def workspace(tmp_path):
"""A workspace that is a real git repo with two analysable sessions, like the server's."""
sessions = tmp_path / "sessions"
sessions.mkdir()
for name, started in (("websocket_first", EARLIER), ("websocket_second", LATER)):
records = _session_records(started)
(sessions / f"{name}.jsonl").write_text(
"\n".join(json.dumps(r, ensure_ascii=False) for r in records), encoding="utf-8"
)
(tmp_path / "reflect").mkdir()
for args in (
["init", "-q"],
["config", "user.email", "test@example.com"],
["config", "user.name", "test"],
["add", "-A"],
["commit", "-q", "-m", "init"],
):
subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True)
return tmp_path
def _args(**overrides):
"""One session per batch (budget_chars=1), so the two fixture sessions make two batches."""
args = {
"dry_run": False,
"all": False,
"budget_chars": 1,
"max_batches": 0,
"deadline_minutes": 20,
"window_days": 0,
}
args.update(overrides)
return SimpleNamespace(**args)
def _state(workspace: Path) -> dict:
return json.loads((workspace / "reflect" / "state.json").read_text(encoding="utf-8"))
def _findings(workspace: Path) -> list[dict]:
path = workspace / "reflect" / "findings.jsonl"
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
def _seed_findings(workspace: Path, *records: dict) -> None:
path = workspace / "reflect" / "findings.jsonl"
path.write_text("\n".join(json.dumps(r, ensure_ascii=False) for r in records) + "\n", encoding="utf-8")
def _run(workspace: Path, bot, monkeypatch, **arg_overrides) -> tuple[str, int]:
monkeypatch.setattr(reflect_auto, "_open_bot", lambda preset: bot)
return asyncio.run(reflect_auto._run(workspace, _args(**arg_overrides), WHEN))
class TestRunPersistence:
"""The 2026-09-02 failure: a 30min timeout threw away two finished batches and kept the cursor."""
def test_every_batch_is_filed_as_it_finishes(self, workspace, monkeypatch):
bot = FakeBot(
[
_reply(_answer(_raw_finding())),
_reply(_answer(_raw_finding(pattern="tool-call-leaked-as-text"))),
]
)
_run(workspace, bot, monkeypatch)
assert {record["pattern"] for record in _findings(workspace)} == {
"retry-without-diagnosis",
"tool-call-leaked-as-text",
}
assert _state(workspace)["cursor"] == LATER
assert [run["batches"] for run in _state(workspace)["runs"]] == [2], "one run entry, updated in place"
def test_the_same_pattern_in_two_batches_folds_into_one_record(self, workspace, monkeypatch):
"""Per-batch filing must not turn cross-batch dedup into duplicate records."""
bot = FakeBot([_reply(_answer(_raw_finding()))])
_run(workspace, bot, monkeypatch)
records = _findings(workspace)
assert len(records) == 1
assert records[0]["occurrences"] == 2
assert records[0]["status"] == "open", "the second sighting is what promotes a watched pattern"
def test_the_report_counts_a_repeated_pattern_once(self, workspace, monkeypatch):
"""The report and the Telegram line have to say what the store holds, not how often it was refiled."""
bot = FakeBot([_reply(_answer(_raw_finding()))])
message, _ = _run(workspace, bot, monkeypatch)
report = (workspace / "results" / f"{WHEN:%Y-%m-%d}_reflect.md").read_text(encoding="utf-8")
assert len([line for line in report.splitlines() if line.startswith("## ")]) == 1
assert "Findings: 1 (1 to review, 0 watched)" in report
assert _state(workspace)["runs"][-1]["open"] == 1
assert "1 findings to review" in message
def test_a_batch_that_dies_does_not_take_the_finished_one_with_it(self, workspace, monkeypatch):
bot = FakeBot([_reply(_answer(_raw_finding())), RuntimeError("provider down")])
with pytest.raises(RuntimeError):
_run(workspace, bot, monkeypatch)
assert len(_findings(workspace)) == 1
assert _state(workspace)["cursor"] == EARLIER
assert _state(workspace)["runs"][-1]["batches"] == 1
def test_the_cursor_never_moves_backwards(self, workspace, monkeypatch):
"""An `--all` walk starts at the oldest session and must not undo the nightly progress."""
reflect_auto._save_state(workspace, {"cursor": "2026-12-31T00:00:00", "runs": []})
bot = FakeBot([_reply(_answer(_raw_finding()))])
_run(workspace, bot, monkeypatch, all=True)
assert _state(workspace)["cursor"] == "2026-12-31T00:00:00"
def test_the_deadline_stops_the_run_and_says_so(self, workspace, monkeypatch):
_seed_findings(workspace, _filed())
bot = FakeBot([_reply(_answer(_raw_finding()))])
message, _ = _run(workspace, bot, monkeypatch, deadline_minutes=0)
assert len(bot.calls) == 1
assert _state(workspace)["cursor"] == EARLIER
assert "Analysed 1/2 batches." in message
def test_a_finished_run_reports_no_partial_progress(self, workspace, monkeypatch):
_seed_findings(workspace, _filed())
bot = FakeBot([_reply(_answer(_raw_finding()))])
message, _ = _run(workspace, bot, monkeypatch)
assert "batches" not in message
def test_an_unfinished_run_speaks_up_even_with_no_findings(self, workspace, monkeypatch):
"""Silence here is how a starved cursor went unnoticed for three nights."""
bot = FakeBot([_reply(_answer(_raw_finding()))])
message, _ = _run(workspace, bot, monkeypatch, deadline_minutes=0)
assert "1 batches left" in message
def test_a_finished_run_with_no_findings_stays_quiet(self, workspace, monkeypatch):
bot = FakeBot([_reply(_answer())])
message, _ = _run(workspace, bot, monkeypatch)
assert message == ""
class TestWindow:
"""Findings must describe recent behaviour; a months-old backlog must not starve the run."""
def test_the_window_skips_everything_older(self, workspace, monkeypatch):
bot = FakeBot([_reply(_answer(_raw_finding()))])
_run(workspace, bot, monkeypatch, window_days=21)
assert len(bot.calls) == 1, "only the session inside the window is analysable"
assert "websocket_second" in bot.calls[0][0]
assert "websocket_first" not in bot.calls[0][0]
assert _state(workspace)["cursor"] == LATER, "the skipped backlog is skipped for good"
def test_the_cursor_still_wins_over_the_window(self, workspace, monkeypatch):
"""The window is a ceiling, not a rewind — nothing already counted may be re-read."""
reflect_auto._save_state(workspace, {"cursor": LATER, "runs": []})
bot = FakeBot([_reply(_answer(_raw_finding()))])
message, sessions = _run(workspace, bot, monkeypatch, window_days=365)
assert bot.calls == []
assert (message, sessions) == ("", 0)
def test_the_report_says_what_the_window_covered(self, workspace, monkeypatch):
bot = FakeBot([_reply(_answer(_raw_finding()))])
_run(workspace, bot, monkeypatch, window_days=21)
report = (workspace / "results" / f"{WHEN:%Y-%m-%d}_reflect.md").read_text(encoding="utf-8")
assert "Window: from 2026-08-18, batches 1/1." in report
def test_a_live_all_run_is_refused(self, workspace):
"""`--all` re-reads counted sessions and merge_findings would sum their occurrences in."""
with pytest.raises(SystemExit):
reflect_auto.main(["--all", "--workspace", str(workspace)])
def test_a_dry_all_run_is_allowed(self, workspace):
assert reflect_auto.main(["--all", "--dry-run", "--workspace", str(workspace)]) == 0
assert list((workspace / "tmp").glob("reflect-batch.*.md"))
class TestLlmErrorHandling:
"""A dead provider is not a malformed answer; conflating the two burned a validator attempt."""
def test_a_provider_failure_re_asks_the_original_question(self, workspace, monkeypatch):
bot = FakeBot([_llm_failure(), _reply(_answer(_raw_finding()))])
parsed = asyncio.run(reflect_auto._resolve_findings(bot, "reflect:test-0", "PROMPT", workspace, 4))
assert len(parsed) == 1
first, second = bot.calls
assert second[0] == "PROMPT", "the retry must re-ask, not blame the model for the error text"
assert second[1] != first[1], "the failed turn poisoned that session; retry needs a fresh key"
def test_a_dead_provider_is_reported_as_such(self, workspace, monkeypatch):
bot = FakeBot([_llm_failure()])
with pytest.raises(reflect_auto.ReflectError, match="model unavailable"):
asyncio.run(reflect_auto._resolve_findings(bot, "reflect:test-0", "PROMPT", workspace, 4))
assert len(bot.calls) == reflect_auto.MAX_LLM_ERROR_RETRIES + 1
def test_a_malformed_answer_still_gets_validator_feedback(self, workspace, monkeypatch):
bot = FakeBot([_reply("no json here"), _reply(_answer(_raw_finding()))])
parsed = asyncio.run(reflect_auto._resolve_findings(bot, "reflect:test-0", "PROMPT", workspace, 4))
assert len(parsed) == 1
assert "rejected by the validator" in bot.calls[1][0]

View File

@@ -0,0 +1,241 @@
"""Tests for reflect_distill.py — mechanical distillation of session logs."""
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import reflect_distill
def _write_session(directory: Path, name: str, records: list[dict]) -> Path:
path = directory / f"{name}.jsonl"
path.write_text("\n".join(json.dumps(r, ensure_ascii=False) for r in records), encoding="utf-8")
return path
def _conversation(turns: int = 3) -> list[dict]:
"""A session long enough to clear MIN_MESSAGES."""
records: list[dict] = [{"_type": "metadata", "key": "websocket:x", "created_at": "2026-07-11T14:02:03"}]
for i in range(turns):
records.append({"role": "user", "content": f"dotaz {i}"})
records.append({"role": "assistant", "content": f"answer {i}"})
return records
class TestNoiseFilter:
@pytest.mark.parametrize(
"name",
[
"reflect_20260908-033000",
"compact-memory-auto_20260715-054147",
"detach_abc",
"dream_xyz",
"cron_e81cda77",
"cli_kimi-ollama-test",
"wiki-compile",
"note-compile",
],
)
def test_machinery_prefixes_are_noise(self, name):
assert reflect_distill.is_noise(name)
@pytest.mark.parametrize("name", ["wiki-capture-test2", "test-note-search-zzz"])
def test_prefix_variants_are_noise(self, name):
"""Exact matching used to let `…test2` through; the filter matches prefixes."""
assert reflect_distill.is_noise(name)
@pytest.mark.parametrize("name", ["websocket_e5a6aac3-2c0a", "telegram_8826147089"])
def test_real_conversations_are_kept(self, name):
assert not reflect_distill.is_noise(name)
def test_base64_session_names_are_decoded(self):
"""Older sessions are stored base64-encoded; `ZHJlYW06…` is `dream:…`."""
assert reflect_distill.is_noise("ZHJlYW06MjAyNjA4MzEtMTA0NjM2")
assert not reflect_distill.is_noise("d2Vic29ja2V0OjUyZDBmMzM4")
def test_reflect_excludes_its_own_sessions(self):
"""Without this the skill would analyse its own runs on the next pass."""
assert reflect_distill.is_noise("reflect_20260908-033000")
class TestDistillSession:
def test_short_sessions_are_skipped(self, tmp_path):
path = _write_session(tmp_path, "websocket_short", [{"role": "user", "content": "ahoj"}])
assert reflect_distill.distill_session(path) is None
def test_noise_sessions_are_skipped(self, tmp_path):
path = _write_session(tmp_path, "dream_nightly", _conversation())
assert reflect_distill.distill_session(path) is None
def test_header_carries_key_time_and_count(self, tmp_path):
path = _write_session(tmp_path, "websocket_abc", _conversation())
digest = reflect_distill.distill_session(path)
assert digest.text.startswith(reflect_distill.SESSION_MARKER)
assert "websocket_abc" in digest.text
assert "2026-07-11 14:02" in digest.text
assert digest.message_count == 6
def test_header_is_not_a_markdown_heading(self, tmp_path):
"""Assistant prose is full of `###`, so a heading would not read as a boundary."""
records = _conversation()
records.append({"role": "assistant", "content": "### Summary\ndone"})
path = _write_session(tmp_path, "websocket_abc", records)
digest = reflect_distill.distill_session(path)
assert not digest.text.startswith("#")
assert digest.text.count(reflect_distill.SESSION_MARKER) == 1
def test_user_and_assistant_prose_is_kept_whole(self, tmp_path):
prose = "this is a long answer " * 40
records = _conversation()
records.append({"role": "assistant", "content": prose})
path = _write_session(tmp_path, "websocket_abc", records)
assert prose.strip() in reflect_distill.distill_session(path).text
def test_slash_commands_are_dropped(self, tmp_path):
records = _conversation()
records.append({"role": "user", "content": "/model", "_command": True})
path = _write_session(tmp_path, "websocket_abc", records)
assert "/model" not in reflect_distill.distill_session(path).text
def test_decoded_key_is_used_as_name(self, tmp_path):
path = _write_session(tmp_path, "d2Vic29ja2V0OjUyZDBmMzM4", _conversation())
assert reflect_distill.distill_session(path).name == "websocket:52d0f338"
class TestToolRendering:
def _digest_with_tool(self, tmp_path, call, result):
records = _conversation()
records.append({"role": "assistant", "content": "", "tool_calls": [call]})
records.append(result)
path = _write_session(tmp_path, "websocket_abc", records)
return reflect_distill.distill_session(path).text
def test_tool_result_body_is_replaced_by_metadata(self, tmp_path):
"""Tool results are 89% of the corpus and carry almost no diagnostic value."""
body = "x" * 5000
text = self._digest_with_tool(
tmp_path,
{"id": "c1", "function": {"name": "read_file", "arguments": '{"path": "/tmp/a.md"}'}},
{"role": "tool", "tool_call_id": "c1", "name": "read_file", "content": body},
)
assert body not in text
assert "read_file: ok, 5.0 kB" in text
def test_errors_are_flagged_with_their_message(self, tmp_path):
text = self._digest_with_tool(
tmp_path,
{"id": "c1", "function": {"name": "web_fetch", "arguments": '{"url": "https://x.dev"}'}},
{"role": "tool", "tool_call_id": "c1", "name": "web_fetch", "content": "Error: 403 Forbidden"},
)
assert "ERROR Error: 403 Forbidden" in text
def test_workspace_path_prefix_is_stripped(self, tmp_path):
text = self._digest_with_tool(
tmp_path,
{
"id": "c1",
"function": {
"name": "read_file",
"arguments": '{"path": "/home/nanobot/.nanobot/workspace/skills/note/SKILL.md"}',
},
},
{"role": "tool", "tool_call_id": "c1", "name": "read_file", "content": "ok"},
)
assert "path=skills/note/SKILL.md" in text
def test_malformed_arguments_still_render(self, tmp_path):
"""A truncated argument blob is itself a signal worth seeing, not a crash."""
text = self._digest_with_tool(
tmp_path,
{"id": "c1", "function": {"name": "exec", "arguments": '{"command": "ls'}},
{"role": "tool", "tool_call_id": "c1", "name": "exec", "content": "ok"},
)
assert "exec(" in text
def test_long_urls_keep_their_distinguishing_tail(self, tmp_path):
"""Front-truncation made different fetches look identical and hid real retries."""
base = "https://raw.githubusercontent.com/some-owner/some-repo/main/packages/core/src/"
first = self._digest_with_tool(
tmp_path,
{"id": "c1", "function": {"name": "web_fetch", "arguments": json.dumps({"url": base + "alpha.ts"})}},
{"role": "tool", "tool_call_id": "c1", "name": "web_fetch", "content": "ok"},
)
second = self._digest_with_tool(
tmp_path,
{"id": "c1", "function": {"name": "web_fetch", "arguments": json.dumps({"url": base + "omega.ts"})}},
{"role": "tool", "tool_call_id": "c1", "name": "web_fetch", "content": "ok"},
)
assert "alpha.ts" in first
assert "omega.ts" in second
assert first != second
def test_repeated_identical_calls_render_identically(self, tmp_path):
"""The LLM detects retry loops by seeing the same line twice — so it must match."""
call = {"id": "c1", "function": {"name": "web_fetch", "arguments": '{"url": "https://x.dev/a"}'}}
records = _conversation()
for _ in range(2):
records.append({"role": "assistant", "content": "", "tool_calls": [call]})
records.append({"role": "tool", "tool_call_id": "c1", "name": "web_fetch", "content": "Error: 403"})
path = _write_session(tmp_path, "websocket_abc", records)
lines = [line for line in reflect_distill.distill_session(path).text.splitlines() if "→ web_fetch" in line]
assert len(lines) == 2
assert lines[0] == lines[1]
def test_reasoning_is_shortened_not_dropped(self, tmp_path):
records = _conversation()
records.append({"role": "assistant", "content": "ok", "reasoning_content": "reasoning " * 200})
path = _write_session(tmp_path, "websocket_abc", records)
text = reflect_distill.distill_session(path).text
assert "~ reasoning" in text
assert len(text) < 2000
class TestBatching:
def test_batches_respect_the_character_budget(self, tmp_path):
for i in range(6):
_write_session(tmp_path, f"websocket_{i}", _conversation(turns=8))
paths = reflect_distill.collect_sessions(tmp_path)
batches = list(reflect_distill.iter_batches(paths, budget_chars=400))
assert len(batches) > 1
assert all(batch for batch in batches)
def test_oversized_session_is_not_split_or_dropped(self, tmp_path):
"""Truncating a huge session would hide exactly the runaway loops worth finding."""
records = _conversation()
records.append({"role": "assistant", "content": "y" * 5000})
_write_session(tmp_path, "websocket_big", records)
batches = list(reflect_distill.iter_batches(reflect_distill.collect_sessions(tmp_path), budget_chars=100))
assert len(batches) == 1
assert len(batches[0][0]) > 5000
def test_collect_sessions_skips_noise(self, tmp_path):
_write_session(tmp_path, "websocket_keep", _conversation())
_write_session(tmp_path, "dream_drop", _conversation())
names = [p.stem for p in reflect_distill.collect_sessions(tmp_path)]
assert names == ["websocket_keep"]
def test_cursor_written_by_a_run_excludes_those_sessions_next_time(self, tmp_path):
"""The round trip reflect_auto actually makes: digest.started becomes the next `since`.
A display-formatted cursor (`2026-07-11 14:02`) compares wrong against the raw ISO in
the log, because `T` sorts above a space — every session of that day came back.
"""
_write_session(tmp_path, "websocket_done", _conversation())
digests = [
d
for batch in reflect_distill.iter_batches(reflect_distill.collect_sessions(tmp_path), 10**6)
for d in batch
]
cursor = max(digest.started for digest in digests)
assert reflect_distill.collect_sessions(tmp_path, since=cursor) == []
def test_since_excludes_already_processed_sessions(self, tmp_path):
_write_session(tmp_path, "websocket_old", _conversation())
new = _conversation()
new[0]["created_at"] = "2026-08-20T09:00:00"
_write_session(tmp_path, "websocket_new", new)
names = [p.stem for p in reflect_distill.collect_sessions(tmp_path, since="2026-08-01T00:00:00")]
assert names == ["websocket_new"]