flowent 0.2.0 → 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 +13 -4
  3. package/backend/src/flowent/compact.py +35 -14
  4. package/backend/src/flowent/llm.py +73 -7
  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-BlaCigkZ.js +0 -82
  37. package/backend/src/flowent/static/assets/index-CRvbsH4K.css +0 -2
  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 -60
  53. package/backend/tests/test_agent_tools.py +0 -1124
  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 -548
  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 -2174
  66. package/dist/frontend/assets/index-BlaCigkZ.js +0 -82
  67. package/dist/frontend/assets/index-CRvbsH4K.css +0 -2
@@ -1,462 +0,0 @@
1
- from pathlib import Path
2
-
3
- from fastapi.testclient import TestClient
4
-
5
- from flowent.main import create_app
6
-
7
-
8
- def configure_provider(client: TestClient) -> None:
9
- client.post(
10
- "/api/providers",
11
- json={
12
- "api_key": "sk-local",
13
- "base_url": "",
14
- "id": "provider-openai",
15
- "models": ["gpt-5.1"],
16
- "name": "OpenAI",
17
- "type": "openai",
18
- },
19
- )
20
- client.put(
21
- "/api/settings",
22
- json={
23
- "reasoning_effort": "default",
24
- "selected_model": "gpt-5.1",
25
- "selected_provider_id": "provider-openai",
26
- },
27
- )
28
-
29
-
30
- def write_skill(
31
- directory: Path,
32
- slug: str,
33
- *,
34
- body: str = "Use the project checklist before answering.",
35
- description: str = "Use the project checklist.",
36
- name: str = "Checklist",
37
- ) -> Path:
38
- skill_dir = directory / slug
39
- skill_dir.mkdir(parents=True)
40
- skill_path = skill_dir / "SKILL.md"
41
- skill_path.write_text(
42
- f"---\nname: {name}\ndescription: {description}\n---\n\n{body}\n"
43
- )
44
- return skill_path
45
-
46
-
47
- def skill_by_slug(skills: list[dict[str, object]], slug: str) -> dict[str, object]:
48
- for skill in skills:
49
- if skill["slug"] == slug:
50
- return skill
51
- raise AssertionError(f"Skill not found: {slug}")
52
-
53
-
54
- def test_state_is_empty_when_no_skills_exist(tmp_path, monkeypatch) -> None:
55
- monkeypatch.chdir(tmp_path)
56
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
57
- client = TestClient(create_app(serve_frontend=False))
58
-
59
- response = client.get("/api/state")
60
-
61
- assert response.status_code == 200
62
- assert response.json()["skills"] == []
63
-
64
-
65
- def test_state_lists_user_skills_from_data_directory(tmp_path, monkeypatch) -> None:
66
- data_dir = tmp_path / "data"
67
- monkeypatch.chdir(tmp_path)
68
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(data_dir))
69
- skill_path = write_skill(
70
- data_dir / "skills",
71
- "checklist",
72
- description="Use the user checklist.",
73
- name="User Checklist",
74
- )
75
- client = TestClient(create_app(serve_frontend=False))
76
-
77
- response = client.get("/api/state")
78
-
79
- assert response.status_code == 200
80
- assert response.json()["skills"] == [
81
- {
82
- "description": "Use the user checklist.",
83
- "enabled": True,
84
- "error": "",
85
- "id": response.json()["skills"][0]["id"],
86
- "name": "User Checklist",
87
- "path": str(skill_path),
88
- "scope": "user",
89
- "slug": "user-checklist",
90
- }
91
- ]
92
-
93
-
94
- def test_state_lists_project_skills_from_project_directory(
95
- tmp_path, monkeypatch
96
- ) -> None:
97
- monkeypatch.chdir(tmp_path)
98
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
99
- skill_path = write_skill(
100
- tmp_path / ".flowent" / "skills",
101
- "review",
102
- description="Review project changes.",
103
- name="Project Review",
104
- )
105
- client = TestClient(create_app(serve_frontend=False))
106
-
107
- response = client.get("/api/state")
108
-
109
- assert response.status_code == 200
110
- assert response.json()["skills"] == [
111
- {
112
- "description": "Review project changes.",
113
- "enabled": True,
114
- "error": "",
115
- "id": response.json()["skills"][0]["id"],
116
- "name": "Project Review",
117
- "path": str(skill_path),
118
- "scope": "project",
119
- "slug": "project-review",
120
- }
121
- ]
122
-
123
-
124
- def test_state_lists_project_skills_from_agents_directory(
125
- tmp_path, monkeypatch
126
- ) -> None:
127
- monkeypatch.chdir(tmp_path)
128
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
129
- skill_path = write_skill(
130
- tmp_path / ".agents" / "skills",
131
- "review",
132
- description="Review agent changes.",
133
- name="Agent Review",
134
- )
135
- client = TestClient(create_app(serve_frontend=False))
136
-
137
- response = client.get("/api/state")
138
-
139
- assert response.status_code == 200
140
- assert response.json()["skills"] == [
141
- {
142
- "description": "Review agent changes.",
143
- "enabled": True,
144
- "error": "",
145
- "id": response.json()["skills"][0]["id"],
146
- "name": "Agent Review",
147
- "path": str(skill_path),
148
- "scope": "project",
149
- "slug": "agent-review",
150
- }
151
- ]
152
-
153
-
154
- def test_state_lists_flowent_and_agents_project_skills(tmp_path, monkeypatch) -> None:
155
- monkeypatch.chdir(tmp_path)
156
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
157
- write_skill(
158
- tmp_path / ".flowent" / "skills",
159
- "project",
160
- description="Use project rules.",
161
- name="Project Rules",
162
- )
163
- write_skill(
164
- tmp_path / ".agents" / "skills",
165
- "agent",
166
- description="Use agent rules.",
167
- name="Agent Rules",
168
- )
169
- client = TestClient(create_app(serve_frontend=False))
170
-
171
- response = client.get("/api/state")
172
-
173
- assert response.status_code == 200
174
- skills = response.json()["skills"]
175
- assert skill_by_slug(skills, "project-rules")["scope"] == "project"
176
- assert skill_by_slug(skills, "agent-rules")["scope"] == "project"
177
-
178
-
179
- def test_skill_reload_reflects_filesystem_changes(tmp_path, monkeypatch) -> None:
180
- monkeypatch.chdir(tmp_path)
181
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
182
- client = TestClient(create_app(serve_frontend=False))
183
-
184
- initial_response = client.get("/api/state")
185
- write_skill(
186
- tmp_path / ".flowent" / "skills",
187
- "release",
188
- description="Prepare release notes.",
189
- name="Release Notes",
190
- )
191
- reload_response = client.post("/api/skills/reload")
192
-
193
- assert initial_response.json()["skills"] == []
194
- assert reload_response.status_code == 200
195
- assert skill_by_slug(reload_response.json(), "release-notes")["name"] == (
196
- "Release Notes"
197
- )
198
-
199
-
200
- def test_invalid_skill_reports_error(tmp_path, monkeypatch) -> None:
201
- monkeypatch.chdir(tmp_path)
202
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
203
- skill_dir = tmp_path / ".flowent" / "skills" / "broken"
204
- skill_dir.mkdir(parents=True)
205
- skill_path = skill_dir / "SKILL.md"
206
- skill_path.write_text("---\nname: Broken Skill\n---\n\nMissing description.\n")
207
- client = TestClient(create_app(serve_frontend=False))
208
-
209
- response = client.get("/api/state")
210
-
211
- assert response.status_code == 200
212
- skill = response.json()["skills"][0]
213
- assert skill == {
214
- "description": "",
215
- "enabled": True,
216
- "error": "Skill needs a name and description.",
217
- "id": skill["id"],
218
- "name": "Broken Skill",
219
- "path": str(skill_path),
220
- "scope": "project",
221
- "slug": "broken-skill",
222
- }
223
-
224
-
225
- def test_skill_enabled_state_persists_across_app_instances(
226
- tmp_path, monkeypatch
227
- ) -> None:
228
- monkeypatch.chdir(tmp_path)
229
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
230
- write_skill(
231
- tmp_path / ".flowent" / "skills",
232
- "review",
233
- description="Review project changes.",
234
- name="Project Review",
235
- )
236
- client = TestClient(create_app(serve_frontend=False))
237
- skill = skill_by_slug(client.get("/api/state").json()["skills"], "project-review")
238
-
239
- response = client.put(f"/api/skills/{skill['id']}", json={"enabled": False})
240
- restarted_client = TestClient(create_app(serve_frontend=False))
241
- restarted_skill = skill_by_slug(
242
- restarted_client.get("/api/state").json()["skills"],
243
- "project-review",
244
- )
245
-
246
- assert response.status_code == 200
247
- assert response.json()["enabled"] is False
248
- assert restarted_skill["enabled"] is False
249
-
250
-
251
- def test_workspace_response_injects_explicit_skill_instruction(
252
- tmp_path, monkeypatch
253
- ) -> None:
254
- monkeypatch.chdir(tmp_path)
255
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
256
- write_skill(
257
- tmp_path / ".flowent" / "skills",
258
- "review",
259
- body="Full only instruction: review every changed file before answering.",
260
- description="Review project changes.",
261
- name="Project Review",
262
- )
263
- captured_request: dict[str, object] = {}
264
-
265
- async def fake_completion(**request: object) -> object:
266
- captured_request.update(request)
267
-
268
- async def chunks() -> object:
269
- yield {"choices": [{"delta": {"content": "Done."}}]}
270
-
271
- return chunks()
272
-
273
- client = TestClient(
274
- create_app(serve_frontend=False, chat_completion=fake_completion)
275
- )
276
- configure_provider(client)
277
-
278
- response = client.post(
279
- "/api/workspace/respond",
280
- json={"content": "$project-review Please inspect the changes."},
281
- )
282
-
283
- assert response.status_code == 200
284
- contents = [str(message["content"]) for message in captured_request["messages"]]
285
- assert any(
286
- "Full only instruction: review every changed file before answering." in content
287
- for content in contents
288
- )
289
- assert captured_request["messages"][-1] == {
290
- "role": "user",
291
- "content": "$project-review Please inspect the changes.",
292
- }
293
-
294
-
295
- def test_workspace_response_injects_agents_skill_instruction(
296
- tmp_path, monkeypatch
297
- ) -> None:
298
- monkeypatch.chdir(tmp_path)
299
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
300
- write_skill(
301
- tmp_path / ".agents" / "skills",
302
- "review",
303
- body="Full only instruction: review agent workspace changes.",
304
- description="Review agent changes.",
305
- name="Agent Review",
306
- )
307
- captured_request: dict[str, object] = {}
308
-
309
- async def fake_completion(**request: object) -> object:
310
- captured_request.update(request)
311
-
312
- async def chunks() -> object:
313
- yield {"choices": [{"delta": {"content": "Done."}}]}
314
-
315
- return chunks()
316
-
317
- client = TestClient(
318
- create_app(serve_frontend=False, chat_completion=fake_completion)
319
- )
320
- configure_provider(client)
321
-
322
- response = client.post(
323
- "/api/workspace/respond",
324
- json={"content": "$agent-review Please inspect the changes."},
325
- )
326
-
327
- assert response.status_code == 200
328
- contents = "\n".join(
329
- str(message["content"]) for message in captured_request["messages"]
330
- )
331
- assert "Full only instruction: review agent workspace changes." in contents
332
-
333
-
334
- def test_workspace_response_injects_multiple_explicit_skill_instructions(
335
- tmp_path, monkeypatch
336
- ) -> None:
337
- monkeypatch.chdir(tmp_path)
338
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
339
- write_skill(
340
- tmp_path / ".flowent" / "skills",
341
- "review",
342
- body="Full only instruction: inspect changes.",
343
- description="Review project changes.",
344
- name="Project Review",
345
- )
346
- write_skill(
347
- tmp_path / ".flowent" / "skills",
348
- "release",
349
- body="Full only instruction: write release notes.",
350
- description="Prepare release notes.",
351
- name="Release Notes",
352
- )
353
- captured_request: dict[str, object] = {}
354
-
355
- async def fake_completion(**request: object) -> object:
356
- captured_request.update(request)
357
-
358
- async def chunks() -> object:
359
- yield {"choices": [{"delta": {"content": "Done."}}]}
360
-
361
- return chunks()
362
-
363
- client = TestClient(
364
- create_app(serve_frontend=False, chat_completion=fake_completion)
365
- )
366
- configure_provider(client)
367
-
368
- response = client.post(
369
- "/api/workspace/respond",
370
- json={"content": "$project-review $release-notes Draft the summary."},
371
- )
372
-
373
- assert response.status_code == 200
374
- contents = "\n".join(
375
- str(message["content"]) for message in captured_request["messages"]
376
- )
377
- assert "Full only instruction: inspect changes." in contents
378
- assert "Full only instruction: write release notes." in contents
379
-
380
-
381
- def test_workspace_response_treats_unknown_skill_reference_as_text(
382
- tmp_path, monkeypatch
383
- ) -> None:
384
- monkeypatch.chdir(tmp_path)
385
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
386
- write_skill(
387
- tmp_path / ".flowent" / "skills",
388
- "review",
389
- body="Full only instruction: inspect changes.",
390
- description="Review project changes.",
391
- name="Project Review",
392
- )
393
- captured_request: dict[str, object] = {}
394
-
395
- async def fake_completion(**request: object) -> object:
396
- captured_request.update(request)
397
-
398
- async def chunks() -> object:
399
- yield {"choices": [{"delta": {"content": "Done."}}]}
400
-
401
- return chunks()
402
-
403
- client = TestClient(
404
- create_app(serve_frontend=False, chat_completion=fake_completion)
405
- )
406
- configure_provider(client)
407
-
408
- response = client.post(
409
- "/api/workspace/respond",
410
- json={"content": "$missing Keep this as text."},
411
- )
412
-
413
- assert response.status_code == 200
414
- contents = "\n".join(
415
- str(message["content"]) for message in captured_request["messages"]
416
- )
417
- assert "Full only instruction: inspect changes." not in contents
418
- assert captured_request["messages"][-1] == {
419
- "role": "user",
420
- "content": "$missing Keep this as text.",
421
- }
422
-
423
-
424
- def test_workspace_response_does_not_inject_disabled_skill(
425
- tmp_path, monkeypatch
426
- ) -> None:
427
- monkeypatch.chdir(tmp_path)
428
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
429
- write_skill(
430
- tmp_path / ".flowent" / "skills",
431
- "review",
432
- body="Full only instruction: inspect changes.",
433
- description="Review project changes.",
434
- name="Project Review",
435
- )
436
- captured_request: dict[str, object] = {}
437
-
438
- async def fake_completion(**request: object) -> object:
439
- captured_request.update(request)
440
-
441
- async def chunks() -> object:
442
- yield {"choices": [{"delta": {"content": "Done."}}]}
443
-
444
- return chunks()
445
-
446
- client = TestClient(
447
- create_app(serve_frontend=False, chat_completion=fake_completion)
448
- )
449
- skill = skill_by_slug(client.get("/api/state").json()["skills"], "project-review")
450
- client.put(f"/api/skills/{skill['id']}", json={"enabled": False})
451
- configure_provider(client)
452
-
453
- response = client.post(
454
- "/api/workspace/respond",
455
- json={"content": "$project-review Please inspect the changes."},
456
- )
457
-
458
- assert response.status_code == 200
459
- contents = "\n".join(
460
- str(message["content"]) for message in captured_request["messages"]
461
- )
462
- assert "Full only instruction: inspect changes." not in contents
@@ -1,144 +0,0 @@
1
- import os
2
- import sys
3
- from types import SimpleNamespace
4
-
5
- import pytest
6
-
7
- from flowent.cli import main
8
- from flowent.main import create_app
9
- from flowent.paths import WORKDIR_ENV_VAR
10
- from flowent.sandbox import SandboxError
11
-
12
-
13
- def test_create_app_fails_when_sandbox_is_missing(monkeypatch) -> None:
14
- monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: None)
15
-
16
- with pytest.raises(SandboxError, match="Install bubblewrap"):
17
- create_app(serve_frontend=False)
18
-
19
-
20
- def test_create_app_starts_when_bwrap_is_available(monkeypatch) -> None:
21
- monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
22
-
23
- app = create_app(serve_frontend=False)
24
-
25
- assert app.title == "Flowent"
26
-
27
-
28
- def test_create_app_starts_when_bubblewrap_fallback_is_available(monkeypatch) -> None:
29
- monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bubblewrap")
30
-
31
- app = create_app(serve_frontend=False)
32
-
33
- assert app.title == "Flowent"
34
-
35
-
36
- def test_doctor_reports_missing_sandbox(monkeypatch, capsys) -> None:
37
- monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: None)
38
-
39
- with pytest.raises(SystemExit) as error:
40
- main(["doctor"])
41
-
42
- assert error.value.code == 1
43
- assert "Sandbox: missing." in capsys.readouterr().err
44
-
45
-
46
- def test_doctor_reports_available_sandbox(monkeypatch, capsys) -> None:
47
- monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
48
-
49
- with pytest.raises(SystemExit) as error:
50
- main(["doctor"])
51
-
52
- assert error.value.code == 0
53
- assert "Sandbox: /usr/bin/bwrap" in capsys.readouterr().out
54
-
55
-
56
- def test_main_sets_workdir_for_server_start(tmp_path, monkeypatch) -> None:
57
- env_workdir = tmp_path / "env-workspace"
58
- workdir = tmp_path / "workspace"
59
- env_workdir.mkdir()
60
- workdir.mkdir()
61
- calls: list[tuple[str, dict[str, object]]] = []
62
-
63
- def fake_run(app: str, **kwargs: object) -> None:
64
- calls.append((app, kwargs))
65
-
66
- monkeypatch.setenv("FLOWENT_WORKDIR", str(env_workdir))
67
- monkeypatch.setitem(sys.modules, "uvicorn", SimpleNamespace(run=fake_run))
68
-
69
- main(
70
- [
71
- "--workdir",
72
- str(workdir),
73
- "--host",
74
- "127.0.0.1",
75
- "--port",
76
- "6899",
77
- ]
78
- )
79
-
80
- assert os.environ["FLOWENT_WORKDIR"] == str(workdir.resolve(strict=False))
81
- assert calls == [("flowent.main:app", {"host": "127.0.0.1", "port": 6899})]
82
-
83
-
84
- def test_main_uses_default_host_when_environment_is_not_set(
85
- tmp_path, monkeypatch
86
- ) -> None:
87
- workdir = tmp_path / "workspace"
88
- workdir.mkdir()
89
- calls: list[tuple[str, dict[str, object]]] = []
90
-
91
- def fake_run(app: str, **kwargs: object) -> None:
92
- calls.append((app, kwargs))
93
-
94
- monkeypatch.delenv("FLOWENT_HOST", raising=False)
95
- monkeypatch.setenv(WORKDIR_ENV_VAR, str(workdir))
96
- monkeypatch.setitem(sys.modules, "uvicorn", SimpleNamespace(run=fake_run))
97
-
98
- main(["--workdir", str(workdir), "--port", "6899"])
99
-
100
- assert calls == [("flowent.main:app", {"host": "127.0.0.1", "port": 6899})]
101
-
102
-
103
- def test_main_reads_host_from_environment(tmp_path, monkeypatch) -> None:
104
- workdir = tmp_path / "workspace"
105
- workdir.mkdir()
106
- calls: list[tuple[str, dict[str, object]]] = []
107
-
108
- def fake_run(app: str, **kwargs: object) -> None:
109
- calls.append((app, kwargs))
110
-
111
- monkeypatch.setenv("FLOWENT_HOST", "0.0.0.0")
112
- monkeypatch.setenv(WORKDIR_ENV_VAR, str(workdir))
113
- monkeypatch.setitem(sys.modules, "uvicorn", SimpleNamespace(run=fake_run))
114
-
115
- main(["--workdir", str(workdir), "--port", "6899"])
116
-
117
- assert calls == [("flowent.main:app", {"host": "0.0.0.0", "port": 6899})]
118
-
119
-
120
- def test_main_prefers_host_argument_over_environment(tmp_path, monkeypatch) -> None:
121
- workdir = tmp_path / "workspace"
122
- workdir.mkdir()
123
- calls: list[tuple[str, dict[str, object]]] = []
124
-
125
- def fake_run(app: str, **kwargs: object) -> None:
126
- calls.append((app, kwargs))
127
-
128
- monkeypatch.setenv("FLOWENT_HOST", "0.0.0.0")
129
- monkeypatch.setenv(WORKDIR_ENV_VAR, str(workdir))
130
- monkeypatch.setitem(sys.modules, "uvicorn", SimpleNamespace(run=fake_run))
131
-
132
- main(["--workdir", str(workdir), "--host", "127.0.0.1", "--port", "6899"])
133
-
134
- assert calls == [("flowent.main:app", {"host": "127.0.0.1", "port": 6899})]
135
-
136
-
137
- def test_main_rejects_missing_workdir(tmp_path, capsys) -> None:
138
- missing = tmp_path / "missing"
139
-
140
- with pytest.raises(SystemExit) as error:
141
- main(["--workdir", str(missing)])
142
-
143
- assert error.value.code == 2
144
- assert f"Workdir does not exist: {missing}" in capsys.readouterr().err