@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,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"