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.
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/PKG-INFO +2 -2
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/README.md +1 -1
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/pyproject.toml +1 -1
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/apps.py +1 -1
- minitest_cli-0.7.0/src/minitest_cli/commands/batch.py +169 -0
- minitest_cli-0.7.0/src/minitest_cli/commands/batch_helpers.py +38 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/build.py +2 -2
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/maintenance_check.py +3 -3
- minitest_cli-0.7.0/src/minitest_cli/commands/run.py +199 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/run_display.py +13 -10
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/run_helpers.py +37 -25
- 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
- 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
- minitest_cli-0.7.0/src/minitest_cli/commands/user_story_modify.py +151 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/main.py +13 -2
- minitest_cli-0.7.0/src/minitest_cli/models/__init__.py +55 -0
- minitest_cli-0.7.0/src/minitest_cli/models/story_run.py +169 -0
- minitest_cli-0.7.0/src/minitest_cli/models/user_story.py +71 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/output.py +15 -2
- minitest_cli-0.7.0/tests/test_batch_commands.py +320 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_run_commands.py +200 -85
- minitest_cli-0.7.0/tests/test_user_story_commands.py +406 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/uv.lock +1 -1
- minitest_cli-0.6.0/src/minitest_cli/commands/flow_modify.py +0 -105
- minitest_cli-0.6.0/src/minitest_cli/commands/run.py +0 -172
- minitest_cli-0.6.0/src/minitest_cli/models/__init__.py +0 -43
- minitest_cli-0.6.0/src/minitest_cli/models/flow_run.py +0 -78
- minitest_cli-0.6.0/src/minitest_cli/models/flow_template.py +0 -53
- minitest_cli-0.6.0/tests/test_flow_commands.py +0 -287
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.env.example +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.github/workflows/ci.yml +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.github/workflows/install-scripts.yml +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.github/workflows/release.yml +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.gitignore +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/.opencode/skill/release/SKILL.md +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/AGENTS.md +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/RELEASE.md +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/install.ps1 +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/install.sh +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/pyrightconfig.json +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/api/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/api/client.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/assets/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/assets/callback.html +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/auth.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/build_helpers.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/skill.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/commands/upgrade.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/app_context.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/auth.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/config.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/credentials.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/oauth.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/core/token_exchange.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/app.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/base.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/build.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/models/maintenance_check.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/src/minitest_cli/utils/update_check.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/__init__.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_apps_commands.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_auth.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_auth_commands.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_build_commands.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_code_quality.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_skill_command.py +0 -0
- {minitest_cli-0.6.0 → minitest_cli-0.7.0}/tests/test_upgrade_command.py +0 -0
- {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.
|
|
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
|
|
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
|
|
88
|
+
| `minitest user-story` | User-story operations |
|
|
89
89
|
| `minitest build` | Build management |
|
|
90
90
|
| `minitest run` | Test execution |
|
|
91
91
|
|
|
@@ -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
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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.
|
|
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", "
|
|
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:
|
|
21
|
-
"""Format a single
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
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
|