adaptorch-cli 0.1.0__tar.gz → 0.1.2__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.
- {adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info → adaptorch_cli-0.1.2}/PKG-INFO +18 -2
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/README.md +16 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/pyproject.toml +2 -2
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli/cli.py +47 -3
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli/config_store.py +38 -2
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli/parser.py +23 -1
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2/src/adaptorch_cli.egg-info}/PKG-INFO +18 -2
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/SOURCES.txt +3 -1
- adaptorch_cli-0.1.2/src/adaptorch_cli.egg-info/requires.txt +1 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/tests/test_cli_commands.py +37 -0
- adaptorch_cli-0.1.2/tests/test_cli_oauth.py +76 -0
- adaptorch_cli-0.1.2/tests/test_cli_wait_exit_codes.py +182 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/requires.txt +0 -1
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/LICENSE +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/setup.cfg +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli/__init__.py +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli/__main__.py +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli/py.typed +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/dependency_links.txt +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/entry_points.txt +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/top_level.txt +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/tests/test_cli_boundary.py +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/tests/test_cli_integration.py +0 -0
- {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.2}/tests/test_cli_login_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adaptorch-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Command-line client for AdaptOrch
|
|
5
5
|
Author: ClassicMate
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://adaptorch.com/mcp-docs
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: adaptorch-client<0.2,>=0.1
|
|
14
|
+
Requires-Dist: adaptorch-client<0.2,>=0.1.2
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# adaptorch-cli
|
|
@@ -41,6 +41,22 @@ A git push does not replace a previously published PyPI version.
|
|
|
41
41
|
|
|
42
42
|
See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
|
|
43
43
|
|
|
44
|
+
## Bounded wait exit codes
|
|
45
|
+
|
|
46
|
+
`adaptorchctl run wait RUN_ID` emits the last observation as JSON. A terminal
|
|
47
|
+
`SUCCEEDED` exits 0, `FAILED` exits 8, and `CANCELLED` exits 9. These codes describe
|
|
48
|
+
run lifecycle, not result correctness or evidence verification.
|
|
49
|
+
|
|
50
|
+
A `deadline`, `poll_limit`, `unsupported_status`, or unrecognized stop reason exits
|
|
51
|
+
10, even if the last observation looks terminal. Missing or unsupported terminal
|
|
52
|
+
observations also exit 10. This means the observation is inconclusive, not that
|
|
53
|
+
the server-side run failed. Inspect `reason` and retain the JSON; continue with the
|
|
54
|
+
same run ID when appropriate. Waiting never resubmits or cancels a run.
|
|
55
|
+
|
|
56
|
+
Automation compatibility: `run wait` previously returned 0 for these observations.
|
|
57
|
+
Scripts using `set -e` must now handle nonzero exits. `run get` keeps its existing
|
|
58
|
+
exit-code behavior; submit and cancel still report command success separately.
|
|
59
|
+
|
|
44
60
|
## License
|
|
45
61
|
|
|
46
62
|
Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
|
|
@@ -25,6 +25,22 @@ A git push does not replace a previously published PyPI version.
|
|
|
25
25
|
|
|
26
26
|
See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
|
|
27
27
|
|
|
28
|
+
## Bounded wait exit codes
|
|
29
|
+
|
|
30
|
+
`adaptorchctl run wait RUN_ID` emits the last observation as JSON. A terminal
|
|
31
|
+
`SUCCEEDED` exits 0, `FAILED` exits 8, and `CANCELLED` exits 9. These codes describe
|
|
32
|
+
run lifecycle, not result correctness or evidence verification.
|
|
33
|
+
|
|
34
|
+
A `deadline`, `poll_limit`, `unsupported_status`, or unrecognized stop reason exits
|
|
35
|
+
10, even if the last observation looks terminal. Missing or unsupported terminal
|
|
36
|
+
observations also exit 10. This means the observation is inconclusive, not that
|
|
37
|
+
the server-side run failed. Inspect `reason` and retain the JSON; continue with the
|
|
38
|
+
same run ID when appropriate. Waiting never resubmits or cancels a run.
|
|
39
|
+
|
|
40
|
+
Automation compatibility: `run wait` previously returned 0 for these observations.
|
|
41
|
+
Scripts using `set -e` must now handle nonzero exits. `run get` keeps its existing
|
|
42
|
+
exit-code behavior; submit and cancel still report command success separately.
|
|
43
|
+
|
|
28
44
|
## License
|
|
29
45
|
|
|
30
46
|
Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "adaptorch-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Command-line client for AdaptOrch"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
license = "LicenseRef-Proprietary"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [{ name = "ClassicMate" }]
|
|
14
|
-
dependencies = ["adaptorch-client>=0.1,<0.2"]
|
|
14
|
+
dependencies = ["adaptorch-client>=0.1.2,<0.2"]
|
|
15
15
|
|
|
16
16
|
[project.urls]
|
|
17
17
|
Homepage = "https://adaptorch.com"
|
|
@@ -13,6 +13,7 @@ from adaptorch_client import (
|
|
|
13
13
|
AdaptOrchAPIError,
|
|
14
14
|
AdaptOrchClient,
|
|
15
15
|
ClientConfig,
|
|
16
|
+
PollPolicy,
|
|
16
17
|
ProviderCredential,
|
|
17
18
|
validate_api_url,
|
|
18
19
|
)
|
|
@@ -39,7 +40,10 @@ def _reject_json_constant(_value: str) -> Never:
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def _finite_float(text: str) -> float:
|
|
42
|
-
|
|
43
|
+
try:
|
|
44
|
+
value = float(text)
|
|
45
|
+
except ValueError:
|
|
46
|
+
_reject_json_constant(text)
|
|
43
47
|
if not math.isfinite(value):
|
|
44
48
|
raise ValueError("non-finite JSON number")
|
|
45
49
|
return value
|
|
@@ -97,11 +101,14 @@ def _require_client(api_url: str) -> AdaptOrchClient:
|
|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
def _submission_credential() -> ProviderCredential | None:
|
|
100
|
-
resolved,
|
|
104
|
+
resolved, source = config_store.resolve_provider_credential()
|
|
101
105
|
if resolved is None:
|
|
102
106
|
return None
|
|
103
107
|
provider, model, key = resolved
|
|
104
|
-
|
|
108
|
+
auth_type, account_id = config_store.resolve_provider_auth(source)
|
|
109
|
+
if auth_type is None and account_id is None:
|
|
110
|
+
return ProviderCredential(provider, model, key)
|
|
111
|
+
return ProviderCredential(provider, model, key, auth_type=auth_type, account_id=account_id)
|
|
105
112
|
|
|
106
113
|
|
|
107
114
|
def _read_secret(prompt: str, *, stdin_flag: bool) -> str:
|
|
@@ -241,6 +248,28 @@ def _run_command(
|
|
|
241
248
|
case "get":
|
|
242
249
|
run_id: str = args.run_id
|
|
243
250
|
return _result_payload(_require_client(api_url).get_run(run_id)), True
|
|
251
|
+
case "wait":
|
|
252
|
+
wait_run_id: str = args.run_id
|
|
253
|
+
timeout_seconds: float = args.timeout
|
|
254
|
+
interval_seconds: float = args.interval
|
|
255
|
+
if not math.isfinite(timeout_seconds) or timeout_seconds <= 0:
|
|
256
|
+
parser.error("--timeout must be finite positive seconds")
|
|
257
|
+
if not math.isfinite(interval_seconds) or interval_seconds <= 0:
|
|
258
|
+
parser.error("--interval must be finite positive seconds")
|
|
259
|
+
wait_result = _require_client(api_url).wait_for_run(
|
|
260
|
+
wait_run_id,
|
|
261
|
+
policy=PollPolicy(
|
|
262
|
+
timeout_seconds=timeout_seconds, interval_seconds=interval_seconds
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
wait_payload: dict[str, JSONValue] = {
|
|
266
|
+
"reason": wait_result.reason.value,
|
|
267
|
+
"polls": wait_result.polls,
|
|
268
|
+
"elapsed_seconds": wait_result.elapsed_seconds,
|
|
269
|
+
}
|
|
270
|
+
if wait_result.run is not None:
|
|
271
|
+
wait_payload["run"] = wait_result.run.to_payload()
|
|
272
|
+
return wait_payload, True
|
|
244
273
|
case "cancel":
|
|
245
274
|
cancel_run_id: str = args.run_id
|
|
246
275
|
reason: str | None = args.reason
|
|
@@ -318,6 +347,19 @@ def _run_status_exit(payload: JSONMapping) -> int:
|
|
|
318
347
|
return 0
|
|
319
348
|
|
|
320
349
|
|
|
350
|
+
def _wait_status_exit(payload: JSONMapping) -> int:
|
|
351
|
+
"""Map a bounded observation without treating a stopped wait as success."""
|
|
352
|
+
if payload.get("reason") != "terminal":
|
|
353
|
+
return 10
|
|
354
|
+
run = payload.get("run")
|
|
355
|
+
if not isinstance(run, dict):
|
|
356
|
+
return 10
|
|
357
|
+
status = run.get("status")
|
|
358
|
+
if not isinstance(status, str):
|
|
359
|
+
return 10
|
|
360
|
+
return {"succeeded": 0, "failed": 8, "cancelled": 9}.get(status.lower(), 10)
|
|
361
|
+
|
|
362
|
+
|
|
321
363
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
322
364
|
parser = build_parser()
|
|
323
365
|
args = parse_args(argv)
|
|
@@ -333,6 +375,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
333
375
|
except ValueError:
|
|
334
376
|
print("response contained non-encodable JSON values", file=sys.stderr)
|
|
335
377
|
return 7
|
|
378
|
+
if args.command == "run" and args.run_command == "wait":
|
|
379
|
+
return _wait_status_exit(payload)
|
|
336
380
|
return _run_status_exit(payload) if check_run_status else 0
|
|
337
381
|
# pi-lens-ignore: unreachable-except
|
|
338
382
|
except KeyboardInterrupt:
|
|
@@ -27,6 +27,8 @@ _PROVIDER_KEY_MAP = {
|
|
|
27
27
|
"provider.name": "provider",
|
|
28
28
|
"provider.model": "model",
|
|
29
29
|
"provider.api_key": "api_key",
|
|
30
|
+
"provider.auth_type": "auth_type",
|
|
31
|
+
"provider.account_id": "account_id",
|
|
30
32
|
}
|
|
31
33
|
_CONFIGURABLE_KEYS = frozenset({"api_url", *_PROVIDER_KEY_MAP})
|
|
32
34
|
|
|
@@ -175,18 +177,50 @@ def resolve_provider_credential() -> tuple[tuple[str, str, str] | None, str]:
|
|
|
175
177
|
env_provider = os.environ.get("ADAPTORCH_PROVIDER", "").strip()
|
|
176
178
|
env_model = os.environ.get("ADAPTORCH_PROVIDER_MODEL", "").strip()
|
|
177
179
|
env_key = os.environ.get("ADAPTORCH_PROVIDER_API_KEY", "").strip()
|
|
178
|
-
if
|
|
180
|
+
if any(
|
|
181
|
+
(
|
|
182
|
+
env_provider,
|
|
183
|
+
env_model,
|
|
184
|
+
env_key,
|
|
185
|
+
os.environ.get("ADAPTORCH_PROVIDER_AUTH_TYPE"),
|
|
186
|
+
os.environ.get("ADAPTORCH_PROVIDER_ACCOUNT_ID"),
|
|
187
|
+
)
|
|
188
|
+
):
|
|
179
189
|
return (env_provider, env_model, env_key), "env"
|
|
180
190
|
provider_cfg = load_config().get("provider")
|
|
181
191
|
if isinstance(provider_cfg, dict):
|
|
182
192
|
provider = str(provider_cfg.get("provider") or "").strip()
|
|
183
193
|
model = str(provider_cfg.get("model") or "").strip()
|
|
184
194
|
key = str(provider_cfg.get("api_key") or "").strip()
|
|
185
|
-
|
|
195
|
+
has_auth_metadata = "auth_type" in provider_cfg or "account_id" in provider_cfg
|
|
196
|
+
if provider or model or key or has_auth_metadata:
|
|
186
197
|
return (provider, model, key), "config"
|
|
187
198
|
return None, "none"
|
|
188
199
|
|
|
189
200
|
|
|
201
|
+
def resolve_provider_auth(source: str) -> tuple[str | None, str | None]:
|
|
202
|
+
"""Read metadata from the same source as the token; never mix identities."""
|
|
203
|
+
if source == "env":
|
|
204
|
+
values = (
|
|
205
|
+
os.environ.get("ADAPTORCH_PROVIDER_AUTH_TYPE"),
|
|
206
|
+
os.environ.get("ADAPTORCH_PROVIDER_ACCOUNT_ID"),
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
provider = load_config().get("provider")
|
|
210
|
+
if not isinstance(provider, dict):
|
|
211
|
+
return None, None
|
|
212
|
+
values = (provider.get("auth_type"), provider.get("account_id"))
|
|
213
|
+
|
|
214
|
+
def optional_text(value: object) -> str | None:
|
|
215
|
+
if value is None:
|
|
216
|
+
return None
|
|
217
|
+
if not isinstance(value, str):
|
|
218
|
+
raise ValueError("provider authentication metadata must be strings")
|
|
219
|
+
return value.strip() or None
|
|
220
|
+
|
|
221
|
+
return optional_text(values[0]), optional_text(values[1])
|
|
222
|
+
|
|
223
|
+
|
|
190
224
|
def mask_secret(value: str | None) -> str | None:
|
|
191
225
|
if not value:
|
|
192
226
|
return None
|
|
@@ -209,6 +243,8 @@ def config_view() -> dict[str, Any]:
|
|
|
209
243
|
"name": provider.get("provider"),
|
|
210
244
|
"model": provider.get("model"),
|
|
211
245
|
"api_key": mask_secret(provider_key if isinstance(provider_key, str) else None),
|
|
246
|
+
"auth_type": provider.get("auth_type"),
|
|
247
|
+
"account_id_configured": bool(provider.get("account_id")),
|
|
212
248
|
"source": resolve_provider_credential()[1],
|
|
213
249
|
},
|
|
214
250
|
"config_path": str(config_path()),
|
|
@@ -33,6 +33,28 @@ def _add_run_commands(parent: argparse._SubParsersAction[argparse.ArgumentParser
|
|
|
33
33
|
get = commands.add_parser("get", help="Get a run")
|
|
34
34
|
get.add_argument("run_id")
|
|
35
35
|
|
|
36
|
+
wait = commands.add_parser(
|
|
37
|
+
"wait",
|
|
38
|
+
help=(
|
|
39
|
+
"Read an existing run until it reaches a terminal status — the "
|
|
40
|
+
"CLI's form of the MCP resume_run_id contract. It never submits "
|
|
41
|
+
"another run; use it after an uncertain submit response."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
wait.add_argument("run_id")
|
|
45
|
+
wait.add_argument(
|
|
46
|
+
"--timeout",
|
|
47
|
+
type=float,
|
|
48
|
+
default=120.0,
|
|
49
|
+
help="Seconds to keep polling before stopping (default: 120)",
|
|
50
|
+
)
|
|
51
|
+
wait.add_argument(
|
|
52
|
+
"--interval",
|
|
53
|
+
type=float,
|
|
54
|
+
default=1.0,
|
|
55
|
+
help="Seconds between status reads (default: 1)",
|
|
56
|
+
)
|
|
57
|
+
|
|
36
58
|
cancel = commands.add_parser("cancel", help="Cancel a run")
|
|
37
59
|
cancel.add_argument("run_id")
|
|
38
60
|
cancel.add_argument("--reason")
|
|
@@ -76,7 +98,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
76
98
|
config_commands.add_parser("get", help="Show the merged configuration (secrets masked)")
|
|
77
99
|
config_set = config_commands.add_parser(
|
|
78
100
|
"set",
|
|
79
|
-
help="Set api_url
|
|
101
|
+
help="Set api_url or provider.name/model/api_key/auth_type/account_id",
|
|
80
102
|
)
|
|
81
103
|
config_set.add_argument("key")
|
|
82
104
|
config_set.add_argument("value", nargs="?")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adaptorch-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Command-line client for AdaptOrch
|
|
5
5
|
Author: ClassicMate
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://adaptorch.com/mcp-docs
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: adaptorch-client<0.2,>=0.1
|
|
14
|
+
Requires-Dist: adaptorch-client<0.2,>=0.1.2
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# adaptorch-cli
|
|
@@ -41,6 +41,22 @@ A git push does not replace a previously published PyPI version.
|
|
|
41
41
|
|
|
42
42
|
See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
|
|
43
43
|
|
|
44
|
+
## Bounded wait exit codes
|
|
45
|
+
|
|
46
|
+
`adaptorchctl run wait RUN_ID` emits the last observation as JSON. A terminal
|
|
47
|
+
`SUCCEEDED` exits 0, `FAILED` exits 8, and `CANCELLED` exits 9. These codes describe
|
|
48
|
+
run lifecycle, not result correctness or evidence verification.
|
|
49
|
+
|
|
50
|
+
A `deadline`, `poll_limit`, `unsupported_status`, or unrecognized stop reason exits
|
|
51
|
+
10, even if the last observation looks terminal. Missing or unsupported terminal
|
|
52
|
+
observations also exit 10. This means the observation is inconclusive, not that
|
|
53
|
+
the server-side run failed. Inspect `reason` and retain the JSON; continue with the
|
|
54
|
+
same run ID when appropriate. Waiting never resubmits or cancels a run.
|
|
55
|
+
|
|
56
|
+
Automation compatibility: `run wait` previously returned 0 for these observations.
|
|
57
|
+
Scripts using `set -e` must now handle nonzero exits. `run get` keeps its existing
|
|
58
|
+
exit-code behavior; submit and cancel still report command success separately.
|
|
59
|
+
|
|
44
60
|
## License
|
|
45
61
|
|
|
46
62
|
Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
|
|
@@ -16,4 +16,6 @@ src/adaptorch_cli.egg-info/top_level.txt
|
|
|
16
16
|
tests/test_cli_boundary.py
|
|
17
17
|
tests/test_cli_commands.py
|
|
18
18
|
tests/test_cli_integration.py
|
|
19
|
-
tests/test_cli_login_config.py
|
|
19
|
+
tests/test_cli_login_config.py
|
|
20
|
+
tests/test_cli_oauth.py
|
|
21
|
+
tests/test_cli_wait_exit_codes.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adaptorch-client<0.2,>=0.1.2
|
|
@@ -118,6 +118,43 @@ def test_run_get_prints_client_json(run_cli: CliRunner) -> None:
|
|
|
118
118
|
assert _json_stdout(result.stdout) == {"id": "run-1", "status": "RUNNING"}
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
def test_run_wait_reads_until_terminal_without_resubmitting(run_cli: CliRunner) -> None:
|
|
122
|
+
result = run_cli(
|
|
123
|
+
[
|
|
124
|
+
"--output",
|
|
125
|
+
"json",
|
|
126
|
+
"run",
|
|
127
|
+
"wait",
|
|
128
|
+
"run-1",
|
|
129
|
+
"--timeout",
|
|
130
|
+
"5",
|
|
131
|
+
"--interval",
|
|
132
|
+
"0.5",
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert result.returncode == 0
|
|
137
|
+
assert _json_stdout(result.stdout) == {
|
|
138
|
+
"reason": "terminal",
|
|
139
|
+
"polls": 1,
|
|
140
|
+
"elapsed_seconds": 0.01,
|
|
141
|
+
"run": {"id": "run-1", "status": "SUCCEEDED"},
|
|
142
|
+
}
|
|
143
|
+
calls = [json.loads(line) for line in result.client_log.splitlines()]
|
|
144
|
+
assert calls[-1] == {
|
|
145
|
+
"args": ["run-1"],
|
|
146
|
+
"kwargs": {"interval_seconds": 0.5, "timeout_seconds": 5.0},
|
|
147
|
+
"method": "wait_for_run",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_run_wait_rejects_non_positive_bounds(run_cli: CliRunner) -> None:
|
|
152
|
+
result = run_cli(["--output", "json", "run", "wait", "run-1", "--timeout", "0"])
|
|
153
|
+
|
|
154
|
+
assert result.returncode != 0
|
|
155
|
+
assert result.client_log.count("wait_for_run") == 0
|
|
156
|
+
|
|
157
|
+
|
|
121
158
|
def test_run_cancel_prints_client_json(run_cli: CliRunner) -> None:
|
|
122
159
|
result = run_cli(["--output", "json", "run", "cancel", "run-1"])
|
|
123
160
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from adaptorch_cli import cli, config_store
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def isolated_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
13
|
+
monkeypatch.setenv("ADAPTORCH_CONFIG_DIR", str(tmp_path))
|
|
14
|
+
for name in (
|
|
15
|
+
"ADAPTORCH_PROVIDER",
|
|
16
|
+
"ADAPTORCH_PROVIDER_MODEL",
|
|
17
|
+
"ADAPTORCH_PROVIDER_API_KEY",
|
|
18
|
+
"ADAPTORCH_PROVIDER_AUTH_TYPE",
|
|
19
|
+
"ADAPTORCH_PROVIDER_ACCOUNT_ID",
|
|
20
|
+
):
|
|
21
|
+
monkeypatch.delenv(name, raising=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_cli_resolves_oauth_from_environment_without_persisting_token(
|
|
25
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
26
|
+
) -> None:
|
|
27
|
+
for name, value in {
|
|
28
|
+
"ADAPTORCH_PROVIDER": "openai_codex",
|
|
29
|
+
"ADAPTORCH_PROVIDER_MODEL": "gpt-5-codex",
|
|
30
|
+
"ADAPTORCH_PROVIDER_API_KEY": "synthetic-oauth-token",
|
|
31
|
+
"ADAPTORCH_PROVIDER_AUTH_TYPE": "oauth",
|
|
32
|
+
"ADAPTORCH_PROVIDER_ACCOUNT_ID": "caller-account",
|
|
33
|
+
}.items():
|
|
34
|
+
monkeypatch.setenv(name, value)
|
|
35
|
+
credential = cli._submission_credential()
|
|
36
|
+
assert credential is not None
|
|
37
|
+
assert credential.headers["X-Provider-Auth-Type"] == "oauth"
|
|
38
|
+
assert credential.headers["X-Provider-Account-Id"] == "caller-account"
|
|
39
|
+
assert not config_store.config_path().exists()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cli_environment_credential_never_borrows_account_from_config(
|
|
43
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
44
|
+
) -> None:
|
|
45
|
+
config_store.save_config(
|
|
46
|
+
{
|
|
47
|
+
"provider": {
|
|
48
|
+
"provider": "openai_codex",
|
|
49
|
+
"model": "gpt-5-codex",
|
|
50
|
+
"api_key": "old-token",
|
|
51
|
+
"auth_type": "oauth",
|
|
52
|
+
"account_id": "old-account",
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
monkeypatch.setenv("ADAPTORCH_PROVIDER", "openai_codex")
|
|
57
|
+
monkeypatch.setenv("ADAPTORCH_PROVIDER_MODEL", "gpt-5-codex")
|
|
58
|
+
monkeypatch.setenv("ADAPTORCH_PROVIDER_API_KEY", "new-token")
|
|
59
|
+
credential = cli._submission_credential()
|
|
60
|
+
assert credential is not None
|
|
61
|
+
assert credential.account_id is None
|
|
62
|
+
assert credential.api_key == "new-token"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_cli_oauth_config_keys_and_view_are_safe() -> None:
|
|
66
|
+
config_store.set_config_value("provider.name", "openai_codex")
|
|
67
|
+
config_store.set_config_value("provider.model", "gpt-5-codex")
|
|
68
|
+
config_store.set_config_value("provider.api_key", "synthetic-oauth-token")
|
|
69
|
+
config_store.set_config_value("provider.auth_type", "oauth")
|
|
70
|
+
config_store.set_config_value("provider.account_id", "caller-account")
|
|
71
|
+
credential = cli._submission_credential()
|
|
72
|
+
assert credential is not None and credential.account_id == "caller-account"
|
|
73
|
+
view = json.dumps(config_store.config_view())
|
|
74
|
+
assert "synthetic-oauth-token" not in view
|
|
75
|
+
assert "caller-account" not in view
|
|
76
|
+
assert config_store.config_view()["provider"]["account_id_configured"] is True
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from adaptorch_client import AdaptOrchClient, ClientConfig, PollPolicy, Run, RunPollResult, polling
|
|
9
|
+
from adaptorch_client import client as client_module
|
|
10
|
+
from adaptorch_client.transport import HTTPTransport, RequestSpec
|
|
11
|
+
from cli_test_support import CliRunner
|
|
12
|
+
|
|
13
|
+
from adaptorch_cli import cli
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize(
|
|
17
|
+
("reason", "status", "expected"),
|
|
18
|
+
[
|
|
19
|
+
("terminal", "SUCCEEDED", 0),
|
|
20
|
+
("terminal", "sUcCeEdEd", 0),
|
|
21
|
+
("terminal", "FAILED", 8),
|
|
22
|
+
("terminal", "failed", 8),
|
|
23
|
+
("terminal", "CANCELLED", 9),
|
|
24
|
+
("terminal", "cancelled", 9),
|
|
25
|
+
("deadline", "RUNNING", 10),
|
|
26
|
+
("deadline", "SUCCEEDED", 10),
|
|
27
|
+
("deadline", "FAILED", 10),
|
|
28
|
+
("poll_limit", "RUNNING", 10),
|
|
29
|
+
("unsupported_status", "INCONCLUSIVE", 10),
|
|
30
|
+
("unsupported_status", "FUTURE_STATUS", 10),
|
|
31
|
+
("future_reason", "SUCCEEDED", 10),
|
|
32
|
+
("terminal", "RUNNING", 10),
|
|
33
|
+
("terminal", "FUTURE_STATUS", 10),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
def test_wait_exit_preserves_observation_and_never_resubmits(
|
|
37
|
+
run_cli: CliRunner,
|
|
38
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
39
|
+
reason: str,
|
|
40
|
+
status: str,
|
|
41
|
+
expected: int,
|
|
42
|
+
) -> None:
|
|
43
|
+
monkeypatch.setenv("FAKE_WAIT_REASON", reason)
|
|
44
|
+
monkeypatch.setenv("FAKE_RUN_STATUS", status)
|
|
45
|
+
result = run_cli(["run", "wait", "run-1"])
|
|
46
|
+
|
|
47
|
+
assert result.returncode == expected
|
|
48
|
+
assert json.loads(result.stdout) == {
|
|
49
|
+
"reason": reason,
|
|
50
|
+
"polls": 1,
|
|
51
|
+
"elapsed_seconds": 0.01,
|
|
52
|
+
"run": {"id": "run-1", "status": status},
|
|
53
|
+
}
|
|
54
|
+
assert result.stdout.count("\n") == 1
|
|
55
|
+
assert result.stderr == ""
|
|
56
|
+
methods = [json.loads(line)["method"] for line in result.client_log.splitlines()]
|
|
57
|
+
assert methods == ["init", "wait_for_run"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize(
|
|
61
|
+
"payload",
|
|
62
|
+
[
|
|
63
|
+
{},
|
|
64
|
+
{"reason": None, "run": {"status": "SUCCEEDED"}},
|
|
65
|
+
{"reason": 1, "run": {"status": "SUCCEEDED"}},
|
|
66
|
+
{"reason": "future_reason", "run": {"status": "SUCCEEDED"}},
|
|
67
|
+
{"reason": "terminal"},
|
|
68
|
+
{"reason": "terminal", "run": None},
|
|
69
|
+
{"reason": "terminal", "run": []},
|
|
70
|
+
{"reason": "terminal", "run": {}},
|
|
71
|
+
{"reason": "terminal", "run": {"status": None}},
|
|
72
|
+
{"reason": "terminal", "run": {"status": 1}},
|
|
73
|
+
{"reason": "terminal", "run": {"status": ""}},
|
|
74
|
+
{"reason": "terminal", "run": {"data": {"status": "SUCCEEDED"}}},
|
|
75
|
+
{"reason": "deadline"},
|
|
76
|
+
{"reason": "poll_limit"},
|
|
77
|
+
{"reason": "unsupported_status"},
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
def test_wait_unknown_or_missing_observation_is_inconclusive(
|
|
81
|
+
payload: cli.JSONMapping,
|
|
82
|
+
) -> None:
|
|
83
|
+
assert cli._wait_status_exit(payload) == 10
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_wait_lifecycle_does_not_claim_result_correctness() -> None:
|
|
87
|
+
assert cli._wait_status_exit(
|
|
88
|
+
{"reason": "terminal", "run": {"status": "SUCCEEDED", "result_status": "FAILED"}}
|
|
89
|
+
) == 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_wait_does_not_reinterpret_server_cancellation_encoding() -> None:
|
|
93
|
+
assert cli._wait_status_exit(
|
|
94
|
+
{"reason": "terminal", "run": {"status": "FAILED", "error_class": "CANCELLED"}}
|
|
95
|
+
) == 8
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
("status", "reason", "expected", "timeout", "latency", "count"),
|
|
100
|
+
[
|
|
101
|
+
("SUCCEEDED", "terminal", 0, "5", 0.0, 1),
|
|
102
|
+
("FAILED", "terminal", 8, "5", 0.0, 1),
|
|
103
|
+
("CANCELLED", "terminal", 9, "5", 0.0, 1),
|
|
104
|
+
("RUNNING", "deadline", 10, "0.5", 0.0, 1),
|
|
105
|
+
("RUNNING", "poll_limit", 10, "200", 0.0, 100),
|
|
106
|
+
("NEW_STATUS", "unsupported_status", 10, "5", 0.0, 1),
|
|
107
|
+
("INCONCLUSIVE", "unsupported_status", 10, "5", 0.0, 1),
|
|
108
|
+
("SUCCEEDED", "deadline", 10, "0.5", 0.5, 1),
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
def test_wait_uses_real_sdk_polling_without_writing_or_resubmitting(
|
|
112
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
113
|
+
tmp_path: Path,
|
|
114
|
+
capsys: pytest.CaptureFixture[str],
|
|
115
|
+
status: str,
|
|
116
|
+
reason: str,
|
|
117
|
+
expected: int,
|
|
118
|
+
timeout: str,
|
|
119
|
+
latency: float,
|
|
120
|
+
count: int,
|
|
121
|
+
) -> None:
|
|
122
|
+
elapsed = 0.0
|
|
123
|
+
requests: list[RequestSpec] = []
|
|
124
|
+
|
|
125
|
+
def clock() -> float:
|
|
126
|
+
return elapsed
|
|
127
|
+
|
|
128
|
+
def sleep(duration: float) -> None:
|
|
129
|
+
nonlocal elapsed
|
|
130
|
+
elapsed += duration
|
|
131
|
+
|
|
132
|
+
def request(_transport: HTTPTransport, spec: RequestSpec) -> cli.JSONMapping:
|
|
133
|
+
nonlocal elapsed
|
|
134
|
+
requests.append(spec)
|
|
135
|
+
elapsed += latency
|
|
136
|
+
return {"run_id": "run-1", "status": status}
|
|
137
|
+
|
|
138
|
+
def poll(fetch: Callable[[float], Run], policy: PollPolicy) -> RunPollResult:
|
|
139
|
+
return polling.wait_for_run(fetch, policy, clock=clock, sleep=sleep)
|
|
140
|
+
|
|
141
|
+
client = AdaptOrchClient(ClientConfig("https://api.example.test", "ado_test"))
|
|
142
|
+
monkeypatch.setenv("ADAPTORCH_CONFIG_DIR", str(tmp_path / "isolated-config"))
|
|
143
|
+
monkeypatch.setattr(cli, "_require_client", lambda _api_url: client)
|
|
144
|
+
monkeypatch.setattr(HTTPTransport, "request", request)
|
|
145
|
+
monkeypatch.setattr(client_module, "poll_run", poll)
|
|
146
|
+
|
|
147
|
+
code = cli.main(["run", "wait", "run-1", "--timeout", timeout, "--interval", "1"])
|
|
148
|
+
|
|
149
|
+
captured = capsys.readouterr()
|
|
150
|
+
assert code == expected
|
|
151
|
+
assert json.loads(captured.out) == {
|
|
152
|
+
"reason": reason,
|
|
153
|
+
"polls": count,
|
|
154
|
+
"elapsed_seconds": elapsed,
|
|
155
|
+
"run": {"run_id": "run-1", "status": status},
|
|
156
|
+
}
|
|
157
|
+
assert captured.out.count("\n") == 1
|
|
158
|
+
assert captured.err == ""
|
|
159
|
+
assert len(requests) == count
|
|
160
|
+
assert all((spec.method, spec.path) == ("GET", "/v1/runs/run-1") for spec in requests)
|
|
161
|
+
assert all(spec.payload is None and spec.provider_credential is None for spec in requests)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.parametrize(
|
|
165
|
+
("status", "expected"),
|
|
166
|
+
[("SUCCEEDED", 0), ("RUNNING", 0), ("NEW_STATUS", 0), ("FAILED", 8), ("CANCELLED", 9)],
|
|
167
|
+
)
|
|
168
|
+
def test_get_retains_its_mapping_with_additive_wait_like_fields(
|
|
169
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
170
|
+
tmp_path: Path,
|
|
171
|
+
capsys: pytest.CaptureFixture[str],
|
|
172
|
+
status: str,
|
|
173
|
+
expected: int,
|
|
174
|
+
) -> None:
|
|
175
|
+
payload: cli.JSONMapping = {"run_id": "run-1", "status": status, "reason": "deadline"}
|
|
176
|
+
monkeypatch.setenv("ADAPTORCH_CONFIG_DIR", str(tmp_path / "isolated-config"))
|
|
177
|
+
monkeypatch.setattr(cli, "_execute", lambda *_args: (payload, True))
|
|
178
|
+
|
|
179
|
+
assert cli.main(["run", "get", "run-1"]) == expected
|
|
180
|
+
captured = capsys.readouterr()
|
|
181
|
+
assert json.loads(captured.out) == payload
|
|
182
|
+
assert captured.err == ""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
adaptorch-client<0.2,>=0.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|