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.
- package/backend/pyproject.toml +31 -5
- package/backend/src/flowent/agent.py +13 -4
- package/backend/src/flowent/compact.py +35 -14
- package/backend/src/flowent/llm.py +73 -7
- package/backend/src/flowent/main.py +260 -59
- package/backend/src/flowent/static/assets/index-CRSV2xu1.css +2 -0
- package/backend/src/flowent/static/assets/index-DUYj6rgD.js +82 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +135 -3
- package/backend/src/flowent/usage.py +315 -0
- package/backend/uv.lock +971 -3
- package/dist/frontend/assets/index-CRSV2xu1.css +2 -0
- package/dist/frontend/assets/index-DUYj6rgD.js +82 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +24 -3
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/approval.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/static/assets/index-BlaCigkZ.js +0 -82
- package/backend/src/flowent/static/assets/index-CRvbsH4K.css +0 -2
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_approval.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_patch.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/conftest.py +0 -60
- package/backend/tests/test_agent_tools.py +0 -1124
- package/backend/tests/test_approval.py +0 -283
- package/backend/tests/test_channels.py +0 -360
- package/backend/tests/test_health.py +0 -12
- package/backend/tests/test_llm_providers.py +0 -548
- package/backend/tests/test_logging.py +0 -212
- package/backend/tests/test_mcp.py +0 -788
- package/backend/tests/test_patch.py +0 -112
- package/backend/tests/test_permissions.py +0 -588
- package/backend/tests/test_persistence.py +0 -249
- package/backend/tests/test_skills.py +0 -462
- package/backend/tests/test_startup_requirements.py +0 -144
- package/backend/tests/test_workspace_chat.py +0 -2174
- package/dist/frontend/assets/index-BlaCigkZ.js +0 -82
- 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
|