podojo-cli 1.26.0__tar.gz → 1.27.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 (37) hide show
  1. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/CHANGELOG.md +18 -0
  2. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/CLAUDE.md +1 -1
  3. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/PKG-INFO +1 -1
  4. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/pyproject.toml +1 -1
  5. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/usertests.py +36 -2
  6. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_usertests.py +100 -0
  7. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/uv.lock +4 -4
  8. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/.github/workflows/publish.yml +0 -0
  9. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/.gitignore +0 -0
  10. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/LICENSE +0 -0
  11. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/README.md +0 -0
  12. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/__init__.py +0 -0
  13. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/client.py +0 -0
  14. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/__init__.py +0 -0
  15. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/aiinterviews.py +0 -0
  16. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/auth.py +0 -0
  17. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/interviews.py +0 -0
  18. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/projects.py +0 -0
  19. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/showreel.py +0 -0
  20. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/synth.py +0 -0
  21. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/transcripts.py +0 -0
  22. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/commands/videos.py +0 -0
  23. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/config.py +0 -0
  24. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/main.py +0 -0
  25. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/synth/__init__.py +0 -0
  26. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/synth/driver.py +0 -0
  27. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/synth/session.py +0 -0
  28. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/version_check.py +0 -0
  29. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/video/__init__.py +0 -0
  30. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/src/podojo_cli/video/showreel.py +0 -0
  31. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/conftest.py +0 -0
  32. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_aiinterviews.py +0 -0
  33. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_auth.py +0 -0
  34. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_interviews.py +0 -0
  35. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_projects.py +0 -0
  36. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_showreel.py +0 -0
  37. {podojo_cli-1.26.0 → podojo_cli-1.27.0}/tests/test_transcripts.py +0 -0
@@ -5,6 +5,24 @@ All notable changes to the Podojo CLI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org).
7
7
 
8
+ ## [1.27.0] - 2026-10-27
9
+
10
+ ### Added
11
+ - User test steps support a new screen variant, `ai-question`: an AI voice
12
+ interviewer asks the step's `text` and up to `max_follow_ups` follow-ups
13
+ (default 2) aimed at `probe_for`. `podojo usertests validate` checks that
14
+ `text` is set and rejects `probe_for` / `max_follow_ups` on other steps.
15
+ `podojo usertests example` shows one.
16
+
17
+ ## [1.26.1] - 2026-10-25
18
+
19
+ ### Security
20
+ - Updated certifi to 2026.10.23, which removes the eMudhra/emSign root
21
+ certificates compromised in October 2026. certifi is the CA bundle httpx
22
+ uses for every API call to Podojo; without this update a man-in-the-middle
23
+ with a fraudulently issued certificate could impersonate the Podojo API to
24
+ the CLI.
25
+
8
26
  ## [1.26.0] - 2026-10-08
9
27
 
10
28
  ### Changed
@@ -32,7 +32,7 @@ podojo usertests validate usertest.yaml # validate without creating
32
32
  User test configs are YAML files. Run `podojo usertests example` for the full template.
33
33
  Required fields: `usertest_id`, `title`, `prototype_url`, `steps`. `logo` is optional —
34
34
  the test app falls back to the account's logo (group mapping in unmoderated-usertests).
35
- Each step needs `type` ("screen" or "prototype") and `title`. Screen steps should have `variant` ("question", "task", or "instruction" — instruction screens render a "Continue" button instead of "Done with Step", useful as a briefing before a prototype step).
35
+ Each step needs `type` ("screen" or "prototype") and `title`. Screen steps should have `variant` ("question", "task", "instruction" or "ai-question" — instruction screens render a "Continue" button instead of "Done with Step", useful as a briefing before a prototype step; ai-question has an AI voice interviewer ask `text` plus follow-ups, with optional `probe_for` and `max_follow_ups`, default 2).
36
36
 
37
37
  ## Synthetic User Tests (experimental)
38
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: podojo-cli
3
- Version: 1.26.0
3
+ Version: 1.27.0
4
4
  Summary: CLI for the Podojo user research platform
5
5
  Project-URL: Homepage, https://github.com/podojo/cli-podojo
6
6
  Project-URL: Source, https://github.com/podojo/cli-podojo
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "1.26.0"
3
+ version = "1.27.0"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -14,7 +14,7 @@ err_console = Console(stderr=True, soft_wrap=True)
14
14
 
