From e29f25b0114b1f8a18b7114065f099043719ab3c Mon Sep 17 00:00:00 2001 From: Sergio Alexander Florez Galeano Date: Mon, 6 Jul 2026 05:02:15 +0000 Subject: [PATCH] fix(forms,checkin): require at least one question at create (questions_required) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The API now rejects `POST /v1/forms/` and `POST /v1/checkins/` without a non-empty `questions` array (`400 questions_required`). Align the CLI: require at least one question client-side on create, and map the new error codes. ## Change Log - authoring_helpers.require_questions(): fail fast on create with no questions (seed with --questions-file or --interactive) - form create + checkin create call it after building questions - error mapping: questions_required, invalid_question_data - docstrings/help + API_REFERENCE updated (questions are required at create; drop the "may have zero questions" note) - tests: +2 rejection tests; existing create tests seed a minimal questions file ## Verification Live-tested against a local org: `form create -n X` / `checkin create -n X --team Y` without questions now fail fast with a clear message; create with --questions-file succeeds. Full gate green (694 tests). ## Risks - Behavior change: creating a form/check-in now requires >= 1 question. Intentional — the server enforces it; the CLI just fails earlier with guidance. Co-Authored-By: Claude Opus 4.8 --- dailybot_cli/commands/authoring_helpers.py | 13 +++++ dailybot_cli/commands/checkin.py | 14 +++--- dailybot_cli/commands/form.py | 19 +++---- dailybot_cli/commands/public_api_helpers.py | 4 ++ docs/API_REFERENCE.md | 4 +- tests/authoring_lifecycle_test.py | 18 +++++-- tests/authoring_security_test.py | 11 +++- tests/checkin_authoring_test.py | 45 +++++++++++++++-- tests/form_authoring_test.py | 56 ++++++++++++++++++--- 9 files changed, 151 insertions(+), 33 deletions(-) diff --git a/dailybot_cli/commands/authoring_helpers.py b/dailybot_cli/commands/authoring_helpers.py index 2af791c..f9aa729 100644 --- a/dailybot_cli/commands/authoring_helpers.py +++ b/dailybot_cli/commands/authoring_helpers.py @@ -159,6 +159,19 @@ def question_extras_options(func: Any) -> Any: return func +def require_questions(questions: list[dict[str, Any]] | None, resource: str) -> None: + """Fail fast when a create call has no questions. + + The server requires at least one question at create time (``questions_required``); + ``resource`` is ``"form"`` or ``"check-in"`` for the message. + """ + if not questions: + raise AuthoringError( + f"A {resource} must have at least one question. Seed questions with " + "--questions-file or --interactive." + ) + + def check_report_channels(report_channels: tuple[str, ...]) -> None: """Fail fast when more than ``MAX_REPORT_CHANNELS`` channels are attached. diff --git a/dailybot_cli/commands/checkin.py b/dailybot_cli/commands/checkin.py index 3078081..995db0b 100644 --- a/dailybot_cli/commands/checkin.py +++ b/dailybot_cli/commands/checkin.py @@ -20,6 +20,7 @@ parse_schedule, prompt_participants_interactively, question_extras_options, + require_questions, require_short_question, require_short_questions, resolve_question_extras, @@ -437,10 +438,11 @@ def checkin_create( """Create a check-in with a schedule, participants, questions, and config. \b - Creating check-ins is role-gated server-side (admins/managers). Seed questions - with --questions-file or --interactive, or add them later with - `dailybot checkin questions add`. The scheduling/behavior flags (frequency, - reminders, timezone mode, submission rules, privacy) mirror the web UI. + Creating check-ins is role-gated server-side (admins/managers). A check-in must + have at least one question at create time — seed them with --questions-file or + --interactive (add/edit/remove more later with `dailybot checkin questions`). The + scheduling/behavior flags (frequency, reminders, timezone mode, submission rules, + privacy) mirror the web UI. \b Examples: @@ -475,8 +477,8 @@ def checkin_create( questions = parse_questions_file(questions_file) else: questions = None - if questions: - require_short_questions(questions, ai_short_question) + require_questions(questions, "check-in") + require_short_questions(questions or [], ai_short_question) try: with console.status("Creating check-in..."): result: dict[str, Any] = client.create_checkin( diff --git a/dailybot_cli/commands/form.py b/dailybot_cli/commands/form.py index 2390066..a179a5a 100644 --- a/dailybot_cli/commands/form.py +++ b/dailybot_cli/commands/form.py @@ -14,6 +14,7 @@ parse_options, parse_questions_file, question_extras_options, + require_questions, require_short_question, require_short_questions, resolve_form_config, @@ -550,15 +551,15 @@ def form_create( json_mode: bool, **config_flags: Any, ) -> None: - """Create a form (optionally seeded with questions and full config). + """Create a form (seeded with questions and full config). \b - Creating forms is role-gated server-side (admins/managers as applicable). - Seed questions with --questions-file or --interactive, or add them later via - `dailybot form questions add`. Each seeded question needs a report title - (--short-question / "short_question") unless you pass --ai-short-question. The - config flags (workflow states, permissions, anonymous/public/approval, command) - mirror the web Setup tab. + Creating forms is role-gated server-side (admins/managers as applicable). A form + must have at least one question at create time — seed them with --questions-file + or --interactive (add/edit/remove more later via `dailybot form questions`). Each + seeded question needs a report title (--short-question / "short_question") unless + you pass --ai-short-question. The config flags (workflow states, permissions, + anonymous/public/approval, command) mirror the web Setup tab. \b Examples: @@ -575,8 +576,8 @@ def form_create( questions = parse_questions_file(questions_file) else: questions = None - if questions: - require_short_questions(questions, ai_short_question) + require_questions(questions, "form") + require_short_questions(questions or [], ai_short_question) config: dict[str, Any] = resolve_form_config(client, **config_flags) try: with console.status("Creating form..."): diff --git a/dailybot_cli/commands/public_api_helpers.py b/dailybot_cli/commands/public_api_helpers.py index a542575..952d360 100644 --- a/dailybot_cli/commands/public_api_helpers.py +++ b/dailybot_cli/commands/public_api_helpers.py @@ -80,6 +80,10 @@ "--ai-short-question to let Dailybot generate it." ), "questions_limit_exceeded": "Too many questions (the limit is 50).", + "questions_required": ( + "At least one question is required. Seed questions with --questions-file or --interactive." + ), + "invalid_question_data": "A question payload is malformed. Check the question fields.", "form_name_required": "A name is required.", "invalid_schedule_days": ("Schedule days must be integers 0-6 (0=Sunday .. 6=Saturday)."), "invalid_schedule_time": "Schedule time must be in HH:MM format (e.g. 09:00).", diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 5f9f5fc..eba6001 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -212,7 +212,7 @@ Creating and configuring forms/check-ins (as opposed to filling them in). All au | `form edit [--name] [--report-channel]` | `PATCH /v1/forms//config/` | Thin subset of `form config`. | | `form config [--name] [--report-channel] [config flags]` | `PATCH /v1/forms//config/` | Full form config (partial). Flags below. | -**Form config flags** (on `create` + `config`; only the ones you pass change): `--active/--inactive`, `--anonymous/--no-anonymous`, `--public/--no-public`, `--brand/--no-brand`, `--require-identity/--no-require-identity`, `--reopen-from-final/--no-reopen-from-final`, `--state "Label:#color"` (repeatable, ordered — enables the workflow) / `--no-workflow`, `--can-edit`/`--can-see`/`--can-change-states` (`everyone`/`owner_and_admins`, or `restricted` via `--{scope}-user`/`--{scope}-team`), `--approval/--no-approval` + `--approver-user`/`--approver-team`, `--command NAME`/`--no-command`. Sent inline; the server rejects unknown fields with `400 unknown_field` and validates each (`workflow_requires_states`, `invalid_workflow_state`, `invalid_permission_audience`, `invalid_approvers`, `invalid_command`, `command_already_exists`). Detail echoes them back. Forms may have zero questions (by design). Unlike check-ins, form `is_anonymous` is freely toggleable (no `anonymous_irreversible`). +**Form config flags** (on `create` + `config`; only the ones you pass change): `--active/--inactive`, `--anonymous/--no-anonymous`, `--public/--no-public`, `--brand/--no-brand`, `--require-identity/--no-require-identity`, `--reopen-from-final/--no-reopen-from-final`, `--state "Label:#color"` (repeatable, ordered — enables the workflow) / `--no-workflow`, `--can-edit`/`--can-see`/`--can-change-states` (`everyone`/`owner_and_admins`, or `restricted` via `--{scope}-user`/`--{scope}-team`), `--approval/--no-approval` + `--approver-user`/`--approver-team`, `--command NAME`/`--no-command`. Sent inline; the server rejects unknown fields with `400 unknown_field` and validates each (`workflow_requires_states`, `invalid_workflow_state`, `invalid_permission_audience`, `invalid_approvers`, `invalid_command`, `command_already_exists`). Detail echoes them back. A form must have **at least one question** at create time (`questions_required`) — seed with `--questions-file`/`--interactive`. Unlike check-ins, form `is_anonymous` is freely toggleable (no `anonymous_irreversible`). | `form archive ` | `DELETE /v1/forms//archive/` | Soft-delete (204). Confirms unless `--yes`. | | `form questions list ` | `GET /v1/forms//` | Canonical question shape (see below). | | `form questions add --type --question [--options] [--required/--optional] [--blocker] [extras]` | `POST /v1/forms//questions/` | `multiple_choice` requires `--options`; `boolean` takes none. `--blocker` tags the blocker question. Extras below. | @@ -220,7 +220,7 @@ Creating and configuring forms/check-ins (as opposed to filling them in). All au **Report title is required by the CLI.** When authoring a question (`questions add`, or seeding via `create --questions-file`/`--interactive`), a `--short-question` / `"short_question"` is **mandatory** — pass `--ai-short-question` to opt into Dailybot's AI generating it instead (AI titling only runs on intelligence-enabled check-ins). Server-side AI titling is a frontend-oriented convenience; agents/scripts should author explicit report titles. (`questions edit` doesn't require it — partial update.) -**Per-question extras** (on `questions add` + `edit`, both forms and check-ins): `--short-question` (title shown in web & chat reports, ≤512 chars), `--variation` (alternate phrasing rotated per run; repeatable, ≤10), and question logic via `--logic-file ` (a JSON `{"rules": {"rules_if": [...], "rules_else": {...}}}` object) or the inline single-jump shortcut `--jump-if-equals VALUE --jump-to N [--else-jump-to M]` (`N`/`M` = target question index, `-1` = end). A **`rules_else` fallback is required** and jump targets are **forward-only** (must be greater than the question's own index, or `-1`); the server owns the question index and rejects backward/out-of-range jumps. Navigation keys (`question_key`/`next_key`/`prev_key`) are computed server-side — never send them. Logic operators (server enforces per type): text — `is_equal_to`, `is_not_equal_to`, `contains`, `not_contains`, `begins_with`, `not_begins_with`, `ends_with`, `not_ends_with`; numeric — `is_equal_to`, `is_not_equal_to`, `lower_than`, `lower_or_equal_than`, `greater_than`, `greater_or_equal_than`; `multiple_choice`/`boolean` — `is_equal_to`, `is_not_equal_to`. Boolean comparison values are JSON `true`/`false` (the CLI coerces `--jump-if-equals true/false`). Actions: `jump_to` (int target), `trigger_checkin` / `trigger_form` (UUID target); connectors: `and`, `or` (`multiple_choice`: `or` only). Empty question text is rejected server-side (`question_label_required`); a check-in/form may exist with zero questions (by design). Question types remain `text`, `multiple_choice`, `boolean`, `numeric` (the complete catalog). +**Per-question extras** (on `questions add` + `edit`, both forms and check-ins): `--short-question` (title shown in web & chat reports, ≤512 chars), `--variation` (alternate phrasing rotated per run; repeatable, ≤10), and question logic via `--logic-file ` (a JSON `{"rules": {"rules_if": [...], "rules_else": {...}}}` object) or the inline single-jump shortcut `--jump-if-equals VALUE --jump-to N [--else-jump-to M]` (`N`/`M` = target question index, `-1` = end). A **`rules_else` fallback is required** and jump targets are **forward-only** (must be greater than the question's own index, or `-1`); the server owns the question index and rejects backward/out-of-range jumps. Navigation keys (`question_key`/`next_key`/`prev_key`) are computed server-side — never send them. Logic operators (server enforces per type): text — `is_equal_to`, `is_not_equal_to`, `contains`, `not_contains`, `begins_with`, `not_begins_with`, `ends_with`, `not_ends_with`; numeric — `is_equal_to`, `is_not_equal_to`, `lower_than`, `lower_or_equal_than`, `greater_than`, `greater_or_equal_than`; `multiple_choice`/`boolean` — `is_equal_to`, `is_not_equal_to`. Boolean comparison values are JSON `true`/`false` (the CLI coerces `--jump-if-equals true/false`). Actions: `jump_to` (int target), `trigger_checkin` / `trigger_form` (UUID target); connectors: `and`, `or` (`multiple_choice`: `or` only). Empty question text is rejected server-side (`question_label_required`); both check-ins and forms **require at least one question at create time** (`questions_required`) — the CLI fails fast if you create without `--questions-file`/`--interactive`. Question types remain `text`, `multiple_choice`, `boolean`, `numeric` (the complete catalog). | `form questions delete ` | `DELETE /v1/forms//questions//delete/` | Confirms unless `--yes`. | | `form questions reorder ...` | `PUT /v1/forms//questions/reorder/` | Unknown UUID → 400. | | `checkin create -n NAME [--time --days --timezone \| --schedule-file] [--user --team] [--questions-file \| --interactive] [--report-channel]` | `POST /v1/checkins/create/` | `days` = ISO weekday ints (0=Sun…6=Sat). **Requires ≥1 participant** — with no `--user`/`--team` an interactive run prompts (default team suggested); non-interactive errors. | diff --git a/tests/authoring_lifecycle_test.py b/tests/authoring_lifecycle_test.py index 15f91e9..f429613 100644 --- a/tests/authoring_lifecycle_test.py +++ b/tests/authoring_lifecycle_test.py @@ -91,7 +91,11 @@ def test_full_form_authoring_flow(self, runner: CliRunner, tmp_path: Any) -> Non class TestCheckinLifecycle: - def test_full_checkin_authoring_flow(self, runner: CliRunner) -> None: + def test_full_checkin_authoring_flow(self, runner: CliRunner, tmp_path: Any) -> None: + qfile = tmp_path / "q.json" + qfile.write_text( + json.dumps([{"question_type": "text", "question": "Q?", "short_question": "Q"}]) + ) checkin: dict[str, Any] = {"id": "fu-1", "name": "Standup", "questions": []} with _auth(), _client() as cls: client: MagicMock = cls.return_value @@ -117,6 +121,8 @@ def test_full_checkin_authoring_flow(self, runner: CliRunner) -> None: "1,2,3", "--team", "Eng", + "--questions-file", + str(qfile), ], [ "checkin", @@ -202,10 +208,16 @@ def test_invalid_question_type_400ish(self, runner: CliRunner) -> None: class TestJsonMode: - def test_create_form_json(self, runner: CliRunner) -> None: + def test_create_form_json(self, runner: CliRunner, tmp_path: Any) -> None: + qfile = tmp_path / "q.json" + qfile.write_text( + json.dumps([{"question_type": "text", "question": "Q?", "short_question": "Q"}]) + ) with _auth(), _client() as cls: cls.return_value.create_form.return_value = {"id": "f-1", "name": "Retro"} - result = runner.invoke(cli, ["form", "create", "-n", "Retro", "--json"]) + result = runner.invoke( + cli, ["form", "create", "-n", "Retro", "--json", "--questions-file", str(qfile)] + ) assert result.exit_code == 0 assert json.loads(result.output)["id"] == "f-1" diff --git a/tests/authoring_security_test.py b/tests/authoring_security_test.py index 331dec8..d07a9b9 100644 --- a/tests/authoring_security_test.py +++ b/tests/authoring_security_test.py @@ -1,6 +1,7 @@ """Security-focused tests for authoring commands: error mapping, confirmations, no-secret-leakage, and client-side validation running before any network call.""" +import json from typing import Any from unittest.mock import MagicMock, patch @@ -124,14 +125,20 @@ def test_form_question_delete_prompts(self, runner: CliRunner) -> None: class TestNoSecretLeakage: - def test_token_not_in_output(self, runner: CliRunner) -> None: + def test_token_not_in_output(self, runner: CliRunner, tmp_path: Any) -> None: secret: str = "super-secret-bearer-token-value" + qfile = tmp_path / "q.json" + qfile.write_text( + json.dumps([{"question_type": "text", "question": "Q?", "short_question": "Q"}]) + ) with _auth(), _client() as cls: client: MagicMock = cls.return_value client.token = secret client.api_key = "super-secret-api-key" client.create_form.return_value = {"id": "f-1", "name": "Retro", "questions": []} - result = runner.invoke(cli, ["form", "create", "-n", "Retro"]) + result = runner.invoke( + cli, ["form", "create", "-n", "Retro", "--questions-file", str(qfile)] + ) assert result.exit_code == 0 assert secret not in result.output assert "super-secret-api-key" not in result.output diff --git a/tests/checkin_authoring_test.py b/tests/checkin_authoring_test.py index 77df3df..d9e58f1 100644 --- a/tests/checkin_authoring_test.py +++ b/tests/checkin_authoring_test.py @@ -1,5 +1,7 @@ """Tests for check-in authoring commands (create/config/archive/questions).""" +import json +from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -22,6 +24,16 @@ def runner() -> CliRunner: return CliRunner() +@pytest.fixture +def qfile(tmp_path: Path) -> str: + """A minimal one-question file — questions are required at create time.""" + path = tmp_path / "q.json" + path.write_text( + json.dumps([{"question_type": "text", "question": "Q?", "short_question": "Q"}]) + ) + return str(path) + + def _auth() -> Any: return patch("dailybot_cli.commands.public_api_helpers.get_agent_auth", return_value="tok") @@ -31,7 +43,7 @@ def _client() -> Any: class TestCheckinCreate: - def test_create_with_schedule(self, runner: CliRunner) -> None: + def test_create_with_schedule(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: client: MagicMock = cls.return_value client.create_checkin.return_value = CHECKIN_PAYLOAD @@ -51,13 +63,15 @@ def test_create_with_schedule(self, runner: CliRunner) -> None: "UTC", "--team", "Eng", + "--questions-file", + qfile, ], ) assert result.exit_code == 0 schedule = client.create_checkin.call_args[1]["schedule"] assert schedule == {"days": [1, 2, 3, 4, 5], "time": "09:00", "timezone": "UTC"} - def test_create_forwards_config_flags(self, runner: CliRunner) -> None: + def test_create_forwards_config_flags(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: client: MagicMock = cls.return_value client.create_checkin.return_value = CHECKIN_PAYLOAD @@ -76,6 +90,8 @@ def test_create_forwards_config_flags(self, runner: CliRunner) -> None: "--reminders", "2", "--no-future", + "--questions-file", + qfile, ], ) assert result.exit_code == 0 @@ -86,6 +102,16 @@ def test_create_forwards_config_flags(self, runner: CliRunner) -> None: "allow_future_responses": False, } + def test_create_without_questions_is_rejected(self, runner: CliRunner) -> None: + # Questions are required at create time (server: questions_required). + with _auth(), _client() as cls: + client: MagicMock = cls.return_value + client.list_teams.return_value = [{"uuid": "t-1", "name": "Eng"}] + result = runner.invoke(cli, ["checkin", "create", "-n", "Standup", "--team", "Eng"]) + client.create_checkin.assert_not_called() + assert result.exit_code != 0 + assert "at least one question" in result.output + def test_create_without_participants_is_rejected(self, runner: CliRunner) -> None: # Non-interactive create with no --user/--team must error, never create empty. with _auth(), _client() as cls: @@ -97,7 +123,7 @@ def test_create_without_participants_is_rejected(self, runner: CliRunner) -> Non assert result.exit_code != 0 assert "at least one participant" in result.output - def test_create_resolves_participants(self, runner: CliRunner) -> None: + def test_create_resolves_participants(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: client: MagicMock = cls.return_value client.create_checkin.return_value = CHECKIN_PAYLOAD @@ -105,7 +131,18 @@ def test_create_resolves_participants(self, runner: CliRunner) -> None: client.list_teams.return_value = [{"uuid": "t-1", "name": "Eng"}] result = runner.invoke( cli, - ["checkin", "create", "-n", "Standup", "--user", "Jane Doe", "--team", "Eng"], + [ + "checkin", + "create", + "-n", + "Standup", + "--user", + "Jane Doe", + "--team", + "Eng", + "--questions-file", + qfile, + ], ) assert result.exit_code == 0 participants = client.create_checkin.call_args[1]["participants"] diff --git a/tests/form_authoring_test.py b/tests/form_authoring_test.py index 9d1a5ec..f22e1f6 100644 --- a/tests/form_authoring_test.py +++ b/tests/form_authoring_test.py @@ -24,6 +24,16 @@ def runner() -> CliRunner: return CliRunner() +@pytest.fixture +def qfile(tmp_path: Any) -> str: + """A minimal one-question file — questions are required at create time.""" + path = tmp_path / "q.json" + path.write_text( + json.dumps([{"question_type": "text", "question": "Q?", "short_question": "Q"}]) + ) + return str(path) + + def _auth() -> Any: return patch("dailybot_cli.commands.public_api_helpers.get_agent_auth", return_value="tok") @@ -33,13 +43,23 @@ def _client() -> Any: class TestFormCreate: - def test_create_minimal(self, runner: CliRunner) -> None: + def test_create_minimal(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: cls.return_value.create_form.return_value = FORM_PAYLOAD - result = runner.invoke(cli, ["form", "create", "--name", "Retro"]) + result = runner.invoke( + cli, ["form", "create", "--name", "Retro", "--questions-file", qfile] + ) assert result.exit_code == 0 assert "Retro" in result.output + def test_create_without_questions_is_rejected(self, runner: CliRunner) -> None: + # Questions are required at create time (server: questions_required). + with _auth(), _client() as cls: + result = runner.invoke(cli, ["form", "create", "--name", "Retro"]) + cls.return_value.create_form.assert_not_called() + assert result.exit_code != 0 + assert "at least one question" in result.output + def test_create_with_questions_file(self, runner: CliRunner, tmp_path: Any) -> None: qfile = tmp_path / "q.json" qfile.write_text( @@ -118,19 +138,29 @@ def test_short_question_required_error_is_friendly(self, runner: CliRunner) -> N assert result.exit_code != 0 assert "--ai-short-question" in result.output - def test_create_with_report_channel_inline(self, runner: CliRunner) -> None: + def test_create_with_report_channel_inline(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: client: MagicMock = cls.return_value client.create_form.return_value = FORM_PAYLOAD result = runner.invoke( - cli, ["form", "create", "-n", "Retro", "--report-channel", "chan-1"] + cli, + [ + "form", + "create", + "-n", + "Retro", + "--report-channel", + "chan-1", + "--questions-file", + qfile, + ], ) assert result.exit_code == 0 # Report channels go directly in the create body — no follow-up config call. assert client.create_form.call_args[1]["report_channels"] == ["chan-1"] client.update_form_config.assert_not_called() - def test_create_bogus_channel_shows_friendly_error(self, runner: CliRunner) -> None: + def test_create_bogus_channel_shows_friendly_error(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: client: MagicMock = cls.return_value client.create_form.side_effect = APIError( @@ -139,7 +169,17 @@ def test_create_bogus_channel_shows_friendly_error(self, runner: CliRunner) -> N code="report_channel_not_found", ) result = runner.invoke( - cli, ["form", "create", "-n", "Retro", "--report-channel", "NOT_REAL"] + cli, + [ + "form", + "create", + "-n", + "Retro", + "--report-channel", + "NOT_REAL", + "--questions-file", + qfile, + ], ) assert result.exit_code != 0 assert "dailybot channels list" in result.output @@ -175,7 +215,7 @@ def test_edit_requires_a_field(self, runner: CliRunner) -> None: class TestFormConfig: - def test_create_forwards_full_config(self, runner: CliRunner) -> None: + def test_create_forwards_full_config(self, runner: CliRunner, qfile: str) -> None: with _auth(), _client() as cls: client: MagicMock = cls.return_value client.create_form.return_value = FORM_PAYLOAD @@ -187,6 +227,8 @@ def test_create_forwards_full_config(self, runner: CliRunner) -> None: "create", "-n", "Release", + "--questions-file", + qfile, "--state", "Draft:#cccccc", "--state",