podojo-cli 1.25.3__tar.gz → 1.26.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-1.25.3 → podojo_cli-1.26.0}/CHANGELOG.md +13 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/PKG-INFO +8 -6
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/README.md +7 -5
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/pyproject.toml +1 -1
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/usertests.py +25 -13
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_usertests.py +59 -1
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/uv.lock +1 -1
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/.github/workflows/publish.yml +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/.gitignore +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/CLAUDE.md +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/LICENSE +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/__init__.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/client.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/__init__.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/aiinterviews.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/auth.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/interviews.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/projects.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/showreel.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/synth.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/transcripts.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/commands/videos.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/config.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/main.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/synth/__init__.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/synth/driver.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/synth/session.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/version_check.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/video/__init__.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/src/podojo_cli/video/showreel.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/conftest.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_aiinterviews.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_auth.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_interviews.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_projects.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_showreel.py +0 -0
- {podojo_cli-1.25.3 → podojo_cli-1.26.0}/tests/test_transcripts.py +0 -0
|
@@ -5,6 +5,19 @@ 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.26.0] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- User test `screening_questions` no longer screen by default, matching AI
|
|
12
|
+
interviews: a question only gates entry when it carries `screener: true`
|
|
13
|
+
(then at least one option needs `qualifies: true`, as before). Questions
|
|
14
|
+
without the flag are plain closed questions — every answer is accepted and
|
|
15
|
+
recorded, and `qualifies` flags are not needed. Mixed `qualifies` flags
|
|
16
|
+
without `screener: true` are rejected by `validate`/`create`/`update` so a
|
|
17
|
+
test can't silently stop screening: add `screener: true` to existing
|
|
18
|
+
screener questions in your YAML files. Tests already stored in Podojo keep
|
|
19
|
+
screening as before.
|
|
20
|
+
|
|
8
21
|
## [1.25.3] - 2026-10-02
|
|
9
22
|
|
|
10
23
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: podojo-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.26.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
|
|
@@ -119,11 +119,13 @@ podojo usertests delete checkout-usability-v1
|
|
|
119
119
|
podojo usertests snippet # recording snippet: two script tags + recorder.js
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
A user test can open with
|
|
123
|
-
single-select
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
A user test can open with optional on-screen `screening_questions` (closed
|
|
123
|
+
questions, single- or multi-select), answered before consent and recording;
|
|
124
|
+
answers are recorded with each session. A question marked `screener: true`
|
|
125
|
+
also gates entry: its options carry `qualifies: true` flags, and participants
|
|
126
|
+
who don't pick a qualifying option see the test's `rejection_message` and
|
|
127
|
+
never reach the recorded test. See `podojo usertests example` for the exact
|
|
128
|
+
shape.
|
|
127
129
|
|
|
128
130
|
Set `required_device: mobile` or `required_device: desktop` to restrict a test
|
|
129
131
|
to one kind of device. Participants who open the link on the other kind are
|
|
@@ -87,11 +87,13 @@ podojo usertests delete checkout-usability-v1
|
|
|
87
87
|
podojo usertests snippet # recording snippet: two script tags + recorder.js
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
A user test can open with
|
|
91
|
-
single-select
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
A user test can open with optional on-screen `screening_questions` (closed
|
|
91
|
+
questions, single- or multi-select), answered before consent and recording;
|
|
92
|
+
answers are recorded with each session. A question marked `screener: true`
|
|
93
|
+
also gates entry: its options carry `qualifies: true` flags, and participants
|
|
94
|
+
who don't pick a qualifying option see the test's `rejection_message` and
|
|
95
|
+
never reach the recorded test. See `podojo usertests example` for the exact
|
|
96
|
+
shape.
|
|
95
97
|
|
|
96
98
|
Set `required_device: mobile` or `required_device: desktop` to restrict a test
|
|
97
99
|
to one kind of device. Participants who open the link on the other kind are
|
|
@@ -58,15 +58,17 @@ EXAMPLE_YAML = """\
|
|
|
58
58
|
# screening_questions, rejection_message, project_name,
|
|
59
59
|
# live, collect_contact, max_responses, required_device
|
|
60
60
|
#
|
|
61
|
-
# Each screening question (optional
|
|
62
|
-
# before consent
|
|
61
|
+
# Each screening question (optional closed questions, answered on screen
|
|
62
|
+
# before consent and recording; answers are stored with the session):
|
|
63
63
|
# text (required) multiple-choice question
|
|
64
64
|
# multi_select (optional, default false) participants can pick several
|
|
65
|
-
# options
|
|
66
|
-
#
|
|
67
|
-
# `qualifies: true
|
|
68
|
-
#
|
|
69
|
-
#
|
|
65
|
+
# options
|
|
66
|
+
# screener (optional, default false) the question screens: participants
|
|
67
|
+
# must pick an option with `qualifies: true`, otherwise they
|
|
68
|
+
# see the rejection_message and the test never starts
|
|
69
|
+
# options (required, at least 2) each with `text`; `qualifies: true`
|
|
70
|
+
# marks the accepted answers of a screener question and is
|
|
71
|
+
# ignored on plain closed questions
|
|
70
72
|
|
|
71
73
|
usertest_id: checkout-usability-v1
|
|
72
74
|
title: Checkout Flow Usability Test
|
|
@@ -113,20 +115,20 @@ project_name: checkout-redesign-q1
|
|
|
113
115
|
# never start a screener or use up a session.
|
|
114
116
|
# required_device: mobile
|
|
115
117
|
|
|
116
|
-
# Optional:
|
|
117
|
-
#
|
|
118
|
-
#
|
|
118
|
+
# Optional: closed questions — shown on screen before consent and recording.
|
|
119
|
+
# Answers are captured with the session; only questions marked
|
|
120
|
+
# `screener: true` can screen participants out. Screen-out answers are still
|
|
121
|
+
# captured for funnel metrics.
|
|
119
122
|
screening_questions:
|
|
120
123
|
- text: How often do you shop online?
|
|
121
124
|
options:
|
|
122
125
|
- text: Rarely or never
|
|
123
126
|
- text: A few times a year
|
|
124
127
|
- text: At least once a month
|
|
125
|
-
qualifies: true
|
|
126
128
|
- text: Weekly or more
|
|
127
|
-
qualifies: true
|
|
128
129
|
|
|
129
130
|
- text: Have you abandoned an online purchase at checkout in the past 3 months?
|
|
131
|
+
screener: true
|
|
130
132
|
options:
|
|
131
133
|
- text: "Yes"
|
|
132
134
|
qualifies: true
|
|
@@ -135,6 +137,7 @@ screening_questions:
|
|
|
135
137
|
|
|
136
138
|
- text: Which devices do you use to shop online?
|
|
137
139
|
multi_select: true
|
|
140
|
+
screener: true
|
|
138
141
|
options:
|
|
139
142
|
- text: Phone
|
|
140
143
|
qualifies: true
|
|
@@ -255,6 +258,10 @@ def validate_usertest_data(data: dict) -> list[str]:
|
|
|
255
258
|
errors.append(
|
|
256
259
|
f"Screening question {i}: 'multi_select' must be true or false"
|
|
257
260
|
)
|
|
261
|
+
screener = question.get("screener", False)
|
|
262
|
+
if not isinstance(screener, bool):
|
|
263
|
+
errors.append(f"Screening question {i}: 'screener' must be true or false")
|
|
264
|
+
screener = False
|
|
258
265
|
options = question.get("options")
|
|
259
266
|
if not isinstance(options, list) or len(options) < 2:
|
|
260
267
|
errors.append(f"Screening question {i}: 'options' must list at least 2 options")
|
|
@@ -273,10 +280,15 @@ def validate_usertest_data(data: dict) -> list[str]:
|
|
|
273
280
|
)
|
|
274
281
|
elif qualifies:
|
|
275
282
|
qualifying += 1
|
|
276
|
-
if qualifying == 0:
|
|
283
|
+
if screener and qualifying == 0:
|
|
277
284
|
errors.append(
|
|
278
285
|
f"Screening question {i}: needs at least one option with 'qualifies: true'"
|
|
279
286
|
)
|
|
287
|
+
if not screener and 0 < qualifying < len(options):
|
|
288
|
+
errors.append(
|
|
289
|
+
f"Screening question {i}: has non-qualifying options but no "
|
|
290
|
+
"'screener: true' — set it, or drop the 'qualifies' flags"
|
|
291
|
+
)
|
|
280
292
|
return errors
|
|
281
293
|
|
|
282
294
|
|
|
@@ -509,12 +509,14 @@ def test_validate_required_device_rejects_other_values(runner, tmp_path):
|
|
|
509
509
|
SCREENING_YAML = VALID_USERTEST_YAML + """\
|
|
510
510
|
screening_questions:
|
|
511
511
|
- text: Do you shop online?
|
|
512
|
+
screener: true
|
|
512
513
|
options:
|
|
513
514
|
- text: "Yes"
|
|
514
515
|
qualifies: true
|
|
515
516
|
- text: "No"
|
|
516
517
|
- text: How often do you shop online?
|
|
517
518
|
multi_select: true
|
|
519
|
+
screener: true
|
|
518
520
|
options:
|
|
519
521
|
- text: Never
|
|
520
522
|
- text: Monthly
|
|
@@ -552,12 +554,13 @@ def test_validate_screening_question_needs_two_options(runner, tmp_path):
|
|
|
552
554
|
assert "'options' must list at least 2 options" in result.output
|
|
553
555
|
|
|
554
556
|
|
|
555
|
-
def
|
|
557
|
+
def test_validate_screener_needs_a_qualifying_option(runner, tmp_path):
|
|
556
558
|
yaml_file = tmp_path / "usertest.yaml"
|
|
557
559
|
yaml_file.write_text(
|
|
558
560
|
VALID_USERTEST_YAML
|
|
559
561
|
+ "screening_questions:\n"
|
|
560
562
|
+ " - text: Nobody can pass this one\n"
|
|
563
|
+
+ " screener: true\n"
|
|
561
564
|
+ " options:\n"
|
|
562
565
|
+ " - text: A\n"
|
|
563
566
|
+ " - text: B\n"
|
|
@@ -569,6 +572,61 @@ def test_validate_screening_question_needs_a_qualifying_option(runner, tmp_path)
|
|
|
569
572
|
assert "needs at least one option with 'qualifies: true'" in result.output
|
|
570
573
|
|
|
571
574
|
|
|
575
|
+
def test_validate_plain_closed_question_needs_no_qualifying_option(runner, tmp_path):
|
|
576
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
577
|
+
yaml_file.write_text(
|
|
578
|
+
VALID_USERTEST_YAML
|
|
579
|
+
+ "screening_questions:\n"
|
|
580
|
+
+ " - text: Which device do you use?\n"
|
|
581
|
+
+ " options:\n"
|
|
582
|
+
+ " - text: Phone\n"
|
|
583
|
+
+ " - text: Laptop\n"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
587
|
+
|
|
588
|
+
assert result.exit_code == 0
|
|
589
|
+
assert "Valid user test config" in result.output
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def test_validate_rejects_mixed_qualifies_without_screener_flag(runner, tmp_path):
|
|
593
|
+
# Non-qualifying options are screener intent; without the flag they would
|
|
594
|
+
# silently stop screening anyone out.
|
|
595
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
596
|
+
yaml_file.write_text(
|
|
597
|
+
VALID_USERTEST_YAML
|
|
598
|
+
+ "screening_questions:\n"
|
|
599
|
+
+ " - text: Do you shop online?\n"
|
|
600
|
+
+ " options:\n"
|
|
601
|
+
+ " - text: \"Yes\"\n"
|
|
602
|
+
+ " qualifies: true\n"
|
|
603
|
+
+ " - text: \"No\"\n"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
607
|
+
|
|
608
|
+
assert result.exit_code == 1
|
|
609
|
+
assert "has non-qualifying options but no 'screener: true'" in result.output
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def test_validate_screening_screener_must_be_bool(runner, tmp_path):
|
|
613
|
+
yaml_file = tmp_path / "usertest.yaml"
|
|
614
|
+
yaml_file.write_text(
|
|
615
|
+
VALID_USERTEST_YAML
|
|
616
|
+
+ "screening_questions:\n"
|
|
617
|
+
+ " - text: Q\n"
|
|
618
|
+
+ " screener: yep\n"
|
|
619
|
+
+ " options:\n"
|
|
620
|
+
+ " - text: A\n"
|
|
621
|
+
+ " - text: B\n"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
|
|
625
|
+
|
|
626
|
+
assert result.exit_code == 1
|
|
627
|
+
assert "'screener' must be true or false" in result.output
|
|
628
|
+
|
|
629
|
+
|
|
572
630
|
def test_validate_screening_multi_select_must_be_bool(runner, tmp_path):
|
|
573
631
|
yaml_file = tmp_path / "usertest.yaml"
|
|
574
632
|
yaml_file.write_text(
|
|
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
|
|
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
|