podojo-cli 0.2.2__tar.gz → 0.3.1__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.1}/.github/workflows/publish.yml +2 -0
- podojo_cli-0.3.1/CHANGELOG.md +49 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/CLAUDE.md +10 -10
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/PKG-INFO +1 -1
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/pyproject.toml +1 -1
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/client.py +10 -10
- podojo_cli-0.2.2/src/podojo_cli/commands/sessions.py → podojo_cli-0.3.1/src/podojo_cli/commands/usertests.py +62 -56
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/main.py +2 -2
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/tests/test_projects.py +15 -5
- podojo_cli-0.3.1/tests/test_usertests.py +282 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/uv.lock +1 -1
- podojo_cli-0.2.2/tests/test_sessions.py +0 -277
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/.gitignore +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/README.md +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/auth.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/gdrive.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/projects.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/showreel.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/transcripts.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/commands/videos.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/config.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/gdrive/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/gdrive/list.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/gdrive/upload.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/video/__init__.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/src/podojo_cli/video/showreel.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/tests/conftest.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/tests/test_auth.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/tests/test_gdrive.py +0 -0
- {podojo_cli-0.2.2 → podojo_cli-0.3.1}/tests/test_showreel.py +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Podojo CLI will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org).
|
|
7
|
+
|
|
8
|
+
## [0.3.1] - 2026-04-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Auto-publish to PyPI on push to main
|
|
12
|
+
- CHANGELOG.md for tracking version history
|
|
13
|
+
|
|
14
|
+
## [0.3.0] - 2026-04-01
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Renamed `sessions` command to `usertests` (breaking change)
|
|
18
|
+
- Removed `client` field from usertest commands
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Show preview and live URLs after `usertest create` and `usertest get`
|
|
22
|
+
|
|
23
|
+
## [0.2.1] - 2026-03-28
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Default `project_name` to session ID on session creation
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Test mocks to match actual API response shape
|
|
30
|
+
|
|
31
|
+
## [0.2.0] - 2026-03-25
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `sessions` command for unmoderated test management
|
|
35
|
+
- Live status column in sessions list
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Publish workflow permissions for checkout
|
|
39
|
+
- Projects list iterating over paginated response dict
|
|
40
|
+
|
|
41
|
+
## [0.1.0] - 2026-03-20
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Initial CLI with Typer, httpx, and Rich
|
|
45
|
+
- `auth login/logout` commands with API key validation
|
|
46
|
+
- `showreel` command with video download
|
|
47
|
+
- `gdrive` upload and list commands
|
|
48
|
+
- `projects` list command
|
|
49
|
+
- PyPI publishing workflow
|
|
@@ -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,22 +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", "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, title, logo, prototype_url, steps
|
|
23
23
|
# Optional fields: welcome_text, privacy_text, promo_code, promo_code_info, project_name
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
client: Acme Corp
|
|
25
|
+
usertest_id: checkout-usability-v1
|
|
27
26
|
title: Checkout Flow Usability Test
|
|
28
27
|
logo: https://example.com/logo.png
|
|
29
28
|
prototype_url: https://figma.com/proto/abc123
|
|
@@ -46,10 +45,10 @@ privacy_text: |
|
|
|
46
45
|
promo_code: THANKS10
|
|
47
46
|
promo_code_info: "Use this code for 10% off your next purchase"
|
|
48
47
|
|
|
49
|
-
# Optional: link
|
|
48
|
+
# Optional: link user test to a project
|
|
50
49
|
project_name: checkout-redesign-q1
|
|
51
50
|
|
|
52
|
-
# Optional: set
|
|
51
|
+
# Optional: set user test live (default: false)
|
|
53
52
|
# live: true
|
|
54
53
|
|
|
55
54
|
# Steps define what participants see and do
|
|
@@ -87,8 +86,8 @@ steps:
|
|
|
87
86
|
"""
|
|
88
87
|
|
|
89
88
|
|
|
90
|
-
def
|
|
91
|
-
"""Validate
|
|
89
|
+
def validate_usertest_data(data: dict) -> list[str]:
|
|
90
|
+
"""Validate user test YAML data, return list of error strings."""
|
|
92
91
|
errors = []
|
|
93
92
|
for field in REQUIRED_FIELDS:
|
|
94
93
|
if field not in data or data[field] is None:
|
|
@@ -154,37 +153,35 @@ def _format_api_error(e: httpx.HTTPStatusError) -> str:
|
|
|
154
153
|
|
|
155
154
|
|
|
156
155
|
@app.command("list")
|
|
157
|
-
def
|
|
158
|
-
skip: int = typer.Option(0, help="Number of
|
|
159
|
-
limit: int = typer.Option(50, help="Max
|
|
156
|
+
def list_usertests(
|
|
157
|
+
skip: int = typer.Option(0, help="Number of user tests to skip"),
|
|
158
|
+
limit: int = typer.Option(50, help="Max user tests to return"),
|
|
160
159
|
):
|
|
161
|
-
"""List all
|
|
160
|
+
"""List all user tests."""
|
|
162
161
|
client = PodojoClient()
|
|
163
162
|
try:
|
|
164
|
-
result = client.
|
|
163
|
+
result = client.list_usertests(skip=skip, limit=limit)
|
|
165
164
|
except httpx.HTTPStatusError as e:
|
|
166
165
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
167
166
|
raise typer.Exit(1)
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
if not
|
|
171
|
-
console.print("No
|
|
168
|
+
usertests = result.get("usertests", [])
|
|
169
|
+
if not usertests:
|
|
170
|
+
console.print("No user tests found.")
|
|
172
171
|
return
|
|
173
172
|
|
|
174
|
-
table = Table(title="
|
|
175
|
-
table.add_column("
|
|
173
|
+
table = Table(title="User Tests")
|
|
174
|
+
table.add_column("User Test ID")
|
|
176
175
|
table.add_column("Title")
|
|
177
|
-
table.add_column("Client")
|
|
178
176
|
table.add_column("Steps", justify="right")
|
|
179
177
|
table.add_column("Live")
|
|
180
178
|
table.add_column("Last Updated")
|
|
181
179
|
|
|
182
|
-
for s in
|
|
180
|
+
for s in usertests:
|
|
183
181
|
live = "[green]Yes[/green]" if s.get("live") else "[dim]No[/dim]"
|
|
184
182
|
table.add_row(
|
|
185
|
-
s.get("
|
|
183
|
+
s.get("usertest_id", ""),
|
|
186
184
|
s.get("title", ""),
|
|
187
|
-
s.get("client", ""),
|
|
188
185
|
str(s.get("step_count", "")),
|
|
189
186
|
live,
|
|
190
187
|
s.get("last_updated", ""),
|
|
@@ -194,35 +191,39 @@ def list_sessions(
|
|
|
194
191
|
|
|
195
192
|
|
|
196
193
|
@app.command("get")
|
|
197
|
-
def
|
|
198
|
-
|
|
194
|
+
def get_usertest(
|
|
195
|
+
usertest_id: str = typer.Argument(help="User test ID to retrieve"),
|
|
199
196
|
):
|
|
200
|
-
"""Get a
|
|
197
|
+
"""Get a user test and output as YAML."""
|
|
201
198
|
client = PodojoClient()
|
|
202
199
|
try:
|
|
203
|
-
|
|
200
|
+
usertest = client.get_usertest(usertest_id)
|
|
204
201
|
except httpx.HTTPStatusError as e:
|
|
205
202
|
if e.response.status_code == 404:
|
|
206
|
-
console.print(f"[red]Error:[/red]
|
|
203
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
207
204
|
else:
|
|
208
205
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
209
206
|
raise typer.Exit(1)
|
|
210
207
|
|
|
211
208
|
# Remove server-managed fields for a clean editable output
|
|
209
|
+
group = usertest.pop("group", "")
|
|
212
210
|
for key in ("id", "created_at", "created_by", "last_updated"):
|
|
213
|
-
|
|
211
|
+
usertest.pop(key, None)
|
|
214
212
|
|
|
215
|
-
console.print(yaml.dump(
|
|
213
|
+
console.print(yaml.dump(usertest, default_flow_style=False, sort_keys=False, allow_unicode=True))
|
|
214
|
+
if group:
|
|
215
|
+
console.print(f"Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
|
|
216
|
+
console.print(f"Live: https://usertests.podojo.com/{group}?test={usertest_id}")
|
|
216
217
|
|
|
217
218
|
|
|
218
219
|
@app.command("create")
|
|
219
|
-
def
|
|
220
|
-
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with
|
|
220
|
+
def create_usertest(
|
|
221
|
+
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with user test config"),
|
|
221
222
|
):
|
|
222
|
-
"""Create a new
|
|
223
|
+
"""Create a new user test from a YAML file."""
|
|
223
224
|
data = _load_yaml(from_file)
|
|
224
225
|
|
|
225
|
-
errors =
|
|
226
|
+
errors = validate_usertest_data(data)
|
|
226
227
|
if errors:
|
|
227
228
|
console.print("[red]Validation errors:[/red]")
|
|
228
229
|
for err in errors:
|
|
@@ -230,82 +231,87 @@ def create_session(
|
|
|
230
231
|
raise typer.Exit(1)
|
|
231
232
|
|
|
232
233
|
if "project_name" not in data:
|
|
233
|
-
data["project_name"] = data["
|
|
234
|
+
data["project_name"] = data["usertest_id"]
|
|
234
235
|
|
|
235
236
|
client = PodojoClient()
|
|
236
237
|
try:
|
|
237
|
-
result = client.
|
|
238
|
+
result = client.create_usertest(data)
|
|
238
239
|
except httpx.HTTPStatusError as e:
|
|
239
240
|
if e.response.status_code == 409:
|
|
240
|
-
console.print(f"[red]Error:[/red]
|
|
241
|
+
console.print(f"[red]Error:[/red] User test '{data.get('usertest_id')}' already exists")
|
|
241
242
|
else:
|
|
242
243
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
243
244
|
raise typer.Exit(1)
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
usertest_id = result.get("usertest_id", "")
|
|
247
|
+
group = result.get("group", "")
|
|
248
|
+
console.print(f"[green]Created user test:[/green] {usertest_id}")
|
|
249
|
+
if group:
|
|
250
|
+
console.print(f" Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
|
|
251
|
+
console.print(f" Live: https://usertests.podojo.com/{group}?test={usertest_id}")
|
|
246
252
|
|
|
247
253
|
|
|
248
254
|
@app.command("update")
|
|
249
|
-
def
|
|
250
|
-
|
|
255
|
+
def update_usertest(
|
|
256
|
+
usertest_id: str = typer.Argument(help="User test ID to update"),
|
|
251
257
|
from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with fields to update"),
|
|
252
258
|
):
|
|
253
|
-
"""Update a
|
|
259
|
+
"""Update a user test from a YAML file (partial updates OK)."""
|
|
254
260
|
data = _load_yaml(from_file)
|
|
255
261
|
|
|
256
262
|
client = PodojoClient()
|
|
257
263
|
try:
|
|
258
|
-
result = client.
|
|
264
|
+
result = client.update_usertest(usertest_id, data)
|
|
259
265
|
except httpx.HTTPStatusError as e:
|
|
260
266
|
if e.response.status_code == 404:
|
|
261
|
-
console.print(f"[red]Error:[/red]
|
|
267
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
262
268
|
else:
|
|
263
269
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
264
270
|
raise typer.Exit(1)
|
|
265
271
|
|
|
266
|
-
console.print(f"[green]Updated
|
|
272
|
+
console.print(f"[green]Updated user test:[/green] {result.get('usertest_id', usertest_id)}")
|
|
267
273
|
|
|
268
274
|
|
|
269
275
|
@app.command("delete")
|
|
270
|
-
def
|
|
271
|
-
|
|
276
|
+
def delete_usertest(
|
|
277
|
+
usertest_id: str = typer.Argument(help="User test ID to delete"),
|
|
272
278
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
273
279
|
):
|
|
274
|
-
"""Delete a
|
|
280
|
+
"""Delete a user test."""
|
|
275
281
|
if not yes:
|
|
276
|
-
typer.confirm(f"Delete
|
|
282
|
+
typer.confirm(f"Delete user test '{usertest_id}'?", abort=True)
|
|
277
283
|
|
|
278
284
|
client = PodojoClient()
|
|
279
285
|
try:
|
|
280
|
-
client.
|
|
286
|
+
client.delete_usertest(usertest_id)
|
|
281
287
|
except httpx.HTTPStatusError as e:
|
|
282
288
|
if e.response.status_code == 404:
|
|
283
|
-
console.print(f"[red]Error:[/red]
|
|
289
|
+
console.print(f"[red]Error:[/red] User test '{usertest_id}' not found")
|
|
284
290
|
else:
|
|
285
291
|
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
286
292
|
raise typer.Exit(1)
|
|
287
293
|
|
|
288
|
-
console.print(f"[green]Deleted
|
|
294
|
+
console.print(f"[green]Deleted user test:[/green] {usertest_id}")
|
|
289
295
|
|
|
290
296
|
|
|
291
297
|
@app.command("validate")
|
|
292
298
|
def validate(
|
|
293
299
|
file: Path = typer.Argument(help="YAML file to validate"),
|
|
294
300
|
):
|
|
295
|
-
"""Validate a
|
|
301
|
+
"""Validate a user test YAML file without creating it."""
|
|
296
302
|
data = _load_yaml(file)
|
|
297
303
|
|
|
298
|
-
errors =
|
|
304
|
+
errors = validate_usertest_data(data)
|
|
299
305
|
if errors:
|
|
300
306
|
console.print("[red]Validation errors:[/red]")
|
|
301
307
|
for err in errors:
|
|
302
308
|
console.print(f" {err}")
|
|
303
309
|
raise typer.Exit(1)
|
|
304
310
|
|
|
305
|
-
console.print("[green]Valid
|
|
311
|
+
console.print("[green]Valid user test config.[/green]")
|
|
306
312
|
|
|
307
313
|
|
|
308
314
|
@app.command("example")
|
|
309
315
|
def example():
|
|
310
|
-
"""Print an example
|
|
316
|
+
"""Print an example user test YAML template."""
|
|
311
317
|
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,282 @@
|
|
|
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
|
+
"group": "test-group",
|
|
50
|
+
"created_at": "2026-03-01T12:00:00",
|
|
51
|
+
"created_by": "user@example.com",
|
|
52
|
+
"last_updated": "2026-03-01T12:00:00",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_list_usertests(runner, httpx_mock):
|
|
57
|
+
httpx_mock.add_response(
|
|
58
|
+
url="http://test.local/api/v1/usertests?skip=0&limit=50",
|
|
59
|
+
json=LIST_RESPONSE,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result = runner.invoke(app, ["usertests", "list"])
|
|
63
|
+
|
|
64
|
+
assert result.exit_code == 0
|
|
65
|
+
assert "User Test One" in result.output
|
|
66
|
+
assert "User Test Two" 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
|
+
# URLs should be displayed
|
|
96
|
+
assert "https://usertests.podojo.com/preview/test-group/ut-1" in result.output
|
|
97
|
+
assert "https://usertests.podojo.com/test-group?test=ut-1" in result.output
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_get_usertest_not_found(runner, httpx_mock):
|
|
101
|
+
httpx_mock.add_response(
|
|
102
|
+
url="http://test.local/api/v1/usertests/nonexistent",
|
|
103
|
+
status_code=404,
|
|
104
|
+
json={"detail": "User test not found"},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result = runner.invoke(app, ["usertests", "get", "nonexistent"])
|
|
108
|
+
|
|
109
|
+
assert result.exit_code == 1
|
|
110
|
+
assert "not found" in result.output
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_create_usertest(runner, httpx_mock, tmp_path):
|
|
114
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
115
|
+
yaml_file.write_text(VALID_USERTEST_YAML)
|
|
116
|
+
|
|
117
|
+
httpx_mock.add_response(
|
|
118
|
+
url="http://test.local/api/v1/usertests",
|
|
119
|
+
method="POST",
|
|
120
|
+
json={"usertest_id": "test-usertest-1", "id": "abc123", "group": "test-group"},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
124
|
+
|
|
125
|
+
assert result.exit_code == 0
|
|
126
|
+
assert "test-usertest-1" in result.output
|
|
127
|
+
assert "https://usertests.podojo.com/preview/test-group/test-usertest-1" in result.output
|
|
128
|
+
assert "https://usertests.podojo.com/test-group?test=test-usertest-1" in result.output
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_create_usertest_file_not_found(runner):
|
|
132
|
+
result = runner.invoke(app, ["usertests", "create", "-f", "/nonexistent/usertest.yaml"])
|
|
133
|
+
|
|
134
|
+
assert result.exit_code == 1
|
|
135
|
+
assert "File not found" in result.output
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_create_usertest_invalid_yaml(runner, tmp_path):
|
|
139
|
+
yaml_file = tmp_path / "bad.yaml"
|
|
140
|
+
yaml_file.write_text(":\n invalid: [yaml\n broken")
|
|
141
|
+
|
|
142
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
143
|
+
|
|
144
|
+
assert result.exit_code == 1
|
|
145
|
+
assert "Invalid YAML" in result.output
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_create_usertest_missing_fields(runner, tmp_path):
|
|
149
|
+
yaml_file = tmp_path / "incomplete.yaml"
|
|
150
|
+
yaml_file.write_text("title: Just a title\n")
|
|
151
|
+
|
|
152
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
153
|
+
|
|
154
|
+
assert result.exit_code == 1
|
|
155
|
+
assert "Missing required field" in result.output
|
|
156
|
+
assert "'usertest_id'" in result.output
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_create_usertest_invalid_step_type(runner, tmp_path):
|
|
160
|
+
yaml_file = tmp_path / "badstep.yaml"
|
|
161
|
+
yaml_file.write_text(
|
|
162
|
+
"usertest_id: test\n"
|
|
163
|
+
"client: Test\n"
|
|
164
|
+
"title: Test\n"
|
|
165
|
+
"logo: https://example.com/logo.png\n"
|
|
166
|
+
"prototype_url: https://example.com/proto\n"
|
|
167
|
+
"steps:\n"
|
|
168
|
+
" - type: invalid\n"
|
|
169
|
+
" title: Bad Step\n"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
173
|
+
|
|
174
|
+
assert result.exit_code == 1
|
|
175
|
+
assert "'type' must be 'screen' or 'prototype'" in result.output
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_create_usertest_duplicate(runner, httpx_mock, tmp_path):
|
|
179
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
180
|
+
yaml_file.write_text(VALID_USERTEST_YAML)
|
|
181
|
+
|
|
182
|
+
httpx_mock.add_response(
|
|
183
|
+
url="http://test.local/api/v1/usertests",
|
|
184
|
+
method="POST",
|
|
185
|
+
status_code=409,
|
|
186
|
+
json={"detail": "User test with this ID already exists"},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
result = runner.invoke(app, ["usertests", "create", "-f", str(yaml_file)])
|
|
190
|
+
|
|
191
|
+
assert result.exit_code == 1
|
|
192
|
+
assert "already exists" in result.output
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_update_usertest(runner, httpx_mock, tmp_path):
|
|
196
|
+
yaml_file = tmp_path / "update.yaml"
|
|
197
|
+
yaml_file.write_text("title: Updated Title\n")
|
|
198
|
+
|
|
199
|
+
httpx_mock.add_response(
|
|
200
|
+
url="http://test.local/api/v1/usertests/ut-1",
|
|
201
|
+
method="PUT",
|
|
202
|
+
json={"usertest_id": "ut-1", "title": "Updated Title"},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
result = runner.invoke(app, ["usertests", "update", "ut-1", "-f", str(yaml_file)])
|
|
206
|
+
|
|
207
|
+
assert result.exit_code == 0
|
|
208
|
+
assert "Updated user test" in result.output
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_update_usertest_not_found(runner, httpx_mock, tmp_path):
|
|
212
|
+
yaml_file = tmp_path / "update.yaml"
|
|
213
|
+
yaml_file.write_text("title: Updated Title\n")
|
|
214
|
+
|
|
215
|
+
httpx_mock.add_response(
|
|
216
|
+
url="http://test.local/api/v1/usertests/nonexistent",
|
|
217
|
+
method="PUT",
|
|
218
|
+
status_code=404,
|
|
219
|
+
json={"detail": "User test not found"},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
result = runner.invoke(app, ["usertests", "update", "nonexistent", "-f", str(yaml_file)])
|
|
223
|
+
|
|
224
|
+
assert result.exit_code == 1
|
|
225
|
+
assert "not found" in result.output
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_delete_usertest(runner, httpx_mock):
|
|
229
|
+
httpx_mock.add_response(
|
|
230
|
+
url="http://test.local/api/v1/usertests/ut-1",
|
|
231
|
+
method="DELETE",
|
|
232
|
+
json={"status": "deleted", "usertest_id": "ut-1"},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result = runner.invoke(app, ["usertests", "delete", "ut-1", "--yes"])
|
|
236
|
+
|
|
237
|
+
assert result.exit_code == 0
|
|
238
|
+
assert "Deleted user test" in result.output
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_delete_usertest_not_found(runner, httpx_mock):
|
|
242
|
+
httpx_mock.add_response(
|
|
243
|
+
url="http://test.local/api/v1/usertests/nonexistent",
|
|
244
|
+
method="DELETE",
|
|
245
|
+
status_code=404,
|
|
246
|
+
json={"detail": "User test not found"},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
result = runner.invoke(app, ["usertests", "delete", "nonexistent", "--yes"])
|
|
250
|
+
|
|
251
|
+
assert result.exit_code == 1
|
|
252
|
+
assert "not found" in result.output
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_validate_valid(runner, tmp_path):
|
|
256
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
257
|
+
yaml_file.write_text(VALID_USERTEST_YAML)
|
|
258
|
+
|
|
259
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
260
|
+
|
|
261
|
+
assert result.exit_code == 0
|
|
262
|
+
assert "Valid user test config" in result.output
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_validate_invalid(runner, tmp_path):
|
|
266
|
+
yaml_file = tmp_path / "bad.yaml"
|
|
267
|
+
yaml_file.write_text("title: Just a title\n")
|
|
268
|
+
|
|
269
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
270
|
+
|
|
271
|
+
assert result.exit_code == 1
|
|
272
|
+
assert "Missing required field" in result.output
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_example(runner):
|
|
276
|
+
result = runner.invoke(app, ["usertests", "example"])
|
|
277
|
+
|
|
278
|
+
assert result.exit_code == 0
|
|
279
|
+
assert "usertest_id:" in result.output
|
|
280
|
+
assert "steps:" in result.output
|
|
281
|
+
assert "prototype" in result.output
|
|
282
|
+
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
|