|
|
@@ -0,0 +1,552 @@
|
|
|
+import asyncio
|
|
|
+import json
|
|
|
+import os
|
|
|
+import tempfile
|
|
|
+import unittest
|
|
|
+from pathlib import Path
|
|
|
+from unittest.mock import patch
|
|
|
+
|
|
|
+from cyber_agent.core.dream import (
|
|
|
+ DreamScope,
|
|
|
+ DreamScopeError,
|
|
|
+ cross_trace_integrate,
|
|
|
+)
|
|
|
+from cyber_agent.core.memory import (
|
|
|
+ MEMORY_IDENTITY_CONTEXT_KEY,
|
|
|
+ MemoryConfig,
|
|
|
+ MemoryPathError,
|
|
|
+ compute_memory_identity,
|
|
|
+ is_declared_memory_path,
|
|
|
+ load_memory_files,
|
|
|
+ resolve_memory_path,
|
|
|
+)
|
|
|
+from cyber_agent.tools.builtin.memory import dream as dream_tool
|
|
|
+from cyber_agent.trace.models import Trace
|
|
|
+
|
|
|
+
|
|
|
+class FakeDreamStore:
|
|
|
+ def __init__(self, base_path: Path, traces):
|
|
|
+ self.base_path = base_path
|
|
|
+ self.traces = {trace.trace_id: trace for trace in traces}
|
|
|
+ self.base_path.mkdir(parents=True, exist_ok=True)
|
|
|
+
|
|
|
+ async def list_traces(self, limit=1000):
|
|
|
+ return list(self.traces.values())[:limit]
|
|
|
+
|
|
|
+ async def get_trace(self, trace_id):
|
|
|
+ return self.traces.get(trace_id)
|
|
|
+
|
|
|
+ async def get_cognition_log(self, trace_id):
|
|
|
+ path = self._get_cognition_log_file(trace_id)
|
|
|
+ if not path.exists():
|
|
|
+ return {"trace_id": trace_id, "events": []}
|
|
|
+ return json.loads(path.read_text(encoding="utf-8"))
|
|
|
+
|
|
|
+ async def mark_reflections_consumed(
|
|
|
+ self, trace_id, reflections, consumed_at
|
|
|
+ ):
|
|
|
+ log = await self.get_cognition_log(trace_id)
|
|
|
+ wanted = {
|
|
|
+ (tuple(item.get("sequence_range") or []), item.get("summary", ""))
|
|
|
+ for item in reflections
|
|
|
+ }
|
|
|
+ changed = 0
|
|
|
+ for event in log.get("events", []):
|
|
|
+ key = (
|
|
|
+ tuple(event.get("sequence_range") or []),
|
|
|
+ event.get("summary", ""),
|
|
|
+ )
|
|
|
+ if (
|
|
|
+ event.get("type") == "reflection"
|
|
|
+ and not event.get("consumed_at")
|
|
|
+ and key in wanted
|
|
|
+ ):
|
|
|
+ event["consumed_at"] = consumed_at
|
|
|
+ changed += 1
|
|
|
+ self._get_cognition_log_file(trace_id).write_text(
|
|
|
+ json.dumps(log),
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ return changed
|
|
|
+
|
|
|
+ def _get_cognition_log_file(self, trace_id):
|
|
|
+ path = self.base_path / trace_id / "cognition_log.json"
|
|
|
+ path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+ return path
|
|
|
+
|
|
|
+ def set_log(self, trace_id, events):
|
|
|
+ self._get_cognition_log_file(trace_id).write_text(
|
|
|
+ json.dumps({"trace_id": trace_id, "events": events}),
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def make_trace(trace_id, scope, *, uid=None, agent_type=None, identity=None):
|
|
|
+ return Trace(
|
|
|
+ trace_id=trace_id,
|
|
|
+ mode="agent",
|
|
|
+ uid=scope.uid if uid is None else uid,
|
|
|
+ agent_type=scope.agent_type if agent_type is None else agent_type,
|
|
|
+ context={
|
|
|
+ MEMORY_IDENTITY_CONTEXT_KEY: (
|
|
|
+ scope.memory_identity if identity is None else identity
|
|
|
+ )
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def reflection(summary):
|
|
|
+ return {
|
|
|
+ "type": "reflection",
|
|
|
+ "timestamp": f"ts-{summary}",
|
|
|
+ "sequence_range": [1, 2],
|
|
|
+ "summary": summary,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+class MemorySecurityTests(unittest.TestCase):
|
|
|
+ def test_identity_uses_real_base_and_file_patterns(self):
|
|
|
+ with tempfile.TemporaryDirectory() as tmp:
|
|
|
+ base = Path(tmp) / "memory"
|
|
|
+ base.mkdir()
|
|
|
+ alias = Path(tmp) / "memory-alias"
|
|
|
+ alias.symlink_to(base, target_is_directory=True)
|
|
|
+
|
|
|
+ first = MemoryConfig(
|
|
|
+ base_path=str(base),
|
|
|
+ files={"journals/**/*.md": "journals", "identity.md": "self"},
|
|
|
+ dream_prompt="first prompt",
|
|
|
+ )
|
|
|
+ second = MemoryConfig(
|
|
|
+ base_path=str(alias),
|
|
|
+ files={"identity.md": "different purpose", "journals/**/*.md": ""},
|
|
|
+ dream_prompt="different prompt",
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ compute_memory_identity(first), compute_memory_identity(second)
|
|
|
+ )
|
|
|
+
|
|
|
+ third = MemoryConfig(
|
|
|
+ base_path=str(base),
|
|
|
+ files={"identity.md": "self"},
|
|
|
+ )
|
|
|
+ self.assertNotEqual(
|
|
|
+ compute_memory_identity(first), compute_memory_identity(third)
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_declared_glob_does_not_let_star_cross_directories(self):
|
|
|
+ config = MemoryConfig(
|
|
|
+ base_path="/tmp/not-used",
|
|
|
+ files={"relationships/*.md": "people", "journals/**/*.md": "logs"},
|
|
|
+ )
|
|
|
+ self.assertTrue(is_declared_memory_path(config, "relationships/a.md"))
|
|
|
+ self.assertFalse(
|
|
|
+ is_declared_memory_path(config, "relationships/private/a.md")
|
|
|
+ )
|
|
|
+ self.assertTrue(is_declared_memory_path(config, "journals/2026/07/a.md"))
|
|
|
+
|
|
|
+ def test_resolution_rejects_non_normal_paths_and_symlink_escape(self):
|
|
|
+ with tempfile.TemporaryDirectory() as tmp:
|
|
|
+ root = Path(tmp)
|
|
|
+ base = root / "memory"
|
|
|
+ outside = root / "outside"
|
|
|
+ base.mkdir()
|
|
|
+ outside.mkdir()
|
|
|
+ (base / "link").symlink_to(outside, target_is_directory=True)
|
|
|
+ config = MemoryConfig(base_path=str(base), files={"**/*.md": "all"})
|
|
|
+
|
|
|
+ for unsafe in (
|
|
|
+ "../outside/escape.md",
|
|
|
+ "./identity.md",
|
|
|
+ "/tmp/escape.md",
|
|
|
+ "nested//identity.md",
|
|
|
+ "nested\\identity.md",
|
|
|
+ "link/escape.md",
|
|
|
+ ):
|
|
|
+ with self.subTest(path=unsafe):
|
|
|
+ with self.assertRaises(MemoryPathError):
|
|
|
+ resolve_memory_path(config, unsafe)
|
|
|
+
|
|
|
+ def test_loader_does_not_follow_symlink_outside_base(self):
|
|
|
+ with tempfile.TemporaryDirectory() as tmp:
|
|
|
+ root = Path(tmp)
|
|
|
+ base = root / "memory"
|
|
|
+ outside = root / "outside"
|
|
|
+ base.mkdir()
|
|
|
+ outside.mkdir()
|
|
|
+ (outside / "secret.md").write_text("secret", encoding="utf-8")
|
|
|
+ (base / "safe.md").write_text("safe", encoding="utf-8")
|
|
|
+ (base / "linked.md").symlink_to(outside / "secret.md")
|
|
|
+ config = MemoryConfig(base_path=str(base), files={"*.md": "root"})
|
|
|
+
|
|
|
+ loaded = load_memory_files(config)
|
|
|
+ self.assertEqual(loaded, [("safe.md", "root", "safe")])
|
|
|
+
|
|
|
+
|
|
|
+class DreamIsolationTests(unittest.IsolatedAsyncioTestCase):
|
|
|
+ async def asyncSetUp(self):
|
|
|
+ self.tmp = tempfile.TemporaryDirectory()
|
|
|
+ self.root = Path(self.tmp.name)
|
|
|
+ self.memory_root = self.root / "memory"
|
|
|
+ self.memory_root.mkdir()
|
|
|
+ self.config = MemoryConfig(
|
|
|
+ base_path=str(self.memory_root),
|
|
|
+ files={"identity.md": "self", "journals/**/*.md": "logs"},
|
|
|
+ )
|
|
|
+ self.scope = DreamScope(
|
|
|
+ uid="user-1",
|
|
|
+ agent_type="writer",
|
|
|
+ memory_identity=compute_memory_identity(self.config),
|
|
|
+ )
|
|
|
+
|
|
|
+ async def asyncTearDown(self):
|
|
|
+ self.tmp.cleanup()
|
|
|
+
|
|
|
+ async def test_filters_uid_agent_type_and_memory_identity_together(self):
|
|
|
+ matching = make_trace("matching", self.scope)
|
|
|
+ wrong_uid = make_trace("wrong-uid", self.scope, uid="user-2")
|
|
|
+ wrong_agent = make_trace("wrong-agent", self.scope, agent_type="researcher")
|
|
|
+ wrong_memory = make_trace("wrong-memory", self.scope, identity="memory-v1:other")
|
|
|
+ store = FakeDreamStore(
|
|
|
+ self.root / "traces",
|
|
|
+ [matching, wrong_uid, wrong_agent, wrong_memory],
|
|
|
+ )
|
|
|
+ for trace in (matching, wrong_uid, wrong_agent, wrong_memory):
|
|
|
+ store.set_log(trace.trace_id, [reflection(trace.trace_id)])
|
|
|
+
|
|
|
+ calls = []
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ calls.append(kwargs)
|
|
|
+ return {"content": '{"updates": [], "reasoning": "merged"}'}
|
|
|
+
|
|
|
+ consumed, updated, reasoning = await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, self.scope
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual((consumed, updated, reasoning), (1, [], "merged"))
|
|
|
+ prompt = calls[0]["messages"][1]["content"]
|
|
|
+ self.assertIn("matching", prompt)
|
|
|
+ self.assertNotIn("wrong-uid", prompt)
|
|
|
+ self.assertNotIn("wrong-agent", prompt)
|
|
|
+ self.assertNotIn("wrong-memory", prompt)
|
|
|
+ self.assertTrue(
|
|
|
+ (await store.get_cognition_log("matching"))["events"][0].get(
|
|
|
+ "consumed_at"
|
|
|
+ )
|
|
|
+ )
|
|
|
+ for trace_id in ("wrong-uid", "wrong-agent", "wrong-memory"):
|
|
|
+ self.assertNotIn(
|
|
|
+ "consumed_at",
|
|
|
+ (await store.get_cognition_log(trace_id))["events"][0],
|
|
|
+ )
|
|
|
+
|
|
|
+ async def test_concurrent_dreams_keep_memory_configs_isolated(self):
|
|
|
+ other_root = self.root / "other-memory"
|
|
|
+ other_root.mkdir()
|
|
|
+ other_config = MemoryConfig(
|
|
|
+ base_path=str(other_root),
|
|
|
+ files={"identity.md": "self"},
|
|
|
+ )
|
|
|
+ other_scope = DreamScope(
|
|
|
+ uid=self.scope.uid,
|
|
|
+ agent_type=self.scope.agent_type,
|
|
|
+ memory_identity=compute_memory_identity(other_config),
|
|
|
+ )
|
|
|
+ first_trace = make_trace("first", self.scope)
|
|
|
+ second_trace = make_trace("second", other_scope)
|
|
|
+ store = FakeDreamStore(
|
|
|
+ self.root / "traces", [first_trace, second_trace]
|
|
|
+ )
|
|
|
+ store.set_log(first_trace.trace_id, [reflection("first-summary")])
|
|
|
+ store.set_log(second_trace.trace_id, [reflection("second-summary")])
|
|
|
+
|
|
|
+ async def first_llm(**kwargs):
|
|
|
+ await asyncio.sleep(0)
|
|
|
+ self.assertIn("first-summary", kwargs["messages"][1]["content"])
|
|
|
+ self.assertNotIn("second-summary", kwargs["messages"][1]["content"])
|
|
|
+ return {
|
|
|
+ "content": json.dumps(
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {"path": "identity.md", "new_content": "first"}
|
|
|
+ ],
|
|
|
+ "reasoning": "first",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ async def second_llm(**kwargs):
|
|
|
+ await asyncio.sleep(0)
|
|
|
+ self.assertIn("second-summary", kwargs["messages"][1]["content"])
|
|
|
+ self.assertNotIn("first-summary", kwargs["messages"][1]["content"])
|
|
|
+ return {
|
|
|
+ "content": json.dumps(
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {"path": "identity.md", "new_content": "second"}
|
|
|
+ ],
|
|
|
+ "reasoning": "second",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ first_result, second_result = await asyncio.gather(
|
|
|
+ cross_trace_integrate(
|
|
|
+ store, first_llm, self.config, self.scope
|
|
|
+ ),
|
|
|
+ cross_trace_integrate(
|
|
|
+ store, second_llm, other_config, other_scope
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual(first_result[0], 1)
|
|
|
+ self.assertEqual(second_result[0], 1)
|
|
|
+ self.assertEqual(
|
|
|
+ (self.memory_root / "identity.md").read_text(encoding="utf-8"),
|
|
|
+ "first",
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ (other_root / "identity.md").read_text(encoding="utf-8"),
|
|
|
+ "second",
|
|
|
+ )
|
|
|
+
|
|
|
+ async def test_scope_identity_must_match_memory_config(self):
|
|
|
+ trace = make_trace("trace", self.scope)
|
|
|
+ store = FakeDreamStore(self.root / "traces", [trace])
|
|
|
+ store.set_log(trace.trace_id, [reflection("summary")])
|
|
|
+ wrong_scope = DreamScope(
|
|
|
+ uid=self.scope.uid,
|
|
|
+ agent_type=self.scope.agent_type,
|
|
|
+ memory_identity="memory-v1:wrong",
|
|
|
+ )
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ self.fail("LLM must not run for a mismatched scope")
|
|
|
+
|
|
|
+ with self.assertRaises(DreamScopeError):
|
|
|
+ await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, wrong_scope
|
|
|
+ )
|
|
|
+
|
|
|
+ async def test_allowed_updates_are_written_then_reflection_is_consumed(self):
|
|
|
+ trace = make_trace("trace", self.scope)
|
|
|
+ store = FakeDreamStore(self.root / "traces", [trace])
|
|
|
+ store.set_log(trace.trace_id, [reflection("summary")])
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ return {
|
|
|
+ "content": json.dumps(
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {"path": "identity.md", "new_content": "new identity"},
|
|
|
+ {
|
|
|
+ "path": "journals/2026/entry.md",
|
|
|
+ "new_content": "new journal",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ "reasoning": "useful",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ result = await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, self.scope
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual(
|
|
|
+ result, (1, ["identity.md", "journals/2026/entry.md"], "useful")
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ (self.memory_root / "identity.md").read_text(encoding="utf-8"),
|
|
|
+ "new identity",
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ (self.memory_root / "journals/2026/entry.md").read_text(
|
|
|
+ encoding="utf-8"
|
|
|
+ ),
|
|
|
+ "new journal",
|
|
|
+ )
|
|
|
+ self.assertTrue(
|
|
|
+ (await store.get_cognition_log(trace.trace_id))["events"][0].get(
|
|
|
+ "consumed_at"
|
|
|
+ )
|
|
|
+ )
|
|
|
+ self.assertEqual(list(self.memory_root.rglob("*.tmp")), [])
|
|
|
+
|
|
|
+ async def test_invalid_update_rejects_entire_plan_without_consuming(self):
|
|
|
+ trace = make_trace("trace", self.scope)
|
|
|
+ store = FakeDreamStore(self.root / "traces", [trace])
|
|
|
+ unsafe_paths = [
|
|
|
+ "../memory-evil/escape.md",
|
|
|
+ "/tmp/escape.md",
|
|
|
+ "not-declared.txt",
|
|
|
+ "journals/../escape.md",
|
|
|
+ "journals\\escape.md",
|
|
|
+ ]
|
|
|
+
|
|
|
+ for index, unsafe_path in enumerate(unsafe_paths):
|
|
|
+ with self.subTest(path=unsafe_path):
|
|
|
+ event = reflection(f"summary-{index}")
|
|
|
+ store.set_log(trace.trace_id, [event])
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ return {
|
|
|
+ "content": json.dumps(
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {
|
|
|
+ "path": unsafe_path,
|
|
|
+ "new_content": "must not write",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "reasoning": "bad",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ result = await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, self.scope
|
|
|
+ )
|
|
|
+ self.assertEqual(result, (0, [], ""))
|
|
|
+ self.assertNotIn(
|
|
|
+ "consumed_at",
|
|
|
+ (await store.get_cognition_log(trace.trace_id))["events"][0],
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertFalse((self.root / "memory-evil" / "escape.md").exists())
|
|
|
+
|
|
|
+ async def test_symlink_escape_is_rejected_without_consuming(self):
|
|
|
+ outside = self.root / "outside"
|
|
|
+ outside.mkdir()
|
|
|
+ (self.memory_root / "journals").symlink_to(
|
|
|
+ outside, target_is_directory=True
|
|
|
+ )
|
|
|
+ trace = make_trace("trace", self.scope)
|
|
|
+ store = FakeDreamStore(self.root / "traces", [trace])
|
|
|
+ store.set_log(trace.trace_id, [reflection("summary")])
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ return {
|
|
|
+ "content": json.dumps(
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {
|
|
|
+ "path": "journals/escape.md",
|
|
|
+ "new_content": "must not escape",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "reasoning": "bad",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ result = await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, self.scope
|
|
|
+ )
|
|
|
+ self.assertEqual(result, (0, [], ""))
|
|
|
+ self.assertFalse((outside / "escape.md").exists())
|
|
|
+ self.assertNotIn(
|
|
|
+ "consumed_at",
|
|
|
+ (await store.get_cognition_log(trace.trace_id))["events"][0],
|
|
|
+ )
|
|
|
+
|
|
|
+ async def test_failed_atomic_replace_preserves_old_file_and_does_not_consume(self):
|
|
|
+ target = self.memory_root / "identity.md"
|
|
|
+ target.write_text("old identity", encoding="utf-8")
|
|
|
+ trace = make_trace("trace", self.scope)
|
|
|
+ store = FakeDreamStore(self.root / "traces", [trace])
|
|
|
+ store.set_log(trace.trace_id, [reflection("summary")])
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ return {
|
|
|
+ "content": json.dumps(
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {"path": "identity.md", "new_content": "new identity"}
|
|
|
+ ],
|
|
|
+ "reasoning": "useful",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ with patch("cyber_agent.core.dream.os.replace", side_effect=OSError("boom")):
|
|
|
+ with self.assertRaises(OSError):
|
|
|
+ await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, self.scope
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual(target.read_text(encoding="utf-8"), "old identity")
|
|
|
+ self.assertNotIn(
|
|
|
+ "consumed_at",
|
|
|
+ (await store.get_cognition_log(trace.trace_id))["events"][0],
|
|
|
+ )
|
|
|
+ self.assertEqual(list(self.memory_root.glob(".identity.md.*.tmp")), [])
|
|
|
+
|
|
|
+ async def test_multi_file_failure_rolls_back_every_published_update(self):
|
|
|
+ identity = self.memory_root / "identity.md"
|
|
|
+ journal = self.memory_root / "journals" / "today.md"
|
|
|
+ journal.parent.mkdir()
|
|
|
+ identity.write_text("old identity", encoding="utf-8")
|
|
|
+ journal.write_text("old journal", encoding="utf-8")
|
|
|
+ trace = make_trace("trace", self.scope)
|
|
|
+ store = FakeDreamStore(self.root / "traces", [trace])
|
|
|
+ store.set_log(trace.trace_id, [reflection("summary")])
|
|
|
+
|
|
|
+ async def llm_call(**kwargs):
|
|
|
+ return {
|
|
|
+ "content": json.dumps({
|
|
|
+ "updates": [
|
|
|
+ {"path": "identity.md", "new_content": "new identity"},
|
|
|
+ {"path": "journals/today.md", "new_content": "new journal"},
|
|
|
+ ],
|
|
|
+ "reasoning": "useful",
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ real_replace = os.replace
|
|
|
+ replace_calls = 0
|
|
|
+
|
|
|
+ def fail_second_target(source, destination):
|
|
|
+ nonlocal replace_calls
|
|
|
+ replace_calls += 1
|
|
|
+ if replace_calls == 2:
|
|
|
+ raise OSError("second replacement failed")
|
|
|
+ return real_replace(source, destination)
|
|
|
+
|
|
|
+ with patch(
|
|
|
+ "cyber_agent.core.dream.os.replace",
|
|
|
+ side_effect=fail_second_target,
|
|
|
+ ):
|
|
|
+ with self.assertRaisesRegex(OSError, "second replacement failed"):
|
|
|
+ await cross_trace_integrate(
|
|
|
+ store, llm_call, self.config, self.scope
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual("old identity", identity.read_text(encoding="utf-8"))
|
|
|
+ self.assertEqual("old journal", journal.read_text(encoding="utf-8"))
|
|
|
+ self.assertNotIn(
|
|
|
+ "consumed_at",
|
|
|
+ (await store.get_cognition_log(trace.trace_id))["events"][0],
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ [],
|
|
|
+ list(self.memory_root.rglob("*.dream-stage")),
|
|
|
+ )
|
|
|
+
|
|
|
+ async def test_builtin_dream_requires_memory_config_and_scope_in_context(self):
|
|
|
+ class Runner:
|
|
|
+ trace_store = object()
|
|
|
+ llm_call = object()
|
|
|
+
|
|
|
+ missing = await dream_tool(context={"runner": Runner()})
|
|
|
+ self.assertEqual(missing.error, "memory_config not in tool context")
|
|
|
+
|
|
|
+ missing_scope = await dream_tool(
|
|
|
+ context={"runner": Runner(), "memory_config": self.config}
|
|
|
+ )
|
|
|
+ self.assertEqual(missing_scope.error, "dream_scope not in tool context")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ unittest.main()
|