-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest_api_endpoints.py
More file actions
248 lines (208 loc) · 10.8 KB
/
Copy pathtest_api_endpoints.py
File metadata and controls
248 lines (208 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
from __future__ import annotations
from app import create_app
from tests._fixture_ids import HAPPY_BUBBLE_ID, HAPPY_COMPOSER_ID, HAPPY_WORKSPACE_ID
from utils.exclusion_rules import tokenize_rule
# ---------------------------------------------------------------------------
# GET /api/workspaces
# ---------------------------------------------------------------------------
class TestListWorkspaces:
def test_happy_path_returns_workspace_list(self, client):
response = client.get("/api/workspaces")
assert response.status_code == 200
body = response.get_json()
assert isinstance(body, dict)
projects = body["projects"]
assert isinstance(projects, list)
ids = [p["id"] for p in projects]
assert HAPPY_WORKSPACE_ID in ids, f"expected {HAPPY_WORKSPACE_ID} in {ids}"
ws = next(p for p in projects if p["id"] == HAPPY_WORKSPACE_ID)
assert "name" in ws
assert "conversationCount" in ws and isinstance(ws["conversationCount"], int)
assert "lastModified" in ws and "T" in ws["lastModified"]
def test_empty_storage_returns_empty_list(self, empty_workspace_client):
response = empty_workspace_client.get("/api/workspaces")
assert response.status_code == 200
assert response.get_json() == {"projects": []}
# ---------------------------------------------------------------------------
# GET /api/workspaces/<id>
# ---------------------------------------------------------------------------
class TestGetWorkspace:
def test_happy_path_returns_workspace_details(self, client):
response = client.get(f"/api/workspaces/{HAPPY_WORKSPACE_ID}")
assert response.status_code == 200
body = response.get_json()
assert body["id"] == HAPPY_WORKSPACE_ID
assert "name" in body
assert "folder" in body
assert "lastModified" in body and "T" in body["lastModified"]
def test_unknown_id_returns_404(self, client):
response = client.get("/api/workspaces/nonexistent-workspace-id")
assert response.status_code == 404
body = response.get_json()
assert "error" in body
def test_global_returns_other_chats(self, client):
response = client.get("/api/workspaces/global")
assert response.status_code == 200
body = response.get_json()
assert body["id"] == "global"
assert body["name"] == "Other chats"
# ---------------------------------------------------------------------------
# GET /api/workspaces/<id>/tabs
# ---------------------------------------------------------------------------
class TestGetWorkspaceTabs:
def test_happy_path_returns_tabs(self, client):
response = client.get(f"/api/workspaces/{HAPPY_WORKSPACE_ID}/tabs")
assert response.status_code == 200
body = response.get_json()
assert "tabs" in body and isinstance(body["tabs"], list)
tab_ids = [t["id"] for t in body["tabs"]]
assert HAPPY_COMPOSER_ID in tab_ids, f"expected {HAPPY_COMPOSER_ID} in {tab_ids}"
tab = next(t for t in body["tabs"] if t["id"] == HAPPY_COMPOSER_ID)
assert "title" in tab
assert "timestamp" in tab and isinstance(tab["timestamp"], int)
assert "bubbles" in tab and isinstance(tab["bubbles"], list)
# The seeded user bubble must be present
bubble_types = [b["type"] for b in tab["bubbles"]]
assert "user" in bubble_types
def test_global_returns_tabs(self, client):
response = client.get("/api/workspaces/global/tabs")
assert response.status_code == 200
body = response.get_json()
assert "tabs" in body and isinstance(body["tabs"], list)
# Isolation: HAPPY_COMPOSER_ID is assigned to HAPPY_WORKSPACE_ID via the
# local ItemTable allComposers row, so it must NOT also surface in the
# /global bucket. If it does, workspace-assignment is leaking unassigned
# composers into both buckets.
global_tab_ids = [t["id"] for t in body["tabs"]]
assert HAPPY_COMPOSER_ID not in global_tab_ids, (
f"{HAPPY_COMPOSER_ID} leaked into /global tabs: {global_tab_ids}"
)
def test_missing_global_storage_returns_404(self, empty_workspace_client):
response = empty_workspace_client.get("/api/workspaces/global/tabs")
assert response.status_code == 404
body = response.get_json()
assert "error" in body
def test_summary_query_returns_tab_list_without_bubbles(self, client):
response = client.get(f"/api/workspaces/{HAPPY_WORKSPACE_ID}/tabs?summary=1")
assert response.status_code == 200
body = response.get_json()
assert "tabs" in body and isinstance(body["tabs"], list)
assert body["tabs"], "expected at least one summary tab"
tab = next(t for t in body["tabs"] if t["id"] == HAPPY_COMPOSER_ID)
assert "title" in tab
assert "timestamp" in tab and isinstance(tab["timestamp"], int)
assert "messageCount" in tab and isinstance(tab["messageCount"], int)
assert "bubbles" not in tab
def test_summary_query_global_workspace(self, client):
response = client.get("/api/workspaces/global/tabs?summary=1")
assert response.status_code == 200
body = response.get_json()
assert "tabs" in body and isinstance(body["tabs"], list)
class TestGetWorkspaceTab:
def test_happy_path_returns_single_tab(self, client):
response = client.get(
f"/api/workspaces/{HAPPY_WORKSPACE_ID}/tabs/{HAPPY_COMPOSER_ID}"
)
assert response.status_code == 200
body = response.get_json()
assert "tab" in body
tab = body["tab"]
assert tab["id"] == HAPPY_COMPOSER_ID
assert "title" in tab
assert "timestamp" in tab and isinstance(tab["timestamp"], int)
assert "bubbles" in tab and isinstance(tab["bubbles"], list)
assert "codeBlockDiffs" in tab
def test_unknown_composer_returns_404(self, client):
response = client.get(
f"/api/workspaces/{HAPPY_WORKSPACE_ID}/tabs/no-such-composer"
)
assert response.status_code == 404
assert "error" in response.get_json()
def test_cli_workspace_returns_400(self, client):
response = client.get("/api/workspaces/cli:proj-1/tabs/cmp-happy")
assert response.status_code == 400
assert "error" in response.get_json()
# ---------------------------------------------------------------------------
# GET /api/search?q=...
# ---------------------------------------------------------------------------
class TestSearch:
def test_happy_path_finds_seeded_term(self, client):
response = client.get("/api/search?q=sentinel-grep")
assert response.status_code == 200
body = response.get_json()
assert "results" in body and isinstance(body["results"], list)
assert len(body["results"]) >= 1, f"expected sentinel match, got {body}"
def test_no_match_returns_empty_results(self, client):
response = client.get("/api/search?q=does-not-match-any-content-xyzzy")
assert response.status_code == 200
body = response.get_json()
assert "results" in body and body["results"] == []
def test_missing_q_returns_400(self, client):
response = client.get("/api/search")
assert response.status_code == 400
body = response.get_json()
assert "error" in body
assert body["error"] == "No search query provided"
def test_empty_q_returns_400(self, client):
response = client.get("/api/search?q=")
assert response.status_code == 400
body = response.get_json()
assert body.get("error") == "No search query provided"
def test_whitespace_only_q_returns_400(self, client):
# api/search.py strips q before the empty-check, so " " is rejected.
response = client.get("/api/search?q=%20%20%20")
assert response.status_code == 400
body = response.get_json()
assert body.get("error") == "No search query provided"
# ---------------------------------------------------------------------------
# Exclusion rules — must be applied across endpoints
# ---------------------------------------------------------------------------
def _client_with_rules(rule_lines):
"""Build a Flask test client whose EXCLUSION_RULES match the given lines.
The standard `client` fixture sets EXCLUSION_RULES = [] because no
rules file exists under the temp workspace. This helper builds a fresh
app on top of the same env (already pointed at workspace_storage) and
overrides the config with parsed rules — exercising the same code path
a real `exclusion-rules.txt` file would.
"""
parsed = [tokenize_rule(line) for line in rule_lines]
app = create_app()
app.config["TESTING"] = True
app.config["EXCLUSION_RULES"] = [r for r in parsed if r]
return app.test_client()
class TestExclusionRules:
def test_workspace_matching_rule_is_filtered_out_of_list(self, workspace_storage):
# The seeded workspace's display name resolves to "happy-project"
# (the basename of the folder linked from workspace.json). A rule of
# "happy-project" must drop it from /api/workspaces entirely.
excluded_client = _client_with_rules(["happy-project"])
response = excluded_client.get("/api/workspaces")
assert response.status_code == 200
body = response.get_json()
ids = [w["id"] for w in body["projects"]]
assert HAPPY_WORKSPACE_ID not in ids, (
f"exclusion rule did not filter {HAPPY_WORKSPACE_ID}; got {ids}"
)
def test_workspace_not_matching_rule_still_listed(self, workspace_storage):
# Negative control: a rule that doesn't match must leave the workspace
# visible, so the test above can't pass for the wrong reason
# (e.g. listing always returning []).
kept_client = _client_with_rules(["unrelated-project-name-xyzzy"])
response = kept_client.get("/api/workspaces")
assert response.status_code == 200
body = response.get_json()
ids = [w["id"] for w in body["projects"]]
assert HAPPY_WORKSPACE_ID in ids, (
f"non-matching rule filtered the workspace; got {ids}"
)
def test_search_skips_conversations_matching_rule(self, workspace_storage):
# The seeded conversation's name is "Happy conversation". Excluding by
# "Happy" must drop the seeded match from /api/search even though the
# bubble text still contains "sentinel-grep".
excluded_client = _client_with_rules(["Happy"])
response = excluded_client.get("/api/search?q=sentinel-grep")
assert response.status_code == 200
body = response.get_json()
assert body.get("results") == [], (
f"exclusion rule did not filter seeded chat from search: {body}"
)