podojo-cli 0.2.1__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.1 → podojo_cli-0.3.0}/CLAUDE.md +10 -10
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/PKG-INFO +1 -1
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/pyproject.toml +1 -1
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/client.py +10 -10
- podojo_cli-0.2.1/src/podojo_cli/commands/sessions.py → podojo_cli-0.3.0/src/podojo_cli/commands/usertests.py +55 -52
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/main.py +2 -2
- {podojo_cli-0.2.1 → 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.1 → podojo_cli-0.3.0}/uv.lock +1 -1
- podojo_cli-0.2.1/tests/test_sessions.py +0 -277
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/.gitignore +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/README.md +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/__init__.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/__init__.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/auth.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/gdrive.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/projects.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/showreel.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/transcripts.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/videos.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/config.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/__init__.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/list.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/upload.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/video/__init__.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/video/showreel.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/conftest.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/test_auth.py +0 -0
- {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/test_gdrive.py +0 -0
- {podojo_cli-0.2.1 → 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,115 +194,118 @@ 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:
|
|
229
229
|
console.print(f" {err}")
|
|
230
230
|
raise typer.Exit(1)
|
|
231
231
|
|
|
232
|
+
if "project_name" not in data:
|
|
233
|
+
data["project_name"] = data["usertest_id"]
|
|
234
|
+
|
|
232
235
|
client = PodojoClient()
|
|
233
236
|
try:
|
|
234
|
-
result = client.
|
|
237
|
+
result = client.create_usertest(data)
|
|
235
238
|
except httpx.HTTPStatusError as e:
|
|
236
239
|
if e.response.status_code == 409:
|
|
237
|
-
console.print(f"[red]Error:[/red]
|
|
240
|
+
console.print(f"[red]Error:[/red] User test '{data.get('usertest_id')}' already exists")
|
|
238
241
|
else:
|
|
239
242
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
240
243
|
raise typer.Exit(1)
|
|
241
244
|
|
|
242
|
-
console.print(f"[green]Created
|
|
245
|
+
console.print(f"[green]Created user test:[/green] {result.get('usertest_id', '')}")
|
|
243
246
|
|
|
244
247
|
|
|
245
248
|
@app.command("update")
|
|
246
|
-
def
|
|
247
|
-
|
|
249
|
+
def update_usertest(
|
|
250
|
+
usertest_id: str = typer.Argument(help="User test ID to update"),
|
|
248
251
|
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with fields to update"),
|
|
249
252
|
):
|
|
250
|
-
"""Update a
|
|
253
|
+
"""Update a user test from a YAML file (partial updates OK)."""
|
|
251
254
|
data = _load_yaml(from_file)
|
|
252
255
|
|
|
253
256
|
client = PodojoClient()
|
|
254
257
|
try:
|
|
255
|
-
result = client.
|
|
258
|
+
result = client.update_usertest(usertest_id, data)
|
|
256
259
|
except httpx.HTTPStatusError as e:
|
|
257
260
|
if e.response.status_code == 404:
|
|
258
|
-
console.print(f"[red]Error:[/red]
|
|
261
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
259
262
|
else:
|
|
260
263
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
261
264
|
raise typer.Exit(1)
|
|
262
265
|
|
|
263
|
-
console.print(f"[green]Updated
|
|
266
|
+
console.print(f"[green]Updated user test:[/green] {result.get('usertest_id', usertest_id)}")
|
|
264
267
|
|
|
265
268
|
|
|
266
269
|
@app.command("delete")
|
|
267
|
-
def
|
|
268
|
-
|
|
270
|
+
def delete_usertest(
|
|
271
|
+
usertest_id: str = typer.Argument(help="User test ID to delete"),
|
|
269
272
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
270
273
|
):
|
|
271
|
-
"""Delete a
|
|
274
|
+
"""Delete a user test."""
|
|
272
275
|
if not yes:
|
|
273
|
-
typer.confirm(f"Delete
|
|
276
|
+
typer.confirm(f"Delete user test '{usertest_id}'?", abort=True)
|
|
274
277
|
|
|
275
278
|
client = PodojoClient()
|
|
276
279
|
try:
|
|
277
|
-
client.
|
|
280
|
+
client.delete_usertest(usertest_id)
|
|
278
281
|
except httpx.HTTPStatusError as e:
|
|
279
282
|
if e.response.status_code == 404:
|
|
280
|
-
console.print(f"[red]Error:[/red]
|
|
283
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
281
284
|
else:
|
|
282
285
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
283
286
|
raise typer.Exit(1)
|
|
284
287
|
|
|
285
|
-
console.print(f"[green]Deleted
|
|
288
|
+
console.print(f"[green]Deleted user test:[/green] {usertest_id}")
|
|
286
289
|
|
|
287
290
|
|
|
288
291
|
@app.command("validate")
|
|
289
292
|
def validate(
|
|
290
293
|
file: Path = typer.Argument(help="YAML file to validate"),
|
|
291
294
|
):
|
|
292
|
-
"""Validate a
|
|
295
|
+
"""Validate a user test YAML file without creating it."""
|
|
293
296
|
data = _load_yaml(file)
|
|
294
297
|
|
|
295
|
-
errors =
|
|
298
|
+
errors = validate_usertest_data(data)
|
|
296
299
|
if errors:
|
|
297
300
|
console.print("[red]Validation errors:[/red]")
|
|
298
301
|
for err in errors:
|
|
299
302
|
console.print(f" {err}")
|
|
300
303
|
raise typer.Exit(1)
|
|
301
304
|
|
|
302
|
-
console.print("[green]Valid
|
|
305
|
+
console.print("[green]Valid user test config.[/green]")
|
|
303
306
|
|
|
304
307
|
|
|
305
308
|
@app.command("example")
|
|
306
309
|
def example():
|
|
307
|
-
"""Print an example
|
|
310
|
+
"""Print an example user test YAML template."""
|
|
308
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
|