minitest-cli 0.6.0__tar.gz → 0.7.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 (73) hide show
  1. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/PKG-INFO +2 -2
  2. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/README.md +1 -1
  3. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/pyproject.toml +1 -1
  4. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/apps.py +1 -1
  5. minitest_cli-0.7.0/src/minitest_cli/commands/batch.py +169 -0
  6. minitest_cli-0.7.0/src/minitest_cli/commands/batch_helpers.py +38 -0
  7. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/build.py +2 -2
  8. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/maintenance_check.py +3 -3
  9. minitest_cli-0.7.0/src/minitest_cli/commands/run.py +199 -0
  10. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/run_display.py +13 -10
  11. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/run_helpers.py +37 -25
  12. minitest_cli-0.6.0/src/minitest_cli/commands/flow.py → minitest_cli-0.7.0/src/minitest_cli/commands/user_story.py +41 -38
  13. minitest_cli-0.6.0/src/minitest_cli/commands/flow_helpers.py → minitest_cli-0.7.0/src/minitest_cli/commands/user_story_helpers.py +56 -30
  14. minitest_cli-0.7.0/src/minitest_cli/commands/user_story_modify.py +151 -0
  15. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/main.py +13 -2
  16. minitest_cli-0.7.0/src/minitest_cli/models/__init__.py +55 -0
  17. minitest_cli-0.7.0/src/minitest_cli/models/story_run.py +169 -0
  18. minitest_cli-0.7.0/src/minitest_cli/models/user_story.py +71 -0
  19. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/output.py +15 -2
  20. minitest_cli-0.7.0/tests/test_batch_commands.py +320 -0
  21. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_run_commands.py +200 -85
  22. minitest_cli-0.7.0/tests/test_user_story_commands.py +406 -0
  23. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/uv.lock +1 -1
  24. minitest_cli-0.6.0/src/minitest_cli/commands/flow_modify.py +0 -105
  25. minitest_cli-0.6.0/src/minitest_cli/commands/run.py +0 -172
  26. minitest_cli-0.6.0/src/minitest_cli/models/__init__.py +0 -43
  27. minitest_cli-0.6.0/src/minitest_cli/models/flow_run.py +0 -78
  28. minitest_cli-0.6.0/src/minitest_cli/models/flow_template.py +0 -53
  29. minitest_cli-0.6.0/tests/test_flow_commands.py +0 -287
  30. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.env.example +0 -0
  31. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.github/workflows/ci.yml +0 -0
  32. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.github/workflows/install-scripts.yml +0 -0
  33. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.github/workflows/release.yml +0 -0
  34. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.gitignore +0 -0
  35. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.opencode/skill/release/SKILL.md +0 -0
  36. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/AGENTS.md +0 -0
  37. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/RELEASE.md +0 -0
  38. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/install.ps1 +0 -0
  39. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/install.sh +0 -0
  40. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/pyrightconfig.json +0 -0
  41. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/__init__.py +0 -0
  42. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/api/__init__.py +0 -0
  43. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/api/client.py +0 -0
  44. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/assets/__init__.py +0 -0
  45. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/assets/callback.html +0 -0
  46. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/__init__.py +0 -0
  47. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/auth.py +0 -0
  48. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/build_helpers.py +0 -0
  49. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/skill.py +0 -0
  50. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/upgrade.py +0 -0
  51. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/__init__.py +0 -0
  52. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/app_context.py +0 -0
  53. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/auth.py +0 -0
  54. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/config.py +0 -0
  55. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/credentials.py +0 -0
  56. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/oauth.py +0 -0
  57. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/token_exchange.py +0 -0
  58. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/app.py +0 -0
  59. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/base.py +0 -0
  60. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/build.py +0 -0
  61. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/maintenance_check.py +0 -0
  62. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/__init__.py +0 -0
  63. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
  64. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/update_check.py +0 -0
  65. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/__init__.py +0 -0
  66. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_apps_commands.py +0 -0
  67. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_auth.py +0 -0
  68. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_auth_commands.py +0 -0
  69. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_build_commands.py +0 -0
  70. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_code_quality.py +0 -0
  71. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_skill_command.py +0 -0
  72. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_upgrade_command.py +0 -0
  73. {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: minitest-cli
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -118,7 +118,7 @@ minitest run --app <app-id>
118
118
  | ---------------- | ------------------------- |
119
119
  | `minitest auth` | Authentication management |
120
120
  | `minitest apps` | App management |
121
- | `minitest flow` | Testing flow operations |
121
+ | `minitest user-story` | User-story operations |
122
122
  | `minitest build` | Build management |
123
123
  | `minitest run` | Test execution |
124
124
 
@@ -85,7 +85,7 @@ minitest run --app <app-id>
85
85
  | ---------------- | ------------------------- |
86
86
  | `minitest auth` | Authentication management |
87
87
  | `minitest apps` | App management |
88
- | `minitest flow` | Testing flow operations |
88
+ | `minitest user-story` | User-story operations |
89
89
  | `minitest build` | Build management |
90
90
  | `minitest run` | Test execution |
91
91
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "minitest-cli"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "Minitest CLI – command-line interface for the Minitest testing platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -61,7 +61,7 @@ def list_apps() -> None:
61
61
  raise typer.Exit(code=EXIT_NETWORK_ERROR) from exc
62
62
 
63
63
  if json_mode:
64
- print_json([a.model_dump(mode="json") for a in data.apps])
64
+ print_json([a.model_dump(mode="json", by_alias=True) for a in data.apps])
65
65
  return
66
66
 
67
67
  rows = [[a.id, a.name] for a in data.apps]
@@ -0,0 +1,169 @@
1
+ """Batch commands: list, get, cancel."""
2
+
3
+ import math
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from minitest_cli.api.client import ApiClient
9
+ from minitest_cli.commands.batch_helpers import batches_base_path
10
+ from minitest_cli.commands.run_helpers import (
11
+ ensure_uuid,
12
+ handle_response_error,
13
+ resolve_app,
14
+ run_api_call,
15
+ )
16
+ from minitest_cli.models.story_run import (
17
+ BatchListItem,
18
+ BatchListResponse,
19
+ BatchResponse,
20
+ )
21
+ from minitest_cli.utils.output import (
22
+ output,
23
+ print_info,
24
+ print_success,
25
+ print_table,
26
+ )
27
+
28
+ app = typer.Typer(name="batch", help="Manage batch runs (multi-story executions).")
29
+
30
+ BATCH_TABLE_HEADERS = ["ID", "Status", "Source", "Commit", "Tag", "Story runs", "Created"]
31
+
32
+
33
+ def _format_batch_row(item: BatchListItem) -> list[str]:
34
+ return [
35
+ item.id,
36
+ item.status.value,
37
+ item.source,
38
+ (item.commit_sha or "")[:10],
39
+ item.tag_name or "",
40
+ str(len(item.story_runs)),
41
+ item.created_at.strftime("%Y-%m-%d %H:%M"),
42
+ ]
43
+
44
+
45
+ @app.command(name="list")
46
+ def list_batches(
47
+ page: Annotated[int, typer.Option(help="Page number.")] = 1,
48
+ page_size: Annotated[int, typer.Option(help="Items per page (max 100).")] = 20,
49
+ status_filter: Annotated[
50
+ list[str] | None,
51
+ typer.Option("--status", help="Filter by status (repeatable)."),
52
+ ] = None,
53
+ result_filter: Annotated[
54
+ list[str] | None,
55
+ typer.Option("--result", help="Filter by derived result (repeatable)."),
56
+ ] = None,
57
+ commit_sha: Annotated[str | None, typer.Option("--commit-sha")] = None,
58
+ user_story_id: Annotated[str | None, typer.Option("--user-story-id")] = None,
59
+ search: Annotated[str | None, typer.Option("--search")] = None,
60
+ all_pages: Annotated[bool, typer.Option("--all", help="Fetch all pages.")] = False,
61
+ ) -> None:
62
+ """List batches for the current app."""
63
+ settings, app_id, json_mode = resolve_app()
64
+ if all_pages:
65
+ page, page_size = 1, 100
66
+
67
+ params: dict[str, object] = {"page": page, "page_size": page_size}
68
+ if status_filter:
69
+ params["status"] = status_filter
70
+ if result_filter:
71
+ params["result"] = result_filter
72
+ if commit_sha:
73
+ params["commit_sha"] = commit_sha
74
+ if user_story_id:
75
+ params["user_story_id"] = user_story_id
76
+ if search:
77
+ params["search"] = search
78
+
79
+ async def _list() -> BatchListResponse:
80
+ async with ApiClient(settings) as client:
81
+ resp = await client.get(batches_base_path(app_id), params=params)
82
+ handle_response_error(resp, resource="Batches")
83
+ return BatchListResponse.model_validate(resp.json())
84
+
85
+ result = run_api_call(_list())
86
+
87
+ if json_mode:
88
+ output(result.model_dump(mode="json", by_alias=True), json_mode=True)
89
+ return
90
+
91
+ if not result.items:
92
+ print_info("No batches found.")
93
+ return
94
+
95
+ total_pages = math.ceil(result.total / result.page_size) if result.total else 1
96
+ start = (result.page - 1) * result.page_size + 1
97
+ end = min(result.page * result.page_size, result.total)
98
+ title = (
99
+ f"Batches — page {result.page} of {total_pages}, showing {start}–{end} of {result.total}"
100
+ )
101
+ rows = [_format_batch_row(item) for item in result.items]
102
+ print_table(BATCH_TABLE_HEADERS, rows, title=title)
103
+ if result.page < total_pages:
104
+ print_info(f"Use --page {result.page + 1} to see more, or --all to fetch everything.")
105
+
106
+
107
+ @app.command(name="get")
108
+ def get_batch(
109
+ batch_id: Annotated[str, typer.Argument(help="Batch ID.")],
110
+ ) -> None:
111
+ """Get a single batch with its story runs."""
112
+ settings, app_id, json_mode = resolve_app()
113
+ ensure_uuid(batch_id, kind="batch id")
114
+
115
+ async def _get() -> BatchResponse:
116
+ async with ApiClient(settings) as client:
117
+ resp = await client.get(f"{batches_base_path(app_id)}/{batch_id}")
118
+ handle_response_error(resp, resource="Batch")
119
+ return BatchResponse.model_validate(resp.json())
120
+
121
+ batch = run_api_call(_get())
122
+
123
+ if json_mode:
124
+ output(batch.model_dump(mode="json", by_alias=True), json_mode=True)
125
+ return
126
+
127
+ print_info(f"Batch {batch.id} — {batch.status.value} ({batch.source})")
128
+ if batch.commit_sha:
129
+ print_info(f" commit: {batch.commit_sha}")
130
+ if batch.tag_name:
131
+ print_info(f" tag: {batch.tag_name}")
132
+
133
+ rows: list[list[str]] = []
134
+ for r in batch.story_runs:
135
+ rows.append(
136
+ [
137
+ r.id,
138
+ r.user_story_name or r.user_story_id,
139
+ r.status.value,
140
+ r.created_at.strftime("%Y-%m-%d %H:%M"),
141
+ ]
142
+ )
143
+ if rows:
144
+ print_table(
145
+ ["Run ID", "User Story", "Status", "Created"],
146
+ rows,
147
+ title=f"Story runs ({len(rows)})",
148
+ )
149
+
150
+
151
+ @app.command()
152
+ def cancel(
153
+ batch_id: Annotated[str, typer.Argument(help="Batch ID to cancel.")],
154
+ ) -> None:
155
+ """Cancel a batch and all its pending/running story runs."""
156
+ settings, app_id, json_mode = resolve_app()
157
+ ensure_uuid(batch_id, kind="batch id")
158
+
159
+ async def _cancel() -> BatchResponse:
160
+ async with ApiClient(settings) as client:
161
+ resp = await client.post(f"{batches_base_path(app_id)}/{batch_id}/cancel")
162
+ handle_response_error(resp, resource="Batch")
163
+ return BatchResponse.model_validate(resp.json())
164
+
165
+ batch = run_api_call(_cancel())
166
+ if json_mode:
167
+ output(batch.model_dump(mode="json", by_alias=True), json_mode=True)
168
+ else:
169
+ print_success(f"Batch cancelled: {batch.id} (status: {batch.status.value})")
@@ -0,0 +1,38 @@
1
+ """Helpers for batch-related endpoints."""
2
+
3
+ from typing import Any
4
+
5
+ from minitest_cli.api.client import ApiClient
6
+ from minitest_cli.commands.run_helpers import handle_response_error
7
+ from minitest_cli.models.story_run import BatchResponse, CreateBatchRequest
8
+
9
+
10
+ def batches_base_path(app_id: str) -> str:
11
+ """Return the base API path for batches."""
12
+ return f"/api/v1/apps/{app_id}/batches"
13
+
14
+
15
+ async def post_batch(client: ApiClient, app_id: str, body: CreateBatchRequest) -> BatchResponse:
16
+ """POST a CreateBatchRequest and return a BatchResponse; handles errors."""
17
+ resp = await client.post(
18
+ batches_base_path(app_id),
19
+ json=body.model_dump(by_alias=True, exclude_none=True),
20
+ )
21
+ handle_response_error(resp, resource="Batch")
22
+ return BatchResponse.model_validate(resp.json())
23
+
24
+
25
+ def batch_summary_payload(batch: BatchResponse) -> dict[str, Any]:
26
+ """Serialise a batch to a compact JSON payload for --json output (camelCase)."""
27
+ return {
28
+ "batchId": batch.id,
29
+ "status": batch.status.value,
30
+ "storyRuns": [
31
+ {
32
+ "runId": r.id,
33
+ "userStory": r.user_story_name or r.user_story_id,
34
+ "status": r.status.value,
35
+ }
36
+ for r in batch.story_runs
37
+ ],
38
+ }
@@ -74,7 +74,7 @@ def upload(
74
74
  return BuildResponse.model_validate(resp.json())
75
75
 
76
76
  result = run_api_call(_run())
77
- result_dict = result.model_dump(mode="json")
77
+ result_dict = result.model_dump(mode="json", by_alias=True)
78
78
 
79
79
  if json_mode:
80
80
  output(result_dict, json_mode=True)
@@ -113,7 +113,7 @@ def list_builds(
113
113
  result = run_api_call(_run())
114
114
 
115
115
  if json_mode:
116
- output(result.model_dump(mode="json"), json_mode=True)
116
+ output(result.model_dump(mode="json", by_alias=True), json_mode=True)
117
117
  return
118
118
 
119
119
  if not result.items:
@@ -43,9 +43,9 @@ def maintenance_check(
43
43
  print_json(
44
44
  {
45
45
  "id": result.id,
46
- "app_id": result.app_id,
47
- "commit_sha": result.commit_sha,
48
- "created_at": result.created_at,
46
+ "appId": result.app_id,
47
+ "commitSha": result.commit_sha,
48
+ "createdAt": result.created_at,
49
49
  }
50
50
  )
51
51
  else:
@@ -0,0 +1,199 @@
1
+ """Test execution commands: start, status, list, cancel, run all."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from minitest_cli.api.client import ApiClient
8
+ from minitest_cli.commands.batch_helpers import batch_summary_payload, post_batch
9
+ from minitest_cli.commands.run_helpers import (
10
+ base_path,
11
+ display_run_result,
12
+ ensure_uuid,
13
+ fetch_runs,
14
+ format_run_pagination_info,
15
+ format_run_row,
16
+ handle_response_error,
17
+ poll_run_status,
18
+ resolve_app,
19
+ resolve_user_story_id,
20
+ run_api_call,
21
+ RUN_TABLE_HEADERS,
22
+ TERMINAL_STATUSES,
23
+ )
24
+ from minitest_cli.models.story_run import (
25
+ BatchResponse,
26
+ CreateBatchRequest,
27
+ StoryRunListResponse,
28
+ StoryRunResponse,
29
+ )
30
+ from minitest_cli.utils.output import (
31
+ output,
32
+ print_error,
33
+ print_info,
34
+ print_json,
35
+ print_success,
36
+ print_table,
37
+ )
38
+
39
+ app = typer.Typer(name="run", help="Test execution.")
40
+
41
+ IosBuildOpt = Annotated[
42
+ str | None, typer.Option("--ios-build", help="iOS build ID (omit for Android-only apps).")
43
+ ]
44
+ AndroidBuildOpt = Annotated[
45
+ str | None, typer.Option("--android-build", help="Android build ID (omit for iOS-only apps).")
46
+ ]
47
+
48
+
49
+ def _require_build(ios_build: str | None, android_build: str | None) -> None:
50
+ if not ios_build and not android_build:
51
+ print_error("Provide at least one of --ios-build or --android-build.")
52
+ raise typer.Exit(code=1)
53
+
54
+
55
+ @app.command()
56
+ def start(
57
+ user_story: Annotated[str, typer.Argument(help="User-story name or UUID to run.")],
58
+ ios_build: IosBuildOpt = None,
59
+ android_build: AndroidBuildOpt = None,
60
+ watch: Annotated[
61
+ bool, typer.Option("--watch/--no-watch", help="Poll for results (default: watch).")
62
+ ] = True,
63
+ ) -> None:
64
+ """Start a new test run for a user story (via the batches endpoint)."""
65
+ settings, app_id, json_mode = resolve_app()
66
+ _require_build(ios_build, android_build)
67
+
68
+ async def _start() -> StoryRunResponse:
69
+ async with ApiClient(settings) as client:
70
+ user_story_id = await resolve_user_story_id(client, app_id, user_story)
71
+ body = CreateBatchRequest(
72
+ user_story_ids=[user_story_id],
73
+ ios_build_id=ios_build,
74
+ android_build_id=android_build,
75
+ )
76
+ batch = await post_batch(client, app_id, body)
77
+ if not batch.story_runs:
78
+ print_error("Batch created but no story runs were returned.")
79
+ raise typer.Exit(code=3)
80
+ run = batch.story_runs[0]
81
+ if not watch:
82
+ return run
83
+ return await poll_run_status(client, app_id, run.id, json_mode)
84
+
85
+ run = run_api_call(_start())
86
+ if not watch:
87
+ if json_mode:
88
+ print_json({"runId": run.id, "status": run.status.value})
89
+ else:
90
+ print_success(f"Run started: {run.id}")
91
+ print_info(f"Use `minitest run status {run.id}` to check progress.")
92
+ return
93
+ display_run_result(run, json_mode)
94
+
95
+
96
+ @app.command()
97
+ def status(
98
+ run_id: Annotated[str, typer.Argument(help="Run ID to check.")],
99
+ watch: Annotated[
100
+ bool, typer.Option("--watch/--no-watch", help="Poll for results (default: no-watch).")
101
+ ] = False,
102
+ ) -> None:
103
+ """Check the status of a test run."""
104
+ settings, app_id, json_mode = resolve_app()
105
+ ensure_uuid(run_id, kind="run id")
106
+
107
+ async def _status() -> StoryRunResponse:
108
+ async with ApiClient(settings) as client:
109
+ resp = await client.get(f"{base_path(app_id)}/{run_id}")
110
+ handle_response_error(resp, resource="Run")
111
+ run = StoryRunResponse.model_validate(resp.json())
112
+ if watch and run.status not in TERMINAL_STATUSES:
113
+ return await poll_run_status(client, app_id, run.id, json_mode)
114
+ return run
115
+
116
+ display_run_result(run_api_call(_status()), json_mode)
117
+
118
+
119
+ @app.command(name="list")
120
+ def list_runs(
121
+ user_story: Annotated[str, typer.Argument(help="User-story name or UUID to list runs for.")],
122
+ page: Annotated[int, typer.Option(help="Page number.")] = 1,
123
+ page_size: Annotated[int, typer.Option(help="Items per page.")] = 20,
124
+ status_filter: Annotated[
125
+ str | None,
126
+ typer.Option(
127
+ "--status",
128
+ help="Filter by status (pending, running, completed, failed, cancelled).",
129
+ ),
130
+ ] = None,
131
+ all_pages: Annotated[bool, typer.Option("--all", help="Fetch all results.")] = False,
132
+ ) -> None:
133
+ """List runs for a user story."""
134
+ settings, app_id, json_mode = resolve_app()
135
+ if all_pages:
136
+ page, page_size = 1, 100
137
+
138
+ async def _list() -> StoryRunListResponse:
139
+ async with ApiClient(settings) as client:
140
+ user_story_id = await resolve_user_story_id(client, app_id, user_story)
141
+ return await fetch_runs(client, app_id, user_story_id, page, page_size, status_filter)
142
+
143
+ result = run_api_call(_list())
144
+ if json_mode:
145
+ output(result.model_dump(mode="json", by_alias=True), json_mode=True)
146
+ return
147
+ if not result.items:
148
+ print_info("No runs found.")
149
+ return
150
+ title, tip = format_run_pagination_info(result)
151
+ rows = [format_run_row(r) for r in result.items]
152
+ print_table(RUN_TABLE_HEADERS, rows, title=title)
153
+ if tip:
154
+ print_info(tip)
155
+
156
+
157
+ @app.command()
158
+ def cancel(run_id: Annotated[str, typer.Argument(help="Run ID to cancel.")]) -> None:
159
+ """Cancel a pending or running story run."""
160
+ settings, app_id, json_mode = resolve_app()
161
+ ensure_uuid(run_id, kind="run id")
162
+
163
+ async def _cancel() -> StoryRunResponse:
164
+ async with ApiClient(settings) as client:
165
+ resp = await client.post(f"{base_path(app_id)}/{run_id}/cancel")
166
+ handle_response_error(resp, resource="Run")
167
+ return StoryRunResponse.model_validate(resp.json())
168
+
169
+ run = run_api_call(_cancel())
170
+ if json_mode:
171
+ output(run.model_dump(mode="json", by_alias=True), json_mode=True)
172
+ else:
173
+ print_success(f"Run cancelled: {run.id} (status: {run.status.value})")
174
+
175
+
176
+ @app.command(name="all")
177
+ def run_all(
178
+ ios_build: IosBuildOpt = None,
179
+ android_build: AndroidBuildOpt = None,
180
+ ) -> None:
181
+ """Start a batch covering every user story for the app."""
182
+ settings, app_id, json_mode = resolve_app()
183
+ _require_build(ios_build, android_build)
184
+
185
+ async def _run_all() -> BatchResponse:
186
+ async with ApiClient(settings) as client:
187
+ body = CreateBatchRequest(ios_build_id=ios_build, android_build_id=android_build)
188
+ return await post_batch(client, app_id, body)
189
+
190
+ batch = run_api_call(_run_all())
191
+ if json_mode:
192
+ print_json(batch_summary_payload(batch))
193
+ return
194
+ rows = [format_run_row(r) for r in batch.story_runs]
195
+ print_table(RUN_TABLE_HEADERS, rows, title=f"Batch {batch.id} — {batch.status.value}")
196
+ print_info(
197
+ f"Started {len(batch.story_runs)} runs. "
198
+ f"Use `minitest batch get {batch.id}` or `minitest run status <id>` to follow up."
199
+ )
@@ -2,7 +2,7 @@
2
2
 
3
3
  import math
4
4
 
5
- from minitest_cli.models.flow_run import FlowRunListResponse, FlowRunResponse, RunStatus
5
+ from minitest_cli.models.story_run import RunStatus, StoryRunListResponse, StoryRunResponse
6
6
  from minitest_cli.utils.output import (
7
7
  err_console,
8
8
  print_error,
@@ -12,22 +12,22 @@ from minitest_cli.utils.output import (
12
12
  print_table,
13
13
  )
14
14
 
15
- RUN_TABLE_HEADERS = ["ID", "Flow", "Status", "Created"]
15
+ RUN_TABLE_HEADERS = ["ID", "User Story", "Status", "Created"]
16
16
 
17
17
  RESULTS_TABLE_HEADERS = ["Criterion ID", "Platform", "Result", "Fail Reason"]
18
18
 
19
19
 
20
- def format_run_row(run: FlowRunResponse) -> list[str]:
21
- """Format a single FlowRunResponse as a table row."""
20
+ def format_run_row(run: StoryRunResponse) -> list[str]:
21
+ """Format a single StoryRunResponse as a table row."""
22
22
  return [
23
23
  run.id,
24
- run.flow_template_name or run.flow_template_id,
24
+ run.user_story_name or run.user_story_id,
25
25
  run.status.value,
26
26
  run.created_at.strftime("%Y-%m-%d %H:%M"),
27
27
  ]
28
28
 
29
29
 
30
- def _platform_status_line(platform: str, run: FlowRunResponse) -> tuple[str, str | None] | None:
30
+ def _platform_status_line(platform: str, run: StoryRunResponse) -> tuple[str, str | None] | None:
31
31
  """Build a status line for a platform from the flat run fields."""
32
32
  if platform == "ios" and not run.ios_build_id:
33
33
  return None
@@ -48,10 +48,10 @@ def _platform_status_line(platform: str, run: FlowRunResponse) -> tuple[str, str
48
48
  return "".join(parts), recording
49
49
 
50
50
 
51
- def display_run_result(run: FlowRunResponse, json_mode: bool) -> None:
51
+ def display_run_result(run: StoryRunResponse, json_mode: bool) -> None:
52
52
  """Display the full results of a completed run."""
53
53
  if json_mode:
54
- print_json(run.model_dump(mode="json"))
54
+ print_json(run.model_dump(mode="json", by_alias=True))
55
55
  return
56
56
 
57
57
  status_icon = {
@@ -59,6 +59,7 @@ def display_run_result(run: FlowRunResponse, json_mode: bool) -> None:
59
59
  RunStatus.failed: "✗",
60
60
  RunStatus.pending: "…",
61
61
  RunStatus.running: "…",
62
+ RunStatus.cancelled: "⊘",
62
63
  }[run.status]
63
64
  print_info(f"Run {run.id} — {status_icon} {run.status.value}")
64
65
 
@@ -74,7 +75,7 @@ def display_run_result(run: FlowRunResponse, json_mode: bool) -> None:
74
75
  rows: list[list[str]] = []
75
76
  for cr in run.results:
76
77
  result_str = "[green]✓ pass[/green]" if cr.success else "[red]✗ fail[/red]"
77
- rows.append([cr.acceptance_criteria_id, cr.platform, result_str, cr.fail_reason or ""])
78
+ rows.append([cr.criterion_version_id, cr.platform, result_str, cr.fail_reason or ""])
78
79
 
79
80
  if rows:
80
81
  print_table(RESULTS_TABLE_HEADERS, rows, title="Acceptance Criteria Results")
@@ -95,9 +96,11 @@ def display_run_result(run: FlowRunResponse, json_mode: bool) -> None:
95
96
  print_error(f"Run failed — {'; '.join(errors)}")
96
97
  else:
97
98
  print_error("Run failed.")
99
+ elif run.status == RunStatus.cancelled:
100
+ print_info("Run cancelled.")
98
101
 
99
102
 
100
- def format_run_pagination_info(data: FlowRunListResponse) -> tuple[str, str]:
103
+ def format_run_pagination_info(data: StoryRunListResponse) -> tuple[str, str]:
101
104
  """Return (title, tip) for paginated run table display."""
102
105
  total_pages = math.ceil(data.total / data.page_size) if data.total else 1
103
106
  start = (data.page - 1) * data.page_size + 1