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,283 +0,0 @@
1
- import json
2
-
3
- import pytest
4
-
5
- from flowent.approval import (
6
- ApprovalReviewRequest,
7
- ApprovalTranscriptEntry,
8
- review_approval_request,
9
- )
10
- from flowent.llm import ProviderConnection, ProviderFormat
11
-
12
-
13
- def provider_connection() -> ProviderConnection:
14
- return ProviderConnection(
15
- model="model",
16
- name="Provider",
17
- provider=ProviderFormat.OPENAI,
18
- secret_reference="secret",
19
- )
20
-
21
-
22
- @pytest.mark.anyio
23
- async def test_review_payload_includes_current_user_request_and_transcript(
24
- tmp_path,
25
- ) -> None:
26
- captured_messages: list[dict[str, object]] = []
27
-
28
- async def fake_completion(**request: object) -> object:
29
- captured_messages.extend(request["messages"])
30
- return {
31
- "choices": [
32
- {
33
- "message": {
34
- "content": json.dumps(
35
- {
36
- "risk_level": "low",
37
- "risk_score": 25,
38
- "rationale": "User approved after concrete risk context.",
39
- "evidence": [
40
- {
41
- "message": "Assistant explained Docker socket impact.",
42
- "why": "Establishes informed consent.",
43
- }
44
- ],
45
- }
46
- ),
47
- "role": "assistant",
48
- }
49
- },
50
- ],
51
- }
52
-
53
- decision = await review_approval_request(
54
- provider_connection(),
55
- ApprovalReviewRequest(
56
- action="additional_permissions",
57
- arguments={"command": "docker compose up -d --build"},
58
- cwd=tmp_path,
59
- tool_name="shell_command",
60
- user_request="确认",
61
- transcript=[
62
- ApprovalTranscriptEntry(
63
- role="assistant",
64
- content=(
65
- "This will recreate the dev container, write to the Docker "
66
- "socket, and briefly interrupt the local service."
67
- ),
68
- ),
69
- ApprovalTranscriptEntry(role="user", content="确认"),
70
- ],
71
- write_paths=[tmp_path / "docker.sock"],
72
- ),
73
- completion=fake_completion,
74
- )
75
-
76
- assert decision.decision == "approved"
77
- assert decision.risk_level == "low"
78
- assert decision.risk_score == 25
79
- assert "informed of the concrete risk" in str(captured_messages[0]["content"])
80
- payload = json.loads(str(captured_messages[-1]["content"]))
81
- assert payload["user_request"] == "确认"
82
- assert payload["transcript"][-1] == {"role": "user", "content": "确认"}
83
-
84
-
85
- @pytest.mark.anyio
86
- async def test_concrete_docker_socket_confirmation_can_be_approved(tmp_path) -> None:
87
- async def fake_completion(**request: object) -> object:
88
- return {
89
- "choices": [
90
- {
91
- "message": {
92
- "content": json.dumps(
93
- {
94
- "risk_level": "medium",
95
- "risk_score": 55,
96
- "rationale": (
97
- "The user approved after being told the command "
98
- "will recreate the dev container through Docker."
99
- ),
100
- "evidence": [],
101
- }
102
- ),
103
- "role": "assistant",
104
- }
105
- }
106
- ]
107
- }
108
-
109
- decision = await review_approval_request(
110
- provider_connection(),
111
- ApprovalReviewRequest(
112
- action="additional_permissions",
113
- arguments={
114
- "command": "docker compose up -d --force-recreate flowent",
115
- },
116
- cwd=tmp_path,
117
- tool_name="shell_command",
118
- user_request="确认",
119
- transcript=[
120
- ApprovalTranscriptEntry(
121
- role="assistant",
122
- content=(
123
- "This will recreate the Flowent dev container through "
124
- "Docker and may briefly interrupt the running service."
125
- ),
126
- ),
127
- ApprovalTranscriptEntry(role="user", content="确认"),
128
- ],
129
- write_paths=[tmp_path / "docker.sock"],
130
- ),
131
- completion=fake_completion,
132
- )
133
-
134
- assert decision.decision == "approved"
135
- assert decision.risk_level == "medium"
136
- assert decision.risk_score == 55
137
-
138
-
139
- @pytest.mark.anyio
140
- async def test_vague_confirmation_without_concrete_risk_context_is_denied(
141
- tmp_path,
142
- ) -> None:
143
- captured_payload: dict[str, object] = {}
144
-
145
- async def fake_completion(**request: object) -> object:
146
- captured_payload.update(json.loads(str(request["messages"][-1]["content"])))
147
- return {
148
- "choices": [
149
- {
150
- "message": {
151
- "content": json.dumps(
152
- {
153
- "risk_level": "high",
154
- "risk_score": 82,
155
- "rationale": (
156
- "The transcript only contains a vague confirmation "
157
- "and no concrete Docker risk explanation."
158
- ),
159
- "evidence": [],
160
- }
161
- ),
162
- "role": "assistant",
163
- }
164
- }
165
- ]
166
- }
167
-
168
- decision = await review_approval_request(
169
- provider_connection(),
170
- ApprovalReviewRequest(
171
- action="additional_permissions",
172
- arguments={
173
- "command": "docker compose up -d --force-recreate flowent",
174
- },
175
- cwd=tmp_path,
176
- tool_name="shell_command",
177
- user_request="确认",
178
- transcript=[ApprovalTranscriptEntry(role="user", content="确认")],
179
- write_paths=[tmp_path / "docker.sock"],
180
- ),
181
- completion=fake_completion,
182
- )
183
-
184
- assert decision.decision == "denied"
185
- assert decision.risk_level == "high"
186
- assert decision.risk_score == 82
187
- assert captured_payload["transcript"] == [{"role": "user", "content": "确认"}]
188
-
189
-
190
- @pytest.mark.anyio
191
- async def test_broad_destructive_action_with_vague_confirmation_is_denied(
192
- tmp_path,
193
- ) -> None:
194
- async def fake_completion(**request: object) -> object:
195
- return {
196
- "choices": [
197
- {
198
- "message": {
199
- "content": json.dumps(
200
- {
201
- "risk_level": "high",
202
- "risk_score": 96,
203
- "rationale": (
204
- "The action can delete broad data and the user "
205
- "did not approve that concrete destructive risk."
206
- ),
207
- "evidence": [
208
- {
209
- "message": "rm -rf /var/lib/postgresql",
210
- "why": "Broad destructive write outside the task.",
211
- }
212
- ],
213
- }
214
- ),
215
- "role": "assistant",
216
- }
217
- }
218
- ]
219
- }
220
-
221
- decision = await review_approval_request(
222
- provider_connection(),
223
- ApprovalReviewRequest(
224
- action="sandbox_failure",
225
- arguments={"command": "rm -rf /var/lib/postgresql"},
226
- cwd=tmp_path,
227
- tool_name="shell_command",
228
- tool_result="Read-only file system",
229
- user_request="确认",
230
- transcript=[ApprovalTranscriptEntry(role="user", content="确认")],
231
- ),
232
- completion=fake_completion,
233
- )
234
-
235
- assert decision.decision == "denied"
236
- assert decision.risk_level == "high"
237
- assert decision.risk_score == 96
238
-
239
-
240
- @pytest.mark.anyio
241
- async def test_invalid_reviewer_json_is_denied(tmp_path) -> None:
242
- async def fake_completion(**request: object) -> object:
243
- return {
244
- "choices": [
245
- {"message": {"content": "approved", "role": "assistant"}},
246
- ],
247
- }
248
-
249
- decision = await review_approval_request(
250
- provider_connection(),
251
- ApprovalReviewRequest(
252
- action="sandbox_failure",
253
- arguments={"command": "touch file.txt"},
254
- cwd=tmp_path,
255
- tool_name="shell_command",
256
- tool_result="Read-only file system",
257
- ),
258
- completion=fake_completion,
259
- )
260
-
261
- assert decision.decision == "denied"
262
- assert "valid JSON" in decision.reason
263
-
264
-
265
- @pytest.mark.anyio
266
- async def test_reviewer_call_failure_is_denied(tmp_path) -> None:
267
- async def fake_completion(**request: object) -> object:
268
- raise RuntimeError("model unavailable")
269
-
270
- decision = await review_approval_request(
271
- provider_connection(),
272
- ApprovalReviewRequest(
273
- action="edit",
274
- arguments={"patch": "*** Begin Patch\n*** End Patch"},
275
- cwd=tmp_path,
276
- tool_name="apply_patch",
277
- write_paths=[tmp_path / "outside"],
278
- ),
279
- completion=fake_completion,
280
- )
281
-
282
- assert decision.decision == "denied"
283
- assert "model unavailable" in decision.reason
@@ -1,360 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from typing import Any
5
-
6
- import pytest
7
- from fastapi.testclient import TestClient
8
-
9
- from flowent.channels import TelegramBotManager, split_telegram_message
10
- from flowent.main import create_app
11
- from flowent.storage import StateStore, StoredTelegramBot, StoredTelegramSession
12
-
13
-
14
- class FakeTelegramTransport:
15
- def __init__(self) -> None:
16
- self.sent_messages: list[dict[str, str]] = []
17
- self.updates: list[dict[str, Any]] = []
18
- self.error: Exception | None = None
19
- self.get_updates_calls = 0
20
-
21
- async def get_updates(
22
- self,
23
- *,
24
- offset: int | None,
25
- timeout: int,
26
- token: str,
27
- ) -> list[dict[str, Any]]:
28
- self.get_updates_calls += 1
29
- if self.error is not None:
30
- raise self.error
31
- return self.updates
32
-
33
- async def send_message(
34
- self,
35
- *,
36
- chat_id: str,
37
- text: str,
38
- token: str,
39
- ) -> None:
40
- self.sent_messages.append(
41
- {
42
- "chat_id": chat_id,
43
- "text": text,
44
- "token": token,
45
- }
46
- )
47
-
48
-
49
- def stored_bot(
50
- *,
51
- bot_token: str = "telegram-secret",
52
- enabled: bool = True,
53
- sessions: list[StoredTelegramSession] | None = None,
54
- ) -> StoredTelegramBot:
55
- return StoredTelegramBot(
56
- bot_token=bot_token,
57
- enabled=enabled,
58
- sessions=sessions or [],
59
- )
60
-
61
-
62
- def stored_session(
63
- *,
64
- chat_id: str = "2001",
65
- display_name: str = "Alice Example",
66
- recent_message: str = "Hello Flowent",
67
- status: str = "approved",
68
- user_id: str = "1001",
69
- username: str = "alice",
70
- ) -> StoredTelegramSession:
71
- return StoredTelegramSession(
72
- chat_id=chat_id,
73
- display_name=display_name,
74
- recent_message=recent_message,
75
- status=status,
76
- user_id=user_id,
77
- username=username,
78
- )
79
-
80
-
81
- def telegram_update(
82
- *,
83
- chat_id: int = 2001,
84
- text: str = "Hello Flowent",
85
- update_id: int = 1,
86
- user_id: int = 1001,
87
- username: str = "alice",
88
- ) -> dict[str, Any]:
89
- return {
90
- "message": {
91
- "chat": {"id": chat_id},
92
- "from": {
93
- "first_name": "Alice",
94
- "id": user_id,
95
- "last_name": "Example",
96
- "username": username,
97
- },
98
- "text": text,
99
- },
100
- "update_id": update_id,
101
- }
102
-
103
-
104
- async def static_reply(_: str) -> str:
105
- return "Reply"
106
-
107
-
108
- @pytest.mark.anyio
109
- async def test_disabled_telegram_bot_does_not_start_polling(
110
- tmp_path, monkeypatch
111
- ) -> None:
112
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
113
- transport = FakeTelegramTransport()
114
- manager = TelegramBotManager(
115
- message_handler=static_reply,
116
- store=StateStore(tmp_path),
117
- telegram_transport=transport,
118
- )
119
-
120
- await manager.poll_once(stored_bot(enabled=False))
121
-
122
- assert transport.get_updates_calls == 0
123
- assert manager.bot_with_status(stored_bot(enabled=False)).status == "disabled"
124
-
125
-
126
- @pytest.mark.anyio
127
- async def test_enabled_telegram_bot_polls_and_reports_running_status(
128
- tmp_path, monkeypatch
129
- ) -> None:
130
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
131
- transport = FakeTelegramTransport()
132
- manager = TelegramBotManager(
133
- message_handler=static_reply,
134
- store=StateStore(tmp_path),
135
- telegram_transport=transport,
136
- )
137
-
138
- await manager.poll_once(stored_bot())
139
-
140
- assert transport.get_updates_calls == 1
141
- assert manager.bot_with_status(stored_bot()).status == "running"
142
-
143
-
144
- @pytest.mark.anyio
145
- async def test_unapproved_telegram_message_creates_pending_request(
146
- tmp_path, monkeypatch
147
- ) -> None:
148
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
149
- handled_messages: list[str] = []
150
- transport = FakeTelegramTransport()
151
- transport.updates = [telegram_update(text="Pair this chat")]
152
- store = StateStore(tmp_path)
153
-
154
- async def handle_message(content: str) -> str:
155
- handled_messages.append(content)
156
- return "Reply"
157
-
158
- manager = TelegramBotManager(
159
- message_handler=handle_message,
160
- store=store,
161
- telegram_transport=transport,
162
- )
163
-
164
- await manager.poll_once(stored_bot())
165
-
166
- assert handled_messages == []
167
- assert store.read_telegram_bot().sessions == [
168
- StoredTelegramSession(
169
- chat_id="2001",
170
- display_name="Alice Example",
171
- recent_message="Pair this chat",
172
- status="pending",
173
- updated_at=store.read_telegram_bot().sessions[0].updated_at,
174
- user_id="1001",
175
- username="alice",
176
- )
177
- ]
178
- assert transport.sent_messages == [
179
- {
180
- "chat_id": "2001",
181
- "text": "Request received. Approve this conversation in Flowent.",
182
- "token": "telegram-secret",
183
- }
184
- ]
185
-
186
-
187
- @pytest.mark.anyio
188
- async def test_approved_telegram_message_enters_workspace_and_replies(
189
- tmp_path, monkeypatch
190
- ) -> None:
191
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
192
- handled_messages: list[str] = []
193
- transport = FakeTelegramTransport()
194
- transport.updates = [telegram_update()]
195
- store = StateStore(tmp_path)
196
- store.save_telegram_session(stored_session())
197
-
198
- async def handle_message(content: str) -> str:
199
- handled_messages.append(content)
200
- return "Reply"
201
-
202
- manager = TelegramBotManager(
203
- message_handler=handle_message,
204
- store=store,
205
- telegram_transport=transport,
206
- )
207
-
208
- await manager.poll_once(stored_bot())
209
-
210
- assert handled_messages == ["Hello Flowent"]
211
- assert transport.sent_messages == [
212
- {
213
- "chat_id": "2001",
214
- "text": "Reply",
215
- "token": "telegram-secret",
216
- }
217
- ]
218
-
219
-
220
- def test_approved_telegram_message_is_persisted_in_workspace(
221
- tmp_path, monkeypatch
222
- ) -> None:
223
- monkeypatch.chdir(tmp_path)
224
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
225
- transport = FakeTelegramTransport()
226
- transport.updates = [telegram_update(text="Draft from Telegram")]
227
-
228
- async def fake_completion(**request: object) -> object:
229
- async def chunks() -> object:
230
- yield {"choices": [{"delta": {"content": "Telegram reply"}}]}
231
-
232
- return chunks()
233
-
234
- app = create_app(
235
- serve_frontend=False,
236
- chat_completion=fake_completion,
237
- telegram_transport=transport,
238
- )
239
- client = TestClient(app)
240
- with client:
241
- client.post(
242
- "/api/providers",
243
- json={
244
- "api_key": "sk-local",
245
- "base_url": "",
246
- "id": "provider-openai",
247
- "models": ["gpt-5.1"],
248
- "name": "OpenAI",
249
- "type": "openai",
250
- },
251
- )
252
- client.put(
253
- "/api/settings",
254
- json={
255
- "reasoning_effort": "default",
256
- "selected_model": "gpt-5.1",
257
- "selected_provider_id": "provider-openai",
258
- },
259
- )
260
- StateStore(tmp_path / "data").save_telegram_session(stored_session())
261
- asyncio.run(app.state.telegram_bot_manager.poll_once(stored_bot()))
262
-
263
- state = client.get("/api/state").json()
264
-
265
- assert [message["content"] for message in state["messages"]] == [
266
- "Draft from Telegram",
267
- "Telegram reply",
268
- ]
269
-
270
-
271
- def test_telegram_bot_config_is_saved_and_reported_in_state(
272
- tmp_path, monkeypatch
273
- ) -> None:
274
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
275
- client = TestClient(create_app(serve_frontend=False))
276
-
277
- response = client.put(
278
- "/api/telegram-bot",
279
- json={
280
- "bot_token": "telegram-secret",
281
- "enabled": False,
282
- "sessions": [],
283
- },
284
- )
285
- state = client.get("/api/state").json()
286
-
287
- assert response.status_code == 200
288
- assert state["telegram_bot"] == {
289
- "bot_token": "telegram-secret",
290
- "enabled": False,
291
- "error": "",
292
- "sessions": [],
293
- "status": "disabled",
294
- }
295
-
296
-
297
- def test_pending_telegram_request_can_be_approved(tmp_path, monkeypatch) -> None:
298
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
299
- store = StateStore(tmp_path)
300
- store.save_telegram_session(stored_session(status="pending"))
301
- client = TestClient(create_app(serve_frontend=False))
302
-
303
- response = client.post(
304
- "/api/telegram-bot/approve",
305
- json={"chat_id": "2001"},
306
- )
307
- state = client.get("/api/state").json()
308
-
309
- assert response.status_code == 200
310
- assert response.json()["status"] == "approved"
311
- assert state["telegram_bot"]["sessions"][0]["status"] == "approved"
312
-
313
-
314
- @pytest.mark.anyio
315
- async def test_telegram_reply_is_split_when_it_is_too_long(
316
- tmp_path, monkeypatch
317
- ) -> None:
318
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
319
- transport = FakeTelegramTransport()
320
- transport.updates = [telegram_update()]
321
- long_reply = "x" * 4100
322
- store = StateStore(tmp_path)
323
- store.save_telegram_session(stored_session())
324
-
325
- async def handle_message(_: str) -> str:
326
- return long_reply
327
-
328
- manager = TelegramBotManager(
329
- message_handler=handle_message,
330
- store=store,
331
- telegram_transport=transport,
332
- )
333
-
334
- await manager.poll_once(stored_bot())
335
-
336
- assert [len(message["text"]) for message in transport.sent_messages] == [4096, 4]
337
-
338
-
339
- @pytest.mark.anyio
340
- async def test_telegram_connection_failure_reports_error_status(
341
- tmp_path, monkeypatch
342
- ) -> None:
343
- monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
344
- transport = FakeTelegramTransport()
345
- transport.error = RuntimeError("Secret is invalid")
346
- manager = TelegramBotManager(
347
- message_handler=static_reply,
348
- store=StateStore(tmp_path),
349
- telegram_transport=transport,
350
- )
351
-
352
- await manager.poll_once(stored_bot())
353
- bot = manager.bot_with_status(stored_bot())
354
-
355
- assert bot.status == "error"
356
- assert bot.error == "Secret is invalid"
357
-
358
-
359
- def test_split_telegram_message_keeps_empty_reply_sendable() -> None:
360
- assert split_telegram_message("") == [""]
@@ -1,12 +0,0 @@
1
- from fastapi.testclient import TestClient
2
-
3
- from flowent.main import create_app
4
-
5
-
6
- def test_health_endpoint() -> None:
7
- client = TestClient(create_app(serve_frontend=False))
8
-
9
- response = client.get("/api/health")
10
-
11
- assert response.status_code == 200
12
- assert response.json() == {"status": "ok"}