ouroboros/tests/test_process_memory.py
Ouroboros 8ccf934043 release 6.96.0: owner answer delivered live before post-task cognition, sealed final ground truth into synthesis (sprint phase C)
Squashed landing of sprint/slime-c-continuity (130f8d54 + fbe67c82 +
dbc155fc, base 0bccde56) onto the phase B mainline (6c53eee4, v6.95.0).

The owner's final answer is sent through the LIVE worker event channel
BEFORE blocking post-task cognition (summary/reflection/consolidation);
task_done stays LAST via the buffered return. The live slot is selected by
the finalizing task's own id — a buffered proactive message can never
hijack it — and delivery_id carries the real task id. Never-lost-
never-doubled: both copies ride one delivery_id and the supervisor
registers the dedupe only AFTER a successful send, so a raising send is
retried by the buffered copy (motivated by the e9108a09 incident's lost
answer and false reflection).

Summary and reflection prompts receive one sealed final package — the
delivered result text plus an artifact manifest REUSED from the stored
result's own artifact records — as mandatory ground truth (a prompt
input, never a validator).

Durable per-project last_task_result_id pointer replaces the newest-64
global scan: stamped at project-task finalization, absent-pointer-only
write-back (an unresolved non-empty pointer is served from scan WITHOUT
overwriting — the split-drive copy-back window cannot regress it) plus a
disclosed repeats-until-found self-heal for pre-pointer projects.

Project reflections are read BACK into project-task context (bounded
tail, same resolver as the writer; the canonical log keeps a bounded
pointer row). project_name is inherited on promotion in a projectless
room; an explicit input losing to a genuine room binding is disclosed,
never silently dropped. Finalized terminal accounting survives stale
child-drive copy-back (TASK_COST_META_FIELDS plus rounds/tokens). A
finished root whose effective tree is off-registry writes one typed
work-location journal row (admission-time sha disclosed by design). The
moved-HEAD fail-closed patch check is restricted to self_worktree — in a
shared tree the parent's own commits legitimately move HEAD.

New module ouroboros/task_finalization.py (in the ARCHITECTURE module
map) carries the delivery + sealed-package mechanics out of
agent_task_pipeline.py at its module ceiling.

Merge resolution: no textual conflicts; verified semantically that all
three supervisor/events.py mechanisms coexist (C's send_message dedupe,
B's provider-death notification, A/B's parent progress stamp), that C's
task_finalization wiring layers over A/B's Q1A substrate fields in
agent_task_pipeline.py, and that context.py carries both A/B's
promoted-task toolset (F6) and C's last-result/reflections read-back.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-08-11 01:39:38 +03:00

