minitest-cli 0.25.2__tar.gz → 0.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.
Files changed (165) hide show
  1. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/PKG-INFO +1 -1
  2. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/pyproject.toml +1 -1
  3. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_create.py +16 -1
  4. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_user_story_commands.py +106 -0
  5. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/.env.example +0 -0
  6. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/.github/workflows/ci.yml +0 -0
  7. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/.github/workflows/install-scripts.yml +0 -0
  8. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/.github/workflows/release.yml +0 -0
  9. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/.gitignore +0 -0
  10. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/.opencode/skill/release/SKILL.md +0 -0
  11. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/AGENTS.md +0 -0
  12. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/README.md +0 -0
  13. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/RELEASE.md +0 -0
  14. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/install.ps1 +0 -0
  15. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/install.sh +0 -0
  16. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/pyrightconfig.json +0 -0
  17. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/renovate.json +0 -0
  18. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/__init__.py +0 -0
  19. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/api/__init__.py +0 -0
  20. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/api/apps_manager_client.py +0 -0
  21. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/api/client.py +0 -0
  22. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/api/errors.py +0 -0
  23. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/assets/__init__.py +0 -0
  24. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/assets/callback.html +0 -0
  25. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/__init__.py +0 -0
  26. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/_response_errors.py +0 -0
  27. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/app_knowledge.py +0 -0
  28. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/app_knowledge_helpers.py +0 -0
  29. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/apps.py +0 -0
  30. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/apps_dependencies.py +0 -0
  31. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/apps_helpers.py +0 -0
  32. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/auth.py +0 -0
  33. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/auth_api_key.py +0 -0
  34. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/batch.py +0 -0
  35. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/batch_helpers.py +0 -0
  36. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/build.py +0 -0
  37. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/build_commit.py +0 -0
  38. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/build_fix_prompts.py +0 -0
  39. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/build_helpers.py +0 -0
  40. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/capabilities.py +0 -0
  41. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/commit_helpers.py +0 -0
  42. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/draft_feature.py +0 -0
  43. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/draft_feature_apply.py +0 -0
  44. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/draft_feature_conflicts.py +0 -0
  45. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/draft_feature_helpers.py +0 -0
  46. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/draft_feature_show.py +0 -0
  47. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/env.py +0 -0
  48. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/env_helpers.py +0 -0
  49. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/flow_types.py +0 -0
  50. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/flow_types_helpers.py +0 -0
  51. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/flow_types_write.py +0 -0
  52. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/init.py +0 -0
  53. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/init_helpers.py +0 -0
  54. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/init_playbook.py +0 -0
  55. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/issues.py +0 -0
  56. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/issues_fix.py +0 -0
  57. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/issues_helpers.py +0 -0
  58. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/issues_projection.py +0 -0
  59. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/issues_service.py +0 -0
  60. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/maintenance.py +0 -0
  61. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/maintenance_callbacks.py +0 -0
  62. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/maintenance_helpers.py +0 -0
  63. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/maintenance_state.py +0 -0
  64. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run.py +0 -0
  65. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run_commit.py +0 -0
  66. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run_commit_helpers.py +0 -0
  67. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run_display.py +0 -0
  68. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run_feedback.py +0 -0
  69. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run_helpers.py +0 -0
  70. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/run_targets.py +0 -0
  71. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/screens.py +0 -0
  72. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/screens_detail.py +0 -0
  73. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/screens_format.py +0 -0
  74. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/screens_helpers.py +0 -0
  75. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/screens_tree.py +0 -0
  76. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/skill.py +0 -0
  77. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_file.py +0 -0
  78. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_file_helpers.py +0 -0
  79. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_file_list.py +0 -0
  80. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_profile.py +0 -0
  81. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_profile_default.py +0 -0
  82. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_profile_helpers.py +0 -0
  83. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/test_profile_list.py +0 -0
  84. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/upgrade.py +0 -0
  85. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story.py +0 -0
  86. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_bindings.py +0 -0
  87. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_camera.py +0 -0
  88. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_criteria.py +0 -0
  89. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_criterion_edits.py +0 -0
  90. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_delete.py +0 -0
  91. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_device_count.py +0 -0
  92. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_helpers.py +0 -0
  93. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_modify.py +0 -0
  94. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_overrides.py +0 -0
  95. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_profiles.py +0 -0
  96. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/user_story_update.py +0 -0
  97. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/verdicts.py +0 -0
  98. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/verdicts_helpers.py +0 -0
  99. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/commands/verdicts_projection.py +0 -0
  100. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/__init__.py +0 -0
  101. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/app_context.py +0 -0
  102. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/auth.py +0 -0
  103. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/config.py +0 -0
  104. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/credentials.py +0 -0
  105. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/oauth.py +0 -0
  106. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/tenants.py +0 -0
  107. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/core/token_exchange.py +0 -0
  108. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/main.py +0 -0
  109. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/__init__.py +0 -0
  110. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/app.py +0 -0
  111. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/app_env_vars.py +0 -0
  112. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/app_failure.py +0 -0
  113. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/app_screen.py +0 -0
  114. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/base.py +0 -0
  115. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/batch.py +0 -0
  116. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/build.py +0 -0
  117. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/commit_build.py +0 -0
  118. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/draft_feature.py +0 -0
  119. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/flow_type.py +0 -0
  120. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/issues.py +0 -0
  121. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/story_run.py +0 -0
  122. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/targets.py +0 -0
  123. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/models/user_story.py +0 -0
  124. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/__init__.py +0 -0
  125. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/confirm.py +0 -0
  126. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/dependency_sim.py +0 -0
  127. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/mermaid.py +0 -0
  128. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/output.py +0 -0
  129. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
  130. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/src/minitest_cli/utils/update_check.py +0 -0
  131. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/__init__.py +0 -0
  132. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/_commit_transport.py +0 -0
  133. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_app_knowledge_commands.py +0 -0
  134. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_apps_commands.py +0 -0
  135. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_apps_dependencies.py +0 -0
  136. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_auth.py +0 -0
  137. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_auth_api_key.py +0 -0
  138. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_auth_commands.py +0 -0
  139. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_batch_commands.py +0 -0
  140. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_build_commands.py +0 -0
  141. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_build_fix_prompts.py +0 -0
  142. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_capabilities_command.py +0 -0
  143. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_channel_header.py +0 -0
  144. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_code_quality.py +0 -0
  145. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_commit_commands.py +0 -0
  146. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_draft_feature_commands.py +0 -0
  147. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_env.py +0 -0
  148. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_flow_types_commands.py +0 -0
  149. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_init_command.py +0 -0
  150. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_issues.py +0 -0
  151. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_issues_commands.py +0 -0
  152. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_mermaid.py +0 -0
  153. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_network_errors.py +0 -0
  154. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_response_errors.py +0 -0
  155. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_run_commands.py +0 -0
  156. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_screens_commands.py +0 -0
  157. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_skill_command.py +0 -0
  158. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_test_file_commands.py +0 -0
  159. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_test_profile_commands.py +0 -0
  160. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_upgrade_command.py +0 -0
  161. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_user_story_bindings_commands.py +0 -0
  162. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_verdicts.py +0 -0
  163. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_verdicts_actionable.py +0 -0
  164. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/tests/test_version.py +0 -0
  165. {minitest_cli-0.25.2 → minitest_cli-0.26.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: minitest-cli
3
- Version: 0.25.2
3
+ Version: 0.26.0
4
4
  Summary: Minitest CLI – command-line interface for the Minitest testing platform
5
5
  Project-URL: Homepage, https://minitap.ai/
6
6
  Project-URL: Source, https://github.com/minitap-ai/minitest-cli
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "minitest-cli"
3
- version = "0.25.2"
3
+ version = "0.26.0"
4
4
  description = "Minitest CLI – command-line interface for the Minitest testing platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -61,8 +61,20 @@ def create_user_story(
61
61
  camera_media: Annotated[
62
62
  str | None, typer.Option("--camera-media", help=CAMERA_MEDIA_HELP)
63
63
  ] = None,
64
+ idempotency_key: Annotated[
65
+ str | None,
66
+ typer.Option(
67
+ "--idempotency-key",
68
+ help="Opaque 1-255 character key for idempotent creation retries.",
69
+ ),
70
+ ] = None,
64
71
  ) -> None:
