@geravant/sinain 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +183 -0
  2. package/index.ts +2096 -0
  3. package/install.js +155 -0
  4. package/openclaw.plugin.json +59 -0
  5. package/package.json +21 -0
  6. package/sinain-memory/common.py +403 -0
  7. package/sinain-memory/demo_knowledge_transfer.sh +85 -0
  8. package/sinain-memory/embedder.py +268 -0
  9. package/sinain-memory/eval/__init__.py +0 -0
  10. package/sinain-memory/eval/assertions.py +288 -0
  11. package/sinain-memory/eval/judges/__init__.py +0 -0
  12. package/sinain-memory/eval/judges/base_judge.py +61 -0
  13. package/sinain-memory/eval/judges/curation_judge.py +46 -0
  14. package/sinain-memory/eval/judges/insight_judge.py +48 -0
  15. package/sinain-memory/eval/judges/mining_judge.py +42 -0
  16. package/sinain-memory/eval/judges/signal_judge.py +45 -0
  17. package/sinain-memory/eval/schemas.py +247 -0
  18. package/sinain-memory/eval_delta.py +109 -0
  19. package/sinain-memory/eval_reporter.py +642 -0
  20. package/sinain-memory/feedback_analyzer.py +221 -0
  21. package/sinain-memory/git_backup.sh +19 -0
  22. package/sinain-memory/insight_synthesizer.py +181 -0
  23. package/sinain-memory/memory/2026-03-01.md +11 -0
  24. package/sinain-memory/memory/playbook-archive/sinain-playbook-2026-03-01-1418.md +15 -0
  25. package/sinain-memory/memory/playbook-logs/2026-03-01.jsonl +1 -0
  26. package/sinain-memory/memory/sinain-playbook.md +21 -0
  27. package/sinain-memory/memory-config.json +39 -0
  28. package/sinain-memory/memory_miner.py +183 -0
  29. package/sinain-memory/module_manager.py +695 -0
  30. package/sinain-memory/playbook_curator.py +225 -0
  31. package/sinain-memory/requirements.txt +3 -0
  32. package/sinain-memory/signal_analyzer.py +141 -0
  33. package/sinain-memory/test_local.py +402 -0
  34. package/sinain-memory/tests/__init__.py +0 -0
  35. package/sinain-memory/tests/conftest.py +189 -0
  36. package/sinain-memory/tests/test_curator_helpers.py +94 -0
  37. package/sinain-memory/tests/test_embedder.py +210 -0
  38. package/sinain-memory/tests/test_extract_json.py +124 -0
  39. package/sinain-memory/tests/test_feedback_computation.py +121 -0
  40. package/sinain-memory/tests/test_miner_helpers.py +71 -0
  41. package/sinain-memory/tests/test_module_management.py +458 -0
  42. package/sinain-memory/tests/test_parsers.py +96 -0
  43. package/sinain-memory/tests/test_tick_evaluator.py +430 -0
  44. package/sinain-memory/tests/test_triple_extractor.py +255 -0
  45. package/sinain-memory/tests/test_triple_ingest.py +191 -0
  46. package/sinain-memory/tests/test_triple_migrate.py +138 -0
  47. package/sinain-memory/tests/test_triplestore.py +248 -0
  48. package/sinain-memory/tick_evaluator.py +392 -0
  49. package/sinain-memory/triple_extractor.py +402 -0
  50. package/sinain-memory/triple_ingest.py +290 -0
  51. package/sinain-memory/triple_migrate.py +275 -0
  52. package/sinain-memory/triple_query.py +184 -0
  53. package/sinain-memory/triplestore.py +498 -0
