adaptorch-cli 0.1.1__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.
Files changed (23) hide show
  1. {adaptorch_cli-0.1.1/src/adaptorch_cli.egg-info → adaptorch_cli-0.1.2}/PKG-INFO +17 -1
  2. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/README.md +16 -0
  3. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/pyproject.toml +1 -1
  4. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli/cli.py +15 -0
  5. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2/src/adaptorch_cli.egg-info}/PKG-INFO +17 -1
  6. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/SOURCES.txt +2 -1
  7. adaptorch_cli-0.1.2/tests/test_cli_wait_exit_codes.py +182 -0
  8. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/LICENSE +0 -0
  9. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/setup.cfg +0 -0
  10. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli/__init__.py +0 -0
  11. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli/__main__.py +0 -0
  12. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli/config_store.py +0 -0
  13. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli/parser.py +0 -0
  14. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli/py.typed +0 -0
  15. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/dependency_links.txt +0 -0
  16. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/entry_points.txt +0 -0
  17. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/requires.txt +0 -0
  18. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/src/adaptorch_cli.egg-info/top_level.txt +0 -0
  19. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/tests/test_cli_boundary.py +0 -0
  20. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/tests/test_cli_commands.py +0 -0
  21. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/tests/test_cli_integration.py +0 -0
  22. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/tests/test_cli_login_config.py +0 -0
  23. {adaptorch_cli-0.1.1 → adaptorch_cli-0.1.2}/tests/test_cli_oauth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptorch-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Command-line client for AdaptOrch
5
5
  Author: ClassicMate
6
6
  License-Expression: LicenseRef-Proprietary
@@ -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,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "adaptorch-cli"
7
- version = "0.1.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"
@@ -347,6 +347,19 @@ def _run_status_exit(payload: JSONMapping) -> int:
347
347
  return 0
348
348
 
349
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
+
350
363
  def main(argv: Sequence[str] | None = None) -> int:
351
364
  parser = build_parser()
352
365
  args = parse_args(argv)
@@ -362,6 +375,8 @@ def main(argv: Sequence[str] | None = None) -> int:
362
375
  except ValueError:
363
376
  print("response contained non-encodable JSON values", file=sys.stderr)
364
377
  return 7
378
+ if args.command == "run" and args.run_command == "wait":
379
+ return _wait_status_exit(payload)
365
380
  return _run_status_exit(payload) if check_run_status else 0
366
381
  # pi-lens-ignore: unreachable-except
367
382
  except KeyboardInterrupt:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptorch-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Command-line client for AdaptOrch
5
5
  Author: ClassicMate
6
6
  License-Expression: LicenseRef-Proprietary
@@ -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).
@@ -17,4 +17,5 @@ tests/test_cli_boundary.py
17
17
  tests/test_cli_commands.py
18
18
  tests/test_cli_integration.py
19
19
  tests/test_cli_login_config.py
20
- tests/test_cli_oauth.py
20
+ tests/test_cli_oauth.py
21
+ tests/test_cli_wait_exit_codes.py
@@ -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 == ""
File without changes
File without changes