@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.
- package/README.md +183 -0
- package/index.ts +2096 -0
- package/install.js +155 -0
- package/openclaw.plugin.json +59 -0
- package/package.json +21 -0
- package/sinain-memory/common.py +403 -0
- package/sinain-memory/demo_knowledge_transfer.sh +85 -0
- package/sinain-memory/embedder.py +268 -0
- package/sinain-memory/eval/__init__.py +0 -0
- package/sinain-memory/eval/assertions.py +288 -0
- package/sinain-memory/eval/judges/__init__.py +0 -0
- package/sinain-memory/eval/judges/base_judge.py +61 -0
- package/sinain-memory/eval/judges/curation_judge.py +46 -0
- package/sinain-memory/eval/judges/insight_judge.py +48 -0
- package/sinain-memory/eval/judges/mining_judge.py +42 -0
- package/sinain-memory/eval/judges/signal_judge.py +45 -0
- package/sinain-memory/eval/schemas.py +247 -0
- package/sinain-memory/eval_delta.py +109 -0
- package/sinain-memory/eval_reporter.py +642 -0
- package/sinain-memory/feedback_analyzer.py +221 -0
- package/sinain-memory/git_backup.sh +19 -0
- package/sinain-memory/insight_synthesizer.py +181 -0
- package/sinain-memory/memory/2026-03-01.md +11 -0
- package/sinain-memory/memory/playbook-archive/sinain-playbook-2026-03-01-1418.md +15 -0
- package/sinain-memory/memory/playbook-logs/2026-03-01.jsonl +1 -0
- package/sinain-memory/memory/sinain-playbook.md +21 -0
- package/sinain-memory/memory-config.json +39 -0
- package/sinain-memory/memory_miner.py +183 -0
- package/sinain-memory/module_manager.py +695 -0
- package/sinain-memory/playbook_curator.py +225 -0
- package/sinain-memory/requirements.txt +3 -0
- package/sinain-memory/signal_analyzer.py +141 -0
- package/sinain-memory/test_local.py +402 -0
- package/sinain-memory/tests/__init__.py +0 -0
- package/sinain-memory/tests/conftest.py +189 -0
- package/sinain-memory/tests/test_curator_helpers.py +94 -0
- package/sinain-memory/tests/test_embedder.py +210 -0
- package/sinain-memory/tests/test_extract_json.py +124 -0
- package/sinain-memory/tests/test_feedback_computation.py +121 -0
- package/sinain-memory/tests/test_miner_helpers.py +71 -0
- package/sinain-memory/tests/test_module_management.py +458 -0
- package/sinain-memory/tests/test_parsers.py +96 -0
- package/sinain-memory/tests/test_tick_evaluator.py +430 -0
- package/sinain-memory/tests/test_triple_extractor.py +255 -0
- package/sinain-memory/tests/test_triple_ingest.py +191 -0
- package/sinain-memory/tests/test_triple_migrate.py +138 -0
- package/sinain-memory/tests/test_triplestore.py +248 -0
- package/sinain-memory/tick_evaluator.py +392 -0
- package/sinain-memory/triple_extractor.py +402 -0
- package/sinain-memory/triple_ingest.py +290 -0
- package/sinain-memory/triple_migrate.py +275 -0
- package/sinain-memory/triple_query.py +184 -0
- package/sinain-memory/triplestore.py +498 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Tests for module_manager.py: management commands (no LLM)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
import argparse
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from module_manager import (
|
|
11
|
+
cmd_list,
|
|
12
|
+
cmd_activate,
|
|
13
|
+
cmd_suspend,
|
|
14
|
+
cmd_priority,
|
|
15
|
+
cmd_stack,
|
|
16
|
+
cmd_info,
|
|
17
|
+
cmd_guidance,
|
|
18
|
+
cmd_export,
|
|
19
|
+
cmd_import,
|
|
20
|
+
_load_registry,
|
|
21
|
+
_save_registry,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _capture_stdout(func, modules_dir, args_dict=None):
|
|
26
|
+
"""Call a command function and capture its stdout JSON."""
|
|
27
|
+
args = argparse.Namespace(**(args_dict or {}))
|
|
28
|
+
with patch("sys.stdout", new_callable=StringIO) as mock_out:
|
|
29
|
+
func(modules_dir, args)
|
|
30
|
+
return json.loads(mock_out.getvalue())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestCmdList:
|
|
34
|
+
def test_lists_all_modules(self, tmp_modules_dir):
|
|
35
|
+
result = _capture_stdout(cmd_list, tmp_modules_dir)
|
|
36
|
+
modules = result["modules"]
|
|
37
|
+
ids = [m["id"] for m in modules]
|
|
38
|
+
assert "react-native-dev" in ids
|
|
39
|
+
assert "ocr-pipeline" in ids
|
|
40
|
+
|
|
41
|
+
def test_shows_status(self, tmp_modules_dir):
|
|
42
|
+
result = _capture_stdout(cmd_list, tmp_modules_dir)
|
|
43
|
+
modules = {m["id"]: m for m in result["modules"]}
|
|
44
|
+
assert modules["react-native-dev"]["status"] == "active"
|
|
45
|
+
assert modules["ocr-pipeline"]["status"] == "suspended"
|
|
46
|
+
|
|
47
|
+
def test_empty_registry(self, tmp_path):
|
|
48
|
+
modules = tmp_path / "modules"
|
|
49
|
+
modules.mkdir()
|
|
50
|
+
result = _capture_stdout(cmd_list, modules)
|
|
51
|
+
assert result["modules"] == []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestCmdActivate:
|
|
55
|
+
def test_activate_suspended_module(self, tmp_modules_dir):
|
|
56
|
+
# Create manifest for ocr-pipeline
|
|
57
|
+
ocr_dir = tmp_modules_dir / "ocr-pipeline"
|
|
58
|
+
ocr_dir.mkdir(exist_ok=True)
|
|
59
|
+
(ocr_dir / "manifest.json").write_text(json.dumps({
|
|
60
|
+
"id": "ocr-pipeline", "name": "OCR Pipeline",
|
|
61
|
+
"priority": {"default": 70, "range": [50, 100]},
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
result = _capture_stdout(cmd_activate, tmp_modules_dir, {"module_id": "ocr-pipeline", "priority": None})
|
|
65
|
+
assert result["activated"] == "ocr-pipeline"
|
|
66
|
+
assert result["status"] == "active"
|
|
67
|
+
|
|
68
|
+
reg = _load_registry(tmp_modules_dir)
|
|
69
|
+
assert reg["modules"]["ocr-pipeline"]["status"] == "active"
|
|
70
|
+
|
|
71
|
+
def test_activate_with_custom_priority(self, tmp_modules_dir):
|
|
72
|
+
ocr_dir = tmp_modules_dir / "ocr-pipeline"
|
|
73
|
+
ocr_dir.mkdir(exist_ok=True)
|
|
74
|
+
(ocr_dir / "manifest.json").write_text(json.dumps({
|
|
75
|
+
"id": "ocr-pipeline", "name": "OCR Pipeline",
|
|
76
|
+
"priority": {"default": 70, "range": [50, 100]},
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
result = _capture_stdout(cmd_activate, tmp_modules_dir, {"module_id": "ocr-pipeline", "priority": 90})
|
|
80
|
+
assert result["priority"] == 90
|
|
81
|
+
|
|
82
|
+
def test_activate_out_of_range(self, tmp_modules_dir):
|
|
83
|
+
ocr_dir = tmp_modules_dir / "ocr-pipeline"
|
|
84
|
+
ocr_dir.mkdir(exist_ok=True)
|
|
85
|
+
(ocr_dir / "manifest.json").write_text(json.dumps({
|
|
86
|
+
"id": "ocr-pipeline", "name": "OCR Pipeline",
|
|
87
|
+
"priority": {"default": 70, "range": [50, 100]},
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
with pytest.raises(SystemExit):
|
|
91
|
+
_capture_stdout(cmd_activate, tmp_modules_dir, {"module_id": "ocr-pipeline", "priority": 200})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestCmdSuspend:
|
|
95
|
+
def test_suspend_active_module(self, tmp_modules_dir):
|
|
96
|
+
result = _capture_stdout(cmd_suspend, tmp_modules_dir, {"module_id": "react-native-dev"})
|
|
97
|
+
assert result["suspended"] == "react-native-dev"
|
|
98
|
+
|
|
99
|
+
reg = _load_registry(tmp_modules_dir)
|
|
100
|
+
assert reg["modules"]["react-native-dev"]["status"] == "suspended"
|
|
101
|
+
|
|
102
|
+
def test_suspend_locked_module(self, tmp_modules_dir):
|
|
103
|
+
reg = _load_registry(tmp_modules_dir)
|
|
104
|
+
reg["modules"]["react-native-dev"]["locked"] = True
|
|
105
|
+
_save_registry(tmp_modules_dir, reg)
|
|
106
|
+
|
|
107
|
+
with pytest.raises(SystemExit):
|
|
108
|
+
_capture_stdout(cmd_suspend, tmp_modules_dir, {"module_id": "react-native-dev"})
|
|
109
|
+
|
|
110
|
+
def test_suspend_nonexistent(self, tmp_modules_dir):
|
|
111
|
+
with pytest.raises(SystemExit):
|
|
112
|
+
_capture_stdout(cmd_suspend, tmp_modules_dir, {"module_id": "nonexistent"})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestCmdPriority:
|
|
116
|
+
def test_change_priority(self, tmp_modules_dir):
|
|
117
|
+
result = _capture_stdout(cmd_priority, tmp_modules_dir,
|
|
118
|
+
{"module_id": "react-native-dev", "new_priority": 95})
|
|
119
|
+
assert result["priority"] == 95
|
|
120
|
+
|
|
121
|
+
reg = _load_registry(tmp_modules_dir)
|
|
122
|
+
assert reg["modules"]["react-native-dev"]["priority"] == 95
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestCmdStack:
|
|
126
|
+
def test_shows_active_and_suspended(self, tmp_modules_dir):
|
|
127
|
+
result = _capture_stdout(cmd_stack, tmp_modules_dir)
|
|
128
|
+
assert len(result["active"]) == 1
|
|
129
|
+
assert result["active"][0]["id"] == "react-native-dev"
|
|
130
|
+
assert len(result["suspended"]) == 1
|
|
131
|
+
|
|
132
|
+
def test_sorted_by_priority_desc(self, tmp_modules_dir):
|
|
133
|
+
# Add another active module
|
|
134
|
+
reg = _load_registry(tmp_modules_dir)
|
|
135
|
+
reg["modules"]["other-module"] = {
|
|
136
|
+
"status": "active", "priority": 95, "locked": False,
|
|
137
|
+
"activatedAt": None, "lastTriggered": None,
|
|
138
|
+
}
|
|
139
|
+
_save_registry(tmp_modules_dir, reg)
|
|
140
|
+
|
|
141
|
+
result = _capture_stdout(cmd_stack, tmp_modules_dir)
|
|
142
|
+
priorities = [m["priority"] for m in result["active"]]
|
|
143
|
+
assert priorities == sorted(priorities, reverse=True)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TestCmdInfo:
|
|
147
|
+
def test_shows_module_info(self, tmp_modules_dir):
|
|
148
|
+
result = _capture_stdout(cmd_info, tmp_modules_dir, {"module_id": "react-native-dev"})
|
|
149
|
+
assert result["id"] == "react-native-dev"
|
|
150
|
+
assert result["manifest"]["name"] == "React Native Development"
|
|
151
|
+
assert result["registry"]["status"] == "active"
|
|
152
|
+
assert result["patternsLines"] > 0
|
|
153
|
+
assert result["guidanceChars"] == 0
|
|
154
|
+
|
|
155
|
+
def test_info_with_guidance(self, tmp_modules_dir):
|
|
156
|
+
guidance = "When user asks about hot reload, suggest Hermes bytecode caching"
|
|
157
|
+
(tmp_modules_dir / "react-native-dev" / "guidance.md").write_text(guidance, encoding="utf-8")
|
|
158
|
+
result = _capture_stdout(cmd_info, tmp_modules_dir, {"module_id": "react-native-dev"})
|
|
159
|
+
assert result["guidanceChars"] == len(guidance)
|
|
160
|
+
|
|
161
|
+
def test_nonexistent_module(self, tmp_modules_dir):
|
|
162
|
+
with pytest.raises(SystemExit):
|
|
163
|
+
_capture_stdout(cmd_info, tmp_modules_dir, {"module_id": "nonexistent"})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestCmdGuidance:
|
|
167
|
+
def test_view_empty_guidance(self, tmp_modules_dir):
|
|
168
|
+
result = _capture_stdout(cmd_guidance, tmp_modules_dir, {
|
|
169
|
+
"module_id": "react-native-dev", "set": None, "clear": False,
|
|
170
|
+
})
|
|
171
|
+
assert result["module"] == "react-native-dev"
|
|
172
|
+
assert result["hasGuidance"] is False
|
|
173
|
+
assert result["guidance"] == ""
|
|
174
|
+
|
|
175
|
+
def test_set_guidance(self, tmp_modules_dir):
|
|
176
|
+
text = "When user asks about hot reload, suggest Hermes bytecode caching"
|
|
177
|
+
result = _capture_stdout(cmd_guidance, tmp_modules_dir, {
|
|
178
|
+
"module_id": "react-native-dev", "set": text, "clear": False,
|
|
179
|
+
})
|
|
180
|
+
assert result["written"] is True
|
|
181
|
+
assert result["guidanceChars"] == len(text)
|
|
182
|
+
# Verify file on disk
|
|
183
|
+
guidance_path = tmp_modules_dir / "react-native-dev" / "guidance.md"
|
|
184
|
+
assert guidance_path.read_text(encoding="utf-8") == text
|
|
185
|
+
|
|
186
|
+
def test_view_existing_guidance(self, tmp_modules_dir):
|
|
187
|
+
text = "Proactively suggest frame batching for OCR"
|
|
188
|
+
(tmp_modules_dir / "react-native-dev" / "guidance.md").write_text(text, encoding="utf-8")
|
|
189
|
+
result = _capture_stdout(cmd_guidance, tmp_modules_dir, {
|
|
190
|
+
"module_id": "react-native-dev", "set": None, "clear": False,
|
|
191
|
+
})
|
|
192
|
+
assert result["hasGuidance"] is True
|
|
193
|
+
assert result["guidance"] == text
|
|
194
|
+
|
|
195
|
+
def test_clear_guidance(self, tmp_modules_dir):
|
|
196
|
+
(tmp_modules_dir / "react-native-dev" / "guidance.md").write_text("some guidance", encoding="utf-8")
|
|
197
|
+
result = _capture_stdout(cmd_guidance, tmp_modules_dir, {
|
|
198
|
+
"module_id": "react-native-dev", "set": None, "clear": True,
|
|
199
|
+
})
|
|
200
|
+
assert result["cleared"] is True
|
|
201
|
+
assert not (tmp_modules_dir / "react-native-dev" / "guidance.md").exists()
|
|
202
|
+
|
|
203
|
+
def test_nonexistent_module(self, tmp_modules_dir):
|
|
204
|
+
with pytest.raises(SystemExit):
|
|
205
|
+
_capture_stdout(cmd_guidance, tmp_modules_dir, {
|
|
206
|
+
"module_id": "nonexistent", "set": None, "clear": False,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestCmdExport:
|
|
211
|
+
def test_export_produces_valid_bundle(self, tmp_modules_dir, tmp_path):
|
|
212
|
+
output = tmp_path / "export.sinain-module.json"
|
|
213
|
+
result = _capture_stdout(cmd_export, tmp_modules_dir, {
|
|
214
|
+
"module_id": "react-native-dev",
|
|
215
|
+
"output": str(output),
|
|
216
|
+
})
|
|
217
|
+
assert result["exported"] == "react-native-dev"
|
|
218
|
+
assert output.exists()
|
|
219
|
+
|
|
220
|
+
bundle = json.loads(output.read_text(encoding="utf-8"))
|
|
221
|
+
assert bundle["format"] == "sinain-module-v1"
|
|
222
|
+
assert bundle["moduleId"] == "react-native-dev"
|
|
223
|
+
assert "exportedAt" in bundle
|
|
224
|
+
assert bundle["manifest"]["name"] == "React Native Development"
|
|
225
|
+
assert "Hermes" in bundle["patterns"]
|
|
226
|
+
|
|
227
|
+
def test_export_includes_context_files(self, tmp_modules_dir, tmp_path):
|
|
228
|
+
# Add a context file
|
|
229
|
+
ctx_dir = tmp_modules_dir / "react-native-dev" / "context"
|
|
230
|
+
ctx_dir.mkdir()
|
|
231
|
+
(ctx_dir / "notes.json").write_text('{"key": "value"}', encoding="utf-8")
|
|
232
|
+
|
|
233
|
+
output = tmp_path / "export.sinain-module.json"
|
|
234
|
+
_capture_stdout(cmd_export, tmp_modules_dir, {
|
|
235
|
+
"module_id": "react-native-dev",
|
|
236
|
+
"output": str(output),
|
|
237
|
+
})
|
|
238
|
+
bundle = json.loads(output.read_text(encoding="utf-8"))
|
|
239
|
+
assert "notes.json" in bundle["context"]
|
|
240
|
+
assert bundle["context"]["notes.json"] == '{"key": "value"}'
|
|
241
|
+
|
|
242
|
+
def test_export_default_output_path(self, tmp_modules_dir):
|
|
243
|
+
result = _capture_stdout(cmd_export, tmp_modules_dir, {
|
|
244
|
+
"module_id": "react-native-dev",
|
|
245
|
+
"output": None,
|
|
246
|
+
})
|
|
247
|
+
default_path = Path("react-native-dev.sinain-module.json")
|
|
248
|
+
assert result["outputPath"] == str(default_path)
|
|
249
|
+
# Clean up
|
|
250
|
+
if default_path.exists():
|
|
251
|
+
default_path.unlink()
|
|
252
|
+
|
|
253
|
+
def test_export_includes_guidance(self, tmp_modules_dir, tmp_path):
|
|
254
|
+
guidance = "Suggest Hermes bytecode caching for hot reload"
|
|
255
|
+
(tmp_modules_dir / "react-native-dev" / "guidance.md").write_text(guidance, encoding="utf-8")
|
|
256
|
+
output = tmp_path / "export.sinain-module.json"
|
|
257
|
+
result = _capture_stdout(cmd_export, tmp_modules_dir, {
|
|
258
|
+
"module_id": "react-native-dev",
|
|
259
|
+
"output": str(output),
|
|
260
|
+
})
|
|
261
|
+
assert result["guidanceChars"] == len(guidance)
|
|
262
|
+
bundle = json.loads(output.read_text(encoding="utf-8"))
|
|
263
|
+
assert bundle["guidance"] == guidance
|
|
264
|
+
|
|
265
|
+
def test_export_no_guidance(self, tmp_modules_dir, tmp_path):
|
|
266
|
+
output = tmp_path / "export.sinain-module.json"
|
|
267
|
+
_capture_stdout(cmd_export, tmp_modules_dir, {
|
|
268
|
+
"module_id": "react-native-dev",
|
|
269
|
+
"output": str(output),
|
|
270
|
+
})
|
|
271
|
+
bundle = json.loads(output.read_text(encoding="utf-8"))
|
|
272
|
+
assert bundle["guidance"] == ""
|
|
273
|
+
|
|
274
|
+
def test_export_nonexistent_module(self, tmp_modules_dir):
|
|
275
|
+
with pytest.raises(SystemExit):
|
|
276
|
+
_capture_stdout(cmd_export, tmp_modules_dir, {
|
|
277
|
+
"module_id": "nonexistent",
|
|
278
|
+
"output": None,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class TestCmdImport:
|
|
283
|
+
def _make_bundle(self, tmp_path, module_id="test-module", patterns="# Test\n- pattern 1\n",
|
|
284
|
+
guidance="", context=None, manifest_extra=None):
|
|
285
|
+
"""Helper to create a valid bundle file."""
|
|
286
|
+
manifest = {
|
|
287
|
+
"id": module_id,
|
|
288
|
+
"name": "Test Module",
|
|
289
|
+
"version": "1.0.0",
|
|
290
|
+
"priority": {"default": 75, "range": [50, 100]},
|
|
291
|
+
"locked": False,
|
|
292
|
+
}
|
|
293
|
+
if manifest_extra:
|
|
294
|
+
manifest.update(manifest_extra)
|
|
295
|
+
bundle = {
|
|
296
|
+
"format": "sinain-module-v1",
|
|
297
|
+
"moduleId": module_id,
|
|
298
|
+
"exportedAt": "2026-03-05T12:00:00Z",
|
|
299
|
+
"manifest": manifest,
|
|
300
|
+
"patterns": patterns,
|
|
301
|
+
"guidance": guidance,
|
|
302
|
+
"context": context or {},
|
|
303
|
+
}
|
|
304
|
+
path = tmp_path / f"{module_id}.sinain-module.json"
|
|
305
|
+
path.write_text(json.dumps(bundle, indent=2), encoding="utf-8")
|
|
306
|
+
return path
|
|
307
|
+
|
|
308
|
+
def test_import_creates_module(self, tmp_modules_dir, tmp_path):
|
|
309
|
+
bundle_path = self._make_bundle(tmp_path)
|
|
310
|
+
result = _capture_stdout(cmd_import, tmp_modules_dir, {
|
|
311
|
+
"bundle": str(bundle_path),
|
|
312
|
+
"activate": False,
|
|
313
|
+
"force": False,
|
|
314
|
+
})
|
|
315
|
+
assert result["imported"] == "test-module"
|
|
316
|
+
assert result["status"] == "suspended"
|
|
317
|
+
|
|
318
|
+
# Check files created
|
|
319
|
+
module_dir = tmp_modules_dir / "test-module"
|
|
320
|
+
assert (module_dir / "manifest.json").exists()
|
|
321
|
+
assert (module_dir / "patterns.md").exists()
|
|
322
|
+
|
|
323
|
+
manifest = json.loads((module_dir / "manifest.json").read_text(encoding="utf-8"))
|
|
324
|
+
assert "importedAt" in manifest
|
|
325
|
+
assert manifest["source"] == "module_manager import"
|
|
326
|
+
|
|
327
|
+
def test_import_registers_as_suspended(self, tmp_modules_dir, tmp_path):
|
|
328
|
+
bundle_path = self._make_bundle(tmp_path)
|
|
329
|
+
_capture_stdout(cmd_import, tmp_modules_dir, {
|
|
330
|
+
"bundle": str(bundle_path),
|
|
331
|
+
"activate": False,
|
|
332
|
+
"force": False,
|
|
333
|
+
})
|
|
334
|
+
reg = _load_registry(tmp_modules_dir)
|
|
335
|
+
assert reg["modules"]["test-module"]["status"] == "suspended"
|
|
336
|
+
assert reg["modules"]["test-module"]["activatedAt"] is None
|
|
337
|
+
|
|
338
|
+
def test_import_with_activate(self, tmp_modules_dir, tmp_path):
|
|
339
|
+
bundle_path = self._make_bundle(tmp_path)
|
|
340
|
+
result = _capture_stdout(cmd_import, tmp_modules_dir, {
|
|
341
|
+
"bundle": str(bundle_path),
|
|
342
|
+
"activate": True,
|
|
343
|
+
"force": False,
|
|
344
|
+
})
|
|
345
|
+
assert result["status"] == "active"
|
|
346
|
+
|
|
347
|
+
reg = _load_registry(tmp_modules_dir)
|
|
348
|
+
assert reg["modules"]["test-module"]["status"] == "active"
|
|
349
|
+
assert reg["modules"]["test-module"]["activatedAt"] is not None
|
|
350
|
+
|
|
351
|
+
def test_import_with_context_files(self, tmp_modules_dir, tmp_path):
|
|
352
|
+
bundle_path = self._make_bundle(tmp_path, context={"config.json": '{"x": 1}'})
|
|
353
|
+
_capture_stdout(cmd_import, tmp_modules_dir, {
|
|
354
|
+
"bundle": str(bundle_path),
|
|
355
|
+
"activate": False,
|
|
356
|
+
"force": False,
|
|
357
|
+
})
|
|
358
|
+
ctx_file = tmp_modules_dir / "test-module" / "context" / "config.json"
|
|
359
|
+
assert ctx_file.exists()
|
|
360
|
+
assert ctx_file.read_text(encoding="utf-8") == '{"x": 1}'
|
|
361
|
+
|
|
362
|
+
def test_import_with_guidance(self, tmp_modules_dir, tmp_path):
|
|
363
|
+
guidance = "- Suggest frame batching for OCR\n- Prefer concise responses"
|
|
364
|
+
bundle_path = self._make_bundle(tmp_path, guidance=guidance)
|
|
365
|
+
_capture_stdout(cmd_import, tmp_modules_dir, {
|
|
366
|
+
"bundle": str(bundle_path),
|
|
367
|
+
"activate": False,
|
|
368
|
+
"force": False,
|
|
369
|
+
})
|
|
370
|
+
guidance_file = tmp_modules_dir / "test-module" / "guidance.md"
|
|
371
|
+
assert guidance_file.exists()
|
|
372
|
+
assert guidance_file.read_text(encoding="utf-8") == guidance
|
|
373
|
+
|
|
374
|
+
def test_import_without_guidance(self, tmp_modules_dir, tmp_path):
|
|
375
|
+
bundle_path = self._make_bundle(tmp_path, guidance="")
|
|
376
|
+
_capture_stdout(cmd_import, tmp_modules_dir, {
|
|
377
|
+
"bundle": str(bundle_path),
|
|
378
|
+
"activate": False,
|
|
379
|
+
"force": False,
|
|
380
|
+
})
|
|
381
|
+
assert not (tmp_modules_dir / "test-module" / "guidance.md").exists()
|
|
382
|
+
|
|
383
|
+
def test_import_existing_module_fails_without_force(self, tmp_modules_dir, tmp_path):
|
|
384
|
+
# react-native-dev already exists in tmp_modules_dir
|
|
385
|
+
bundle_path = self._make_bundle(tmp_path, module_id="react-native-dev")
|
|
386
|
+
with pytest.raises(SystemExit):
|
|
387
|
+
_capture_stdout(cmd_import, tmp_modules_dir, {
|
|
388
|
+
"bundle": str(bundle_path),
|
|
389
|
+
"activate": False,
|
|
390
|
+
"force": False,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
def test_import_with_force_overwrites(self, tmp_modules_dir, tmp_path):
|
|
394
|
+
bundle_path = self._make_bundle(
|
|
395
|
+
tmp_path, module_id="react-native-dev",
|
|
396
|
+
patterns="# Overwritten\n- new pattern\n",
|
|
397
|
+
)
|
|
398
|
+
result = _capture_stdout(cmd_import, tmp_modules_dir, {
|
|
399
|
+
"bundle": str(bundle_path),
|
|
400
|
+
"activate": False,
|
|
401
|
+
"force": True,
|
|
402
|
+
})
|
|
403
|
+
assert result["imported"] == "react-native-dev"
|
|
404
|
+
|
|
405
|
+
patterns = (tmp_modules_dir / "react-native-dev" / "patterns.md").read_text(encoding="utf-8")
|
|
406
|
+
assert "Overwritten" in patterns
|
|
407
|
+
|
|
408
|
+
def test_import_invalid_format_fails(self, tmp_modules_dir, tmp_path):
|
|
409
|
+
bad_bundle = tmp_path / "bad.sinain-module.json"
|
|
410
|
+
bad_bundle.write_text(json.dumps({"format": "unknown-v99"}), encoding="utf-8")
|
|
411
|
+
with pytest.raises(SystemExit):
|
|
412
|
+
_capture_stdout(cmd_import, tmp_modules_dir, {
|
|
413
|
+
"bundle": str(bad_bundle),
|
|
414
|
+
"activate": False,
|
|
415
|
+
"force": False,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
def test_roundtrip_export_import(self, tmp_modules_dir, tmp_path):
|
|
419
|
+
"""Export a module, import into same instance under new name, verify match."""
|
|
420
|
+
# Add guidance before export
|
|
421
|
+
guidance = "- Suggest Hermes bytecode caching\n- Recommend flipper for debugging"
|
|
422
|
+
(tmp_modules_dir / "react-native-dev" / "guidance.md").write_text(guidance, encoding="utf-8")
|
|
423
|
+
|
|
424
|
+
# Export existing module
|
|
425
|
+
export_path = tmp_path / "exported.sinain-module.json"
|
|
426
|
+
_capture_stdout(cmd_export, tmp_modules_dir, {
|
|
427
|
+
"module_id": "react-native-dev",
|
|
428
|
+
"output": str(export_path),
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
# Modify bundle to use different module ID (simulates transfer)
|
|
432
|
+
bundle = json.loads(export_path.read_text(encoding="utf-8"))
|
|
433
|
+
bundle["moduleId"] = "rn-dev-copy"
|
|
434
|
+
bundle["manifest"]["id"] = "rn-dev-copy"
|
|
435
|
+
modified_path = tmp_path / "modified.sinain-module.json"
|
|
436
|
+
modified_path.write_text(json.dumps(bundle), encoding="utf-8")
|
|
437
|
+
|
|
438
|
+
# Import
|
|
439
|
+
result = _capture_stdout(cmd_import, tmp_modules_dir, {
|
|
440
|
+
"bundle": str(modified_path),
|
|
441
|
+
"activate": True,
|
|
442
|
+
"force": False,
|
|
443
|
+
})
|
|
444
|
+
assert result["imported"] == "rn-dev-copy"
|
|
445
|
+
|
|
446
|
+
# Verify patterns match original
|
|
447
|
+
original = (tmp_modules_dir / "react-native-dev" / "patterns.md").read_text(encoding="utf-8")
|
|
448
|
+
copied = (tmp_modules_dir / "rn-dev-copy" / "patterns.md").read_text(encoding="utf-8")
|
|
449
|
+
assert original == copied
|
|
450
|
+
|
|
451
|
+
# Verify guidance survives roundtrip
|
|
452
|
+
copied_guidance = (tmp_modules_dir / "rn-dev-copy" / "guidance.md").read_text(encoding="utf-8")
|
|
453
|
+
assert copied_guidance == guidance
|
|
454
|
+
|
|
455
|
+
# Verify manifest has import metadata
|
|
456
|
+
manifest = json.loads((tmp_modules_dir / "rn-dev-copy" / "manifest.json").read_text(encoding="utf-8"))
|
|
457
|
+
assert manifest["importedAt"] is not None
|
|
458
|
+
assert manifest["name"] == "React Native Development"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Tests for common.py parser functions: parse_module_stack, parse_mining_index, parse_effectiveness."""
|
|
2
|
+
|
|
3
|
+
from common import parse_module_stack, parse_mining_index, parse_effectiveness
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestParseModuleStack:
|
|
7
|
+
def test_standard_stack(self):
|
|
8
|
+
text = "<!-- module-stack: react-native-dev(85), ocr-pipeline(70) -->\nplaybook body"
|
|
9
|
+
result = parse_module_stack(text)
|
|
10
|
+
assert len(result) == 2
|
|
11
|
+
assert result[0] == {"id": "react-native-dev", "priority": 85}
|
|
12
|
+
assert result[1] == {"id": "ocr-pipeline", "priority": 70}
|
|
13
|
+
|
|
14
|
+
def test_sorted_by_priority_desc(self):
|
|
15
|
+
text = "<!-- module-stack: low(10), high(90), mid(50) -->"
|
|
16
|
+
result = parse_module_stack(text)
|
|
17
|
+
assert result[0]["id"] == "high"
|
|
18
|
+
assert result[1]["id"] == "mid"
|
|
19
|
+
assert result[2]["id"] == "low"
|
|
20
|
+
|
|
21
|
+
def test_single_module(self):
|
|
22
|
+
text = "<!-- module-stack: only-one(42) -->"
|
|
23
|
+
result = parse_module_stack(text)
|
|
24
|
+
assert len(result) == 1
|
|
25
|
+
assert result[0] == {"id": "only-one", "priority": 42}
|
|
26
|
+
|
|
27
|
+
def test_no_priority_parentheses(self):
|
|
28
|
+
text = "<!-- module-stack: bare-module -->"
|
|
29
|
+
result = parse_module_stack(text)
|
|
30
|
+
assert len(result) == 1
|
|
31
|
+
assert result[0] == {"id": "bare-module", "priority": 0}
|
|
32
|
+
|
|
33
|
+
def test_absent_comment(self):
|
|
34
|
+
text = "Just a regular playbook with no module-stack comment"
|
|
35
|
+
assert parse_module_stack(text) == []
|
|
36
|
+
|
|
37
|
+
def test_empty_stack(self):
|
|
38
|
+
text = "<!-- module-stack: -->"
|
|
39
|
+
assert parse_module_stack(text) == []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestParseMiningIndex:
|
|
43
|
+
def test_standard_index(self):
|
|
44
|
+
text = "<!-- mining-index: 2026-02-21,2026-02-20,2026-02-19 -->"
|
|
45
|
+
result = parse_mining_index(text)
|
|
46
|
+
assert result == ["2026-02-21", "2026-02-20", "2026-02-19"]
|
|
47
|
+
|
|
48
|
+
def test_single_date(self):
|
|
49
|
+
text = "<!-- mining-index: 2026-02-21 -->"
|
|
50
|
+
result = parse_mining_index(text)
|
|
51
|
+
assert result == ["2026-02-21"]
|
|
52
|
+
|
|
53
|
+
def test_empty_index(self):
|
|
54
|
+
text = "<!-- mining-index: -->"
|
|
55
|
+
result = parse_mining_index(text)
|
|
56
|
+
assert result == []
|
|
57
|
+
|
|
58
|
+
def test_absent_comment(self):
|
|
59
|
+
text = "No mining index here"
|
|
60
|
+
assert parse_mining_index(text) == []
|
|
61
|
+
|
|
62
|
+
def test_extra_whitespace(self):
|
|
63
|
+
text = "<!-- mining-index: 2026-02-21 , 2026-02-20 -->"
|
|
64
|
+
result = parse_mining_index(text)
|
|
65
|
+
assert result == ["2026-02-21", "2026-02-20"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestParseEffectiveness:
|
|
69
|
+
def test_standard_metrics(self):
|
|
70
|
+
text = "<!-- effectiveness: outputs=8,positive=5,negative=1,neutral=2,rate=0.63,updated=2026-02-21 -->"
|
|
71
|
+
result = parse_effectiveness(text)
|
|
72
|
+
assert result is not None
|
|
73
|
+
assert result["outputs"] == 8
|
|
74
|
+
assert result["positive"] == 5
|
|
75
|
+
assert result["rate"] == 0.63
|
|
76
|
+
assert result["updated"] == "2026-02-21"
|
|
77
|
+
|
|
78
|
+
def test_absent_comment(self):
|
|
79
|
+
assert parse_effectiveness("No effectiveness comment") is None
|
|
80
|
+
|
|
81
|
+
def test_integer_conversion(self):
|
|
82
|
+
text = "<!-- effectiveness: outputs=10 -->"
|
|
83
|
+
result = parse_effectiveness(text)
|
|
84
|
+
assert result["outputs"] == 10
|
|
85
|
+
assert isinstance(result["outputs"], int)
|
|
86
|
+
|
|
87
|
+
def test_float_conversion(self):
|
|
88
|
+
text = "<!-- effectiveness: rate=0.75 -->"
|
|
89
|
+
result = parse_effectiveness(text)
|
|
90
|
+
assert result["rate"] == 0.75
|
|
91
|
+
assert isinstance(result["rate"], float)
|
|
92
|
+
|
|
93
|
+
def test_string_value(self):
|
|
94
|
+
text = "<!-- effectiveness: updated=2026-02-21 -->"
|
|
95
|
+
result = parse_effectiveness(text)
|
|
96
|
+
assert result["updated"] == "2026-02-21"
|