Lexai vovkes222 Claude Opus 4.6 commited on
Commit
4b7d391
·
unverified ·
1 Parent(s): 2114c64

feat(ci): add test suite and run tests in CI before deploy (#7)

Browse files

36 tests covering ConsultationState, Config, and app construction.
CI now runs pytest on ubuntu-latest before deploy-prod and sync-hf.

Co-authored-by: overthelex <mcvovkes@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

.github/workflows/deploy.yml CHANGED
@@ -10,8 +10,26 @@ concurrency:
10
  cancel-in-progress: false
11
 
12
  jobs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  deploy-prod:
14
  name: Deploy to prod
 
15
  runs-on: [self-hosted, local]
16
  timeout-minutes: 10
17
  steps:
@@ -45,6 +63,7 @@ jobs:
45
 
46
  sync-hf:
47
  name: Sync to HuggingFace Space
 
48
  runs-on: ubuntu-latest
49
  timeout-minutes: 5
50
  steps:
 
10
  cancel-in-progress: false
11
 
12
  jobs:
13
+ test:
14
+ name: Test
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 5
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install dependencies
25
+ run: pip install -r requirements.txt pytest
26
+
27
+ - name: Run tests
28
+ run: python -m pytest tests/ -v
29
+
30
  deploy-prod:
31
  name: Deploy to prod
32
+ needs: test
33
  runs-on: [self-hosted, local]
34
  timeout-minutes: 10
35
  steps:
 
63
 
64
  sync-hf:
65
  name: Sync to HuggingFace Space
66
+ needs: test
67
  runs-on: ubuntu-latest
68
  timeout-minutes: 5
69
  steps:
tests/conftest.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
5
+ sys.path.insert(0, str(Path(__file__).parent.parent))
tests/test_app.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Gradio app construction."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ gradio = pytest.importorskip("gradio")
8
+
9
+
10
+ def test_architecture_html_exists():
11
+ path = Path(__file__).parent.parent / "architecture.html"
12
+ assert path.exists(), "architecture.html not found"
13
+ content = path.read_text()
14
+ assert "lmaf-arch" in content
15
+ assert "<script>" in content
16
+ assert "d3" in content
17
+
18
+
19
+ def test_architecture_html_is_valid_document():
20
+ path = Path(__file__).parent.parent / "architecture.html"
21
+ content = path.read_text()
22
+ assert content.strip().startswith("<!DOCTYPE html>")
23
+ assert "</html>" in content
24
+
25
+
26
+ def test_app_builds():
27
+ import app
28
+ demo = app.build_app()
29
+ assert demo is not None
30
+
31
+
32
+ def test_app_has_iframe_srcdoc():
33
+ import app
34
+ assert "iframe" in app.ARCHITECTURE_HTML
35
+ assert "srcdoc" in app.ARCHITECTURE_HTML
tests/test_config.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Config."""
2
+
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ from lmaf.core.config import Config
11
+
12
+
13
+ class TestConfig:
14
+ def test_defaults(self):
15
+ config = Config()
16
+ assert config.max_iterations == 15
17
+ assert config.critic_every_n == 3
18
+ assert config.max_consecutive_failures == 3
19
+ assert "eu.anthropic" in config.default_model
20
+
21
+ def test_model_for_default(self):
22
+ config = Config()
23
+ assert config.model_for("surveyor") == config.default_model
24
+
25
+ def test_model_for_override(self):
26
+ config = Config(model_overrides={"surveyor": "custom-model"})
27
+ assert config.model_for("surveyor") == "custom-model"
28
+ assert config.model_for("planner") == config.default_model
29
+
30
+ def test_from_yaml(self):
31
+ with tempfile.NamedTemporaryFile(suffix=".yaml", mode="w", delete=False) as f:
32
+ f.write("max_iterations: 5\ncritic_every_n: 2\n")
33
+ path = Path(f.name)
34
+
35
+ config = Config.from_yaml(path)
36
+ assert config.max_iterations == 5
37
+ assert config.critic_every_n == 2
38
+ path.unlink()
39
+
40
+ def test_from_yaml_ignores_unknown_fields(self):
41
+ with tempfile.NamedTemporaryFile(suffix=".yaml", mode="w", delete=False) as f:
42
+ f.write("max_iterations: 10\nunknown_field: true\n")
43
+ path = Path(f.name)
44
+
45
+ config = Config.from_yaml(path)
46
+ assert config.max_iterations == 10
47
+ path.unlink()
48
+
49
+ def test_from_env(self):
50
+ env = {
51
+ "AWS_REGION": "us-east-1",
52
+ "AWS_ACCESS_KEY_ID": "test-key",
53
+ "AWS_SECRET_ACCESS_KEY": "test-secret",
54
+ "SECONDLAYER_API_KEY": "sl-key",
55
+ }
56
+ with patch.dict(os.environ, env, clear=False):
57
+ config = Config.from_env()
58
+ assert config.aws_region == "us-east-1"
59
+ assert config.aws_access_key_id == "test-key"
60
+ assert config.secondlayer_api_key == "sl-key"
61
+
62
+ def test_from_env_defaults(self):
63
+ with patch.dict(os.environ, {}, clear=True):
64
+ config = Config.from_env()
65
+ assert config.aws_region == ""
66
+ assert config.secondlayer_api_url == "https://legal.org.ua/api"
tests/test_state.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for ConsultationState and its data structures."""
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from lmaf.state.research_state import (
10
+ ConsultationState,
11
+ Critique,
12
+ CritiqueStatus,
13
+ HypothesisStatus,
14
+ LegalEvidence,
15
+ LegalHypothesis,
16
+ LegalStrategy,
17
+ RQStatus,
18
+ ResearchQuestion,
19
+ Severity,
20
+ )
21
+
22
+
23
+ class TestConsultationState:
24
+ def test_empty_state(self):
25
+ state = ConsultationState()
26
+ assert state.client_question == ""
27
+ assert state.hypotheses == []
28
+ assert state.evidence == []
29
+ assert state.questions == []
30
+ assert state.critiques == []
31
+ assert state.iteration == 0
32
+
33
+ def test_add_hypothesis(self):
34
+ state = ConsultationState()
35
+ h = state.add_hypothesis("Позовна давність становить 3 роки", iteration=1)
36
+ assert h.id == "H-001"
37
+ assert h.status == HypothesisStatus.WORKING
38
+ assert len(state.hypotheses) == 1
39
+
40
+ def test_add_multiple_hypotheses(self):
41
+ state = ConsultationState()
42
+ state.add_hypothesis("First")
43
+ state.add_hypothesis("Second")
44
+ state.add_hypothesis("Third")
45
+ assert len(state.hypotheses) == 3
46
+ assert state.hypotheses[2].id == "H-003"
47
+
48
+ def test_add_evidence(self):
49
+ state = ConsultationState()
50
+ ev = state.add_evidence(
51
+ type="case_law",
52
+ source="edrsr",
53
+ summary="Рішення ВС щодо строків давності",
54
+ citation="Справа №757/12345/22",
55
+ )
56
+ assert ev.id == "EV-001"
57
+ assert ev.type == "case_law"
58
+ assert not ev.refuted
59
+
60
+ def test_add_question(self):
61
+ state = ConsultationState()
62
+ rq = state.add_question("Чи застосовується ст. 625 ЦК?", iteration=2)
63
+ assert rq.id == "RQ-001"
64
+ assert rq.status == RQStatus.OPEN
65
+
66
+ def test_add_critique(self):
67
+ state = ConsultationState()
68
+ c = state.add_critique(
69
+ type="strategy",
70
+ severity=Severity.HIGH,
71
+ summary="Не враховано зустрічний позов",
72
+ )
73
+ assert c.id == "CR-001"
74
+ assert c.status == CritiqueStatus.ACTIVE
75
+
76
+ def test_open_questions(self):
77
+ state = ConsultationState()
78
+ q1 = state.add_question("Open question")
79
+ q2 = state.add_question("Resolved question")
80
+ q2.status = RQStatus.RESOLVED
81
+ assert len(state.open_questions()) == 1
82
+ assert state.open_questions()[0].id == q1.id
83
+
84
+ def test_active_critiques(self):
85
+ state = ConsultationState()
86
+ c1 = state.add_critique(type="strategy", summary="Active")
87
+ c2 = state.add_critique(type="reasoning", summary="Resolved")
88
+ c2.status = CritiqueStatus.RESOLVED
89
+ assert len(state.active_critiques()) == 1
90
+ assert state.active_critiques()[0].id == c1.id
91
+
92
+ def test_working_hypotheses(self):
93
+ state = ConsultationState()
94
+ h1 = state.add_hypothesis("Working")
95
+ h2 = state.add_hypothesis("Established")
96
+ h2.status = HypothesisStatus.ESTABLISHED
97
+ assert len(state.working_hypotheses()) == 1
98
+ assert len(state.established_hypotheses()) == 1
99
+
100
+ def test_serialization_roundtrip(self):
101
+ state = ConsultationState()
102
+ state.client_question = "Тестове питання"
103
+ state.jurisdiction = "civil"
104
+ state.survey_summary = "Огляд правового ландшафту"
105
+ state.strategy = LegalStrategy(
106
+ approach="Аналіз строків давності",
107
+ legal_domains=["цивільне право"],
108
+ key_questions=["Який строк?"],
109
+ )
110
+ state.add_hypothesis("Гіпотеза 1", iteration=1)
111
+ state.add_evidence(type="legislation", citation="ст. 257 ЦК")
112
+ state.add_question("Питання 1")
113
+ state.add_critique(type="completeness", summary="Прогалина")
114
+
115
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
116
+ path = Path(f.name)
117
+
118
+ state.save(path)
119
+ loaded = ConsultationState.load(path)
120
+
121
+ assert loaded.client_question == "Тестове питання"
122
+ assert loaded.jurisdiction == "civil"
123
+ assert loaded.strategy.approach == "Аналіз строків давності"
124
+ assert len(loaded.hypotheses) == 1
125
+ assert len(loaded.evidence) == 1
126
+ assert len(loaded.questions) == 1
127
+ assert len(loaded.critiques) == 1
128
+ path.unlink()
129
+
130
+ def test_to_dict(self):
131
+ state = ConsultationState()
132
+ state.client_question = "Test"
133
+ d = state.to_dict()
134
+ assert isinstance(d, dict)
135
+ assert d["client_question"] == "Test"
136
+
137
+
138
+ class TestLegalEvidence:
139
+ def test_short(self):
140
+ ev = LegalEvidence(id="EV-001", citation="ст. 625 ЦК", summary="Пеня за прострочення")
141
+ assert "EV-001" in ev.short()
142
+ assert "ст. 625 ЦК" in ev.short()
143
+
144
+ def test_defaults(self):
145
+ ev = LegalEvidence()
146
+ assert ev.refuted is False
147
+ assert ev.iteration is None
148
+
149
+
150
+ class TestLegalHypothesis:
151
+ def test_short(self):
152
+ h = LegalHypothesis(id="H-001", statement="Позов обгрунтований", status=HypothesisStatus.WORKING)
153
+ assert "H-001" in h.short()
154
+ assert "working" in h.short()