flowent 0.1.5 → 0.2.1

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 (67) hide show
  1. package/backend/pyproject.toml +31 -5
  2. package/backend/src/flowent/agent.py +107 -37
  3. package/backend/src/flowent/compact.py +35 -14
  4. package/backend/src/flowent/llm.py +198 -12
  5. package/backend/src/flowent/main.py +260 -59
  6. package/backend/src/flowent/static/assets/index-CRSV2xu1.css +2 -0
  7. package/backend/src/flowent/static/assets/index-DUYj6rgD.js +82 -0
  8. package/backend/src/flowent/static/index.html +2 -2
  9. package/backend/src/flowent/storage.py +135 -3
  10. package/backend/src/flowent/usage.py +315 -0
  11. package/backend/uv.lock +971 -3
  12. package/dist/frontend/assets/index-CRSV2xu1.css +2 -0
  13. package/dist/frontend/assets/index-DUYj6rgD.js +82 -0
  14. package/dist/frontend/index.html +2 -2
  15. package/package.json +24 -3
  16. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/approval.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
  23. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  24. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  25. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  26. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  27. package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
  28. package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
  29. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  30. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  31. package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
  32. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  33. package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
  34. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  35. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  36. package/backend/src/flowent/static/assets/index-Cl20cARb.css +0 -2
  37. package/backend/src/flowent/static/assets/index-dsDDsEym.js +0 -81
  38. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  39. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/__pycache__/test_approval.cpython-313-pytest-9.0.3.pyc +0 -0
  41. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  42. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  43. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  44. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  45. package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
  46. package/backend/tests/__pycache__/test_patch.cpython-313-pytest-9.0.3.pyc +0 -0
  47. package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
  48. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  49. package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
  50. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  51. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  52. package/backend/tests/conftest.py +0 -21
  53. package/backend/tests/test_agent_tools.py +0 -988
  54. package/backend/tests/test_approval.py +0 -283
  55. package/backend/tests/test_channels.py +0 -360
  56. package/backend/tests/test_health.py +0 -12
  57. package/backend/tests/test_llm_providers.py +0 -387
  58. package/backend/tests/test_logging.py +0 -212
  59. package/backend/tests/test_mcp.py +0 -788
  60. package/backend/tests/test_patch.py +0 -112
  61. package/backend/tests/test_permissions.py +0 -588
  62. package/backend/tests/test_persistence.py +0 -249
  63. package/backend/tests/test_skills.py +0 -462
  64. package/backend/tests/test_startup_requirements.py +0 -144
  65. package/backend/tests/test_workspace_chat.py +0 -2122
  66. package/dist/frontend/assets/index-Cl20cARb.css +0 -2
  67. package/dist/frontend/assets/index-dsDDsEym.js +0 -81
