zwarm 2.3.5__py3-none-any.whl

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.
@@ -0,0 +1,160 @@
1
+ """Tests for the config system."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from zwarm.core.config import (
9
+ ZwarmConfig,
10
+ apply_overrides,
11
+ deep_merge,
12
+ load_config,
13
+ load_yaml_config,
14
+ )
15
+
16
+
17
+ def test_default_config():
18
+ """Test default configuration values."""
19
+ config = ZwarmConfig()
20
+ assert config.executor.adapter == "codex_mcp"
21
+ assert config.executor.sandbox == "workspace-write"
22
+ assert config.orchestrator.lm == "gpt-5-mini"
23
+ assert config.orchestrator.sync_first is True
24
+ assert config.state_dir == ".zwarm"
25
+
26
+
27
+ def test_config_from_dict():
28
+ """Test creating config from dictionary."""
29
+ data = {
30
+ "executor": {"adapter": "claude_code", "model": "claude-sonnet"},
31
+ "orchestrator": {"lm": "gpt-5", "max_steps": 100},
32
+ "state_dir": ".my_state",
33
+ }
34
+ config = ZwarmConfig.from_dict(data)
35
+ assert config.executor.adapter == "claude_code"
36
+ assert config.executor.model == "claude-sonnet"
37
+ assert config.orchestrator.lm == "gpt-5"
38
+ assert config.orchestrator.max_steps == 100
39
+ assert config.state_dir == ".my_state"
40
+
41
+
42
+ def test_config_to_dict():
43
+ """Test converting config to dictionary."""
44
+ config = ZwarmConfig()
45
+ data = config.to_dict()
46
+ assert data["executor"]["adapter"] == "codex_mcp"
47
+ assert data["orchestrator"]["lm"] == "gpt-5-mini"
48
+
49
+
50
+ def test_deep_merge():
51
+ """Test deep merging of dictionaries."""
52
+ base = {"a": 1, "b": {"c": 2, "d": 3}}
53
+ override = {"b": {"c": 99}, "e": 4}
54
+ result = deep_merge(base, override)
55
+ assert result == {"a": 1, "b": {"c": 99, "d": 3}, "e": 4}
56
+
57
+
58
+ def test_apply_overrides():
59
+ """Test CLI override application."""
60
+ config = {"executor": {"adapter": "codex"}, "orchestrator": {"max_steps": 10}}
61
+
62
+ # Override nested value
63
+ result = apply_overrides(config, ["orchestrator.max_steps=50"])
64
+ assert result["orchestrator"]["max_steps"] == 50
65
+
66
+ # Override with string
67
+ result = apply_overrides(config, ["executor.adapter=claude_code"])
68
+ assert result["executor"]["adapter"] == "claude_code"
69
+
70
+ # Override with boolean
71
+ result = apply_overrides(config, ["orchestrator.sync_first=false"])
72
+ assert result["orchestrator"]["sync_first"] is False
73
+
74
+ # Create new nested path
75
+ result = apply_overrides(config, ["weave.project=my-project"])
76
+ assert result["weave"]["project"] == "my-project"
77
+
78
+
79
+ def test_yaml_inheritance():
80
+ """Test YAML config inheritance via extends."""
81
+ with tempfile.TemporaryDirectory() as tmpdir:
82
+ base_path = Path(tmpdir) / "base.yaml"
83
+ child_path = Path(tmpdir) / "child.yaml"
84
+
85
+ # Write base config
86
+ base_path.write_text("""
87
+ executor:
88
+ adapter: codex_mcp
89
+ timeout: 3600
90
+ orchestrator:
91
+ lm: gpt-5-mini
92
+ max_steps: 30
93
+ """)
94
+
95
+ # Write child config that extends base
96
+ child_path.write_text("""
97
+ extends: base.yaml
98
+ orchestrator:
99
+ lm: gpt-5
100
+ prompt: prompts/aggressive.yaml
101
+ """)
102
+
103
+ config = load_yaml_config(child_path)
104
+ assert config["executor"]["adapter"] == "codex_mcp"
105
+ assert config["executor"]["timeout"] == 3600
106
+ assert config["orchestrator"]["lm"] == "gpt-5" # overridden
107
+ assert config["orchestrator"]["max_steps"] == 30 # inherited
108
+ assert config["orchestrator"]["prompt"] == "prompts/aggressive.yaml" # new
109
+
110
+
111
+ def test_load_config_full_chain():
112
+ """Test full config loading with precedence."""
113
+ import os
114
+
115
+ # Save and clear WEAVE_PROJECT to test config precedence
116
+ orig_weave = os.environ.pop("WEAVE_PROJECT", None)
117
+
118
+ try:
119
+ with tempfile.TemporaryDirectory() as tmpdir:
120
+ tmpdir = Path(tmpdir)
121
+
122
+ # Write config.toml
123
+ toml_path = tmpdir / "config.toml"
124
+ toml_path.write_text("""
125
+ [weave]
126
+ project = "my-weave-project"
127
+
128
+ [executor]
129
+ adapter = "codex_mcp"
130
+ """)
131
+
132
+ # Write yaml config
133
+ yaml_path = tmpdir / "experiment.yaml"
134
+ yaml_path.write_text("""
135
+ orchestrator:
136
+ lm: claude-sonnet
137
+ max_steps: 100
138
+ """)
139
+
140
+ # Load with override (use non-existent env_path to prevent loading cwd's .env)
141
+ config = load_config(
142
+ config_path=yaml_path,
143
+ toml_path=toml_path,
144
+ env_path=tmpdir / ".env.nonexistent",
145
+ overrides=["orchestrator.max_steps=200"],
146
+ )
147
+
148
+ # Check precedence: override > yaml > toml > default
149
+ assert config.weave.project == "my-weave-project" # from toml
150
+ assert config.executor.adapter == "codex_mcp" # from toml
151
+ assert config.orchestrator.lm == "claude-sonnet" # from yaml
152
+ assert config.orchestrator.max_steps == 200 # from override
153
+ finally:
154
+ # Restore WEAVE_PROJECT
155
+ if orig_weave is not None:
156
+ os.environ["WEAVE_PROJECT"] = orig_weave
157
+
158
+
159
+ if __name__ == "__main__":
160
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,265 @@
1
+ """Tests for core models and state management."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from zwarm.core.models import (
9
+ ConversationSession,
10
+ Event,
11
+ Message,
12
+ SessionMode,
13
+ SessionStatus,
14
+ Task,
15
+ TaskStatus,
16
+ event_session_completed,
17
+ event_session_started,
18
+ event_task_created,
19
+ )
20
+ from zwarm.core.state import StateManager
21
+
22
+
23
+ class TestMessage:
24
+ def test_create_message(self):
25
+ msg = Message(role="user", content="Hello")
26
+ assert msg.role == "user"
27
+ assert msg.content == "Hello"
28
+ assert msg.timestamp is not None
29
+
30
+ def test_message_serialization(self):
31
+ msg = Message(role="assistant", content="Hi there")
32
+ data = msg.to_dict()
33
+ restored = Message.from_dict(data)
34
+ assert restored.role == msg.role
35
+ assert restored.content == msg.content
36
+
37
+
38
+ class TestConversationSession:
39
+ def test_create_session(self):
40
+ session = ConversationSession(
41
+ adapter="codex_mcp",
42
+ mode=SessionMode.SYNC,
43
+ task_description="Test task",
44
+ )
45
+ assert session.id is not None
46
+ assert session.adapter == "codex_mcp"
47
+ assert session.mode == SessionMode.SYNC
48
+ assert session.status == SessionStatus.ACTIVE
49
+
50
+ def test_add_message(self):
51
+ session = ConversationSession()
52
+ session.add_message("user", "Hello")
53
+ session.add_message("assistant", "Hi there")
54
+ assert len(session.messages) == 2
55
+ assert session.messages[0].role == "user"
56
+ assert session.messages[1].role == "assistant"
57
+
58
+ def test_complete_session(self):
59
+ session = ConversationSession()
60
+ session.complete("Done!")
61
+ assert session.status == SessionStatus.COMPLETED
62
+ assert session.completed_at is not None
63
+ assert session.exit_message == "Done!"
64
+
65
+ def test_fail_session(self):
66
+ session = ConversationSession()
67
+ session.fail("Error occurred")
68
+ assert session.status == SessionStatus.FAILED
69
+ assert session.exit_message == "Error occurred"
70
+
71
+ def test_session_serialization(self):
72
+ session = ConversationSession(
73
+ adapter="claude_code",
74
+ mode=SessionMode.ASYNC,
75
+ task_description="Build feature",
76
+ model="claude-sonnet",
77
+ )
78
+ session.add_message("user", "Start")
79
+ session.conversation_id = "conv-123"
80
+
81
+ data = session.to_dict()
82
+ restored = ConversationSession.from_dict(data)
83
+
84
+ assert restored.id == session.id
85
+ assert restored.adapter == "claude_code"
86
+ assert restored.mode == SessionMode.ASYNC
87
+ assert restored.conversation_id == "conv-123"
88
+ assert len(restored.messages) == 1
89
+
90
+
91
+ class TestTask:
92
+ def test_create_task(self):
93
+ task = Task(description="Fix the bug")
94
+ assert task.id is not None
95
+ assert task.status == TaskStatus.PENDING
96
+
97
+ def test_task_lifecycle(self):
98
+ task = Task(description="Implement feature")
99
+
100
+ # Start
101
+ task.start("session-123")
102
+ assert task.status == TaskStatus.IN_PROGRESS
103
+ assert task.session_id == "session-123"
104
+
105
+ # Complete
106
+ task.complete("Feature implemented")
107
+ assert task.status == TaskStatus.COMPLETED
108
+ assert task.result == "Feature implemented"
109
+
110
+ def test_task_serialization(self):
111
+ task = Task(description="Test task", parent_task_id="parent-123")
112
+ task.start("session-456")
113
+
114
+ data = task.to_dict()
115
+ restored = Task.from_dict(data)
116
+
117
+ assert restored.id == task.id
118
+ assert restored.description == "Test task"
119
+ assert restored.parent_task_id == "parent-123"
120
+ assert restored.session_id == "session-456"
121
+
122
+
123
+ class TestEvent:
124
+ def test_create_event(self):
125
+ event = Event(
126
+ kind="test_event",
127
+ session_id="session-123",
128
+ payload={"key": "value"},
129
+ )
130
+ assert event.id is not None
131
+ assert event.kind == "test_event"
132
+
133
+ def test_event_factories(self):
134
+ session = ConversationSession(task_description="Test")
135
+ event = event_session_started(session)
136
+ assert event.kind == "session_started"
137
+ assert event.session_id == session.id
138
+
139
+ task = Task(description="Do something")
140
+ event = event_task_created(task)
141
+ assert event.kind == "task_created"
142
+ assert event.task_id == task.id
143
+
144
+
145
+ class TestStateManager:
146
+ def test_init_creates_directories(self):
147
+ with tempfile.TemporaryDirectory() as tmpdir:
148
+ state_dir = Path(tmpdir) / ".zwarm"
149
+ manager = StateManager(state_dir)
150
+ manager.init()
151
+
152
+ assert state_dir.exists()
153
+ assert (state_dir / "sessions").exists()
154
+ assert (state_dir / "orchestrator").exists()
155
+ assert (state_dir / "events.jsonl").exists()
156
+
157
+ def test_session_crud(self):
158
+ with tempfile.TemporaryDirectory() as tmpdir:
159
+ manager = StateManager(Path(tmpdir) / ".zwarm")
160
+ manager.init()
161
+
162
+ # Add
163
+ session = ConversationSession(task_description="Test")
164
+ manager.add_session(session)
165
+
166
+ # Get
167
+ retrieved = manager.get_session(session.id)
168
+ assert retrieved is not None
169
+ assert retrieved.task_description == "Test"
170
+
171
+ # Update
172
+ session.add_message("user", "Hello")
173
+ manager.update_session(session)
174
+
175
+ # List
176
+ sessions = manager.list_sessions()
177
+ assert len(sessions) == 1
178
+
179
+ # Filter by status
180
+ active = manager.list_sessions(status="active")
181
+ assert len(active) == 1
182
+
183
+ def test_task_crud(self):
184
+ with tempfile.TemporaryDirectory() as tmpdir:
185
+ manager = StateManager(Path(tmpdir) / ".zwarm")
186
+ manager.init()
187
+
188
+ # Add
189
+ task = Task(description="Build feature")
190
+ manager.add_task(task)
191
+
192
+ # Get
193
+ retrieved = manager.get_task(task.id)
194
+ assert retrieved is not None
195
+
196
+ # Update
197
+ task.start("session-123")
198
+ manager.update_task(task)
199
+
200
+ # List
201
+ tasks = manager.list_tasks(status="in_progress")
202
+ assert len(tasks) == 1
203
+
204
+ def test_event_logging(self):
205
+ with tempfile.TemporaryDirectory() as tmpdir:
206
+ manager = StateManager(Path(tmpdir) / ".zwarm")
207
+ manager.init()
208
+
209
+ # Log events
210
+ session = ConversationSession()
211
+ manager.log_event(event_session_started(session))
212
+ manager.log_event(event_session_completed(session))
213
+
214
+ # Read events
215
+ events = manager.get_events()
216
+ assert len(events) == 2
217
+ assert events[0].kind == "session_completed" # Most recent first
218
+
219
+ # Filter by session
220
+ events = manager.get_events(session_id=session.id)
221
+ assert len(events) == 2
222
+
223
+ # Filter by kind
224
+ events = manager.get_events(kind="session_started")
225
+ assert len(events) == 1
226
+
227
+ def test_orchestrator_messages(self):
228
+ with tempfile.TemporaryDirectory() as tmpdir:
229
+ manager = StateManager(Path(tmpdir) / ".zwarm")
230
+ manager.init()
231
+
232
+ messages = [
233
+ {"role": "system", "content": "You are an orchestrator"},
234
+ {"role": "user", "content": "Build a feature"},
235
+ ]
236
+ manager.save_orchestrator_messages(messages)
237
+
238
+ loaded = manager.load_orchestrator_messages()
239
+ assert len(loaded) == 2
240
+ assert loaded[0]["role"] == "system"
241
+
242
+ def test_state_persistence(self):
243
+ with tempfile.TemporaryDirectory() as tmpdir:
244
+ state_dir = Path(tmpdir) / ".zwarm"
245
+
246
+ # Create and save state
247
+ manager1 = StateManager(state_dir)
248
+ manager1.init()
249
+
250
+ session = ConversationSession(task_description="Persistent session")
251
+ manager1.add_session(session)
252
+
253
+ task = Task(description="Persistent task")
254
+ manager1.add_task(task)
255
+
256
+ # Load in new manager
257
+ manager2 = StateManager(state_dir)
258
+ manager2.load()
259
+
260
+ assert manager2.get_session(session.id) is not None
261
+ assert manager2.get_task(task.id) is not None
262
+
263
+
264
+ if __name__ == "__main__":
265
+ pytest.main([__file__, "-v"])