expt-logger 0.1.0.dev19__tar.gz → 0.1.0.dev21__tar.gz

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 (28) hide show
  1. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/PKG-INFO +1 -1
  2. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/pyproject.toml +1 -1
  3. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/__init__.py +22 -2
  4. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/client.py +41 -0
  5. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/run.py +44 -2
  6. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/types.py +9 -0
  7. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_client.py +12 -0
  8. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_global_api.py +23 -0
  9. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_integration_e2e.py +130 -0
  10. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_run.py +74 -37
  11. expt_logger-0.1.0.dev19/.claude/settings.local.json +0 -30
  12. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/.gitignore +0 -0
  13. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/.pre-commit-config.yaml +0 -0
  14. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/DEVELOPMENT.md +0 -0
  15. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/README.md +0 -0
  16. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/buffer.py +0 -0
  17. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/config.py +0 -0
  18. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/env.py +0 -0
  19. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/exceptions.py +0 -0
  20. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/py.typed +0 -0
  21. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/validation.py +0 -0
  22. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/conftest.py +0 -0
  23. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_buffer.py +0 -0
  24. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_client_integration.py +0 -0
  25. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_config.py +0 -0
  26. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_env.py +0 -0
  27. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_exceptions.py +0 -0
  28. {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: expt-logger
3
- Version: 0.1.0.dev19
3
+ Version: 0.1.0.dev21
4
4
  Summary: Simple experiment logging library
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: requests>=2.31.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "expt-logger"
3
- version = "0.1.0.dev19"
3
+ version = "0.1.0.dev21"
4
4
  description = "Simple experiment logging library"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -31,8 +31,7 @@ def init(
31
31
  When attaching to an existing experiment:
32
32
  - Validates the experiment exists (raises APIError if not found)
33
33
  - Sets experiment status to "active"
34
- - Main process: Syncs provided config to server (if any)
35
- - Subprocess (is_main_process=False): Config parameter is ignored to avoid conflicts
34
+ - Syncs provided config to server (if any)
36
35
 
37
36
  Args:
38
37
  name: Optional experiment name (used when creating new experiment)
@@ -113,6 +112,7 @@ def log_rollout(
113
112
  step: int | None = None,
114
113
  mode: str | None = None,
115
114
  commit: bool = True,
115
+ id: str | None = None,
116
116
  ) -> None:
117
117
  """Log a rollout (conversation + rewards).
118
118
 
@@ -152,9 +152,28 @@ def log_rollout(
152
152
  step=step,
153
153
  mode=mode,
154
154
  commit=commit,
155
+ id=id
155
156
  )
156
157
 
157
158
 
159
+ def log_environment(
160
+ rollout_id: str,
161
+ content: str,
162
+ ) -> None:
163
+ """Log an environment log entry associated with a rollout.
164
+
165
+ Args:
166
+ rollout_id: ID of the rollout this log entry is associated with
167
+ content: Log content string
168
+
169
+ Raises:
170
+ RuntimeError: If no active run exists
171
+ """
172
+ if _active_run is None:
173
+ raise RuntimeError("No active run. Call init() first.")
174
+ _active_run.log_environment(rollout_id=rollout_id, content=content)
175
+
176
+
158
177
  def log_error(
159
178
  error: Exception | str,
160
179
  step: int | None = None,
@@ -298,6 +317,7 @@ __all__ = [
298
317
  "init",
299
318
  "log",
300
319
  "log_rollout",
320
+ "log_environment",
301
321
  "log_error",
302
322
  "commit",
303
323
  "end",
@@ -280,6 +280,47 @@ class APIClient:
280
280
  payload = {"rollouts": chunk}
281
281
  self._request("POST", url, json=payload)
282
282
 
283
+ def log_env_logs(
284
+ self,
285
+ experiment_id: str,
286
+ rollout_id: str,
287
+ content: str,
288
+ ) -> None:
289
+ """Log an environment log entry for an experiment.
290
+
291
+ Args:
292
+ experiment_id: Experiment ID
293
+ rollout_id: Rollout ID this log entry is associated with
294
+ content: Log content string
295
+
296
+ Raises:
297
+ APIError: If request fails
298
+ """
299
+ url = f"{self.base_url}/api/experiments/{experiment_id}/env-logs"
300
+ payload = {"rolloutId": rollout_id, "content": content}
301
+ self._request("POST", url, json=payload)
302
+
303
+ def get_env_logs(
304
+ self,
305
+ experiment_id: str,
306
+ rollout_id: str,
307
+ ) -> list[dict]:
308
+ """Fetch environment logs for a specific rollout.
309
+
310
+ Args:
311
+ experiment_id: Experiment ID
312
+ rollout_id: Rollout ID to filter logs by
313
+
314
+ Returns:
315
+ List of env log entries
316
+
317
+ Raises:
318
+ APIError: If request fails
319
+ """
320
+ url = f"{self.base_url}/api/experiments/{experiment_id}/env-logs"
321
+ response = self._request("GET", url, params={"rolloutId": rollout_id})
322
+ return response.json()["logs"]
323
+
283
324
  def log_errors(
284
325
  self,
285
326
  experiment_id: str,
@@ -23,6 +23,7 @@ from expt_logger.types import (
23
23
  ConfigUpdateCommand,
24
24
  ErrorItem,
25
25
  LogCommand,
26
+ LogEnvironmentCommand,
26
27
  LogErrorCommand,
27
28
  LogRolloutCommand,
28
29
  MessageItem,
@@ -81,8 +82,9 @@ class Run:
81
82
  resolved_experiment_id = get_experiment_id(experiment_id, is_main_process=False)
82
83
  if resolved_experiment_id is not None:
83
84
  self._experiment_id = resolved_experiment_id
84
- # Subprocesses should not update config to avoid conflicts
85
- self._validate_and_attach_experiment(config=None)
85
+ # Do not set the config if already attached to an experiment to avoid
86
+ # overwriting existing settings
87
+ self._validate_and_attach_experiment()
86
88
  logger.info(
87
89
  f"[expt_logger] Attached to experiment ID: {self._experiment_id} (subprocess)"
88
90
  )
@@ -229,6 +231,7 @@ class Run:
229
231
  step: int | None = None,
230
232
  mode: str | None = None,
231
233
  commit: bool = True,
234
+ id: str | None = None,
232
235
  ) -> None:
233
236
  """Log a rollout (conversation + rewards).
234
237
 
@@ -239,6 +242,7 @@ class Run:
239
242
  step: Optional step number (overrides worker's step counter if provided)
240
243
  mode: Optional mode (defaults to "train")
241
244
  commit: Whether to flush buffer after logging
245
+ id: Optional identifier for this rollout
242
246
  """
243
247
  from expt_logger.validation import (
244
248
  validate_messages,
@@ -274,6 +278,7 @@ class Run:
274
278
  "rewards": reward_items,
275
279
  "mode": mode or "train",
276
280
  "step": step,
281
+ "id": id,
277
282
  }
278
283
 
279
284
  try:
@@ -284,6 +289,28 @@ class Run:
284
289
  if commit:
285
290
  self.commit()
286
291
 
292
+ def log_environment(
293
+ self,
294
+ rollout_id: str,
295
+ content: str,
296
+ ) -> None:
297
+ """Log an environment log entry associated with a rollout.
298
+
299
+ Args:
300
+ rollout_id: ID of the rollout this log entry is associated with
301
+ content: Log content string
302
+ """
303
+ env_cmd: LogEnvironmentCommand = {
304
+ "rollout_id": rollout_id,
305
+ "content": content,
306
+ }
307
+ try:
308
+ self._queue.put_nowait(("log_environment", env_cmd))
309
+ except Full:
310
+ logger.warning(
311
+ f"Command queue full, dropping environment log for rollout: {rollout_id}"
312
+ )
313
+
287
314
  def log_error(
288
315
  self,
289
316
  error: Exception | str,
@@ -423,6 +450,8 @@ class Run:
423
450
  self._handle_log(cast(LogCommand, data))
424
451
  elif command == "log_rollout":
425
452
  self._handle_log_rollout(cast(LogRolloutCommand, data))
453
+ elif command == "log_environment":
454
+ self._handle_log_environment(cast(LogEnvironmentCommand, data))
426
455
  elif command == "log_error":
427
456
  self._handle_log_error(cast(LogErrorCommand, data))
428
457
  elif command == "commit":
@@ -533,9 +562,22 @@ class Run:
533
562
  "messages": data["messages"],
534
563
  "rewards": data["rewards"],
535
564
  }
565
+ if data["id"] is not None:
566
+ rollout["id"] = data["id"]
536
567
 
537
568
  self._buffer.add_rollout(rollout)
538
569
 
570
+ def _handle_log_environment(self, data: LogEnvironmentCommand) -> None:
571
+ """Handle log_environment command.
572
+
573
+ Args:
574
+ data: Log environment command data with rollout_id and content
575
+ """
576
+ try:
577
+ self._client.log_env_logs(self._experiment_id, data["rollout_id"], data["content"])
578
+ except Exception as e:
579
+ logger.error(f"Error logging environment log: {e}", exc_info=True)
580
+
539
581
  def _handle_log_error(self, data: LogErrorCommand) -> None:
540
582
  """Handle log_error command.
541
583
 
@@ -36,6 +36,7 @@ class RolloutItem(TypedDict):
36
36
  promptText: str
37
37
  messages: list[MessageItem]
38
38
  rewards: list[RewardItem]
39
+ id: NotRequired[str]
39
40
 
40
41
 
41
42
  class ErrorItem(TypedDict):
@@ -75,6 +76,7 @@ class LogRolloutCommand(TypedDict):
75
76
  rewards: list[RewardItem]
76
77
  mode: str
77
78
  step: int | None
79
+ id: str | None
78
80
 
79
81
 
80
82
  class LogErrorCommand(TypedDict):
@@ -87,6 +89,13 @@ class LogErrorCommand(TypedDict):
87
89
  step: int | None
88
90
 
89
91
 
92
+ class LogEnvironmentCommand(TypedDict):
93
+ """Command to log an environment log entry."""
94
+
95
+ rollout_id: str
96
+ content: str
97
+
98
+
90
99
  class CommitCommand(TypedDict):
91
100
  """Command to commit (flush) the buffer."""
92
101
 
@@ -294,6 +294,18 @@ def test_log_rollouts(client):
294
294
  assert call_args[1]["json"] == {"rollouts": rollouts}
295
295
 
296
296
 
297
+ def test_log_env_logs(client):
298
+ """Test logging environment logs."""
299
+ with patch.object(client, "_request") as mock_request:
300
+ client.log_env_logs("exp-123", "rollout-456", "step 1 observation")
301
+
302
+ mock_request.assert_called_once()
303
+ call_args = mock_request.call_args
304
+ assert call_args[0][0] == "POST"
305
+ assert call_args[0][1] == "https://test.example.com/api/experiments/exp-123/env-logs"
306
+ assert call_args[1]["json"] == {"rolloutId": "rollout-456", "content": "step 1 observation"}
307
+
308
+
297
309
  def test_log_errors(client):
298
310
  """Test logging errors."""
299
311
  with patch.object(client, "_request") as mock_request:
@@ -134,6 +134,28 @@ def test_log_rollout_raises_if_no_active_run(mock_run):
134
134
  assert "No active run" in str(exc_info.value)
135
135
 
136
136
 
137
+ def test_log_environment_delegates_to_run(mock_run):
138
+ """Test log_environment() delegates to the active run."""
139
+ _, run_instance = mock_run
140
+
141
+ expt_logger.init()
142
+ expt_logger.log_environment(rollout_id="rollout-abc", content="step 1 observation")
143
+
144
+ run_instance.log_environment.assert_called_once_with(
145
+ rollout_id="rollout-abc",
146
+ content="step 1 observation",
147
+ )
148
+
149
+
150
+ def test_log_environment_raises_if_no_active_run():
151
+ """Test log_environment() raises RuntimeError if no active run."""
152
+ with pytest.raises(RuntimeError) as exc_info:
153
+ expt_logger.log_environment("rollout-abc", "some content")
154
+
155
+ assert "No active run" in str(exc_info.value)
156
+ assert "init()" in str(exc_info.value)
157
+
158
+
137
159
  def test_log_error_delegates_to_run(mock_run):
138
160
  """Test log_error() delegates to the active run."""
139
161
  _, run_instance = mock_run
@@ -393,6 +415,7 @@ def test_all_exports():
393
415
  "init",
394
416
  "log",
395
417
  "log_rollout",
418
+ "log_environment",
396
419
  "log_error",
397
420
  "commit",
398
421
  "end",
@@ -112,6 +112,22 @@ def fetch_rollouts(shared_api_key: str, base_url: str):
112
112
  return _fetch
113
113
 
114
114
 
115
+ @pytest.fixture
116
+ def fetch_env_logs(shared_api_key: str, base_url: str):
117
+ """Factory fixture for fetching environment log data."""
118
+
119
+ def _fetch(experiment_id: str, rollout_id: str):
120
+ response = requests.get(
121
+ f"{base_url}/api/experiments/{experiment_id}/env-logs",
122
+ params={"rolloutId": rollout_id},
123
+ headers={"Authorization": f"Bearer {shared_api_key}"},
124
+ )
125
+ assert response.status_code == 200
126
+ return response.json()["logs"]
127
+
128
+ return _fetch
129
+
130
+
115
131
  # ============================================================================
116
132
  # Test Class 1: Basic Workflow Tests
117
133
  # ============================================================================
@@ -1054,3 +1070,117 @@ class TestMultiProcess:
1054
1070
  # Worker metrics
1055
1071
  for i in range(num_workers):
1056
1072
  assert f"worker-{i + 1}-metric" in scalars
1073
+
1074
+
1075
+ # ============================================================================
1076
+ # Test Class 12: Environment Log Tests
1077
+ # ============================================================================
1078
+
1079
+
1080
+ @pytest.mark.integration
1081
+ class TestEnvironmentLogs:
1082
+ """Environment log functionality."""
1083
+
1084
+ def test_log_environment_basic(
1085
+ self,
1086
+ shared_api_key: str,
1087
+ base_url: str,
1088
+ cleanup_experiments: list[str],
1089
+ fetch_env_logs,
1090
+ ) -> None:
1091
+ """Test basic environment log persists to server."""
1092
+ run = expt_logger.init(
1093
+ name="test-env-log-basic",
1094
+ api_key=shared_api_key,
1095
+ base_url=base_url,
1096
+ )
1097
+ cleanup_experiments.append(run._experiment_id)
1098
+
1099
+ expt_logger.log_environment("00000000-0000-0000-0000-000000000001", "observation: step=1, reward=0.9")
1100
+ time.sleep(0.5)
1101
+
1102
+ env_logs = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-000000000001")
1103
+ assert len(env_logs) == 1
1104
+ assert env_logs[0]["content"] == "observation: step=1, reward=0.9"
1105
+
1106
+ expt_logger.end()
1107
+
1108
+ def test_log_environment_multiple_for_same_rollout(
1109
+ self,
1110
+ shared_api_key: str,
1111
+ base_url: str,
1112
+ cleanup_experiments: list[str],
1113
+ fetch_env_logs,
1114
+ ) -> None:
1115
+ """Test multiple environment logs for the same rollout are all stored."""
1116
+ run = expt_logger.init(
1117
+ name="test-env-log-multiple",
1118
+ api_key=shared_api_key,
1119
+ base_url=base_url,
1120
+ )
1121
+ cleanup_experiments.append(run._experiment_id)
1122
+
1123
+ expt_logger.log_environment("00000000-0000-0000-0000-000000000002", "step 1: action=left")
1124
+ expt_logger.log_environment("00000000-0000-0000-0000-000000000002", "step 2: action=right")
1125
+ expt_logger.log_environment("00000000-0000-0000-0000-000000000002", "step 3: action=jump")
1126
+ time.sleep(0.5)
1127
+
1128
+ env_logs = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-000000000002")
1129
+ assert len(env_logs) == 3
1130
+ contents = {log["content"] for log in env_logs}
1131
+ assert contents == {"step 1: action=left", "step 2: action=right", "step 3: action=jump"}
1132
+
1133
+ expt_logger.end()
1134
+
1135
+ def test_log_environment_different_rollouts(
1136
+ self,
1137
+ shared_api_key: str,
1138
+ base_url: str,
1139
+ cleanup_experiments: list[str],
1140
+ fetch_env_logs,
1141
+ ) -> None:
1142
+ """Test environment logs for different rollouts stay separate."""
1143
+ run = expt_logger.init(
1144
+ name="test-env-log-separate",
1145
+ api_key=shared_api_key,
1146
+ base_url=base_url,
1147
+ )
1148
+ cleanup_experiments.append(run._experiment_id)
1149
+
1150
+ expt_logger.log_environment("00000000-0000-0000-0000-00000000003a", "log for rollout A")
1151
+ expt_logger.log_environment("00000000-0000-0000-0000-00000000003b", "log for rollout B")
1152
+ time.sleep(0.5)
1153
+
1154
+ logs_a = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-00000000003a")
1155
+ logs_b = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-00000000003b")
1156
+ assert len(logs_a) == 1
1157
+ assert logs_a[0]["content"] == "log for rollout A"
1158
+ assert len(logs_b) == 1
1159
+ assert logs_b[0]["content"] == "log for rollout B"
1160
+
1161
+ expt_logger.end()
1162
+
1163
+ def test_log_environment_multiline_content(
1164
+ self,
1165
+ shared_api_key: str,
1166
+ base_url: str,
1167
+ cleanup_experiments: list[str],
1168
+ fetch_env_logs,
1169
+ ) -> None:
1170
+ """Test environment log with multiline content."""
1171
+ run = expt_logger.init(
1172
+ name="test-env-log-multiline",
1173
+ api_key=shared_api_key,
1174
+ base_url=base_url,
1175
+ )
1176
+ cleanup_experiments.append(run._experiment_id)
1177
+
1178
+ content = "obs: {x: 1.0, y: 2.0}\nreward: 0.5\ndone: false\ninfo: {step: 10}"
1179
+ expt_logger.log_environment("00000000-0000-0000-0000-000000000004", content)
1180
+ time.sleep(0.5)
1181
+
1182
+ env_logs = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-000000000004")
1183
+ assert len(env_logs) == 1
1184
+ assert env_logs[0]["content"] == content
1185
+
1186
+ expt_logger.end()
@@ -1482,49 +1482,86 @@ def test_log_error_with_invalid_mode_empty(mock_client):
1482
1482
  run.end()
1483
1483
 
1484
1484
 
1485
- def test_subprocess_does_not_update_config(mock_client):
1486
- """Test subprocess (is_main_process=False) does not update config even if provided."""
1487
- import os
1488
- import tempfile
1485
+ # ========== log_environment Tests ==========
1489
1486
 
1487
+
1488
+ def test_log_environment_calls_client(mock_client):
1489
+ """Test log_environment() calls client with correct args."""
1490
1490
  _, client_instance = mock_client
1491
1491
 
1492
- # Write experiment ID to temp file
1493
- temp_dir = tempfile.gettempdir()
1494
- experiment_id_file = os.path.join(temp_dir, "expt-logger-experiment-id.txt")
1495
- with open(experiment_id_file, "w") as f:
1496
- f.write("existing-exp-123")
1492
+ run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
1497
1493
 
1498
- try:
1499
- # Subprocess tries to init with a config
1500
- run = Run(
1501
- config={"subprocess_key": "should_be_ignored"},
1502
- api_key="test-key",
1503
- base_url="https://test.example.com",
1504
- is_main_process=False,
1505
- )
1494
+ run.log_environment("rollout-abc", "step 1 observation")
1506
1495
 
1507
- # Should NOT create a new experiment
1508
- assert not client_instance.create_experiment.called
1496
+ # Give worker time to process
1497
+ time.sleep(0.1)
1509
1498
 
1510
- # Should call update_experiment to set status to active
1511
- assert client_instance.update_experiment.called
1499
+ assert client_instance.log_env_logs.called
1500
+ call_args = client_instance.log_env_logs.call_args
1501
+ assert call_args[0][0] == "test-exp-id"
1502
+ assert call_args[0][1] == "rollout-abc"
1503
+ assert call_args[0][2] == "step 1 observation"
1512
1504
 
1513
- # Check that config was NOT included in the update call
1514
- update_calls = client_instance.update_experiment.call_args_list
1515
- # Find the call that sets status to active (during attach)
1516
- attach_call = None
1517
- for call in update_calls:
1518
- if call.kwargs.get("status") == "active":
1519
- attach_call = call
1520
- break
1505
+ run.end()
1521
1506
 
1522
- assert attach_call is not None, "Should have called update_experiment with status='active'"
1523
- # Verify config was NOT passed (should be None)
1524
- assert attach_call.kwargs.get("config") is None
1525
1507
 
1526
- run.end()
1527
- finally:
1528
- # Cleanup
1529
- if os.path.isfile(experiment_id_file):
1530
- os.remove(experiment_id_file)
1508
+ def test_log_environment_multiple_calls(mock_client):
1509
+ """Test log_environment() can be called multiple times for different rollouts."""
1510
+ _, client_instance = mock_client
1511
+
1512
+ run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
1513
+
1514
+ run.log_environment("rollout-1", "log content 1")
1515
+ run.log_environment("rollout-2", "log content 2")
1516
+
1517
+ # Give worker time to process
1518
+ time.sleep(0.1)
1519
+
1520
+ assert client_instance.log_env_logs.call_count == 2
1521
+ first_call = client_instance.log_env_logs.call_args_list[0]
1522
+ second_call = client_instance.log_env_logs.call_args_list[1]
1523
+ assert first_call[0][1] == "rollout-1"
1524
+ assert first_call[0][2] == "log content 1"
1525
+ assert second_call[0][1] == "rollout-2"
1526
+ assert second_call[0][2] == "log content 2"
1527
+
1528
+ run.end()
1529
+
1530
+
1531
+ def test_log_environment_queue_full_handling(mock_client):
1532
+ """Test that queue full is handled gracefully for environment logs."""
1533
+ _, _ = mock_client
1534
+
1535
+ run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
1536
+ run._queue = queue.Queue(maxsize=1)
1537
+
1538
+ with patch("expt_logger.run.logger") as mock_logger:
1539
+ run.log_environment("rollout-1", "log 1")
1540
+ run.log_environment("rollout-2", "log 2") # Should trigger queue full
1541
+
1542
+ assert mock_logger.warning.called
1543
+ warning_msg = mock_logger.warning.call_args[0][0]
1544
+ assert "queue full" in warning_msg.lower()
1545
+
1546
+ run.end()
1547
+
1548
+
1549
+ def test_log_environment_api_error_is_logged(mock_client):
1550
+ """Test that API errors from log_environment are caught and logged, not raised."""
1551
+ _, client_instance = mock_client
1552
+
1553
+ client_instance.log_env_logs.side_effect = APIError("Server error")
1554
+
1555
+ run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
1556
+
1557
+ with patch("expt_logger.run.logger") as mock_logger:
1558
+ run.log_environment("rollout-abc", "some content")
1559
+
1560
+ # Give worker time to process and handle error
1561
+ time.sleep(0.2)
1562
+
1563
+ assert run._worker_thread is not None
1564
+ assert run._worker_thread.is_alive()
1565
+ assert mock_logger.error.called
1566
+
1567
+ run.end()
@@ -1,30 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(uv run mypy:*)",
5
- "Bash(uv run pytest:*)",
6
- "Bash(python -m pytest:*)",
7
- "Bash(python example_config_validation.py:*)",
8
- "Bash(uv run python:*)",
9
- "Bash(ls:*)",
10
- "Bash(curl:*)",
11
- "Bash(cat:*)",
12
- "Bash(EXPT_LOGGER_LOG_LEVEL=DEBUG uv run pytest:*)",
13
- "Bash(EXPT_LOGGER_API_KEY=test EXPT_LOGGER_BASE_URL=http://localhost:3000 uv run python:*)",
14
- "Bash(EXPT_LOGGER_API_KEY=test uv run python:*)",
15
- "Bash(uv sync:*)",
16
- "Bash(python -m json.tool:*)",
17
- "Bash(uv run ruff:*)",
18
- "Bash(find:*)",
19
- "Bash(uv add:*)",
20
- "Bash(python:*)",
21
- "Bash(EXPT_LOGGER_LOG_LEVEL=DEBUG uv run python:*)",
22
- "Bash(uv run:*)",
23
- "Bash(git restore:*)",
24
- "Bash(git checkout:*)",
25
- "Bash(git rebase:*)",
26
- "Bash(git add:*)",
27
- "Bash(pip install:*)"
28
- ]
29
- }
30
- }