@geravant/sinain 1.12.0 → 1.14.0
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/.env.example +4 -2
- package/config-shared.js +1 -0
- package/package.json +4 -1
- package/sinain-agent/run.sh +36 -4
- package/sinain-core/package-lock.json +963 -0
- package/sinain-core/package.json +1 -0
- package/sinain-core/src/buffers/feed-buffer.ts +34 -0
- package/sinain-core/src/embedding/service.ts +66 -0
- package/sinain-core/src/index.ts +65 -17
- package/sinain-core/src/learning/local-curation.ts +137 -7
- package/sinain-core/src/server.ts +31 -0
- package/sinain-memory/README.md +105 -0
- package/sinain-memory/embed_client.py +117 -0
- package/sinain-memory/graph_query.py +269 -18
- package/sinain-memory/knowledge_integrator.py +551 -74
- package/sinain-memory/memory-config.json +1 -1
- package/sinain-memory/session_distiller.py +43 -19
- package/sinain-memory/triplestore.py +60 -0
- package/sinain-memory/__pycache__/common.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/graph_query.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/knowledge_integrator.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/session_distiller.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
- package/sinain-memory/eval/__init__.py +0 -0
- package/sinain-memory/eval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/assertions.py +0 -267
- package/sinain-memory/eval/benchmarks/__init__.py +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/base_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/config.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/evaluate.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/ingest.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/longmemeval_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/query.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/report.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/base_adapter.py +0 -43
- package/sinain-memory/eval/benchmarks/config.py +0 -23
- package/sinain-memory/eval/benchmarks/evaluate.py +0 -146
- package/sinain-memory/eval/benchmarks/ingest.py +0 -152
- package/sinain-memory/eval/benchmarks/judges/__init__.py +0 -0
- package/sinain-memory/eval/benchmarks/judges/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/judges/__pycache__/qa_judge.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/judges/qa_judge.py +0 -81
- package/sinain-memory/eval/benchmarks/longmemeval_adapter.py +0 -177
- package/sinain-memory/eval/benchmarks/query.py +0 -172
- package/sinain-memory/eval/benchmarks/report.py +0 -87
- package/sinain-memory/eval/benchmarks/runner.py +0 -276
- package/sinain-memory/eval/judges/__init__.py +0 -0
- package/sinain-memory/eval/judges/base_judge.py +0 -61
- package/sinain-memory/eval/judges/curation_judge.py +0 -46
- package/sinain-memory/eval/judges/insight_judge.py +0 -48
- package/sinain-memory/eval/judges/mining_judge.py +0 -42
- package/sinain-memory/eval/judges/signal_judge.py +0 -45
- package/sinain-memory/eval/retrieval_benchmark.jsonl +0 -12
- package/sinain-memory/eval/retrieval_evaluator.py +0 -186
- package/sinain-memory/eval/schemas.py +0 -247
- package/sinain-memory/tests/__init__.py +0 -0
- package/sinain-memory/tests/conftest.py +0 -189
- package/sinain-memory/tests/test_curator_helpers.py +0 -94
- package/sinain-memory/tests/test_embedder.py +0 -210
- package/sinain-memory/tests/test_extract_json.py +0 -124
- package/sinain-memory/tests/test_feedback_computation.py +0 -121
- package/sinain-memory/tests/test_miner_helpers.py +0 -71
- package/sinain-memory/tests/test_module_management.py +0 -458
- package/sinain-memory/tests/test_parsers.py +0 -96
- package/sinain-memory/tests/test_tick_evaluator.py +0 -430
- package/sinain-memory/tests/test_triple_extractor.py +0 -255
- package/sinain-memory/tests/test_triple_ingest.py +0 -191
- package/sinain-memory/tests/test_triple_migrate.py +0 -138
- package/sinain-memory/tests/test_triplestore.py +0 -248
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Tests for feedback_analyzer.py: compute_effectiveness() and determine_directive()."""
|
|
2
|
-
|
|
3
|
-
from feedback_analyzer import compute_effectiveness, determine_directive, extract_feedback_scores
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class TestComputeEffectiveness:
|
|
7
|
-
def test_no_logs(self):
|
|
8
|
-
result = compute_effectiveness([])
|
|
9
|
-
assert result == {"outputs": 0, "positive": 0, "negative": 0, "neutral": 0, "rate": 0.0}
|
|
10
|
-
|
|
11
|
-
def test_all_skipped(self):
|
|
12
|
-
logs = [{"ts": "2026-02-28T10:00:00Z", "skipped": True}] * 5
|
|
13
|
-
result = compute_effectiveness(logs)
|
|
14
|
-
assert result["outputs"] == 0
|
|
15
|
-
|
|
16
|
-
def test_positive_outcomes(self):
|
|
17
|
-
logs = [
|
|
18
|
-
{"ts": "2026-02-28T10:00:00Z", "skipped": False},
|
|
19
|
-
{"ts": "2026-02-28T10:30:00Z", "skipped": True, "feedbackScores": {"avg": 0.5}},
|
|
20
|
-
{"ts": "2026-02-28T11:00:00Z", "skipped": False},
|
|
21
|
-
{"ts": "2026-02-28T11:30:00Z", "skipped": True, "feedbackScores": {"avg": 0.6}},
|
|
22
|
-
]
|
|
23
|
-
result = compute_effectiveness(logs)
|
|
24
|
-
assert result["outputs"] == 2
|
|
25
|
-
assert result["positive"] == 2
|
|
26
|
-
assert result["rate"] == 1.0
|
|
27
|
-
|
|
28
|
-
def test_negative_outcomes(self):
|
|
29
|
-
logs = [
|
|
30
|
-
{"ts": "2026-02-28T10:00:00Z", "skipped": False},
|
|
31
|
-
{"ts": "2026-02-28T10:30:00Z", "skipped": True, "feedbackScores": {"avg": -0.5}},
|
|
32
|
-
]
|
|
33
|
-
result = compute_effectiveness(logs)
|
|
34
|
-
assert result["negative"] == 1
|
|
35
|
-
|
|
36
|
-
def test_neutral_for_last_tick(self):
|
|
37
|
-
"""Last tick with output has no next tick for feedback — counted as neutral."""
|
|
38
|
-
logs = [{"ts": "2026-02-28T10:00:00Z", "skipped": False}]
|
|
39
|
-
result = compute_effectiveness(logs)
|
|
40
|
-
assert result["neutral"] == 1
|
|
41
|
-
|
|
42
|
-
def test_mixed_outcomes(self):
|
|
43
|
-
logs = [
|
|
44
|
-
{"ts": "2026-02-28T10:00:00Z", "skipped": False},
|
|
45
|
-
{"ts": "2026-02-28T10:30:00Z", "feedbackScores": {"avg": 0.5}}, # positive
|
|
46
|
-
{"ts": "2026-02-28T11:00:00Z", "skipped": False},
|
|
47
|
-
{"ts": "2026-02-28T11:30:00Z", "feedbackScores": {"avg": -0.3}}, # negative
|
|
48
|
-
{"ts": "2026-02-28T12:00:00Z", "skipped": False},
|
|
49
|
-
{"ts": "2026-02-28T12:30:00Z", "feedbackScores": {"avg": 0.05}}, # neutral
|
|
50
|
-
]
|
|
51
|
-
result = compute_effectiveness(logs)
|
|
52
|
-
assert result["outputs"] == 3
|
|
53
|
-
assert result["positive"] == 1
|
|
54
|
-
assert result["negative"] == 1
|
|
55
|
-
assert result["neutral"] == 1
|
|
56
|
-
assert result["rate"] == round(1 / 3, 2)
|
|
57
|
-
|
|
58
|
-
def test_rate_is_rounded(self):
|
|
59
|
-
logs = [
|
|
60
|
-
{"ts": f"2026-02-28T1{i}:00:00Z", "skipped": False}
|
|
61
|
-
for i in range(3)
|
|
62
|
-
] + [
|
|
63
|
-
{"ts": f"2026-02-28T1{i}:30:00Z", "feedbackScores": {"avg": 0.5}}
|
|
64
|
-
for i in range(3)
|
|
65
|
-
]
|
|
66
|
-
result = compute_effectiveness(logs)
|
|
67
|
-
assert isinstance(result["rate"], float)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class TestDetermineDirective:
|
|
71
|
-
def test_insufficient_data(self):
|
|
72
|
-
assert determine_directive({"outputs": 3, "rate": 0.8}) == "insufficient_data"
|
|
73
|
-
assert determine_directive({"outputs": 0, "rate": 0.0}) == "insufficient_data"
|
|
74
|
-
assert determine_directive({"outputs": 4, "rate": 0.5}) == "insufficient_data"
|
|
75
|
-
|
|
76
|
-
def test_aggressive_prune(self):
|
|
77
|
-
assert determine_directive({"outputs": 10, "rate": 0.2}) == "aggressive_prune"
|
|
78
|
-
assert determine_directive({"outputs": 5, "rate": 0.39}) == "aggressive_prune"
|
|
79
|
-
|
|
80
|
-
def test_stability(self):
|
|
81
|
-
assert determine_directive({"outputs": 10, "rate": 0.8}) == "stability"
|
|
82
|
-
assert determine_directive({"outputs": 5, "rate": 0.71}) == "stability"
|
|
83
|
-
|
|
84
|
-
def test_normal(self):
|
|
85
|
-
assert determine_directive({"outputs": 10, "rate": 0.5}) == "normal"
|
|
86
|
-
assert determine_directive({"outputs": 5, "rate": 0.4}) == "normal"
|
|
87
|
-
assert determine_directive({"outputs": 5, "rate": 0.7}) == "normal"
|
|
88
|
-
|
|
89
|
-
def test_boundary_values(self):
|
|
90
|
-
# Exactly 5 outputs is enough data
|
|
91
|
-
assert determine_directive({"outputs": 5, "rate": 0.5}) != "insufficient_data"
|
|
92
|
-
# rate=0.4 is normal (not aggressive_prune)
|
|
93
|
-
assert determine_directive({"outputs": 5, "rate": 0.4}) == "normal"
|
|
94
|
-
# rate=0.7 is normal (not stability)
|
|
95
|
-
assert determine_directive({"outputs": 5, "rate": 0.7}) == "normal"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class TestExtractFeedbackScores:
|
|
99
|
-
def test_empty_logs(self):
|
|
100
|
-
result = extract_feedback_scores([])
|
|
101
|
-
assert result["avg"] == 0
|
|
102
|
-
assert result["high"] == []
|
|
103
|
-
assert result["low"] == []
|
|
104
|
-
|
|
105
|
-
def test_with_scores(self):
|
|
106
|
-
logs = [
|
|
107
|
-
{"feedbackScores": {"avg": 0.5, "high": ["good A"], "low": ["bad A"]}},
|
|
108
|
-
{"feedbackScores": {"avg": 0.3, "high": ["good B"], "low": []}},
|
|
109
|
-
]
|
|
110
|
-
result = extract_feedback_scores(logs)
|
|
111
|
-
assert result["avg"] == 0.4
|
|
112
|
-
assert "good A" in result["high"]
|
|
113
|
-
assert "good B" in result["high"]
|
|
114
|
-
|
|
115
|
-
def test_limits_to_5_patterns(self):
|
|
116
|
-
logs = [
|
|
117
|
-
{"feedbackScores": {"avg": 0.1, "high": [f"pattern {i}"], "low": []}}
|
|
118
|
-
for i in range(10)
|
|
119
|
-
]
|
|
120
|
-
result = extract_feedback_scores(logs)
|
|
121
|
-
assert len(result["high"]) <= 5
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Tests for memory_miner.py: get_unmined_files() and update_mining_index()."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from memory_miner import get_unmined_files, update_mining_index
|
|
5
|
-
from common import parse_mining_index
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TestGetUnminedFiles:
|
|
9
|
-
def test_all_mined(self, tmp_memory_dir):
|
|
10
|
-
memory_dir = str(tmp_memory_dir)
|
|
11
|
-
mined = ["2026-02-21", "2026-02-20", "2026-02-19"]
|
|
12
|
-
result = get_unmined_files(memory_dir, mined)
|
|
13
|
-
assert result == []
|
|
14
|
-
|
|
15
|
-
def test_some_unmined(self, tmp_memory_dir):
|
|
16
|
-
memory_dir = str(tmp_memory_dir)
|
|
17
|
-
mined = ["2026-02-21"]
|
|
18
|
-
result = get_unmined_files(memory_dir, mined)
|
|
19
|
-
basenames = [Path(f).stem for f in result]
|
|
20
|
-
assert "2026-02-20" in basenames
|
|
21
|
-
assert "2026-02-19" in basenames
|
|
22
|
-
assert "2026-02-21" not in basenames
|
|
23
|
-
|
|
24
|
-
def test_none_mined(self, tmp_memory_dir):
|
|
25
|
-
memory_dir = str(tmp_memory_dir)
|
|
26
|
-
result = get_unmined_files(memory_dir, [])
|
|
27
|
-
assert len(result) == 3 # all 3 daily files
|
|
28
|
-
|
|
29
|
-
def test_no_daily_files(self, tmp_path):
|
|
30
|
-
empty_memory = tmp_path / "empty-memory"
|
|
31
|
-
empty_memory.mkdir()
|
|
32
|
-
result = get_unmined_files(str(empty_memory), [])
|
|
33
|
-
assert result == []
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class TestUpdateMiningIndex:
|
|
37
|
-
def test_adds_new_dates(self, tmp_memory_dir):
|
|
38
|
-
memory_dir = str(tmp_memory_dir)
|
|
39
|
-
playbook = (tmp_memory_dir / "sinain-playbook.md").read_text()
|
|
40
|
-
update_mining_index(memory_dir, playbook, ["2026-02-22"])
|
|
41
|
-
|
|
42
|
-
updated = (tmp_memory_dir / "sinain-playbook.md").read_text()
|
|
43
|
-
index = parse_mining_index(updated)
|
|
44
|
-
assert "2026-02-22" in index
|
|
45
|
-
|
|
46
|
-
def test_deduplicates(self, tmp_memory_dir):
|
|
47
|
-
memory_dir = str(tmp_memory_dir)
|
|
48
|
-
playbook = (tmp_memory_dir / "sinain-playbook.md").read_text()
|
|
49
|
-
# 2026-02-21 is already in index
|
|
50
|
-
update_mining_index(memory_dir, playbook, ["2026-02-21"])
|
|
51
|
-
|
|
52
|
-
updated = (tmp_memory_dir / "sinain-playbook.md").read_text()
|
|
53
|
-
index = parse_mining_index(updated)
|
|
54
|
-
assert index.count("2026-02-21") == 1
|
|
55
|
-
|
|
56
|
-
def test_creates_playbook_if_missing(self, tmp_path):
|
|
57
|
-
memory_dir = str(tmp_path)
|
|
58
|
-
update_mining_index(memory_dir, "", ["2026-02-25"])
|
|
59
|
-
|
|
60
|
-
playbook = (tmp_path / "sinain-playbook.md").read_text()
|
|
61
|
-
assert "mining-index:" in playbook
|
|
62
|
-
assert "2026-02-25" in playbook
|
|
63
|
-
|
|
64
|
-
def test_sorted_descending(self, tmp_memory_dir):
|
|
65
|
-
memory_dir = str(tmp_memory_dir)
|
|
66
|
-
playbook = (tmp_memory_dir / "sinain-playbook.md").read_text()
|
|
67
|
-
update_mining_index(memory_dir, playbook, ["2026-02-22", "2026-02-23"])
|
|
68
|
-
|
|
69
|
-
updated = (tmp_memory_dir / "sinain-playbook.md").read_text()
|
|
70
|
-
index = parse_mining_index(updated)
|
|
71
|
-
assert index == sorted(index, reverse=True)
|
|
@@ -1,458 +0,0 @@
|
|
|
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"
|