@misterhuydo/sentinel 1.5.4 → 1.5.6

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 (38) hide show
  1. package/.cairn/.hint-lock +1 -1
  2. package/.cairn/minify-map.json +13 -1
  3. package/.cairn/session.json +2 -2
  4. package/.cairn/views/23edf4_sentinel_boss.py +3664 -0
  5. package/.cairn/views/5f5141_main.py +1067 -0
  6. package/.cairn/views/62a614_bundle.js +4 -1
  7. package/.cairn/views/7802b9_cicd_trigger.py +171 -0
  8. package/.cairn/views/ac3df4_repo_task_engine.py +351 -0
  9. package/lib/.cairn/minify-map.json +6 -0
  10. package/lib/.cairn/views/2a85cc_init.js +380 -0
  11. package/lib/.cairn/views/e26996_slack-setup.js +97 -0
  12. package/lib/.cairn/views/fb78ac_upgrade.js +36 -1
  13. package/lib/.cairn/views/fc4a1a_add.js +164 -51
  14. package/lib/init.js +54 -0
  15. package/lib/maven.js +212 -0
  16. package/lib/slack-setup.js +5 -0
  17. package/package.json +1 -1
  18. package/python/requirements.txt +1 -0
  19. package/python/sentinel/.cairn/.cairn-project +0 -0
  20. package/python/sentinel/.cairn/.hint-lock +1 -0
  21. package/python/sentinel/.cairn/session.json +9 -0
  22. package/python/sentinel/__pycache__/sentinel_boss.cpython-311.pyc +0 -0
  23. package/python/sentinel/config_loader.py +29 -10
  24. package/python/sentinel/dependency_manager.py +9 -2
  25. package/python/sentinel/git_manager.py +23 -0
  26. package/python/sentinel/issue_watcher.py +7 -1
  27. package/python/sentinel/main.py +353 -8
  28. package/python/sentinel/notify.py +44 -12
  29. package/python/sentinel/repo_task_engine.py +49 -7
  30. package/python/sentinel/sentinel_boss.py +117 -3
  31. package/python/sentinel/slack_bot.py +15 -2
  32. package/python/sentinel/state_store.py +0 -1
  33. package/python/tests/__init__.py +0 -0
  34. package/python/tests/test_config_loader.py +138 -0
  35. package/python/tests/test_log_parser.py +62 -0
  36. package/python/tests/test_repo_router.py +73 -0
  37. package/python/tests/test_smoke.py +96 -0
  38. package/python/tests/test_state_store.py +128 -0