15
15
  REQUIRED_FIELDS = ["usertest_id", "title", "prototype_url", "steps"]
16
16
  VALID_STEP_TYPES = {"screen", "prototype"}
17
- VALID_STEP_VARIANTS = {"question", "task", "instruction"}
17
+ VALID_STEP_VARIANTS = {"question", "task", "instruction", "ai-question"}
18
18
  REQUIRED_STEP_FIELDS = ["type", "title"]
19
19
 
20
20
  # The rrweb.record() call lives in recorder.js rather than an inline <script>:
@@ -153,7 +153,10 @@ rejection_message: >
153
153
  # Steps define what participants see and do
154
154
  # Each step requires: type ("screen" or "prototype") and title
155
155
  # Screen steps should have a variant: "question" (open-ended), "task" (action-based),
156
- # or "instruction" (briefing screen shown before a prototype step — uses a "Continue" button)
156
+ # "instruction" (briefing screen shown before a prototype step — uses a "Continue" button),
157
+ # or "ai-question" (an AI voice interviewer asks `text` and up to max_follow_ups follow-ups;
158
+ # `text` is required, optional probe_for says what the follow-ups should dig into,
159
+ # max_follow_ups defaults to 2)
157
160
  # Optional per step: text (markdown), and either:
158
161
  # image: a URL to an externally-hosted image, or
159
162
  # image_file: a path to a local image (uploaded to Podojo storage on create/update;
@@ -184,6 +187,13 @@ steps:
184
187
  Think aloud as you go through each step.
185
188
  # url: https://figma.com/proto/abc123?state=basic-active
186
189
 
190
+ - type: screen
191
+ variant: ai-question
192
+ title: About buying the pass
193
+ text: How did buying the pass go for you?
194
+ probe_for: What they expected after paying; whether the price was clear
195
+ max_follow_ups: 2
196
+
187
197
  - type: screen
188
198
  variant: task
189
199
  title: Find the Return Policy
@@ -243,6 +253,30 @@ def validate_usertest_data(data: dict) -> list[str]:
243
253
  )
244
254
  if "url" in step and not (isinstance(step["url"], str) and step["url"]):
245
255
  errors.append(f"Step {i}: 'url' must be a non-empty string")
256
+ is_ai_question = step_type == "screen" and variant == "ai-question"
257
+ if is_ai_question:
258
+ text = step.get("text")
259
+ if not (isinstance(text, str) and text.strip()):
260
+ errors.append(
261
+ f"Step {i}: ai-question steps need a non-empty 'text' (the question)"
262
+ )
263
+ max_follow_ups = step.get("max_follow_ups")
264
+ if max_follow_ups is not None and (
265
+ isinstance(max_follow_ups, bool)
266
+ or not isinstance(max_follow_ups, int)
267
+ or max_follow_ups < 0
268
+ ):
269
+ errors.append(f"Step {i}: 'max_follow_ups' must be a non-negative integer")
270
+ probe_for = step.get("probe_for")
271
+ if probe_for is not None and not isinstance(probe_for, str):
272
+ errors.append(f"Step {i}: 'probe_for' must be a string")
273
+ else:
274
+ # `usertests get` prints them as null on every step; only values count.
275
+ for field in ("probe_for", "max_follow_ups"):
276
+ if step.get(field) is not None:
277
+ errors.append(
278
+ f"Step {i}: '{field}' is only allowed on screen steps with variant 'ai-question'"
279
+ )
246
280
 
247
281
  screening_questions = data.get("screening_questions")
248
282
  if screening_questions is not None:
@@ -699,3 +699,103 @@ def test_snippet(runner):
699
699
  assert "rrweb.record" in result.output
700
700
  # the recording must start from recorder.js: Boltable refuses inline scripts
701
701
  assert "<script>" not in result.output