@@ -0,0 +1,191 @@
1
+ """Tests for triple_ingest.py — CLI entry point."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ from triplestore import TripleStore
10
+
11
+
12
+ KOOG_DIR = Path(__file__).resolve().parent.parent
13
+
14
+
15
+ @pytest.fixture
16
+ def memory_dir(tmp_path):
17
+ """Create a temporary memory directory with playbook."""
18
+ mem = tmp_path / "memory"
19
+ mem.mkdir()
20
+ (mem / "sinain-playbook.md").write_text(
21
+ "## Established Patterns\n"
22
+ "- OCR pipeline stalls when queue depth > 10 (score: 0.8)\n"
23
+ "- Use frame batching for throughput (score: 0.6)\n"
24
+ "- Spawn research agent proactively\n",
25
+ encoding="utf-8",
26
+ )
27
+ return str(mem)
28
+
29
+
30
+ @pytest.fixture
31
+ def modules_dir(tmp_path):
32
+ """Create a temporary modules directory with a test module."""
33
+ modules = tmp_path / "modules"
34
+ modules.mkdir()
35
+ mod_dir = modules / "test-mod"
36
+ mod_dir.mkdir()
37
+ (mod_dir / "manifest.json").write_text(json.dumps({
38
+ "name": "Test Module",
39
+ "description": "Testing patterns",
40
+ "version": "1.0.0",
41
+ }))
42
+ (mod_dir / "patterns.md").write_text("## Patterns\n- Test pattern one\n- Test pattern two\n")
43
+ return str(modules)
44
+
45
+
46
+ class TestSignalIngest:
47
+ def test_signal_ingest_creates_db(self, memory_dir):
48
+ signal = json.dumps({
49
+ "signals": [{"description": "OCR stall", "priority": "high"}],
50
+ "output": {"suggestion": "Try batching"},
51
+ })
52
+ result = subprocess.run(
53
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
54
+ "--memory-dir", memory_dir,
55
+ "--signal-result", signal,
56
+ "--tick-ts", "2026-03-01T10:00:00Z"],
57
+ capture_output=True, text=True, timeout=30,
58
+ )
59
+ assert result.returncode == 0, f"stderr: {result.stderr}"
60
+ data = json.loads(result.stdout.strip())
61
+ assert data["ingested"] > 0
62
+ assert data["source"] == "signal"
63
+ assert "txId" in data
64
+ # DB should exist
65
+ assert Path(memory_dir, "triplestore.db").exists()
66
+
67
+ def test_signal_ingest_requires_tick_ts(self, memory_dir):
68
+ result = subprocess.run(
69
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
70
+ "--memory-dir", memory_dir,
71
+ "--signal-result", '{"signals":[]}'],
72
+ capture_output=True, text=True, timeout=10,
73
+ )
74
+ assert result.returncode != 0
75
+
76
+
77
+ class TestPlaybookIngest:
78
+ def test_playbook_ingest(self, memory_dir):
79
+ result = subprocess.run(
80
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
81
+ "--memory-dir", memory_dir,
82
+ "--ingest-playbook"],
83
+ capture_output=True, text=True, timeout=30,
84
+ )
85
+ assert result.returncode == 0, f"stderr: {result.stderr}"
86
+ data = json.loads(result.stdout.strip())
87
+ assert data["ingested"] > 0
88
+ assert data["source"] == "playbook"
89
+
90
+
91
+ class TestSessionIngest:
92
+ def test_session_ingest(self, memory_dir):
93
+ session = json.dumps({
94
+ "ts": "2026-03-01T09:00:00Z",
95
+ "summary": "Debugging OCR pipeline issues",
96
+ "toolsUsed": ["Read", "Edit"],
97
+ })
98
+ result = subprocess.run(
99
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
100
+ "--memory-dir", memory_dir,
101
+ "--ingest-session", session],
102
+ capture_output=True, text=True, timeout=30,
103
+ )
104
+ assert result.returncode == 0, f"stderr: {result.stderr}"
105
+ data = json.loads(result.stdout.strip())
106
+ assert data["ingested"] > 0
107
+ assert data["source"] == "session"
108
+
109
+
110
+ class TestMiningIngest:
111
+ def test_mining_ingest(self, memory_dir):
112
+ mining = json.dumps({
113
+ "newPatterns": ["Frame dropping improves OCR"],
114
+ "preferences": ["User prefers minimal output"],
115
+ "contradictions": [],
116
+ })
117
+ result = subprocess.run(
118
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
119
+ "--memory-dir", memory_dir,
120
+ "--ingest-mining", mining],
121
+ capture_output=True, text=True, timeout=30,
122
+ )
123
+ assert result.returncode == 0, f"stderr: {result.stderr}"
124
+ data = json.loads(result.stdout.strip())
125
+ assert data["ingested"] > 0
126
+ assert data["source"] == "mining"
127
+
128
+
129
+ class TestModuleIngest:
130
+ def test_module_ingest(self, memory_dir, modules_dir):
131
+ result = subprocess.run(
132
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
133
+ "--memory-dir", memory_dir,
134
+ "--ingest-module", "test-mod",
135
+ "--modules-dir", modules_dir],
136
+ capture_output=True, text=True, timeout=30,
137
+ )
138
+ assert result.returncode == 0, f"stderr: {result.stderr}"
139
+ data = json.loads(result.stdout.strip())
140
+ assert data["ingested"] > 0
141
+ assert data["source"] == "module"
142
+ assert data["module"] == "test-mod"
143
+
144
+ def test_module_requires_modules_dir(self, memory_dir):
145
+ result = subprocess.run(
146
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
147
+ "--memory-dir", memory_dir,
148
+ "--ingest-module", "test-mod"],
149
+ capture_output=True, text=True, timeout=10,
150
+ )
151
+ assert result.returncode != 0
152
+
153
+
154
+ class TestRetractModule:
155
+ def test_retract_module(self, memory_dir, modules_dir):
156
+ # First ingest
157
+ subprocess.run(
158
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
159
+ "--memory-dir", memory_dir,
160
+ "--ingest-module", "test-mod",
161
+ "--modules-dir", modules_dir],
162
+ capture_output=True, text=True, timeout=30,
163
+ )
164
+ # Then retract
165
+ result = subprocess.run(
166
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
167
+ "--memory-dir", memory_dir,
168
+ "--retract-module", "test-mod"],
169
+ capture_output=True, text=True, timeout=30,
170
+ )
171
+ assert result.returncode == 0, f"stderr: {result.stderr}"
172
+ data = json.loads(result.stdout.strip())
173
+ assert data["source"] == "module"
174
+ assert data["module"] == "test-mod"
175
+
176
+
177
+ class TestOutputFormat:
178
+ def test_output_is_valid_json(self, memory_dir):
179
+ signal = json.dumps({"signals": []})
180
+ result = subprocess.run(
181
+ [sys.executable, str(KOOG_DIR / "triple_ingest.py"),
182
+ "--memory-dir", memory_dir,
183
+ "--signal-result", signal,
184
+ "--tick-ts", "2026-03-01"],
185
+ capture_output=True, text=True, timeout=30,
186
+ )
187
+ assert result.returncode == 0
188
+ data = json.loads(result.stdout.strip())
189
+ assert isinstance(data, dict)
190
+ assert "ingested" in data
191
+ assert "source" in data
@@ -0,0 +1,138 @@
1
+ """Tests for triple_migrate.py — historical data migration to triple store."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ # Ensure sinain-koog source is importable
10
+ KOOG_DIR = Path(__file__).resolve().parent.parent
11
+ if str(KOOG_DIR) not in sys.path:
12
+ sys.path.insert(0, str(KOOG_DIR))
13
+
14
+ from triple_extractor import TripleExtractor
15
+ from triple_migrate import (
16
+ MIGRATION_ENTITY,
17
+ migrate_daily_memories,
18
+ migrate_modules,
19
+ migrate_playbook,
20
+ migrate_playbook_logs,
21
+ )
22
+ from triplestore import TripleStore
23
+
24
+
25
+ @pytest.fixture
26
+ def store(tmp_path):
27
+ db = tmp_path / "triplestore.db"
28
+ return TripleStore(str(db))
29
+
30
+
31
+ @pytest.fixture
32
+ def extractor(store):
33
+ return TripleExtractor(store)
34
+
35
+
36
+ class TestMigratePlaybook:
37
+ def test_extracts_patterns(self, extractor, store, tmp_memory_dir):
38
+ count = migrate_playbook(extractor, store, str(tmp_memory_dir))
39
+ assert count > 0
40
+ # Should find at least the OCR and research-agent patterns
41
+ patterns = store.entities_with_attr("text")
42
+ pattern_ids = [eid for eid, _ in patterns if eid.startswith("pattern:")]
43
+ assert len(pattern_ids) >= 2
44
+
45
+ def test_empty_playbook_returns_zero(self, extractor, store, tmp_path):
46
+ memory = tmp_path / "empty_memory"
47
+ memory.mkdir()
48
+ count = migrate_playbook(extractor, store, str(memory))
49
+ assert count == 0
50
+
51
+
52
+ class TestMigrateModules:
53
+ def test_active_only(self, extractor, store, tmp_modules_dir):
54
+ mod_count, triple_count = migrate_modules(extractor, store, str(tmp_modules_dir))
55
+ # Only react-native-dev is active; ocr-pipeline is suspended
56
+ assert mod_count == 1
57
+ assert triple_count > 0
58
+ # Module entity should exist
59
+ ent = store.entity("module:react-native-dev")
60
+ assert "name" in ent
61
+ assert ent["name"] == ["React Native Development"]
62
+
63
+ def test_no_registry(self, extractor, store, tmp_path):
64
+ mod_count, triple_count = migrate_modules(extractor, store, str(tmp_path / "nope"))
65
+ assert mod_count == 0
66
+ assert triple_count == 0
67
+
68
+
69
+ class TestMigratePlaybookLogs:
70
+ def test_skips_idle_no_signals(self, extractor, store, tmp_memory_dir):
71
+ """Idle entries with empty signals should be skipped."""
72
+ file_count, triple_count = migrate_playbook_logs(
73
+ extractor, store, str(tmp_memory_dir)
74
+ )
75
+ assert file_count >= 1
76
+ assert triple_count > 0
77
+ # The second entry in conftest is idle=True + signals=[], should be skipped
78
+ # First entry has ts=2026-02-28T10:00:00Z — should be ingested
79
+ ent = store.entity("signal:2026-02-28T10:00:00Z")
80
+ assert ent # non-idle entry should exist
81
+ # Idle entry should NOT exist
82
+ idle_ent = store.entity("signal:2026-02-28T10:30:00Z")
83
+ assert not idle_ent
84
+
85
+ def test_no_log_dir(self, extractor, store, tmp_path):
86
+ memory = tmp_path / "empty"
87
+ memory.mkdir()
88
+ file_count, triple_count = migrate_playbook_logs(extractor, store, str(memory))
89
+ assert file_count == 0
90
+
91
+
92
+ class TestMigrateDailyMemories:
93
+ def test_creates_observation_entities(self, extractor, store, tmp_memory_dir):
94
+ file_count, triple_count = migrate_daily_memories(
95
+ extractor, store, str(tmp_memory_dir)
96
+ )
97
+ # conftest creates 3 daily memory files
98
+ assert file_count == 3
99
+ assert triple_count > 0
100
+ # Check one observation entity
101
+ ent = store.entity("observation:2026-02-21")
102
+ assert "text" in ent
103
+ assert "source" in ent
104
+ assert ent["source"] == ["daily_memory"]
105
+
106
+ def test_truncates_long_text(self, extractor, store, tmp_path):
107
+ memory = tmp_path / "memory"
108
+ memory.mkdir()
109
+ (memory / "2026-01-01.md").write_text("x" * 5000, encoding="utf-8")
110
+ migrate_daily_memories(extractor, store, str(memory))
111
+ ent = store.entity("observation:2026-01-01")
112
+ assert len(ent["text"][0]) == 2000
113
+
114
+
115
+ class TestIdempotency:
116
+ def test_stamp_prevents_remigration(self, store, tmp_path):
117
+ """Once migration:v1 exists, the script should be a no-op."""
118
+ tx = store.begin_tx("test")
119
+ store.assert_triple(tx, MIGRATION_ENTITY, "completed_at", "2026-03-05T00:00:00Z")
120
+ existing = store.entity(MIGRATION_ENTITY)
121
+ assert existing # guard entity exists
122
+
123
+ def test_full_migration_stamps(self, extractor, store, tmp_memory_dir, tmp_modules_dir):
124
+ """Full migration should create the stamp entity."""
125
+ migrate_playbook(extractor, store, str(tmp_memory_dir))
126
+ migrate_modules(extractor, store, str(tmp_modules_dir))
127
+ migrate_playbook_logs(extractor, store, str(tmp_memory_dir))
128
+ migrate_daily_memories(extractor, store, str(tmp_memory_dir))
129
+
130
+ # Simulate stamping
131
+ stats = store.stats()
132
+ stamp_tx = store.begin_tx("migration:stamp")
133
+ store.assert_triple(stamp_tx, MIGRATION_ENTITY, "completed_at", "2026-03-05T00:00:00Z")
134
+ store.assert_triple(stamp_tx, MIGRATION_ENTITY, "total_triples", str(stats["triples"]))
135
+
136
+ ent = store.entity(MIGRATION_ENTITY)
137
+ assert "completed_at" in ent
138
+ assert int(ent["total_triples"][0]) > 0
@@ -0,0 +1,248 @@
1
+ """Tests for triplestore.py — EAV triple store."""
2
+
3
+ import os
4
+ import pytest
5
+ from triplestore import TripleStore
6
+
7
+
8
+ @pytest.fixture
9
+ def store(tmp_path):
10
+ """Create a fresh triple store in a temp directory."""
11
+ db_path = tmp_path / "test.db"
12
+ s = TripleStore(str(db_path))
13
+ yield s
14
+ s.close()
15
+
16
+
17
+ @pytest.fixture
18
+ def populated_store(store):
19
+ """Store with sample data across two transactions."""
20
+ tx1 = store.begin_tx("test", session_key="sess-1")
21
+ store.assert_triple(tx1, "signal:2026-03-01T10:00", "description", "OCR stall")
22
+ store.assert_triple(tx1, "signal:2026-03-01T10:00", "priority", "high")
23
+ store.assert_triple(tx1, "signal:2026-03-01T10:00", "related_to", "concept:ocr", "ref")
24
+ store.assert_triple(tx1, "concept:ocr", "name", "OCR")
25
+ store.assert_triple(tx1, "pattern:frame-batch", "text", "Frame batching improves OCR")
26
+ store.assert_triple(tx1, "pattern:frame-batch", "related_to", "concept:ocr", "ref")
27
+ store.assert_triple(tx1, "session:2026-03-01T09:00", "summary", "Debugging OCR pipeline")
28
+ store.assert_triple(tx1, "session:2026-03-01T09:00", "related_to", "concept:ocr", "ref")
29
+ store._tx1 = tx1 # stash for tests
30
+ return store
31
+
32
+
33
+ class TestTransactions:
34
+ def test_begin_tx_returns_positive_id(self, store):
35
+ tx = store.begin_tx("test")
36
+ assert tx > 0
37
+
38
+ def test_latest_tx_empty_store(self, tmp_path):
39
+ s = TripleStore(str(tmp_path / "empty.db"))
40
+ assert s.latest_tx() == 0
41
+ s.close()
42
+
43
+ def test_latest_tx_after_writes(self, store):
44
+ tx1 = store.begin_tx("a")
45
+ tx2 = store.begin_tx("b")
46
+ assert store.latest_tx() == tx2
47
+ assert tx2 > tx1
48
+
49
+ def test_tx_metadata(self, store):
50
+ tx = store.begin_tx("test", metadata={"foo": "bar"})
51
+ row = store._conn.execute(
52
+ "SELECT metadata FROM transactions WHERE tx_id = ?", (tx,)
53
+ ).fetchone()
54
+ import json
55
+ assert json.loads(row["metadata"]) == {"foo": "bar"}
56
+
57
+ def test_tx_parent(self, store):
58
+ tx1 = store.begin_tx("parent")
59
+ tx2 = store.begin_tx("child", parent_tx=tx1)
60
+ row = store._conn.execute(
61
+ "SELECT parent_tx FROM transactions WHERE tx_id = ?", (tx2,)
62
+ ).fetchone()
63
+ assert row["parent_tx"] == tx1
64
+
65
+
66
+ class TestAssertAndEntity:
67
+ def test_assert_returns_id(self, store):
68
+ tx = store.begin_tx("test")
69
+ tid = store.assert_triple(tx, "e:1", "name", "Test")
70
+ assert tid > 0
71
+
72
+ def test_entity_returns_all_attrs(self, populated_store):
73
+ ent = populated_store.entity("signal:2026-03-01T10:00")
74
+ assert ent["description"] == ["OCR stall"]
75
+ assert ent["priority"] == ["high"]
76
+ assert ent["related_to"] == ["concept:ocr"]
77
+
78
+ def test_entity_missing_returns_empty(self, store):
79
+ assert store.entity("nonexistent:1") == {}
80
+
81
+ def test_multiple_values_per_attr(self, store):
82
+ tx = store.begin_tx("test")
83
+ store.assert_triple(tx, "e:1", "tag", "alpha")
84
+ store.assert_triple(tx, "e:1", "tag", "beta")
85
+ ent = store.entity("e:1")
86
+ assert set(ent["tag"]) == {"alpha", "beta"}
87
+
88
+ def test_entity_type_auto_populated(self, store):
89
+ tx = store.begin_tx("test")
90
+ store.assert_triple(tx, "signal:abc", "x", "y")
91
+ row = store._conn.execute(
92
+ "SELECT entity_type FROM entity_types WHERE entity_id = ?",
93
+ ("signal:abc",),
94
+ ).fetchone()
95
+ assert row["entity_type"] == "signal"
96
+
97
+
98
+ class TestRetraction:
99
+ def test_retract_by_attr(self, populated_store):
100
+ tx2 = populated_store.begin_tx("retract")
101
+ count = populated_store.retract_triple(tx2, "signal:2026-03-01T10:00", "priority")
102
+ assert count == 1
103
+ ent = populated_store.entity("signal:2026-03-01T10:00")
104
+ assert "priority" not in ent
105
+
106
+ def test_retract_by_attr_and_value(self, store):
107
+ tx = store.begin_tx("test")
108
+ store.assert_triple(tx, "e:1", "tag", "a")
109
+ store.assert_triple(tx, "e:1", "tag", "b")
110
+ tx2 = store.begin_tx("retract")
111
+ count = store.retract_triple(tx2, "e:1", "tag", "a")
112
+ assert count == 1
113
+ ent = store.entity("e:1")
114
+ assert ent["tag"] == ["b"]
115
+
116
+ def test_retract_nonexistent_returns_zero(self, store):
117
+ tx = store.begin_tx("test")
118
+ count = store.retract_triple(tx, "e:nope", "attr")
119
+ assert count == 0
120
+
121
+
122
+ class TestEAVTAsOfTx:
123
+ def test_as_of_tx_sees_old_state(self, populated_store):
124
+ tx1 = populated_store._tx1
125
+ tx2 = populated_store.begin_tx("change")
126
+ populated_store.retract_triple(tx2, "signal:2026-03-01T10:00", "priority")
127
+
128
+ # Current state: no priority
129
+ assert "priority" not in populated_store.entity("signal:2026-03-01T10:00")
130
+ # as_of tx1: has priority
131
+ assert "priority" in populated_store.entity("signal:2026-03-01T10:00", as_of_tx=tx1)
132
+
133
+
134
+ class TestAEVT:
135
+ def test_entities_with_attr(self, populated_store):
136
+ results = populated_store.entities_with_attr("name")
137
+ assert ("concept:ocr", "OCR") in results
138
+
139
+ def test_entities_with_attr_multiple(self, populated_store):
140
+ results = populated_store.entities_with_attr("related_to")
141
+ eids = [r[0] for r in results]
142
+ assert "signal:2026-03-01T10:00" in eids
143
+ assert "pattern:frame-batch" in eids
144
+
145
+
146
+ class TestVAET:
147
+ def test_backrefs(self, populated_store):
148
+ refs = populated_store.backrefs("concept:ocr")
149
+ eids = [r[0] for r in refs]
150
+ assert "signal:2026-03-01T10:00" in eids
151
+ assert "pattern:frame-batch" in eids
152
+ assert "session:2026-03-01T09:00" in eids
153
+
154
+ def test_backrefs_with_attribute_filter(self, populated_store):
155
+ refs = populated_store.backrefs("concept:ocr", attribute="related_to")
156
+ eids = [r[0] for r in refs]
157
+ assert "signal:2026-03-01T10:00" in eids
158
+
159
+ def test_backrefs_no_results(self, store):
160
+ assert store.backrefs("nonexistent:1") == []
161
+
162
+
163
+ class TestAVET:
164
+ def test_lookup(self, populated_store):
165
+ found = populated_store.lookup("name", "OCR")
166
+ assert "concept:ocr" in found
167
+
168
+ def test_lookup_no_match(self, populated_store):
169
+ found = populated_store.lookup("name", "nonexistent")
170
+ assert found == []
171
+
172
+
173
+ class TestNeighbors:
174
+ def test_neighbors_depth_1(self, populated_store):
175
+ nbrs = populated_store.neighbors("concept:ocr", depth=1)
176
+ assert "concept:ocr" in nbrs
177
+ # Should find signal, pattern, and session via backrefs
178
+ found_eids = set(nbrs.keys())
179
+ assert "signal:2026-03-01T10:00" in found_eids or "pattern:frame-batch" in found_eids
180
+
181
+ def test_neighbors_depth_0(self, populated_store):
182
+ nbrs = populated_store.neighbors("concept:ocr", depth=0)
183
+ assert "concept:ocr" in nbrs
184
+ assert len(nbrs) == 1 # only the entity itself
185
+
186
+
187
+ class TestNovelty:
188
+ def test_novelty_after_tx(self, populated_store):
189
+ tx1 = populated_store._tx1
190
+ tx2 = populated_store.begin_tx("new")
191
+ populated_store.assert_triple(tx2, "concept:new", "name", "New Concept")
192
+ changes = populated_store.novelty(tx1)
193
+ assert len(changes) >= 1
194
+ assert any(c["entity_id"] == "concept:new" for c in changes)
195
+
196
+ def test_novelty_bounded(self, populated_store):
197
+ tx1 = populated_store._tx1
198
+ tx2 = populated_store.begin_tx("a")
199
+ populated_store.assert_triple(tx2, "concept:a", "name", "A")
200
+ tx3 = populated_store.begin_tx("b")
201
+ populated_store.assert_triple(tx3, "concept:b", "name", "B")
202
+ # Only changes between tx1 and tx2
203
+ changes = populated_store.novelty(tx1, until_tx=tx2)
204
+ eids = [c["entity_id"] for c in changes]
205
+ assert "concept:a" in eids
206
+ assert "concept:b" not in eids
207
+
208
+ def test_novelty_empty(self, store):
209
+ tx = store.begin_tx("test")
210
+ assert store.novelty(tx) == []
211
+
212
+
213
+ class TestGC:
214
+ def test_gc_deletes_old_retracted(self, store):
215
+ tx = store.begin_tx("test")
216
+ store.assert_triple(tx, "e:1", "x", "y")
217
+ tx2 = store.begin_tx("retract")
218
+ store.retract_triple(tx2, "e:1", "x")
219
+ # GC with 0 days should delete it
220
+ count = store.gc(older_than_days=0)
221
+ assert count >= 1
222
+
223
+ def test_gc_preserves_active(self, store):
224
+ tx = store.begin_tx("test")
225
+ store.assert_triple(tx, "e:1", "x", "y")
226
+ count = store.gc(older_than_days=0)
227
+ assert count == 0 # not retracted, shouldn't be deleted
228
+
229
+
230
+ class TestStats:
231
+ def test_stats_populated(self, populated_store):
232
+ s = populated_store.stats()
233
+ assert s["triples"] >= 8
234
+ assert s["entities"] >= 4
235
+ assert s["transactions"] >= 1
236
+ assert s["db_size_bytes"] > 0
237
+
238
+ def test_stats_empty(self, store):
239
+ s = store.stats()
240
+ assert s["triples"] == 0
241
+ assert s["entities"] == 0
242
+ assert s["transactions"] == 0
243
+
244
+
245
+ class TestWALMode:
246
+ def test_wal_mode_enabled(self, store):
247
+ mode = store._conn.execute("PRAGMA journal_mode").fetchone()[0]
248
+ assert mode == "wal"