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.
Files changed (31) hide show
  1. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/CLAUDE.md +10 -10
  2. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/PKG-INFO +1 -1
  3. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/pyproject.toml +1 -1
  4. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/client.py +10 -10
  5. podojo_cli-0.2.1/src/podojo_cli/commands/sessions.py → podojo_cli-0.3.0/src/podojo_cli/commands/usertests.py +55 -52
  6. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/main.py +2 -2
  7. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/test_projects.py +15 -5
  8. podojo_cli-0.3.0/tests/test_usertests.py +277 -0
  9. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/uv.lock +1 -1
  10. podojo_cli-0.2.1/tests/test_sessions.py +0 -277
  11. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  12. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/.gitignore +0 -0
  13. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/README.md +0 -0
  14. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/__init__.py +0 -0
  15. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/__init__.py +0 -0
  16. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/auth.py +0 -0
  17. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/gdrive.py +0 -0
  18. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/projects.py +0 -0
  19. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/showreel.py +0 -0
  20. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/transcripts.py +0 -0
  21. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/commands/videos.py +0 -0
  22. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/config.py +0 -0
  23. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/__init__.py +0 -0
  24. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/list.py +0 -0
  25. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/gdrive/upload.py +0 -0
  26. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/video/__init__.py +0 -0
  27. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/src/podojo_cli/video/showreel.py +0 -0
  28. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/conftest.py +0 -0
  29. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/test_auth.py +0 -0
  30. {podojo_cli-0.2.1 → podojo_cli-0.3.0}/tests/test_gdrive.py +0 -0
  31. {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
- ## Sessions (Unmoderated Tests)
28
+ ## User Tests (Unmoderated Tests)
29
29
 
30
30
  ```bash
31
- podojo sessions list
32
- podojo sessions get <session_id>
33
- podojo sessions create --from-file session.yaml
34
- podojo sessions update <session_id> --from-file updates.yaml
35
- podojo sessions delete <session_id> [--yes]
36
- podojo sessions example # print template YAML to stdout
37
- podojo sessions validate session.yaml # validate without creating
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
- Session configs are YAML files. Run `podojo sessions example` for the full template.
41
- Required fields: `session_id`, `client`, `title`, `logo`, `prototype_url`, `steps`.
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: podojo-cli
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: CLI for the Podojo user research platform
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -53,44 +53,44 @@ class PodojoClient:
53
53
  r.raise_for_status()
54
54
  return r.json()
55
55
 
56
- def list_sessions(self, skip: int = 0, limit: int = 50) -> dict:
56
+ def list_usertests(self, skip: int = 0, limit: int = 50) -> dict:
57
57
  r = httpx.get(
58
- f"{self.base_url}/sessions",
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 get_session(self, session_id: str) -> dict:
65
+ def get_usertest(self, usertest_id: str) -> dict:
66
66
  r = httpx.get(
67
- f"{self.base_url}/sessions/{session_id}",
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 create_session(self, data: dict) -> dict:
73
+ def create_usertest(self, data: dict) -> dict:
74
74
  r = httpx.post(
75
- f"{self.base_url}/sessions",
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 update_session(self, session_id: str, data: dict) -> dict:
82
+ def update_usertest(self, usertest_id: str, data: dict) -> dict:
83
83
  r = httpx.put(
84
- f"{self.base_url}/sessions/{session_id}",
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 delete_session(self, session_id: str) -> dict:
91
+ def delete_usertest(self, usertest_id: str) -> dict:
92
92
  r = httpx.delete(
93
- f"{self.base_url}/sessions/{session_id}",
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 test sessions")
11
+ app = typer.Typer(help="Manage unmoderated user tests")
12
12
  console = Console()
13
13
 
14
- REQUIRED_FIELDS = ["session_id", "client", "title", "logo", "prototype_url", "steps"]
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 Session Configuration
20
+ # Podojo Unmoderated User Test Configuration
21
21
  #
22
- # Required fields: session_id, client, title, logo, prototype_url, steps
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
- session_id: checkout-usability-v1
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 session to a project
49
+ # Optional: link user test to a project
50
50
  project_name: checkout-redesign-q1
51
51
 
52
- # Optional: set session live (default: false)
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 validate_session_data(data: dict) -> list[str]:
91
- """Validate session YAML data, return list of error strings."""
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 list_sessions(
158
- skip: int = typer.Option(0, help="Number of sessions to skip"),
159
- limit: int = typer.Option(50, help="Max sessions to return"),
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 sessions."""
161
+ """List all user tests."""
162
162
  client = PodojoClient()
163
163
  try:
164
- result = client.list_sessions(skip=skip, limit=limit)
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
- sessions = result.get("sessions", [])
170
- if not sessions:
171
- console.print("No sessions found.")
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="Sessions")
175
- table.add_column("Session ID")
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 sessions:
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("session_id", ""),
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 get_session(
198
- session_id: str = typer.Argument(help="Session ID to retrieve"),
197
+ def get_usertest(
198
+ usertest_id: str = typer.Argument(help="User test ID to retrieve"),
199
199
  ):
200
- """Get a session and output as YAML."""
200
+ """Get a user test and output as YAML."""
201
201
  client = PodojoClient()
202
202
  try:
203
- session = client.get_session(session_id)
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] Session '{session_id}' not found")
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
- session.pop(key, None)
213
+ usertest.pop(key, None)
214
214
 
215
- console.print(yaml.dump(session, default_flow_style=False, sort_keys=False, allow_unicode=True))
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 create_session(
220
- from_file: Path = typer.Option(..., "--from-file", "-f", help="YAML file with session config"),
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 session from a YAML file."""
222
+ """Create a new user test from a YAML file."""
223
223
  data = _load_yaml(from_file)
224
224
 
225
- errors = validate_session_data(data)
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.create_session(data)
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] Session '{data.get('session_id')}' already exists")
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 session:[/green] {result.get('session_id', '')}")
245
+ console.print(f"[green]Created user test:[/green] {result.get('usertest_id', '')}")
243
246
 
244
247
 
245
248
  @app.command("update")
246
- def update_session(
247
- session_id: str = typer.Argument(help="Session ID to update"),
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 session from a YAML file (partial updates OK)."""
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.update_session(session_id, data)
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] Session '{session_id}' not found")
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 session:[/green] {result.get('session_id', session_id)}")
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 delete_session(
268
- session_id: str = typer.Argument(help="Session ID to delete"),
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 session."""
274
+ """Delete a user test."""
272
275
  if not yes:
273
- typer.confirm(f"Delete session '{session_id}'?", abort=True)
276
+ typer.confirm(f"Delete user test '{usertest_id}'?", abort=True)
274
277
 
275
278
  client = PodojoClient()
276
279
  try:
277
- client.delete_session(session_id)
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] Session '{session_id}' not found")
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 session:[/green] {session_id}")
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 session YAML file without creating it."""
295
+ """Validate a user test YAML file without creating it."""
293
296
  data = _load_yaml(file)
294
297
 
295
- errors = validate_session_data(data)
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 session config.[/green]")
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 session YAML template."""
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, sessions, showreel, transcripts, videos
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(sessions.app, name="sessions")
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
- {"project_name": "Alpha", "description": "First project"},
9
- {"project_name": "Beta", "description": "Second project"},
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
@@ -404,7 +404,7 @@ wheels = [
404
404
 
405
405
  [[package]]
406
406
  name = "podojo-cli"
407
- version = "0.2.1"
407
+ version = "0.2.2"
408
408
  source = { editable = "." }
409
409
  dependencies = [
410
410
  { name = "google-api-python-client" },
@@ -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