612 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for process memory infrastructure.
Covers:
- Execution reflection trigger logic (should_generate_reflection)
- Error detail collection and marker detection
- Reflection loading into context
"""
import inspect
import os
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, REPO)
# ─────────────────────────────────────────────────────────────────────────────
# should_generate_reflection
# ─────────────────────────────────────────────────────────────────────────────
class TestReflectionTrigger:
"""should_generate_reflection(llm_trace) must detect error conditions."""
def test_clean_trace_no_reflection(self):
from ouroboros.reflection import should_generate_reflection
trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "file contents", "is_error": False},
{"tool": "run_command", "args": {}, "result": "ok", "is_error": False},
]}
assert should_generate_reflection(trace) is False
def test_error_tool_triggers_reflection(self):
from ouroboros.reflection import should_generate_reflection
trace = {"tool_calls": [
{"tool": "run_command", "args": {}, "result": "⚠️ TOOL_ERROR: failed", "is_error": True},
]}
assert should_generate_reflection(trace) is True
def test_review_blocked_marker_triggers_reflection(self):
from ouroboros.reflection import should_generate_reflection
trace = {"tool_calls": [
{"tool": "commit_reviewed", "args": {}, "is_error": False,
"result": "⚠️ REVIEW_BLOCKED (attempt 1/3): reviewer flagged version sync"},
]}
assert should_generate_reflection(trace) is True
def test_tests_failed_marker_triggers_reflection(self):
from ouroboros.reflection import should_generate_reflection
trace = {"tool_calls": [
{"tool": "commit_reviewed", "args": {}, "is_error": False,
"result": "OK: committed\n\n⚠️ TESTS_FAILED: VERSION not in README"},
]}
assert should_generate_reflection(trace) is True
def test_empty_trace_no_reflection(self):
from ouroboros.reflection import should_generate_reflection
assert should_generate_reflection({"tool_calls": []}) is False
assert should_generate_reflection({}) is False
def test_nontrivial_rounds_triggers_reflection(self):
"""rounds >= NONTRIVIAL_ROUNDS_THRESHOLD fires even on a clean trace."""
from ouroboros.reflection import should_generate_reflection, NONTRIVIAL_ROUNDS_THRESHOLD
clean_trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "file contents", "is_error": False},
]}
assert should_generate_reflection(clean_trace, rounds=NONTRIVIAL_ROUNDS_THRESHOLD) is True
assert should_generate_reflection(clean_trace, rounds=NONTRIVIAL_ROUNDS_THRESHOLD - 1) is False
def test_nontrivial_cost_triggers_reflection(self):
"""cost_usd >= NONTRIVIAL_COST_THRESHOLD fires even on a clean trace."""
from ouroboros.reflection import should_generate_reflection, NONTRIVIAL_COST_THRESHOLD
clean_trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "file contents", "is_error": False},
]}
assert should_generate_reflection(clean_trace, rounds=0, cost_usd=NONTRIVIAL_COST_THRESHOLD) is True
assert should_generate_reflection(clean_trace, rounds=0, cost_usd=NONTRIVIAL_COST_THRESHOLD - 0.01) is False
def test_unknown_cost_is_not_treated_as_confirmed_zero(self):
from ouroboros.reflection import should_generate_reflection
clean_trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "ok", "is_error": False},
]}
assert should_generate_reflection(clean_trace, rounds=0, cost_usd=None) is False
def test_default_kwargs_clean_trace_no_reflection(self):
"""Default kwargs (rounds=0, cost unknown) keep clean behavior unchanged."""
from ouroboros.reflection import should_generate_reflection
clean_trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "file contents", "is_error": False},
]}
assert should_generate_reflection(clean_trace) is False
def test_structured_non_zero_exit_triggers_reflection(self):
from ouroboros.reflection import should_generate_reflection
trace = {"tool_calls": [
{
"tool": "run_command",
"args": {},
"result": "exit_code=-9",
"is_error": False,
"status": "non_zero_exit",
"exit_code": -9,
"signal": "SIGKILL",
},
]}
assert should_generate_reflection(trace) is True
# ─────────────────────────────────────────────────────────────────────────────
# Helper functions
# ─────────────────────────────────────────────────────────────────────────────
class TestHelperFunctions:
"""_detect_markers and _collect_error_details must extract structured info."""
def test_detect_markers_finds_all(self):
from ouroboros.reflection import _detect_markers
trace = {"tool_calls": [
{"tool": "commit_reviewed", "is_error": True,
"result": "⚠️ REVIEW_BLOCKED: test"},
{"tool": "run_command", "is_error": False,
"result": "⚠️ TESTS_FAILED: something"},
]}
markers = _detect_markers(trace)
assert "REVIEW_BLOCKED" in markers
assert "TESTS_FAILED" in markers
def test_detect_markers_empty_trace(self):
from ouroboros.reflection import _detect_markers
assert _detect_markers({}) == []
assert _detect_markers({"tool_calls": []}) == []
def test_collect_error_details_includes_tool_name(self):
from ouroboros.reflection import _collect_error_details
trace = {"tool_calls": [
{"tool": "commit_reviewed", "is_error": True,
"result": "⚠️ REVIEW_BLOCKED: test"},
]}
details = _collect_error_details(trace)
assert "commit_reviewed" in details
assert "REVIEW_BLOCKED" in details
def test_collect_error_details_respects_cap(self):
from ouroboros.reflection import _collect_error_details
trace = {"tool_calls": [
{"tool": "run_command", "is_error": True,
"result": "x" * 5000},
]}
details = _collect_error_details(trace, cap=200)
# v6.70.0: the canonical OMISSION NOTE is appended AFTER the cap
# (utils.truncate_review_artifact semantics), replacing the legacy
# in-budget "... [+N chars]" marker.
assert details.startswith("[run_command]: " + "x" * 20)
assert "OMISSION NOTE" in details
assert len(details) <= 200 + 90 # cap + canonical marker overhead
def test_collect_error_details_skips_clean_results(self):
from ouroboros.reflection import _collect_error_details
trace = {"tool_calls": [
{"tool": "read_file", "is_error": False, "result": "file contents"},
{"tool": "run_command", "is_error": True, "result": "error happened"},
]}
details = _collect_error_details(trace)
assert "read_file" not in details
assert "run_command" in details
def test_collect_error_details_includes_structured_status(self):
from ouroboros.reflection import _collect_error_details
trace = {"tool_calls": [
{
"tool": "run_command",
"is_error": True,
"status": "non_zero_exit",
"exit_code": -9,
"signal": "SIGKILL",
"result": "⚠️ SHELL_EXIT_ERROR: command exited with exit_code=-9 (signal=SIGKILL).",
},
]}
details = _collect_error_details(trace)
assert "status=non_zero_exit" in details
assert "signal=SIGKILL" in details
def test_marker_mid_body_of_ok_result_is_not_error_evidence(self):
"""v6.71.1 round-2: evidence-parity widened trace results to 15k-80k+, so a
read_file of ARCHITECTURE.md embeds marker strings literally MID-BODY. The
scan mirrors the pre-parity head+tail (350+350) view — a mid-file doc
mention (outside both head and tail) must not classify the task as errored."""
from ouroboros.reflection import _detect_markers, _has_error_evidence, should_generate_reflection
trace = {"tool_calls": [
{"tool": "read_file", "is_error": False, "status": "ok",
"result": ("doc body " * 100) + "⚠️ SHELL_EXIT_ERROR appears in prose" + (" more doc" * 100)},
]}
assert len(trace["tool_calls"][0]["result"]) > 1400
assert _has_error_evidence(trace) is False
assert _detect_markers(trace) == []
assert should_generate_reflection(trace) is False
def test_marker_at_result_head_is_detected(self):
from ouroboros.reflection import _detect_markers, _has_error_evidence
trace = {"tool_calls": [
{"tool": "run_command", "is_error": False, "status": "ok",
"result": "⚠️ SHELL_EXIT_ERROR: command exited with exit_code=1." + ("x" * 2000)},
]}
assert _has_error_evidence(trace) is True
assert _detect_markers(trace) == ["SHELL_EXIT_ERROR"]
def test_marker_in_result_tail_is_detected(self):
"""A late verdict marker (e.g. TESTS_FAILED at the end of a long blocked-commit
output) sat in the retained tail of the pre-parity trace copy — the bounded
scan must keep catching it (triad round-1 regression_surface finding)."""
from ouroboros.reflection import _detect_markers, _has_error_evidence
trace = {"tool_calls": [
{"tool": "commit_reviewed", "is_error": False, "status": "ok",
"result": ("preflight log line\n" * 300) + "⚠️ TESTS_FAILED: 2 failed in suite"},
]}
assert _has_error_evidence(trace) is True
assert _detect_markers(trace) == ["TESTS_FAILED"]
def test_collect_error_details_keeps_breadth_across_large_errors(self):
"""v6.71.1 round-2: one oversized first error must not monopolize the 3000
budget and hide later distinct errors — each snippet is pre-capped."""
from ouroboros.reflection import _collect_error_details
trace = {"tool_calls": [
{"tool": "run_command", "is_error": True, "result": "a" * 5000},
{"tool": "run_script", "is_error": True, "result": "b" * 5000},
{"tool": "edit_text", "is_error": True, "result": "c" * 5000},
]}
details = _collect_error_details(trace)
assert "run_command" in details
assert "run_script" in details
assert "edit_text" in details
def test_run_reflection_pipeline_maps_usage_keys_correctly(self):
"""_run_reflection maps usage['rounds'] and usage['cost'] to the correct kwargs.
Pins the dict-key contract: if 'cost' were renamed to 'cost_usd' inside
_run_reflection, the cost threshold trigger would silently return 0.0 and
this test would catch it.
"""
import unittest.mock as mock
from ouroboros.agent_task_pipeline import _run_reflection
class FakeEnv:
drive_root = __import__("pathlib").Path("/tmp/fake_drive")
class FakeLlm:
pass
clean_trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "ok", "is_error": False},
]}
high_cost_usage = {"rounds": 20, "cost": 6.0}
with mock.patch("ouroboros.reflection.should_generate_reflection",
wraps=lambda trace, *, task=None, rounds=0, cost_usd=0.0: True) as mock_sgr, \
mock.patch("ouroboros.reflection.generate_reflection",
return_value={"reflection": "ok", "backlog_candidates": []}) as mock_gen, \
mock.patch("ouroboros.reflection.append_reflection") as mock_append:
result = _run_reflection(
FakeEnv(),
FakeLlm(),
{"id": "t1", "type": "task", "text": "goal"},
high_cost_usage,
clean_trace,
{},
)
# should_generate_reflection must be called with the correct kwargs from usage dict
mock_sgr.assert_called_once()
_, kwargs = mock_sgr.call_args
assert kwargs.get("rounds") == 20, f"Expected rounds=20, got {kwargs.get('rounds')}"
assert kwargs.get("cost_usd") == 6.0, f"Expected cost_usd=6.0, got {kwargs.get('cost_usd')}"
# When should_generate_reflection returns True, generate+append must be called
mock_gen.assert_called_once()
mock_append.assert_called_once()
assert result is not None
def test_run_reflection_pipeline_propagates_unknown_cost(self):
import unittest.mock as mock
from ouroboros.agent_task_pipeline import _run_reflection
class FakeEnv:
drive_root = __import__("pathlib").Path("/tmp/fake_drive")
captured = {}
def should_run(trace, *, task=None, rounds=0, cost_usd=None):
captured["cost_usd"] = cost_usd
return False
with mock.patch("ouroboros.reflection.should_generate_reflection", side_effect=should_run):
result = _run_reflection(
FakeEnv(),
object(),
{"id": "t-unknown", "type": "task", "text": "goal"},
{"rounds": 1, "cost": None, "cost_final": False},
{"tool_calls": []},
{},
)
assert result is None
assert captured["cost_usd"] is None
def test_generate_reflection_uses_nontrivial_prompt_for_clean_trace(self):
"""generate_reflection picks the non-error prompt for a clean, high-round trace."""
from ouroboros.reflection import generate_reflection
captured = {}
class FakeLlm:
def chat(self, *, messages, model, reasoning_effort, max_tokens):
captured["prompt"] = messages[0]["content"]
return {"content": "Friction was in repeated advisory runs."}, {"cost": 0}
clean_trace = {"tool_calls": [
{"tool": "read_file", "args": {}, "result": "file contents", "is_error": False},
{"tool": "run_command", "args": {}, "result": "ok", "is_error": False},
]}
entry = generate_reflection(
task={"id": "task-2", "type": "task", "text": "A 20-round clean task"},
llm_trace=clean_trace,
trace_summary="20 tool calls, 0 errors",
llm_client=FakeLlm(),
usage_dict={"rounds": 20, "cost": 6.0},
)
prompt = captured["prompt"]
# Non-error prompt markers must be present
assert "high round count or high cost" in prompt, "Expected nontrivial prompt framing"
assert "Where was the friction?" in prompt, "Expected friction question"
# Error-only prompt text must NOT appear
assert "The task had errors" not in prompt, "Error-only prompt must not be used for clean trace"
assert entry["reflection"] == "Friction was in repeated advisory runs."
def test_generate_reflection_uses_error_prompt_for_error_trace(self):
"""generate_reflection picks the error prompt when trace contains blocking markers."""
from ouroboros.reflection import generate_reflection
captured = {}
class FakeLlm:
def chat(self, *, messages, model, reasoning_effort, max_tokens):
captured["prompt"] = messages[0]["content"]
return {"content": "Root cause was missing tests."}, {"cost": 0}
error_trace = {"tool_calls": [
{"tool": "commit_reviewed", "args": {}, "is_error": False,
"result": "⚠️ REVIEW_BLOCKED: tests_affected"},
]}
generate_reflection(
task={"id": "task-3", "type": "task", "text": "Blocked commit"},
llm_trace=error_trace,
trace_summary="1 tool call, 0 errors (but REVIEW_BLOCKED)",
llm_client=FakeLlm(),
usage_dict={"rounds": 5, "cost": 1.0},
)
prompt = captured["prompt"]
assert "The task had errors or blocking events" in prompt
assert "high round count or high cost" not in prompt
def test_generate_reflection_includes_review_evidence(self):
from ouroboros.reflection import generate_reflection
captured = {}
class FakeLlm:
def chat(self, *, messages, model, reasoning_effort, max_tokens):
captured["prompt"] = messages[0]["content"]
return {"content": "Reflection mentions tests_affected."}, {"cost": 0}
entry = generate_reflection(
task={"id": "task-1", "type": "task", "text": "Fix commit flow"},
llm_trace={"tool_calls": [{
"tool": "commit_reviewed",
"is_error": False,
"result": "⚠️ REVIEW_BLOCKED: blocked by tests_affected",
}]},
trace_summary="repo_commit blocked",
llm_client=FakeLlm(),
usage_dict={"rounds": 3, "cost": 0.01},
review_evidence={
"has_evidence": True,
"recent_attempts": [{
"status": "blocked",
"critical_findings": [{
"severity": "critical",
"item": "tests_affected",
"reason": "broken",
}],
}],
},
)
assert "Structured review evidence" in captured["prompt"]
assert "tests_affected" in captured["prompt"]
assert entry["review_evidence"]["has_evidence"] is True
# ─────────────────────────────────────────────────────────────────────────────
# Reflection context loading
# ─────────────────────────────────────────────────────────────────────────────
class TestReflectionContextLoading:
"""build_recent_sections must load execution reflections from JSONL."""
def test_reflections_loaded_when_file_exists(self):
from ouroboros.context import build_recent_sections
source = inspect.getsource(build_recent_sections)
assert "task_reflections.jsonl" in source, (
"build_recent_sections must load from task_reflections.jsonl"
)
assert "Execution reflections" in source, (
"Section header must contain 'Execution reflections'"
)
def test_reflection_entry_format(self):
"""Reflection entries must include required fields."""
from ouroboros.reflection import _detect_markers, _collect_error_details
trace = {"tool_calls": [
{"tool": "commit_reviewed", "is_error": True,
"result": "⚠️ REVIEW_BLOCKED: test"},
]}
markers = _detect_markers(trace)
assert "REVIEW_BLOCKED" in markers
details = _collect_error_details(trace)
assert "commit_reviewed" in details
assert "REVIEW_BLOCKED" in details
# ─────────────────────────────────────────────────────────────────────────────
# emit_task_results: reflection stays OFF the reply critical path
# ─────────────────────────────────────────────────────────────────────────────
class TestEmitTaskResultsReflectionNotOnCriticalPath:
"""Regression guard for the v4.39.0 UX regression: reflection and backlog
persistence must not run on the reply critical path. The synchronous
variant added a 13 s LLM round before `send_message` was dispatched;
the async daemon-thread variant keeps reply latency low.
"""
def _make_minimal_env(self, tmp_path):
import pathlib
class FakeEnv:
drive_root = tmp_path
repo_dir = str(tmp_path)
def drive_path(self, sub):
return pathlib.Path(self.drive_root) / sub
return FakeEnv()
def test_reflection_not_called_synchronously_in_emit_task_results(self, tmp_path):
"""emit_task_results must NOT invoke `_run_reflection` inline — it
belongs inside the daemon thread started by
`_run_post_task_processing_async`."""
import unittest.mock as mock
from ouroboros.agent_task_pipeline import emit_task_results
env = self._make_minimal_env(tmp_path)
(tmp_path / "logs").mkdir(parents=True, exist_ok=True)
(tmp_path / "memory").mkdir(parents=True, exist_ok=True)
task = {"id": "t1", "type": "task", "text": "goal", "chat_id": 1}
usage = {"rounds": 5, "cost": 1.0, "prompt_tokens": 0, "completion_tokens": 0}
llm_trace = {"tool_calls": []}
with mock.patch("ouroboros.agent_task_pipeline._run_reflection") as mock_refl, \
mock.patch("ouroboros.agent_task_pipeline._update_improvement_backlog") as mock_bl, \
mock.patch("ouroboros.agent_task_pipeline._run_post_task_processing_async") as mock_async, \
mock.patch("ouroboros.agent_task_pipeline._run_chat_consolidation"), \
mock.patch("ouroboros.agent_task_pipeline._run_scratchpad_consolidation"), \
mock.patch("ouroboros.agent_task_pipeline._store_task_result"), \
mock.patch("ouroboros.review_evidence.collect_review_evidence",
return_value={}, create=True):
pending_events: list = []
emit_task_results(
env=env, memory=mock.MagicMock(), llm=mock.MagicMock(),
pending_events=pending_events,
task=task, text="reply",
usage=usage, llm_trace=llm_trace,
start_time=0.0,
drive_logs=tmp_path / "logs",
)
# Reflection and backlog are the responsibility of the async helper —
# emit_task_results must not call them directly. Otherwise reply
# latency regresses.
mock_refl.assert_not_called()
mock_bl.assert_not_called()
# The async helper must still be invoked exactly once.
mock_async.assert_called_once()
# ─────────────────────────────────────────────────────────────────────────────
# F5: durable reflection routing (project drive + canonical pointer)
# ─────────────────────────────────────────────────────────────────────────────
def test_append_reflection_routed_project_task_writes_project_drive_and_pointer(tmp_path, monkeypatch):
"""A project-scoped root's FULL reflection lands on the project drive; the
canonical log gets a bounded pointer row (never the full text — it feeds
future global context); the prunable mirror drive gets nothing."""
import json
import types
import ouroboros.project_facts as pf
from ouroboros.reflection import append_reflection_routed
monkeypatch.setattr(pf, "_project_store_root", lambda pid: tmp_path / "projects" / pid)
canonical = tmp_path / "data"
mirror = tmp_path / "mirror" # headless mirror drive — prunable, never the home
env = types.SimpleNamespace(drive_root=mirror)
task = {"id": "t-proj", "project_id": "slime", "budget_drive_root": str(canonical)}
entry = {
"ts": "2026-08-10T00:00:00Z", "task_id": "t-proj",
"reflection": "full project-local reflection text",
}
append_reflection_routed(env, task, entry)
project_log = tmp_path / "projects" / "slime" / "logs" / "task_reflections.jsonl"
rows = [json.loads(line) for line in project_log.read_text(encoding="utf-8").splitlines()]
assert rows[0]["reflection"] == "full project-local reflection text"
canonical_log = canonical / "logs" / "task_reflections.jsonl"
pointer_rows = [json.loads(line) for line in canonical_log.read_text(encoding="utf-8").splitlines()]
assert pointer_rows[0]["type"] == "project_reflection_pointer"
assert pointer_rows[0]["task_id"] == "t-proj"
assert pointer_rows[0]["project_id"] == "slime"
assert pointer_rows[0]["reflection_path"] == str(project_log)
assert "write_failed" not in pointer_rows[0] # successful project append
assert "full project-local reflection text" not in canonical_log.read_text(encoding="utf-8")
assert not (mirror / "logs" / "task_reflections.jsonl").exists()
def test_append_reflection_routed_stamps_pointer_when_project_write_fails(tmp_path, monkeypatch):
"""Finding 4: a failed project-drive append must not leave a pointer that
claims a full text exists — the pointer row is stamped write_failed."""
import json
import types
import ouroboros.project_facts as pf
import ouroboros.reflection as refl
monkeypatch.setattr(pf, "_project_store_root", lambda pid: tmp_path / "projects" / pid)
canonical = tmp_path / "data"
env = types.SimpleNamespace(drive_root=tmp_path / "mirror")
task = {"id": "t-proj", "project_id": "slime", "budget_drive_root": str(canonical)}
entry = {"ts": "2026-08-10T00:00:00Z", "task_id": "t-proj",
"reflection": "full project-local reflection text"}
real_append = refl.append_jsonl
def _selective(path, row):
if str(tmp_path / "projects") in str(path):
raise OSError("disk full")
return real_append(path, row)
monkeypatch.setattr(refl, "append_jsonl", _selective)
refl.append_reflection_routed(env, task, entry)
canonical_log = canonical / "logs" / "task_reflections.jsonl"
pointer = json.loads(canonical_log.read_text(encoding="utf-8").splitlines()[0])
assert pointer["type"] == "project_reflection_pointer"
assert pointer["write_failed"] is True
assert "full project-local reflection text" not in canonical_log.read_text(encoding="utf-8")
def test_recent_reflections_renders_pointer_row_as_single_line():
"""Finding 3: a canonical pointer row (no reflection text) renders one
informative line instead of an empty block burning a context slot."""
from ouroboros.context import _format_recent_reflections
text = _format_recent_reflections([{
"ts": "2026-08-10T00:00:00Z",
"task_id": "t-proj",
"type": "project_reflection_pointer",
"project_id": "slime",
"reflection_path": "/proj/slime/logs/task_reflections.jsonl",
}])
assert ("Full reflection lives on project drive: slime — "
"/proj/slime/logs/task_reflections.jsonl") in text
assert len(text.strip().splitlines()) == 2 # header + one pointer line
def test_append_reflection_routed_non_project_task_uses_canonical_drive(tmp_path):
"""A non-project root reflects on the canonical budget drive in full — never
on the prunable mirror the split root executes on."""
import json
import types
from ouroboros.reflection import append_reflection_routed
canonical = tmp_path / "data"
mirror = tmp_path / "mirror"
env = types.SimpleNamespace(drive_root=mirror)
task = {"id": "t-plain", "budget_drive_root": str(canonical)}
entry = {"ts": "2026-08-10T00:00:00Z", "task_id": "t-plain", "reflection": "plain reflection"}
append_reflection_routed(env, task, entry)
canonical_log = canonical / "logs" / "task_reflections.jsonl"
rows = [json.loads(line) for line in canonical_log.read_text(encoding="utf-8").splitlines()]
assert rows[0]["reflection"] == "plain reflection"
assert not (mirror / "logs" / "task_reflections.jsonl").exists()