702
+
703
+
704
+ def _validate_steps(runner, tmp_path, steps_yaml):
705
+ yaml_file = tmp_path / "usertest.yaml"
706
+ yaml_file.write_text(
707
+ VALID_USERTEST_YAML.split("steps:")[0] + "steps:\n" + steps_yaml
708
+ )
709
+ return runner.invoke(app, ["usertests", "validate", str(yaml_file)])
710
+
711
+
712
+ def test_validate_ai_question_valid(runner, tmp_path):
713
+ result = _validate_steps(
714
+ runner,
715
+ tmp_path,
716
+ " - type: screen\n variant: ai-question\n title: Pass\n"
717
+ " text: How did buying the pass go?\n probe_for: price clarity\n"
718
+ " max_follow_ups: 0\n",
719
+ )
720
+
721
+ assert result.exit_code == 0
722
+
723
+
724
+ def test_validate_ai_question_follow_up_fields_optional(runner, tmp_path):
725
+ result = _validate_steps(
726
+ runner,
727
+ tmp_path,
728
+ " - type: screen\n variant: ai-question\n title: Pass\n text: How was it?\n",
729
+ )
730
+
731
+ assert result.exit_code == 0
732
+
733
+
734
+ def test_validate_ai_question_requires_text(runner, tmp_path):
735
+ result = _validate_steps(
736
+ runner, tmp_path, " - type: screen\n variant: ai-question\n title: Pass\n"
737
+ )
738
+
739
+ assert result.exit_code == 1
740
+ assert "non-empty 'text'" in result.output
741
+
742
+
743
+ def test_validate_ai_question_max_follow_ups_must_be_non_negative_int(runner, tmp_path):
744
+ for value in ("-1", "1.5", "two", "true"):
745
+ result = _validate_steps(
746
+ runner,
747
+ tmp_path,
748
+ " - type: screen\n variant: ai-question\n title: Pass\n"
749
+ f" text: How was it?\n max_follow_ups: {value}\n",
750
+ )
751
+
752
+ assert result.exit_code == 1, value
753
+ assert "max_follow_ups" in result.output
754
+
755
+
756
+ def test_validate_ai_question_probe_for_must_be_string(runner, tmp_path):
757
+ result = _validate_steps(
758
+ runner,
759
+ tmp_path,
760
+ " - type: screen\n variant: ai-question\n title: Pass\n"
761
+ " text: How was it?\n probe_for: 5\n",
762
+ )
763
+
764
+ assert result.exit_code == 1
765
+ assert "'probe_for' must be a string" in result.output
766
+
767
+
768
+ def test_validate_probe_for_rejected_on_other_steps(runner, tmp_path):
769
+ result = _validate_steps(
770
+ runner,
771
+ tmp_path,
772
+ " - type: screen\n variant: question\n title: Q\n text: Hi\n"
773
+ " probe_for: x\n max_follow_ups: 2\n",
774
+ )
775
+
776
+ assert result.exit_code == 1
777
+ assert "'probe_for' is only allowed" in result.output
778
+ assert "'max_follow_ups' is only allowed" in result.output
779
+
780
+
781
+ def test_validate_null_ai_question_fields_on_other_steps(runner, tmp_path):
782
+ # `usertests get` prints every stored step with these keys, so a fetched
783
+ # config must validate again unchanged.
784
+ result = _validate_steps(
785
+ runner,
786
+ tmp_path,
787
+ " - type: prototype\n title: Task\n probe_for: null\n max_follow_ups: null\n",
788
+ )
789
+
790
+ assert result.exit_code == 0
791
+
792
+
793
+ def test_example_template_validates(runner, tmp_path):
794
+ example = runner.invoke(app, ["usertests", "example"])
795
+ yaml_file = tmp_path / "example.yaml"
796
+ yaml_file.write_text(example.output)
797
+
798
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
799
+
800
+ assert "variant: ai-question" in example.output
801
+ assert result.exit_code == 0
@@ -35,11 +35,11 @@ wheels = [
35
35
 
36
36
  [[package]]
37
37
  name = "certifi"
38
- version = "2026.7.22"
38
+ version = "2026.10.23"
39
39
  source = { registry = "https://pypi.org/simple" }
40
- sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
40
+ sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" }
41
41
  wheels = [
42
- { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
42
+ { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" },
43
43
  ]
44
44
 
45
45
  [[package]]
@@ -438,7 +438,7 @@ wheels = [
438
438
 
439
439
  [[package]]
440
440
  name = "podojo-cli"
441
- version = "1.26.0"
441
+ version = "1.27.0"
442
442
  source = { editable = "." }
443
443
  dependencies = [
444
444
  { name = "httpx" },
File without changes
File without changes
File without changes