@@ -0,0 +1,73 @@
1
+ """
2
+ test_repo_router.py — Unit tests for repo routing logic.
3
+ """
4
+ import pytest
5
+ from sentinel.repo_router import route
6
+ from sentinel.log_parser import ErrorEvent
7
+ from sentinel.config_loader import RepoConfig
8
+
9
+
10
+ def _make_event(source: str, fingerprint: str = "fp1") -> ErrorEvent:
11
+ return ErrorEvent(
12
+ source=source,
13
+ fingerprint=fingerprint,
14
+ level="ERROR",
15
+ timestamp="2026-01-01T00:00:00",
16
+ log_file="",
17
+ thread="",
18
+ logger_name="com.example.Test",
19
+ message="some error",
20
+ stack_trace=[],
21
+ )
22
+
23
+
24
+ def _make_repo(name: str) -> RepoConfig:
25
+ r = RepoConfig()
26
+ r.repo_name = name
27
+ r.repo_url = f"git@github.com:org/{name}.git"
28
+ r.local_path = f"/repos/{name}"
29
+ return r
30
+
31
+
32
+ # ── routing by source name ────────────────────────────────────────────────────
33
+
34
+ def test_routes_by_source_name():
35
+ repos = {"my-service": _make_repo("my-service")}
36
+ event = _make_event("my-service")
37
+ result = route(event, repos)
38
+ assert result is not None
39
+ assert result.repo_name == "my-service"
40
+
41
+
42
+ def test_returns_none_for_unknown_source(caplog):
43
+ repos = {"other-service": _make_repo("other-service")}
44
+ event = _make_event("unknown-source")
45
+ result = route(event, repos)
46
+ assert result is None
47
+
48
+
49
+ def test_returns_none_for_empty_repos():
50
+ event = _make_event("any-source")
51
+ result = route(event, {})
52
+ assert result is None
53
+
54
+
55
+ def test_exact_match_only():
56
+ repos = {
57
+ "service-a": _make_repo("service-a"),
58
+ "service-b": _make_repo("service-b"),
59
+ }
60
+ event = _make_event("service-a")
61
+ result = route(event, repos)
62
+ assert result.repo_name == "service-a"
63
+
64
+
65
+ def test_multiple_repos_correct_routing():
66
+ repos = {
67
+ "auth-service": _make_repo("auth-service"),
68
+ "order-service": _make_repo("order-service"),
69
+ }
70
+ for source, expected in [("auth-service", "auth-service"), ("order-service", "order-service")]:
71
+ result = route(_make_event(source), repos)
72
+ assert result is not None
73
+ assert result.repo_name == expected
@@ -0,0 +1,96 @@
1
+ """
2
+ test_smoke.py — Import-level smoke tests.
3
+ If any of these fail, a Patch fix broke a module import or removed a critical symbol.
4
+ """
5
+ import textwrap
6
+
7
+
8
+ def test_import_config_loader():
9
+ from sentinel.config_loader import ConfigLoader, SentinelConfig, RepoConfig, LogSourceConfig
10
+ assert callable(ConfigLoader)
11
+ assert hasattr(SentinelConfig, "__dataclass_fields__")
12
+ assert hasattr(RepoConfig, "ssh_key_file") # property must exist
13
+
14
+
15
+ def test_import_log_parser():
16
+ from sentinel.log_parser import parse_log_file, ErrorEvent, _fingerprint
17
+ assert callable(parse_log_file)
18
+ assert callable(_fingerprint)
19
+
20
+
21
+ def test_import_repo_router():
22
+ from sentinel.repo_router import route
23
+ assert callable(route)
24
+
25
+
26
+ def test_import_state_store():
27
+ from sentinel.state_store import StateStore
28
+ assert callable(StateStore)
29
+
30
+
31
+ def test_import_notify():
32
+ from sentinel.notify import slack_alert, notify_fix_applied
33
+ assert callable(slack_alert)
34
+ assert callable(notify_fix_applied)
35
+
36
+
37
+ def test_import_git_manager():
38
+ from sentinel.git_manager import _git_env
39
+ assert callable(_git_env)
40
+
41
+
42
+ def test_ssh_key_priority(tmp_path):
43
+ """Deploy key takes priority over user key on RepoConfig.ssh_key_file."""
44
+ from sentinel.config_loader import ConfigLoader
45
+
46
+ (tmp_path / "log-configs").mkdir()
47
+ (tmp_path / "repo-configs").mkdir()
48
+ (tmp_path / "sentinel.properties").write_text(
49
+ "GIT_SSH_USER_KEY=~/.ssh/user.key\n"
50
+ )
51
+ (tmp_path / "repo-configs" / "my-repo.properties").write_text(textwrap.dedent("""\
52
+ REPO_URL=git@github.com:org/my-repo.git
53
+ GIT_SSH_DEPLOY_KEY=~/.ssh/deploy.key
54
+ """))
55
+
56
+ loader = ConfigLoader(str(tmp_path))
57
+ repo = loader.repos["my-repo"]
58
+ assert "deploy.key" in repo.ssh_key_file # deploy key wins
59
+
60
+
61
+ def test_ssh_user_key_fallback(tmp_path):
62
+ """User key is used when no deploy key is set."""
63
+ from sentinel.config_loader import ConfigLoader
64
+
65
+ (tmp_path / "log-configs").mkdir()
66
+ (tmp_path / "repo-configs").mkdir()
67
+ (tmp_path / "sentinel.properties").write_text(
68
+ "GIT_SSH_USER_KEY=~/.ssh/user.key\n"
69
+ )
70
+ (tmp_path / "repo-configs" / "my-repo.properties").write_text(
71
+ "REPO_URL=git@github.com:org/my-repo.git\n"
72
+ )
73
+
74
+ loader = ConfigLoader(str(tmp_path))
75
+ repo = loader.repos["my-repo"]
76
+ assert "user.key" in repo.ssh_key_file # falls back to user key
77
+
78
+
79
+ def test_severity_critical_on_oom():
80
+ from sentinel.log_parser import ErrorEvent
81
+ e = ErrorEvent(
82
+ source="svc", log_file="", timestamp="2026-01-01T00:00:00",
83
+ level="ERROR", thread="main", logger_name="com.example.App",
84
+ message="java.lang.OutOfMemoryError: Java heap space",
85
+ )
86
+ assert e.severity == "CRITICAL"
87
+
88
+
89
+ def test_severity_infra_issue():
90
+ from sentinel.log_parser import ErrorEvent
91
+ e = ErrorEvent(
92
+ source="svc", log_file="", timestamp="2026-01-01T00:00:00",
93
+ level="ERROR", thread="main", logger_name="com.example.App",
94
+ message="ConnectException: Connection refused",
95
+ )
96
+ assert e.is_infra_issue is True
@@ -0,0 +1,128 @@
1
+ """
2
+ test_state_store.py — Unit tests for StateStore (in-memory SQLite).
3
+ """
4
+ import pytest
5
+ from sentinel.state_store import StateStore
6
+
7
+
8
+ @pytest.fixture
9
+ def store(tmp_path):
10
+ return StateStore(str(tmp_path / "test.db"))
11
+
12
+
13
+ # ── errors ────────────────────────────────────────────────────────────────────
14
+
15
+ def test_record_and_get_error(store):
16
+ store.record_error("fp1", "source-A", "NullPointerException in Foo.bar")
17
+ errors = store.get_recent_errors(24)
18
+ assert len(errors) == 1
19
+ assert errors[0]["fingerprint"] == "fp1"
20
+ assert errors[0]["source"] == "source-A"
21
+ assert errors[0]["count"] == 1
22
+
23
+
24
+ def test_error_count_increments(store):
25
+ store.record_error("fp1", "source-A", "some error")
26
+ store.record_error("fp1", "source-A", "some error")
27
+ errors = store.get_recent_errors(24)
28
+ assert errors[0]["count"] == 2
29
+
30
+
31
+ def test_is_seen(store):
32
+ assert not store.seen("fp1")
33
+ store.record_error("fp1", "source-A", "msg")
34
+ assert store.seen("fp1")
35
+
36
+
37
+ # ── fixes ─────────────────────────────────────────────────────────────────────
38
+
39
+ def test_record_fix(store):
40
+ store.record_error("fp1", "src", "msg")
41
+ store.record_fix("fp1", "applied", patch_path="/tmp/x.diff",
42
+ commit_hash="abc123", branch="sentinel/fix-fp1", repo_name="my-repo")
43
+ fixes = store.get_recent_fixes(24)
44
+ assert len(fixes) == 1
45
+ assert fixes[0]["status"] == "applied"
46
+ assert fixes[0]["repo_name"] == "my-repo"
47
+
48
+
49
+ def test_get_open_prs(store):
50
+ store.record_error("fp1", "src", "msg")
51
+ store.record_fix("fp1", "pending", pr_url="https://github.com/org/repo/pull/1",
52
+ branch="sentinel/fix-fp1", repo_name="repo")
53
+ prs = store.get_open_prs()
54
+ assert len(prs) == 1
55
+ assert prs[0]["pr_url"] == "https://github.com/org/repo/pull/1"
56
+
57
+
58
+ # ── slack_users ───────────────────────────────────────────────────────────────
59
+
60
+ def test_upsert_and_get_user(store):
61
+ store.upsert_user("U123", "huy")
62
+ assert store.get_user_name("U123") == "huy"
63
+
64
+
65
+ def test_get_user_unknown_returns_id(store):
66
+ assert store.get_user_name("UNKNOWN") == "UNKNOWN"
67
+
68
+
69
+ def test_get_all_users(store):
70
+ store.upsert_user("U1", "alice")
71
+ store.upsert_user("U2", "bob")
72
+ users = store.get_all_users()
73
+ assert users == {"U1": "alice", "U2": "bob"}
74
+
75
+
76
+ def test_upsert_user_updates_name(store):
77
+ store.upsert_user("U1", "old-name")
78
+ store.upsert_user("U1", "new-name")
79
+ assert store.get_user_name("U1") == "new-name"
80
+
81
+
82
+ # ── conversations ─────────────────────────────────────────────────────────────
83
+
84
+ def test_save_and_load_conversation(store):
85
+ history = [{"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi"}]
86
+ store.save_conversation("U1", history)
87
+ loaded = store.load_conversation("U1")
88
+ assert loaded == history
89
+
90
+
91
+ def test_load_conversation_empty(store):
92
+ assert store.load_conversation("U_NOBODY") == []
93
+
94
+
95
+ def test_clear_conversation(store):
96
+ store.save_conversation("U1", [{"role": "user", "content": "test"}])
97
+ store.save_conversation("U1", [])
98
+ assert store.load_conversation("U1") == []
99
+
100
+
101
+ # ── admin ops ─────────────────────────────────────────────────────────────────
102
+
103
+ def test_reset_fingerprint(store):
104
+ store.record_error("fp1", "src", "msg")
105
+ store.record_fix("fp1", "applied", branch="b", repo_name="r")
106
+ assert store.reset_fingerprint("fp1") is True
107
+ fixes = store.get_recent_fixes(24)
108
+ assert all(f["fingerprint"] != "fp1" for f in fixes)
109
+
110
+
111
+ def test_reset_fingerprint_not_found(store):
112
+ assert store.reset_fingerprint("nonexistent") is False
113
+
114
+
115
+ def test_get_all_errors(store):
116
+ store.record_error("fp1", "src", "msg1")
117
+ store.record_error("fp2", "src", "msg2")
118
+ errors = store.get_all_errors()
119
+ assert len(errors) == 2
120
+
121
+
122
+ def test_get_all_user_stats(store):
123
+ store.upsert_user("U1", "alice")
124
+ store.upsert_user("U2", "bob")
125
+ stats = store.get_all_user_stats()
126
+ assert len(stats) == 2
127
+ ids = {s["user_id"] for s in stats}
128
+ assert ids == {"U1", "U2"}