podojo-cli 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/CLAUDE.md +10 -10
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/PKG-INFO +1 -1
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/pyproject.toml +1 -1
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/client.py +10 -10
- podojo_cli-0.2.2/src/podojo_cli/commands/sessions.py → podojo_cli-0.3.0/src/podojo_cli/commands/usertests.py +53 -53
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/main.py +2 -2
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/tests/test_projects.py +15 -5
- podojo_cli-0.3.0/tests/test_usertests.py +277 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/uv.lock +1 -1
- podojo_cli-0.2.2/tests/test_sessions.py +0 -277
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/.gitignore +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/README.md +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/auth.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/gdrive.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/projects.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/showreel.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/transcripts.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/commands/videos.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/config.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/list.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/upload.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/video/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/src/podojo_cli/video/showreel.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/tests/conftest.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/tests/test_auth.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/tests/test_gdrive.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.0}/tests/test_showreel.py +0 -0
|
@@ -25,20 +25,20 @@ podojo gdrive setup ~/.podojo-gdrive.json
|
|
|
25
25
|
podojo gdrive upload report.md --title "My Report"
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## User Tests (Unmoderated Tests)
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
podojo
|
|
32
|
-
podojo
|
|
33
|
-
podojo
|
|
34
|
-
podojo
|
|
35
|
-
podojo
|
|
36
|
-
podojo
|
|
37
|
-
podojo
|
|
31
|
+
podojo usertests list
|
|
32
|
+
podojo usertests get <usertest_id>
|
|
33
|
+
podojo usertests create --from-file usertest.yaml
|
|
34
|
+
podojo usertests update <usertest_id> --from-file updates.yaml
|
|
35
|
+
podojo usertests delete <usertest_id> [--yes]
|
|
36
|
+
podojo usertests example # print template YAML to stdout
|
|
37
|
+
podojo usertests validate usertest.yaml # validate without creating
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
Required fields: `
|
|
40
|
+
User test configs are YAML files. Run `podojo usertests example` for the full template.
|
|
41
|
+
Required fields: `usertest_id`, `client`, `title`, `logo`, `prototype_url`, `steps`.
|
|
42
42
|
Each step needs `type` ("screen" or "prototype") and `title`. Screen steps should have `variant` ("question" or "task").
|
|
43
43
|
|
|
44
44
|
## Configuration
|
|
@@ -53,44 +53,44 @@ class PodojoClient:
|
|
|
53
53
|
r.raise_for_status()
|
|
54
54
|
return r.json()
|
|
55
55
|
|
|
56
|
-
def
|
|
56
|
+
def list_usertests(self, skip: int = 0, limit: int = 50) -> dict:
|
|
57
57
|
r = httpx.get(
|
|
58
|
-
f"{self.base_url}/
|
|
58
|
+
f"{self.base_url}/usertests",
|
|
59
59
|
params={"skip": skip, "limit": limit},
|
|
60
60
|
headers=self._headers(),
|
|
61
61
|
)
|
|
62
62
|
r.raise_for_status()
|
|
63
63
|
return r.json()
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def get_usertest(self, usertest_id: str) -> dict:
|
|
66
66
|
r = httpx.get(
|
|
67
|
-
f"{self.base_url}/
|
|
67
|
+
f"{self.base_url}/usertests/{usertest_id}",
|
|
68
68
|
headers=self._headers(),
|
|
69
69
|
)
|
|
70
70
|
r.raise_for_status()
|
|
71
71
|
return r.json()
|
|
72
72
|
|
|
73
|
-
def
|
|
73
|
+
def create_usertest(self, data: dict) -> dict:
|
|
74
74
|
r = httpx.post(
|
|
75
|
-
f"{self.base_url}/
|
|
75
|
+
f"{self.base_url}/usertests",
|
|
76
76
|
json=data,
|
|
77
77
|
headers=self._headers(),
|
|
78
78
|
)
|
|
79
79
|
r.raise_for_status()
|
|
80
80
|
return r.json()
|
|
81
81
|
|
|
82
|
-
def
|
|
82
|
+
def update_usertest(self, usertest_id: str, data: dict) -> dict:
|
|
83
83
|
r = httpx.put(
|
|
84
|
-
f"{self.base_url}/
|
|
84
|
+
f"{self.base_url}/usertests/{usertest_id}",
|
|
85
85
|
json=data,
|
|
86
86
|
headers=self._headers(),
|
|
87
87
|
)
|
|
88
88
|
r.raise_for_status()
|
|
89
89
|
return r.json()
|
|
90
90
|
|
|
91
|
-
def
|
|
91
|
+
def delete_usertest(self, usertest_id: str) -> dict:
|
|
92
92
|
r = httpx.delete(
|
|
93
|
-
f"{self.base_url}/
|
|
93
|
+
f"{self.base_url}/usertests/{usertest_id}",
|
|
94
94
|
headers=self._headers(),
|
|
95
95
|
)
|
|
96
96
|
r.raise_for_status()
|
|
@@ -8,21 +8,21 @@ from rich.table import Table
|
|
|
8
8
|
|
|
9
9
|
from ..client import PodojoClient
|
|
10
10
|
|
|
11
|
-
app = typer.Typer(help="Manage unmoderated
|
|
11
|
+
app = typer.Typer(help="Manage unmoderated user tests")
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
14
|
-
REQUIRED_FIELDS = ["
|
|
14
|
+
REQUIRED_FIELDS = ["usertest_id", "client", "title", "logo", "prototype_url", "steps"]
|
|
15
15
|
VALID_STEP_TYPES = {"screen", "prototype"}
|
|
16
16
|
VALID_STEP_VARIANTS = {"question", "task"}
|
|
17
17
|
REQUIRED_STEP_FIELDS = ["type", "title"]
|
|
18
18
|
|
|
19
19
|
EXAMPLE_YAML = """\
|
|
20
|
-
# Podojo Unmoderated Test
|
|
20
|
+
# Podojo Unmoderated User Test Configuration
|
|
21
21
|
#
|
|
22
|
-
# Required fields:
|
|
22
|
+
# Required fields: usertest_id, client, title, logo, prototype_url, steps
|
|
23
23
|
# Optional fields: welcome_text, privacy_text, promo_code, promo_code_info, project_name
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
usertest_id: checkout-usability-v1
|
|
26
26
|
client: Acme Corp
|
|
27
27
|
title: Checkout Flow Usability Test
|
|
28
28
|
logo: https://example.com/logo.png
|
|
@@ -46,10 +46,10 @@ privacy_text: |
|
|
|
46
46
|
promo_code: THANKS10
|
|
47
47
|
promo_code_info: "Use this code for 10% off your next purchase"
|
|
48
48
|
|
|
49
|
-
# Optional: link
|
|
49
|
+
# Optional: link user test to a project
|
|
50
50
|
project_name: checkout-redesign-q1
|
|
51
51
|
|
|
52
|
-
# Optional: set
|
|
52
|
+
# Optional: set user test live (default: false)
|
|
53
53
|
# live: true
|
|
54
54
|
|
|
55
55
|
# Steps define what participants see and do
|
|
@@ -87,8 +87,8 @@ steps:
|
|
|
87
87
|
"""
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def
|
|
91
|
-
"""Validate
|
|
90
|
+
def validate_usertest_data(data: dict) -> list[str]:
|
|
91
|
+
"""Validate user test YAML data, return list of error strings."""
|
|
92
92
|
errors = []
|
|
93
93
|
for field in REQUIRED_FIELDS:
|
|
94
94
|
if field not in data or data[field] is None:
|
|
@@ -154,35 +154,35 @@ def _format_api_error(e: httpx.HTTPStatusError) -> str:
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
@app.command("list")
|
|
157
|
-
def
|
|
158
|
-
skip: int = typer.Option(0, help="Number of
|
|
159
|
-
limit: int = typer.Option(50, help="Max
|
|
157
|
+
def list_usertests(
|
|
158
|
+
skip: int = typer.Option(0, help="Number of user tests to skip"),
|
|
159
|
+
limit: int = typer.Option(50, help="Max user tests to return"),
|
|
160
160
|
):
|
|
161
|
-
"""List all
|
|
161
|
+
"""List all user tests."""
|
|
162
162
|
client = PodojoClient()
|
|
163
163
|
try:
|
|
164
|
-
result = client.
|
|
164
|
+
result = client.list_usertests(skip=skip, limit=limit)
|
|
165
165
|
except httpx.HTTPStatusError as e:
|
|
166
166
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
167
167
|
raise typer.Exit(1)
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
if not
|
|
171
|
-
console.print("No
|
|
169
|
+
usertests = result.get("usertests", [])
|
|
170
|
+
if not usertests:
|
|
171
|
+
console.print("No user tests found.")
|
|
172
172
|
return
|
|
173
173
|
|
|
174
|
-
table = Table(title="
|
|
175
|
-
table.add_column("
|
|
174
|
+
table = Table(title="User Tests")
|
|
175
|
+
table.add_column("User Test ID")
|
|
176
176
|
table.add_column("Title")
|
|
177
177
|
table.add_column("Client")
|
|
178
178
|
table.add_column("Steps", justify="right")
|
|
179
179
|
table.add_column("Live")
|
|
180
180
|
table.add_column("Last Updated")
|
|
181
181
|
|
|
182
|
-
for s in
|
|
182
|
+
for s in usertests:
|
|
183
183
|
live = "[green]Yes[/green]" if s.get("live") else "[dim]No[/dim]"
|
|
184
184
|
table.add_row(
|
|
185
|
-
s.get("
|
|
185
|
+
s.get("usertest_id", ""),
|
|
186
186
|
s.get("title", ""),
|
|
187
187
|
s.get("client", ""),
|
|
188
188
|
str(s.get("step_count", "")),
|
|
@@ -194,35 +194,35 @@ def list_sessions(
|
|
|
194
194
|
|
|
195
195
|
|
|
196
196
|
@app.command("get")
|
|
197
|
-
def
|
|
198
|
-
|
|
197
|
+
def get_usertest(
|
|
198
|
+
usertest_id: str = typer.Argument(help="User test ID to retrieve"),
|
|
199
199
|
):
|
|
200
|
-
"""Get a
|
|
200
|
+
"""Get a user test and output as YAML."""
|
|
201
201
|
client = PodojoClient()
|
|
202
202
|
try:
|
|
203
|
-
|
|
203
|
+
usertest = client.get_usertest(usertest_id)
|
|
204
204
|
except httpx.HTTPStatusError as e:
|
|
205
205
|
if e.response.status_code == 404:
|
|
206
|
-
console.print(f"[red]Error:[/red]
|
|
206
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
207
207
|
else:
|
|
208
208
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
209
209
|
raise typer.Exit(1)
|
|
210
210
|
|
|
211
211
|
# Remove server-managed fields for a clean editable output
|
|
212
212
|
for key in ("id", "created_at", "created_by", "last_updated"):
|
|
213
|
-
|
|
213
|
+
usertest.pop(key, None)
|
|
214
214
|
|
|
215
|
-
console.print(yaml.dump(
|
|
215
|
+
console.print(yaml.dump(usertest, default_flow_style=False, sort_keys=False, allow_unicode=True))
|
|
216
216
|
|
|
217
217
|
|
|
218
218
|
@app.command("create")
|
|
219
|
-
def
|
|
220
|
-
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with
|
|
219
|
+
def create_usertest(
|
|
220
|
+
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with user test config"),
|
|
221
221
|
):
|
|
222
|
-
"""Create a new
|
|
222
|
+
"""Create a new user test from a YAML file."""
|
|
223
223
|
data = _load_yaml(from_file)
|
|
224
224
|
|
|
225
|
-
errors =
|
|
225
|
+
errors = validate_usertest_data(data)
|
|
226
226
|
if errors:
|
|
227
227
|
console.print("[red]Validation errors:[/red]")
|
|
228
228
|
for err in errors:
|
|
@@ -230,82 +230,82 @@ def create_session(
|
|
|
230
230
|
raise typer.Exit(1)
|
|
231
231
|
|
|
232
232
|
if "project_name" not in data:
|
|
233
|
-
data["project_name"] = data["
|
|
233
|
+
data["project_name"] = data["usertest_id"]
|
|
234
234
|
|
|
235
235
|
client = PodojoClient()
|
|
236
236
|
try:
|
|
237
|
-
result = client.
|
|
237
|
+
result = client.create_usertest(data)
|
|
238
238
|
except httpx.HTTPStatusError as e:
|
|
239
239
|
if e.response.status_code == 409:
|
|
240
|
-
console.print(f"[red]Error:[/red]
|
|
240
|
+
console.print(f"[red]Error:[/red] User test '{data.get('usertest_id')}' already exists")
|
|
241
241
|
else:
|
|
242
242
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
243
243
|
raise typer.Exit(1)
|
|
244
244
|
|
|
245
|
-
console.print(f"[green]Created
|
|
245
|
+
console.print(f"[green]Created user test:[/green] {result.get('usertest_id', '')}")
|
|
246
246
|
|
|
247
247
|
|
|
248
248
|
@app.command("update")
|
|
249
|
-
def
|
|
250
|
-
|
|
249
|
+
def update_usertest(
|
|
250
|
+
usertest_id: str = typer.Argument(help="User test ID to update"),
|
|
251
251
|
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with fields to update"),
|
|
252
252
|
):
|
|
253
|
-
"""Update a
|
|
253
|
+
"""Update a user test from a YAML file (partial updates OK)."""
|
|
254
254
|
data = _load_yaml(from_file)
|
|
255
255
|
|
|
256
256
|
client = PodojoClient()
|
|
257
257
|
try:
|
|
258
|
-
result = client.
|
|
258
|
+
result = client.update_usertest(usertest_id, data)
|
|
259
259
|
except httpx.HTTPStatusError as e:
|
|
260
260
|
if e.response.status_code == 404:
|
|
261
|
-
console.print(f"[red]Error:[/red]
|
|
261
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
262
262
|
else:
|
|
263
263
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
264
264
|
raise typer.Exit(1)
|
|
265
265
|
|
|
266
|
-
console.print(f"[green]Updated
|
|
266
|
+
console.print(f"[green]Updated user test:[/green] {result.get('usertest_id', usertest_id)}")
|
|
267
267
|
|
|
268
268
|
|
|
269
269
|
@app.command("delete")
|
|
270
|
-
def
|
|
271
|
-
|
|
270
|
+
def delete_usertest(
|
|
271
|
+
usertest_id: str = typer.Argument(help="User test ID to delete"),
|
|
272
272
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
273
273
|
):
|
|
274
|
-
"""Delete a
|
|
274
|
+
"""Delete a user test."""
|
|
275
275
|
if not yes:
|
|
276
|
-
typer.confirm(f"Delete
|
|
276
|
+
typer.confirm(f"Delete user test '{usertest_id}'?", abort=True)
|
|
277
277
|
|
|
278
278
|
client = PodojoClient()
|
|
279
279
|
try:
|
|
280
|
-
client.
|
|
280
|
+
client.delete_usertest(usertest_id)
|
|
281
281
|
except httpx.HTTPStatusError as e:
|
|
282
282
|
if e.response.status_code == 404:
|
|
283
|
-
console.print(f"[red]Error:[/red]
|
|
283
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
284
284
|
else:
|
|
285
285
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
286
286
|
raise typer.Exit(1)
|
|
287
287
|
|
|
288
|
-
console.print(f"[green]Deleted
|
|
288
|
+
console.print(f"[green]Deleted user test:[/green] {usertest_id}")
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
@app.command("validate")
|
|
292
292
|
def validate(
|
|
293
293
|
file: Path = typer.Argument(help="YAML file to validate"),
|
|
294
294
|
):
|
|
295
|
-
"""Validate a
|
|
295
|
+
"""Validate a user test YAML file without creating it."""
|
|
296
296
|
data = _load_yaml(file)
|
|
297
297
|
|
|
298
|
-
errors =
|
|
298
|
+
errors = validate_usertest_data(data)
|
|
299
299
|
if errors:
|
|
300
300
|
console.print("[red]Validation errors:[/red]")
|
|
301
301
|
for err in errors:
|
|
302
302
|
console.print(f" {err}")
|
|
303
303
|
raise typer.Exit(1)
|
|
304
304
|
|
|
305
|
-
console.print("[green]Valid
|
|
305
|
+
console.print("[green]Valid user test config.[/green]")
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
@app.command("example")
|
|
309
309
|
def example():
|
|
310
|
-
"""Print an example
|
|
310
|
+
"""Print an example user test YAML template."""
|
|
311
311
|
print(EXAMPLE_YAML)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
|
|
3
|
-
from .commands import auth, gdrive, projects,
|
|
3
|
+
from .commands import auth, gdrive, projects, usertests, showreel, transcripts, videos
|
|
4
4
|
|
|
5
5
|
app = typer.Typer(
|
|
6
6
|
name="podojo",
|
|
@@ -10,7 +10,7 @@ app = typer.Typer(
|
|
|
10
10
|
|
|
11
11
|
app.add_typer(auth.app, name="auth")
|
|
12
12
|
app.add_typer(projects.app, name="projects")
|
|
13
|
-
app.add_typer(
|
|
13
|
+
app.add_typer(usertests.app, name="usertests")
|
|
14
14
|
app.add_typer(transcripts.app, name="transcripts")
|
|
15
15
|
app.add_typer(videos.app, name="videos")
|
|
16
16
|
app.add_typer(showreel.app, name="showreel")
|
|
@@ -4,10 +4,15 @@ from podojo_cli.main import app
|
|
|
4
4
|
def test_list_projects(runner, httpx_mock):
|
|
5
5
|
httpx_mock.add_response(
|
|
6
6
|
url="http://test.local/api/v1/projects?skip=0&limit=50",
|
|
7
|
-
json=
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
json={
|
|
8
|
+
"projects": [
|
|
9
|
+
{"name": "Alpha", "brief": "First project"},
|
|
10
|
+
{"name": "Beta", "brief": "Second project"},
|
|
11
|
+
],
|
|
12
|
+
"total": 2,
|
|
13
|
+
"skip": 0,
|
|
14
|
+
"limit": 50,
|
|
15
|
+
},
|
|
11
16
|
)
|
|
12
17
|
|
|
13
18
|
result = runner.invoke(app, ["projects", "list"])
|
|
@@ -20,7 +25,12 @@ def test_list_projects(runner, httpx_mock):
|
|
|
20
25
|
def test_list_projects_empty(runner, httpx_mock):
|
|
21
26
|
httpx_mock.add_response(
|
|
22
27
|
url="http://test.local/api/v1/projects?skip=0&limit=50",
|
|
23
|
-
json=
|
|
28
|
+
json={
|
|
29
|
+
"projects": [],
|
|
30
|
+
"total": 0,
|
|
31
|
+
"skip": 0,
|
|
32
|
+
"limit": 50,
|
|
33
|
+
},
|
|
24
34
|
)
|
|
25
35
|
|
|
26
36
|
result = runner.invoke(app, ["projects", "list"])
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from podojo_cli.main import app
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
VALID_USERTEST_YAML = """\
|
|
5
|
+
usertest_id: test-usertest-1
|
|
6
|
+
client: Test Corp
|
|
7
|
+
title: Test User Test
|
|
8
|
+
logo: https://example.com/logo.png
|
|
9
|
+
prototype_url: https://example.com/proto
|
|
10
|
+
steps:
|
|
11
|
+
- type: screen
|
|
12
|
+
variant: question
|
|
13
|
+
title: First Question
|
|
14
|
+
text: What do you see?
|
|
15
|
+
- type: prototype
|
|
16
|
+
title: Try the Flow
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
LIST_RESPONSE = {
|
|
20
|
+
"usertests": [
|
|
21
|
+
{
|
|
22
|
+
"usertest_id": "ut-1",
|
|
23
|
+
"title": "User Test One",
|
|
24
|
+
"client": "Acme",
|
|
25
|
+
"step_count": 3,
|
|
26
|
+
"live": True,
|
|
27
|
+
"last_updated": "2026-03-01T12:00:00",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"usertest_id": "ut-2",
|
|
31
|
+
"title": "User Test Two",
|
|
32
|
+
"client": "Beta",
|
|
33
|
+
"step_count": 5,
|
|
34
|
+
"live": False,
|
|
35
|
+
"last_updated": "2026-03-02T12:00:00",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
"total": 2,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
GET_RESPONSE = {
|
|
42
|
+
"id": "abc123",
|
|
43
|
+
"usertest_id": "ut-1",
|
|
44
|
+
"title": "User Test One",
|
|
45
|
+
"client": "Acme",
|
|
46
|
+
"logo": "https://example.com/logo.png",
|
|
47
|
+
"prototype_url": "https://example.com/proto",
|
|
48
|
+
"steps": [{"type": "screen", "variant": "question", "title": "Q1"}],
|
|
49
|
+
"created_at": "2026-03-01T12:00:00",
|
|
50
|
+
"created_by": "user@example.com",
|
|
51
|
+
"last_updated": "2026-03-01T12:00:00",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_list_usertests(runner, httpx_mock):
|
|
56
|
+
httpx_mock.add_response(
|
|
57
|
+
url="http://test.local/api/v1/usertests?skip=0&limit=50",
|
|
58
|
+
json=LIST_RESPONSE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
result = runner.invoke(app, ["usertests", "list"])
|
|
62
|
+
|
|
63
|
+
assert result.exit_code == 0
|
|
64
|
+
assert "User Test One" in result.output
|
|
65
|
+
assert "User Test Two" in result.output
|
|
66
|
+
assert "Acme" in result.output
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_list_usertests_empty(runner, httpx_mock):
|
|
70
|
+
httpx_mock.add_response(
|
|
71
|
+
url="http://test.local/api/v1/usertests?skip=0&limit=50",
|
|
72
|
+
json={"usertests": [], "total": 0},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
result = runner.invoke(app, ["usertests", "list"])
|
|
76
|
+
|
|
77
|
+
assert result.exit_code == 0
|
|
78
|
+
assert "No user tests found" in result.output
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_get_usertest(runner, httpx_mock):
|
|
82
|
+
httpx_mock.add_response(
|
|
83
|
+
url="http://test.local/api/v1/usertests/ut-1",
|
|
84
|
+
json=GET_RESPONSE,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
result = runner.invoke(app, ["usertests", "get", "ut-1"])
|
|
88
|
+
|
|
89
|
+
assert result.exit_code == 0
|
|
90
|
+
assert "usertest_id: ut-1" in result.output
|
|
91
|
+
assert "title: User Test One" in result.output
|
|
92
|
+
# Server-managed fields should be stripped
|
|
93
|
+
assert "created_at" not in result.output
|
|
94
|
+
assert "created_by" not in result.output
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_get_usertest_not_found(runner, httpx_mock):
|
|
98
|
+
httpx_mock.add_response(
|
|
99
|
+
url="http://test.local/api/v1/usertests/nonexistent",
|
|
100
|
+
status_code=404,
|
|
101
|
+
json={"detail": "User test not found"},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = runner.invoke(app, ["usertests", "get", "nonexistent"])
|
|
105
|
+
|
|
106
|
+
assert result.exit_code == 1
|
|
107
|
+
assert "not found" in result.output
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_create_usertest(runner, httpx_mock, tmp_path):
|
|
111
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
112
|
+
yaml_file.write_text(VALID_USERTEST_YAML)
|
|
113
|
+
|
|
114
|
+
httpx_mock.add_response(
|
|
115
|
+
url="http://test.local/api/v1/usertests",
|
|
116
|
+
method="POST",
|
|
117
|
+
json={"usertest_id": "test-usertest-1", "id": "abc123"},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
121
|
+
|
|
122
|
+
assert result.exit_code == 0
|
|
123
|
+
assert "test-usertest-1" in result.output
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_create_usertest_file_not_found(runner):
|
|
127
|
+
result = runner.invoke(app, ["usertests", "create", "-f", "/nonexistent/usertest.yaml"])
|
|
128
|
+
|
|
129
|
+
assert result.exit_code == 1
|
|
130
|
+
assert "File not found" in result.output
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_create_usertest_invalid_yaml(runner, tmp_path):
|
|
134
|
+
yaml_file = tmp_path / "bad.yaml"
|
|
135
|
+
yaml_file.write_text(":\n invalid: [yaml\n broken")
|
|
136
|
+
|
|
137
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
138
|
+
|
|
139
|
+
assert result.exit_code == 1
|
|
140
|
+
assert "Invalid YAML" in result.output
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_create_usertest_missing_fields(runner, tmp_path):
|
|
144
|
+
yaml_file = tmp_path / "incomplete.yaml"
|
|
145
|
+
yaml_file.write_text("title: Just a title\n")
|
|
146
|
+
|
|
147
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
148
|
+
|
|
149
|
+
assert result.exit_code == 1
|
|
150
|
+
assert "Missing required field" in result.output
|
|
151
|
+
assert "'usertest_id'" in result.output
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_create_usertest_invalid_step_type(runner, tmp_path):
|
|
155
|
+
yaml_file = tmp_path / "badstep.yaml"
|
|
156
|
+
yaml_file.write_text(
|
|
157
|
+
"usertest_id: test\n"
|
|
158
|
+
"client: Test\n"
|
|
159
|
+
"title: Test\n"
|
|
160
|
+
"logo: https://example.com/logo.png\n"
|
|
161
|
+
"prototype_url: https://example.com/proto\n"
|
|
162
|
+
"steps:\n"
|
|
163
|
+
" - type: invalid\n"
|
|
164
|
+
" title: Bad Step\n"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
168
|
+
|
|
169
|
+
assert result.exit_code == 1
|
|
170
|
+
assert "'type' must be 'screen' or 'prototype'" in result.output
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_create_usertest_duplicate(runner, httpx_mock, tmp_path):
|
|
174
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
175
|
+
yaml_file.write_text(VALID_USERTEST_YAML)
|
|
176
|
+
|
|
177
|
+
httpx_mock.add_response(
|
|
178
|
+
url="http://test.local/api/v1/usertests",
|
|
179
|
+
method="POST",
|
|
180
|
+
status_code=409,
|
|
181
|
+
json={"detail": "User test with this ID already exists"},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
185
|
+
|
|
186
|
+
assert result.exit_code == 1
|
|
187
|
+
assert "already exists" in result.output
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_update_usertest(runner, httpx_mock, tmp_path):
|
|
191
|
+
yaml_file = tmp_path / "update.yaml"
|
|
192
|
+
yaml_file.write_text("title: Updated Title\n")
|
|
193
|
+
|
|
194
|
+
httpx_mock.add_response(
|
|
195
|
+
url="http://test.local/api/v1/usertests/ut-1",
|
|
196
|
+
method="PUT",
|
|
197
|
+
json={"usertest_id": "ut-1", "title": "Updated Title"},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
result = runner.invoke(app, ["usertests", "update", "ut-1", "-f", str(yaml_file)])
|
|
201
|
+
|
|
202
|
+
assert result.exit_code == 0
|
|
203
|
+
assert "Updated user test" in result.output
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_update_usertest_not_found(runner, httpx_mock, tmp_path):
|
|
207
|
+
yaml_file = tmp_path / "update.yaml"
|
|
208
|
+
yaml_file.write_text("title: Updated Title\n")
|
|
209
|
+
|
|
210
|
+
httpx_mock.add_response(
|
|
211
|
+
url="http://test.local/api/v1/usertests/nonexistent",
|
|
212
|
+
method="PUT",
|
|
213
|
+
status_code=404,
|
|
214
|
+
json={"detail": "User test not found"},
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
result = runner.invoke(app, ["usertests", "update", "nonexistent", "-f", str(yaml_file)])
|
|
218
|
+
|
|
219
|
+
assert result.exit_code == 1
|
|
220
|
+
assert "not found" in result.output
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_delete_usertest(runner, httpx_mock):
|
|
224
|
+
httpx_mock.add_response(
|
|
225
|
+
url="http://test.local/api/v1/usertests/ut-1",
|
|
226
|
+
method="DELETE",
|
|
227
|
+
json={"status": "deleted", "usertest_id": "ut-1"},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
result = runner.invoke(app, ["usertests", "delete", "ut-1", "--yes"])
|
|
231
|
+
|
|
232
|
+
assert result.exit_code == 0
|
|
233
|
+
assert "Deleted user test" in result.output
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_delete_usertest_not_found(runner, httpx_mock):
|
|
237
|
+
httpx_mock.add_response(
|
|
238
|
+
url="http://test.local/api/v1/usertests/nonexistent",
|
|
239
|
+
method="DELETE",
|
|
240
|
+
status_code=404,
|
|
241
|
+
json={"detail": "User test not found"},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
result = runner.invoke(app, ["usertests", "delete", "nonexistent", "--yes"])
|
|
245
|
+
|
|
246
|
+
assert result.exit_code == 1
|
|
247
|
+
assert "not found" in result.output
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_validate_valid(runner, tmp_path):
|
|
251
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
252
|
+
yaml_file.write_text(VALID_USERTEST_YAML)
|
|
253
|
+
|
|
254
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
255
|
+
|
|
256
|
+
assert result.exit_code == 0
|
|
257
|
+
assert "Valid user test config" in result.output
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_validate_invalid(runner, tmp_path):
|
|
261
|
+
yaml_file = tmp_path / "bad.yaml"
|
|
262
|
+
yaml_file.write_text("title: Just a title\n")
|
|
263
|
+
|
|
264
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
265
|
+
|
|
266
|
+
assert result.exit_code == 1
|
|
267
|
+
assert "Missing required field" in result.output
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_example(runner):
|
|
271
|
+
result = runner.invoke(app, ["usertests", "example"])
|
|
272
|
+
|
|
273
|
+
assert result.exit_code == 0
|
|
274
|
+
assert "usertest_id:" in result.output
|
|
275
|
+
assert "steps:" in result.output
|
|
276
|
+
assert "prototype" in result.output
|
|
277
|
+
assert "screen" in result.output
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
from podojo_cli.main import app
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
VALID_SESSION_YAML = """\
|
|
5
|
-
session_id: test-session-1
|
|
6
|
-
client: Test Corp
|
|
7
|
-
title: Test Session
|
|
8
|
-
logo: https://example.com/logo.png
|
|
9
|
-
prototype_url: https://example.com/proto
|
|
10
|
-
steps:
|
|
11
|
-
- type: screen
|
|
12
|
-
variant: question
|
|
13
|
-
title: First Question
|
|
14
|
-
text: What do you see?
|
|
15
|
-
- type: prototype
|
|
16
|
-
title: Try the Flow
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
LIST_RESPONSE = {
|
|
20
|
-
"sessions": [
|
|
21
|
-
{
|
|
22
|
-
"session_id": "sess-1",
|
|
23
|
-
"title": "Session One",
|
|
24
|
-
"client": "Acme",
|
|
25
|
-
"step_count": 3,
|
|
26
|
-
"live": True,
|
|
27
|
-
"last_updated": "2026-03-01T12:00:00",
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"session_id": "sess-2",
|
|
31
|
-
"title": "Session Two",
|
|
32
|
-
"client": "Beta",
|
|
33
|
-
"step_count": 5,
|
|
34
|
-
"live": False,
|
|
35
|
-
"last_updated": "2026-03-02T12:00:00",
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
"total": 2,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
GET_RESPONSE = {
|
|
42
|
-
"id": "abc123",
|
|
43
|
-
"session_id": "sess-1",
|
|
44
|
-
"title": "Session One",
|
|
45
|
-
"client": "Acme",
|
|
46
|
-
"logo": "https://example.com/logo.png",
|
|
47
|
-
"prototype_url": "https://example.com/proto",
|
|
48
|
-
"steps": [{"type": "screen", "variant": "question", "title": "Q1"}],
|
|
49
|
-
"created_at": "2026-03-01T12:00:00",
|
|
50
|
-
"created_by": "user@example.com",
|
|
51
|
-
"last_updated": "2026-03-01T12:00:00",
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_list_sessions(runner, httpx_mock):
|
|
56
|
-
httpx_mock.add_response(
|
|
57
|
-
url="http://test.local/api/v1/sessions?skip=0&limit=50",
|
|
58
|
-
json=LIST_RESPONSE,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
result = runner.invoke(app, ["sessions", "list"])
|
|
62
|
-
|
|
63
|
-
assert result.exit_code == 0
|
|
64
|
-
assert "Session One" in result.output
|
|
65
|
-
assert "Session Two" in result.output
|
|
66
|
-
assert "Acme" in result.output
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_list_sessions_empty(runner, httpx_mock):
|
|
70
|
-
httpx_mock.add_response(
|
|
71
|
-
url="http://test.local/api/v1/sessions?skip=0&limit=50",
|
|
72
|
-
json={"sessions": [], "total": 0},
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
result = runner.invoke(app, ["sessions", "list"])
|
|
76
|
-
|
|
77
|
-
assert result.exit_code == 0
|
|
78
|
-
assert "No sessions found" in result.output
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def test_get_session(runner, httpx_mock):
|
|
82
|
-
httpx_mock.add_response(
|
|
83
|
-
url="http://test.local/api/v1/sessions/sess-1",
|
|
84
|
-
json=GET_RESPONSE,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
result = runner.invoke(app, ["sessions", "get", "sess-1"])
|
|
88
|
-
|
|
89
|
-
assert result.exit_code == 0
|
|
90
|
-
assert "session_id: sess-1" in result.output
|
|
91
|
-
assert "title: Session One" in result.output
|
|
92
|
-
# Server-managed fields should be stripped
|
|
93
|
-
assert "created_at" not in result.output
|
|
94
|
-
assert "created_by" not in result.output
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_get_session_not_found(runner, httpx_mock):
|
|
98
|
-
httpx_mock.add_response(
|
|
99
|
-
url="http://test.local/api/v1/sessions/nonexistent",
|
|
100
|
-
status_code=404,
|
|
101
|
-
json={"detail": "Session not found"},
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
result = runner.invoke(app, ["sessions", "get", "nonexistent"])
|
|
105
|
-
|
|
106
|
-
assert result.exit_code == 1
|
|
107
|
-
assert "not found" in result.output
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_create_session(runner, httpx_mock, tmp_path):
|
|
111
|
-
yaml_file = tmp_path / "session.yaml"
|
|
112
|
-
yaml_file.write_text(VALID_SESSION_YAML)
|
|
113
|
-
|
|
114
|
-
httpx_mock.add_response(
|
|
115
|
-
url="http://test.local/api/v1/sessions",
|
|
116
|
-
method="POST",
|
|
117
|
-
json={"session_id": "test-session-1", "id": "abc123"},
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
result = runner.invoke(app, ["sessions", "create", "-f", str(yaml_file)])
|
|
121
|
-
|
|
122
|
-
assert result.exit_code == 0
|
|
123
|
-
assert "test-session-1" in result.output
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def test_create_session_file_not_found(runner):
|
|
127
|
-
result = runner.invoke(app, ["sessions", "create", "-f", "/nonexistent/session.yaml"])
|
|
128
|
-
|
|
129
|
-
assert result.exit_code == 1
|
|
130
|
-
assert "File not found" in result.output
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_create_session_invalid_yaml(runner, tmp_path):
|
|
134
|
-
yaml_file = tmp_path / "bad.yaml"
|
|
135
|
-
yaml_file.write_text(":\n invalid: [yaml\n broken")
|
|
136
|
-
|
|
137
|
-
result = runner.invoke(app, ["sessions", "create", "-f", str(yaml_file)])
|
|
138
|
-
|
|
139
|
-
assert result.exit_code == 1
|
|
140
|
-
assert "Invalid YAML" in result.output
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_create_session_missing_fields(runner, tmp_path):
|
|
144
|
-
yaml_file = tmp_path / "incomplete.yaml"
|
|
145
|
-
yaml_file.write_text("title: Just a title\n")
|
|
146
|
-
|
|
147
|
-
result = runner.invoke(app, ["sessions", "create", "-f", str(yaml_file)])
|
|
148
|
-
|
|
149
|
-
assert result.exit_code == 1
|
|
150
|
-
assert "Missing required field" in result.output
|
|
151
|
-
assert "'session_id'" in result.output
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_create_session_invalid_step_type(runner, tmp_path):
|
|
155
|
-
yaml_file = tmp_path / "badstep.yaml"
|
|
156
|
-
yaml_file.write_text(
|
|
157
|
-
"session_id: test\n"
|
|
158
|
-
"client: Test\n"
|
|
159
|
-
"title: Test\n"
|
|
160
|
-
"logo: https://example.com/logo.png\n"
|
|
161
|
-
"prototype_url: https://example.com/proto\n"
|
|
162
|
-
"steps:\n"
|
|
163
|
-
" - type: invalid\n"
|
|
164
|
-
" title: Bad Step\n"
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
result = runner.invoke(app, ["sessions", "create", "-f", str(yaml_file)])
|
|
168
|
-
|
|
169
|
-
assert result.exit_code == 1
|
|
170
|
-
assert "'type' must be 'screen' or 'prototype'" in result.output
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def test_create_session_duplicate(runner, httpx_mock, tmp_path):
|
|
174
|
-
yaml_file = tmp_path / "session.yaml"
|
|
175
|
-
yaml_file.write_text(VALID_SESSION_YAML)
|
|
176
|
-
|
|
177
|
-
httpx_mock.add_response(
|
|
178
|
-
url="http://test.local/api/v1/sessions",
|
|
179
|
-
method="POST",
|
|
180
|
-
status_code=409,
|
|
181
|
-
json={"detail": "Session with this ID already exists"},
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
result = runner.invoke(app, ["sessions", "create", "-f", str(yaml_file)])
|
|
185
|
-
|
|
186
|
-
assert result.exit_code == 1
|
|
187
|
-
assert "already exists" in result.output
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def test_update_session(runner, httpx_mock, tmp_path):
|
|
191
|
-
yaml_file = tmp_path / "update.yaml"
|
|
192
|
-
yaml_file.write_text("title: Updated Title\n")
|
|
193
|
-
|
|
194
|
-
httpx_mock.add_response(
|
|
195
|
-
url="http://test.local/api/v1/sessions/sess-1",
|
|
196
|
-
method="PUT",
|
|
197
|
-
json={"session_id": "sess-1", "title": "Updated Title"},
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
result = runner.invoke(app, ["sessions", "update", "sess-1", "-f", str(yaml_file)])
|
|
201
|
-
|
|
202
|
-
assert result.exit_code == 0
|
|
203
|
-
assert "Updated session" in result.output
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def test_update_session_not_found(runner, httpx_mock, tmp_path):
|
|
207
|
-
yaml_file = tmp_path / "update.yaml"
|
|
208
|
-
yaml_file.write_text("title: Updated Title\n")
|
|
209
|
-
|
|
210
|
-
httpx_mock.add_response(
|
|
211
|
-
url="http://test.local/api/v1/sessions/nonexistent",
|
|
212
|
-
method="PUT",
|
|
213
|
-
status_code=404,
|
|
214
|
-
json={"detail": "Session not found"},
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
result = runner.invoke(app, ["sessions", "update", "nonexistent", "-f", str(yaml_file)])
|
|
218
|
-
|
|
219
|
-
assert result.exit_code == 1
|
|
220
|
-
assert "not found" in result.output
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def test_delete_session(runner, httpx_mock):
|
|
224
|
-
httpx_mock.add_response(
|
|
225
|
-
url="http://test.local/api/v1/sessions/sess-1",
|
|
226
|
-
method="DELETE",
|
|
227
|
-
json={"status": "deleted", "session_id": "sess-1"},
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
result = runner.invoke(app, ["sessions", "delete", "sess-1", "--yes"])
|
|
231
|
-
|
|
232
|
-
assert result.exit_code == 0
|
|
233
|
-
assert "Deleted session" in result.output
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def test_delete_session_not_found(runner, httpx_mock):
|
|
237
|
-
httpx_mock.add_response(
|
|
238
|
-
url="http://test.local/api/v1/sessions/nonexistent",
|
|
239
|
-
method="DELETE",
|
|
240
|
-
status_code=404,
|
|
241
|
-
json={"detail": "Session not found"},
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
result = runner.invoke(app, ["sessions", "delete", "nonexistent", "--yes"])
|
|
245
|
-
|
|
246
|
-
assert result.exit_code == 1
|
|
247
|
-
assert "not found" in result.output
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def test_validate_valid(runner, tmp_path):
|
|
251
|
-
yaml_file = tmp_path / "session.yaml"
|
|
252
|
-
yaml_file.write_text(VALID_SESSION_YAML)
|
|
253
|
-
|
|
254
|
-
result = runner.invoke(app, ["sessions", "validate", str(yaml_file)])
|
|
255
|
-
|
|
256
|
-
assert result.exit_code == 0
|
|
257
|
-
assert "Valid session config" in result.output
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def test_validate_invalid(runner, tmp_path):
|
|
261
|
-
yaml_file = tmp_path / "bad.yaml"
|
|
262
|
-
yaml_file.write_text("title: Just a title\n")
|
|
263
|
-
|
|
264
|
-
result = runner.invoke(app, ["sessions", "validate", str(yaml_file)])
|
|
265
|
-
|
|
266
|
-
assert result.exit_code == 1
|
|
267
|
-
assert "Missing required field" in result.output
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def test_example(runner):
|
|
271
|
-
result = runner.invoke(app, ["sessions", "example"])
|
|
272
|
-
|
|
273
|
-
assert result.exit_code == 0
|
|
274
|
-
assert "session_id:" in result.output
|
|
275
|
-
assert "steps:" in result.output
|
|
276
|
-
assert "prototype" in result.output
|
|
277
|
-
assert "screen" in result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|