@@ -1,387 +0,0 @@
1
- import json
2
-
3
- import pytest
4
-
5
- from flowent.llm import (
6
- ChatMessage,
7
- ProviderConnection,
8
- ProviderFormat,
9
- ReasoningEffort,
10
- build_litellm_request,
11
- chunk_delta_reasoning,
12
- complete_chat,
13
- normalize_system_messages,
14
- stream_chat,
15
- )
16
-
17
-
18
- def read_single_llm_request_diagnostic(tmp_path):
19
- files = sorted((tmp_path / "logs" / "llm-requests").glob("llm-request-*.json"))
20
- assert len(files) == 1
21
- return json.loads(files[0].read_text())
22
-
23
-
24
- def test_supported_provider_formats_match_product_choices() -> None:
25
- assert [provider.value for provider in ProviderFormat] == [
26
- "openai",
27
- "openai_responses",
28
- "anthropic",
29
- "gemini",
30
- ]
31
-
32
-
33
- def test_build_litellm_request_maps_provider_connection_to_completion_args() -> None:
34
- connection = ProviderConnection(
35
- name="Primary",
36
- provider=ProviderFormat.ANTHROPIC,
37
- model="claude-sonnet-4-5",
38
- secret_reference="connection-primary",
39
- base_url="https://example.test/v1",
40
- )
41
- messages = [
42
- ChatMessage(role="system", content="Keep answers direct."),
43
- ChatMessage(role="user", content="Draft a launch checklist."),
44
- ]
45
-
46
- request = build_litellm_request(connection, messages)
47
-
48
- assert request == {
49
- "api_base": "https://example.test/v1",
50
- "api_key": "connection-primary",
51
- "messages": [
52
- {"role": "system", "content": "Keep answers direct."},
53
- {"role": "user", "content": "Draft a launch checklist."},
54
- ],
55
- "model": "anthropic/claude-sonnet-4-5",
56
- }
57
-
58
-
59
- def test_build_litellm_request_omits_default_reasoning_effort() -> None:
60
- connection = ProviderConnection(
61
- name="Primary",
62
- provider=ProviderFormat.OPENAI,
63
- model="gpt-5.1",
64
- secret_reference="connection-primary",
65
- reasoning_effort=ReasoningEffort.DEFAULT,
66
- )
67
-
68
- request = build_litellm_request(
69
- connection, [ChatMessage(role="user", content="Draft a checklist.")]
70
- )
71
-
72
- assert "reasoning_effort" not in request
73
-
74
-
75
- def test_build_litellm_request_includes_selected_reasoning_effort() -> None:
76
- connection = ProviderConnection(
77
- name="Primary",
78
- provider=ProviderFormat.OPENAI,
79
- model="gpt-5.1",
80
- secret_reference="connection-primary",
81
- reasoning_effort=ReasoningEffort.XHIGH,
82
- )
83
-
84
- request = build_litellm_request(
85
- connection, [ChatMessage(role="user", content="Draft a checklist.")]
86
- )
87
-
88
- assert request["reasoning_effort"] == "xhigh"
89
-
90
-
91
- def test_chunk_delta_reasoning_reads_litellm_reasoning_fields() -> None:
92
- assert (
93
- chunk_delta_reasoning(
94
- {"choices": [{"delta": {"reasoning_content": "Checking files."}}]}
95
- )
96
- == "Checking files."
97
- )
98
- assert (
99
- chunk_delta_reasoning(
100
- {
101
- "choices": [
102
- {
103
- "delta": {
104
- "thinking_blocks": [{"thinking": "Read files."}],
105
- "reasoning_items": [{"summary": "Summarize."}],
106
- }
107
- }
108
- ]
109
- }
110
- )
111
- == "Read files.Summarize."
112
- )
113
-
114
-
115
- @pytest.mark.anyio
116
- async def test_complete_chat_uses_injected_litellm_completion() -> None:
117
- captured_request: dict[str, object] = {}
118
-
119
- async def fake_completion(**request: object) -> dict[str, object]:
120
- captured_request.update(request)
121
- return {
122
- "choices": [
123
- {
124
- "message": {
125
- "content": "Here is the checklist.",
126
- "role": "assistant",
127
- },
128
- }
129
- ]
130
- }
131
-
132
- connection = ProviderConnection(
133
- name="Responses",
134
- provider=ProviderFormat.OPENAI_RESPONSES,
135
- model="gpt-5.1",
136
- secret_reference="connection-responses",
137
- )
138
-
139
- answer = await complete_chat(
140
- connection,
141
- [ChatMessage(role="user", content="Create a checklist.")],
142
- completion=fake_completion,
143
- )
144
-
145
- assert captured_request["model"] == "openai/gpt-5.1"
146
- assert answer == ChatMessage(role="assistant", content="Here is the checklist.")
147
-
148
-
149
- @pytest.mark.anyio
150
- async def test_development_mode_writes_completion_request_diagnostic_file(
151
- tmp_path, monkeypatch
152
- ) -> None:
153
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
154
- monkeypatch.setenv("DEBUG", "true")
155
-
156
- async def fake_completion(**request: object) -> dict[str, object]:
157
- return {
158
- "choices": [
159
- {
160
- "message": {
161
- "content": "Here is the checklist.",
162
- "role": "assistant",
163
- },
164
- }
165
- ]
166
- }
167
-
168
- connection = ProviderConnection(
169
- name="Responses",
170
- provider=ProviderFormat.OPENAI_RESPONSES,
171
- model="gpt-5.1",
172
- secret_reference="sk-request-secret",
173
- )
174
- messages = [ChatMessage(role="user", content="Create a checklist.")]
175
- tools = [
176
- {
177
- "type": "function",
178
- "function": {
179
- "name": "create_checklist",
180
- "description": "Create a checklist.",
181
- },
182
- }
183
- ]
184
-
185
- await complete_chat(
186
- connection,
187
- messages,
188
- completion=fake_completion,
189
- tools=tools,
190
- )
191
-
192
- diagnostic = read_single_llm_request_diagnostic(tmp_path)
193
-
194
- assert diagnostic == {
195
- "base_url": None,
196
- "litellm_model": "openai/gpt-5.1",
197
- "messages": [{"content": "Create a checklist.", "role": "user"}],
198
- "model": "gpt-5.1",
199
- "provider": "openai_responses",
200
- "reasoning_effort": "default",
201
- "stream": False,
202
- "tools": tools,
203
- }
204
-
205
-
206
- @pytest.mark.anyio
207
- async def test_stream_chat_uses_litellm_streaming() -> None:
208
- captured_request: dict[str, object] = {}
209
-
210
- async def fake_completion(**request: object) -> object:
211
- captured_request.update(request)
212
-
213
- async def chunks() -> object:
214
- yield {"choices": [{"delta": {"content": "Here is "}}]}
215
- yield {"choices": [{"delta": {"content": "the checklist."}}]}
216
-
217
- return chunks()
218
-
219
- connection = ProviderConnection(
220
- name="Responses",
221
- provider=ProviderFormat.OPENAI_RESPONSES,
222
- model="gpt-5.1",
223
- secret_reference="connection-responses",
224
- )
225
-
226
- chunks = [
227
- chunk
228
- async for chunk in stream_chat(
229
- connection,
230
- [ChatMessage(role="user", content="Create a checklist.")],
231
- completion=fake_completion,
232
- )
233
- ]
234
-
235
- assert captured_request["stream"] is True
236
- assert captured_request["model"] == "openai/gpt-5.1"
237
- assert chunks == ["Here is ", "the checklist."]
238
-
239
-
240
- @pytest.mark.anyio
241
- async def test_development_mode_writes_one_streaming_request_diagnostic_file(
242
- tmp_path, monkeypatch
243
- ) -> None:
244
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
245
- monkeypatch.setenv("DEBUG", "true")
246
-
247
- async def fake_completion(**request: object) -> object:
248
- async def chunks() -> object:
249
- yield {"choices": [{"delta": {"content": "Here is "}}]}
250
- yield {"choices": [{"delta": {"content": "the checklist."}}]}
251
-
252
- return chunks()
253
-
254
- connection = ProviderConnection(
255
- name="Responses",
256
- provider=ProviderFormat.OPENAI_RESPONSES,
257
- model="gpt-5.1",
258
- secret_reference="sk-request-secret",
259
- )
260
-
261
- chunks = [
262
- chunk
263
- async for chunk in stream_chat(
264
- connection,
265
- [ChatMessage(role="user", content="Create a checklist.")],
266
- completion=fake_completion,
267
- )
268
- ]
269
- diagnostic = read_single_llm_request_diagnostic(tmp_path)
270
-
271
- assert chunks == ["Here is ", "the checklist."]
272
- assert diagnostic["stream"] is True
273
-
274
-
275
- @pytest.mark.anyio
276
- async def test_development_request_diagnostic_omits_api_key_and_secret_values(
277
- tmp_path, monkeypatch
278
- ) -> None:
279
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
280
- monkeypatch.setenv("DEBUG", "true")
281
-
282
- async def fake_completion(**request: object) -> dict[str, object]:
283
- return {
284
- "choices": [
285
- {
286
- "message": {
287
- "content": "Here is the checklist.",
288
- "role": "assistant",
289
- },
290
- }
291
- ]
292
- }
293
-
294
- connection = ProviderConnection(
295
- name="Responses",
296
- provider=ProviderFormat.OPENAI_RESPONSES,
297
- model="gpt-5.1",
298
- secret_reference="sk-provider-secret",
299
- )
300
- tools = [
301
- {
302
- "type": "function",
303
- "function": {
304
- "name": "create_checklist",
305
- "description": "Uses api_key=sk-tool-secret when configured.",
306
- },
307
- }
308
- ]
309
-
310
- await complete_chat(
311
- connection,
312
- [ChatMessage(role="user", content="authorization=Bearer sk-message-secret")],
313
- completion=fake_completion,
314
- tools=tools,
315
- )
316
-
317
- rendered = next(
318
- (tmp_path / "logs" / "llm-requests").glob("llm-request-*.json")
319
- ).read_text()
320
-
321
- assert "api_key" not in rendered
322
- assert "sk-provider-secret" not in rendered
323
- assert "sk-tool-secret" not in rendered
324
- assert "sk-message-secret" not in rendered
325
-
326
-
327
- @pytest.mark.anyio
328
- async def test_non_development_mode_skips_request_diagnostic_file(
329
- tmp_path, monkeypatch
330
- ) -> None:
331
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
332
- monkeypatch.delenv("DEBUG", raising=False)
333
-
334
- async def fake_completion(**request: object) -> dict[str, object]:
335
- return {
336
- "choices": [
337
- {
338
- "message": {
339
- "content": "Here is the checklist.",
340
- "role": "assistant",
341
- },
342
- }
343
- ]
344
- }
345
-
346
- connection = ProviderConnection(
347
- name="Responses",
348
- provider=ProviderFormat.OPENAI_RESPONSES,
349
- model="gpt-5.1",
350
- secret_reference="sk-request-secret",
351
- )
352
-
353
- await complete_chat(
354
- connection,
355
- [ChatMessage(role="user", content="Create a checklist.")],
356
- completion=fake_completion,
357
- )
358
-
359
- assert not (tmp_path / "logs" / "llm-requests").exists()
360
-
361
-
362
- def test_normalize_system_messages_keeps_multiple_system_messages_for_openai() -> None:
363
- messages = [
364
- {"role": "system", "content": "Base prompt."},
365
- {"role": "system", "content": "Configured prompt."},
366
- {"role": "user", "content": "Hello."},
367
- ]
368
-
369
- assert normalize_system_messages(messages, ProviderFormat.OPENAI) == messages
370
-
371
-
372
- def test_normalize_system_messages_converts_additional_system_messages_for_anthropic() -> (
373
- None
374
- ):
375
- messages = [
376
- {"role": "system", "content": "Base prompt."},
377
- {"role": "system", "content": "Configured prompt."},
378
- {"role": "system", "content": "Project prompt."},
379
- {"role": "user", "content": "Hello."},
380
- ]
381
-
382
- assert normalize_system_messages(messages, ProviderFormat.ANTHROPIC) == [
383
- {"role": "system", "content": "Base prompt."},
384
- {"role": "user", "content": "Configured prompt."},
385
- {"role": "user", "content": "Project prompt."},
386
- {"role": "user", "content": "Hello."},
387
- ]
@@ -1,212 +0,0 @@
1
- import logging
2
- import sys
3
-
4
- from flowent.logging import (
5
- LITELLM_LOGGER_NAMES,
6
- TRACE_LEVEL,
7
- configure_litellm_logging,
8
- configure_logging,
9
- ensure_logging_configured,
10
- redact_log_value,
11
- sanitize_diagnostic_value,
12
- )
13
-
14
-
15
- def test_logging_creates_run_file_under_data_logs(tmp_path, monkeypatch) -> None:
16
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
17
-
18
- log_file = configure_logging()
19
-
20
- assert log_file.parent == tmp_path / "logs"
21
- assert log_file.name.startswith("flowent-")
22
- assert log_file.suffix == ".log"
23
- assert log_file.is_file()
24
-
25
-
26
- def test_file_logging_accepts_trace_and_console_uses_mode_levels(
27
- tmp_path, monkeypatch
28
- ) -> None:
29
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
30
- monkeypatch.setenv("DEBUG", "False")
31
-
32
- log_file = configure_logging()
33
- handlers = logging.getLogger().handlers
34
- file_handler = next(
35
- handler for handler in handlers if isinstance(handler, logging.FileHandler)
36
- )
37
- console_handler = next(
38
- handler for handler in handlers if not isinstance(handler, logging.FileHandler)
39
- )
40
-
41
- assert file_handler.level == TRACE_LEVEL
42
- assert console_handler.level == logging.INFO
43
-
44
- logging.getLogger("flowent.test").log(TRACE_LEVEL, "trace-only detail")
45
- for handler in logging.getLogger().handlers:
46
- handler.flush()
47
-
48
- assert "trace-only detail" in log_file.read_text()
49
-
50
- monkeypatch.setenv("DEBUG", "True")
51
- configure_logging()
52
- console_handler = next(
53
- handler
54
- for handler in logging.getLogger().handlers
55
- if not isinstance(handler, logging.FileHandler)
56
- )
57
-
58
- assert console_handler.level == logging.DEBUG
59
-
60
-
61
- def test_logging_prunes_old_run_logs(tmp_path, monkeypatch) -> None:
62
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
63
- logs = tmp_path / "logs"
64
- logs.mkdir(parents=True)
65
- for index in range(6):
66
- (logs / f"flowent-20260101-00000{index}-1.log").write_text(str(index))
67
-
68
- configure_logging()
69
-
70
- files = sorted(log.name for log in logs.glob("flowent-*.log"))
71
- assert len(files) == 5
72
- assert "flowent-20260101-000000-1.log" not in files
73
-
74
-
75
- def test_logging_path_follows_flowent_data_dir(tmp_path, monkeypatch) -> None:
76
- data_dir = tmp_path / "custom-flowent"
77
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(data_dir))
78
-
79
- log_file = configure_logging()
80
-
81
- assert log_file.parent == data_dir / "logs"
82
-
83
-
84
- def test_logging_redacts_full_api_key_but_keeps_context(tmp_path, monkeypatch) -> None:
85
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
86
- log_file = configure_logging()
87
-
88
- logging.getLogger("flowent.test").log(
89
- TRACE_LEVEL,
90
- "provider=OpenAI model=gpt-5 output=hello api_key=sk-secret-value",
91
- )
92
- for handler in logging.getLogger().handlers:
93
- handler.flush()
94
-
95
- rendered = log_file.read_text()
96
-
97
- assert "provider=OpenAI" in rendered
98
- assert "model=gpt-5" in rendered
99
- assert "output=hello" in rendered
100
- assert "sk-secret-value" not in rendered
101
- assert "api_key=[REDACTED]" in rendered
102
- assert redact_log_value("authorization=Bearer sk-secret-value") == (
103
- "authorization=[REDACTED]"
104
- )
105
-
106
-
107
- def test_diagnostic_sanitizer_removes_secret_fields_and_values() -> None:
108
- sanitized = sanitize_diagnostic_value(
109
- {
110
- "api_key": "sk-root-secret",
111
- "messages": [
112
- {
113
- "role": "user",
114
- "content": "authorization=Bearer sk-message-secret",
115
- }
116
- ],
117
- "tools": [
118
- {
119
- "function": {
120
- "name": "send_message",
121
- "description": "Needs api_key=sk-tool-secret.",
122
- }
123
- }
124
- ],
125
- }
126
- )
127
-
128
- rendered = str(sanitized)
129
-
130
- assert "api_key" not in rendered
131
- assert "sk-root-secret" not in rendered
132
- assert "sk-message-secret" not in rendered
133
- assert "sk-tool-secret" not in rendered
134
-
135
-
136
- def test_direct_main_app_import_creates_data_log_file(tmp_path, monkeypatch) -> None:
137
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
138
- sys.modules.pop("flowent.main", None)
139
-
140
- try:
141
- __import__("flowent.main")
142
- finally:
143
- sys.modules.pop("flowent.main", None)
144
-
145
- files = sorted((tmp_path / "logs").glob("flowent-*.log"))
146
- assert len(files) == 1
147
-
148
-
149
- def test_create_app_creates_data_log_file(tmp_path, monkeypatch) -> None:
150
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
151
-
152
- from flowent.main import create_app
153
-
154
- create_app(serve_frontend=False)
155
-
156
- files = sorted((tmp_path / "logs").glob("flowent-*.log"))
157
- assert len(files) == 1
158
-
159
-
160
- def test_create_app_reuses_logging_handlers(tmp_path, monkeypatch) -> None:
161
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
162
-
163
- from flowent.main import create_app
164
-
165
- create_app(serve_frontend=False)
166
- create_app(serve_frontend=False)
167
-
168
- handlers = logging.getLogger().handlers
169
- file_handlers = [
170
- handler for handler in handlers if isinstance(handler, logging.FileHandler)
171
- ]
172
- console_handlers = [
173
- handler for handler in handlers if not isinstance(handler, logging.FileHandler)
174
- ]
175
- files = sorted((tmp_path / "logs").glob("flowent-*.log"))
176
-
177
- assert len(file_handlers) == 1
178
- assert len(console_handlers) == 1
179
- assert len(files) == 1
180
-
181
-
182
- def test_any_logging_path_follows_flowent_data_dir(tmp_path, monkeypatch) -> None:
183
- data_dir = tmp_path / "custom-flowent"
184
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(data_dir))
185
-
186
- log_file = ensure_logging_configured()
187
-
188
- assert log_file.parent == data_dir / "logs"
189
-
190
-
191
- def test_litellm_debug_logs_use_flowent_handlers(tmp_path, monkeypatch, capsys) -> None:
192
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
193
- monkeypatch.delenv("LITELLM_LOG", raising=False)
194
-
195
- log_file = configure_logging()
196
- import litellm # noqa: F401
197
-
198
- configure_litellm_logging()
199
- logging.getLogger("LiteLLM").debug("stream chunk detail")
200
- for handler in logging.getLogger().handlers:
201
- handler.flush()
202
-
203
- captured = capsys.readouterr()
204
- litellm_handlers = [
205
- handler
206
- for logger_name in LITELLM_LOGGER_NAMES
207
- for handler in logging.getLogger(logger_name).handlers
208
- ]
209
-
210
- assert litellm_handlers == []
211
- assert "stream chunk detail" not in captured.err
212
- assert "stream chunk detail" in log_file.read_text()