fix(dream): ground memory audit records in the real git diff (#4673)
* fix(dream): ground commit messages and cursor advance in the real git diff Dream consolidation could emit a /dream-log audit record that did not match the actual file changes: build_dream_commit_message appended the LLM's unverified resp.content, dream_run_completed only checked the stop reason, and file contents were deliberately omitted from the prompt. The combination let a single-turn self-report become the durable audit record. - gitstore: add summarize_working_tree() — a structured, machine-derived summary (per-file +N/-M, totals, capped unified diff) of working-tree changes vs HEAD. Pure filesystem/git ground truth, never LLM narrative. - memory: build_dream_commit_message now takes the diff body instead of resp; dream_content_diff() exposes the real delta over SOUL/USER/MEMORY.md only (excludes .dream_cursor so cursor writes aren't mistaken for edits); build_dream_prompt embeds current file contents so the model edits reality, not a stale mental model. - builtin/cli: both Dream paths now compute the diff, gate cursor advance on a non-empty delta (no-op runs no longer swallow history), and commit with the diff-grounded message. Non-git workspaces fall back to the completion check. - dream.md: document that contents are embedded, and add a chain-of- verification guardrail so the model's summary cannot claim unmade edits. A regression test proves a lying resp.content never reaches the audit log while the real diff does. * fix(dream): mark non-UTF-8 memory files as binary in diff summary Address review feedback (Q1 on PR #4673): summarize_working_tree read working-tree files with errors="replace", which would emit U+FFFD replacement chars into the audit record if a memory file ever held invalid UTF-8 — misrepresenting the diff it is meant to make truthful. Switch to errors="strict" and catch UnicodeDecodeError: a non-UTF-8 (or binary/corrupt) file is now recorded as "{path}: binary or non-UTF-8 file changed" and omitted from the unified diff, so the audit record stays honest. An empty diff block is also suppressed when all changes are binary. Adds a defensive regression test asserting no replacement char leaks.
This commit is contained in:
@@ -98,6 +98,51 @@ class TestLineAges:
|
||||
assert age_by_line["- keep"] == 30
|
||||
|
||||
|
||||
class TestSummarizeWorkingTree:
|
||||
"""Ground-truth diff summary used to keep Dream audit records honest."""
|
||||
|
||||
def test_empty_when_not_initialized(self, tmp_path):
|
||||
git = GitStore(tmp_path, tracked_files=["MEMORY.md"])
|
||||
assert git.summarize_working_tree(["MEMORY.md"]) == ""
|
||||
|
||||
def test_empty_when_no_changes(self, git):
|
||||
assert git.summarize_working_tree(["MEMORY.md", "SOUL.md"]) == ""
|
||||
|
||||
def test_summarizes_real_change(self, git, tmp_path):
|
||||
(tmp_path / "MEMORY.md").write_text("# Memory\n- new fact\n", encoding="utf-8")
|
||||
summary = git.summarize_working_tree(["MEMORY.md"])
|
||||
assert "MEMORY.md: +2 -0" in summary
|
||||
assert "new fact" in summary
|
||||
assert "1 file changed, 2 insertions(+), 0 deletions(-)" in summary
|
||||
|
||||
def test_only_reports_requested_paths(self, git, tmp_path):
|
||||
# MEMORY.md changes, but we only ask about the unchanged SOUL.md.
|
||||
(tmp_path / "MEMORY.md").write_text("changed\n", encoding="utf-8")
|
||||
assert git.summarize_working_tree(["SOUL.md"]) == ""
|
||||
|
||||
def test_counts_additions_and_removals(self, git, tmp_path):
|
||||
(tmp_path / "MEMORY.md").write_text("# M\n- keep\n- new\n", encoding="utf-8")
|
||||
summary = git.summarize_working_tree(["MEMORY.md"])
|
||||
assert "MEMORY.md: +3 -0" in summary
|
||||
|
||||
def test_detects_deletion(self, git, tmp_path):
|
||||
# File removed from the working tree (must have content first; the
|
||||
# fixture's tracked files start empty, so an empty-file delete is a no-op).
|
||||
(tmp_path / "MEMORY.md").write_text("has content\n", encoding="utf-8")
|
||||
git.auto_commit("add content")
|
||||
(tmp_path / "MEMORY.md").unlink()
|
||||
summary = git.summarize_working_tree(["MEMORY.md"])
|
||||
assert summary # a removal is still a change
|
||||
assert "deletion" in summary
|
||||
|
||||
def test_non_utf8_file_marked_binary_without_replacement_chars(self, git, tmp_path):
|
||||
# Invalid UTF-8 must not leak replacement chars into the audit record.
|
||||
(tmp_path / "MEMORY.md").write_bytes(b"\x89PNG\r\n\x1a\n\xff\xfe\x00\x01")
|
||||
summary = git.summarize_working_tree(["MEMORY.md"])
|
||||
assert "MEMORY.md: binary or non-UTF-8 file changed" in summary
|
||||
assert "\ufffd" not in summary # no U+FFFD replacement chars leaked
|
||||
|
||||
|
||||
class TestNestedRepoProtection:
|
||||
"""Regression tests for GitHub issue #2980: nested repo protection."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user