65
72
  """Create a new user story."""
73
+ if idempotency_key is not None and not 1 <= len(idempotency_key) <= 255:
74
+ raise typer.BadParameter(
75
+ "must contain between 1 and 255 characters",
76
+ param_hint="--idempotency-key",
77
+ )
66
78
  settings = get_settings()
67
79
  json_mode = is_json_mode()
68
80
  require_auth(settings)
@@ -89,7 +101,10 @@ def create_user_story(
89
101
  payload["camera_media_file_id"] = await resolve_camera_media_file_id(
90
102
  client, app_id, camera_source
91
103
  )
92
- resp = await client.post(base_path(app_id), json=payload)
104
+ request_options: dict[str, Any] = {"json": payload}
105
+ if idempotency_key is not None:
106
+ request_options["headers"] = {"Idempotency-Key": idempotency_key}
107
+ resp = await client.post(base_path(app_id), **request_options)
93
108
  handle_response_error(resp)
94
109
  created = resp.json()
95
110
  if depends_on:
@@ -11,6 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
11
11
  import httpx
12
12
  import pytest
13
13
  import typer
14
+ from click import unstyle
14
15
  from typer.testing import CliRunner
15
16
 
16
17
  from minitest_cli.commands.user_story import app as user_story_app
@@ -87,6 +88,17 @@ def _mock_response(status_code=200, json_data=None):
87
88
  return resp
88
89
 
89
90
 
91
+ @contextmanager
92
+ def _patch_http_transport(handler):
93
+ async_client = httpx.AsyncClient
94
+ transport = httpx.MockTransport(handler)
95
+ with patch(
96
+ "minitest_cli.api.client.httpx.AsyncClient",
97
+ side_effect=lambda **kwargs: async_client(**kwargs, transport=transport),
98
+ ):
99
+ yield
100
+
101
+
90
102
  SAMPLE_USER_STORY = {
91
103
  "id": "story-1",
92
104
  "name": "Login Story",
@@ -143,6 +155,100 @@ class TestCreateUserStory:
143
155
  )
144
156
  assert result.exit_code == 0
145
157
 
158
+ def test_absent_idempotency_key_preserves_legacy_request_and_human_output(self, tmp_path):
159
+ settings = _make_settings(tmp_path)
160
+ requests = []
161
+
162
+ def handler(request):
163
+ requests.append(request)
164
+ return httpx.Response(
165
+ 201,
166
+ json={"id": "legacy-story", "name": "Story", "type": "login"},
167
+ )
168
+
169
+ with _patch_flow_types(), _patch_http_transport(handler):
170
+ result = _run_with_context(["create", "--name", "Story", "--type", "login"], settings)
171
+
172
+ assert result.exit_code == 0
173
+ assert "Idempotency-Key" not in requests[0].headers
174
+ assert "id: legacy-story" in result.stdout
175
+ assert "User story created: legacy-story" in result.stderr
176
+
177
+ def test_idempotency_replay_and_conflict_follow_server_contract(self, tmp_path):
178
+ settings = _make_settings(tmp_path)
179
+ key = "Draft-Operation-AbC"
180
+ requests = []
181
+ stored_payload = None
182
+ created = {
183
+ "id": "stable-story-id",
184
+ "acceptanceCriteria": [
185
+ {
186
+ "id": "stable-criterion-version-id",
187
+ "criterionId": "stable-criterion-id",
188
+ "content": "The user can sign in.",
189
+ "createdAt": "2026-08-28T20:00:00Z",
190
+ }
191
+ ],
192
+ }
193
+
194
+ def handler(request):
195
+ nonlocal stored_payload
196
+ requests.append(request)
197
+ payload = json.loads(request.content)
198
+ if stored_payload is None:
199
+ stored_payload = payload
200
+ return httpx.Response(201, json=created)
201
+ if payload == stored_payload:
202
+ return httpx.Response(201, json=created)
203
+ return httpx.Response(
204
+ 409,
205
+ json={"detail": "Idempotency key is already used for a different request."},
206
+ )
207
+
208
+ base_args = ["create", "--name", "Story", "--type", "login", "--idempotency-key", key]
209
+ with _patch_flow_types(), _patch_http_transport(handler):
210
+ first = _run_with_context(base_args, settings, json_mode=True)
211
+ replay = _run_with_context(base_args, settings, json_mode=True)
212
+ conflict = _run_with_context(
213
+ [
214
+ "create",
215
+ "--name",
216
+ "Different story",
217
+ "--type",
218
+ "login",
219
+ "--idempotency-key",
220
+ key,
221
+ ],
222
+ settings,
223
+ json_mode=True,
224
+ )
225
+
226
+ assert first.exit_code == replay.exit_code == 0
227
+ first_output = json.loads(first.stdout)
228
+ replay_output = json.loads(replay.stdout)
229
+ assert first_output == replay_output == created
230
+ assert replay_output["id"] == first_output["id"] == "stable-story-id"
231
+ assert replay_output["acceptanceCriteria"][0]["criterionId"] == "stable-criterion-id"
232
+ assert replay_output["acceptanceCriteria"][0]["id"] == "stable-criterion-version-id"
233
+ assert [request.headers["Idempotency-Key"] for request in requests] == [key, key, key]
234
+ assert conflict.exit_code == 3
235
+ assert conflict.stdout == ""
236
+ assert "already used for a different request" in conflict.stderr
237
+
238
+ @pytest.mark.parametrize("key", ["", "x" * 256])
239
+ def test_invalid_idempotency_key_is_rejected_locally(self, tmp_path, key):
240
+ settings = _make_settings(tmp_path)
241
+ with _patch_flow_types():
242
+ result = _run_with_context(
243
+ ["create", "--name", "Story", "--type", "login", "--idempotency-key", key],
244
+ settings,
245
+ )
246
+
247
+ assert result.exit_code == 2
248
+ plain_output = unstyle(result.output)
249
+ assert "Invalid value for --idempotency-key" in plain_output
250
+ assert "between 1 and 255" in plain_output
251
+
146
252
 
147
253
  class TestCreateUserStoryProfiles:
148
254
  def test_repeated_profile_sends_test_profile_ids(self, tmp_path):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes