pocketshell 0.4.39__tar.gz → 0.4.41__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.
- {pocketshell-0.4.39 → pocketshell-0.4.41}/PKG-INFO +1 -1
- {pocketshell-0.4.39 → pocketshell-0.4.41}/pyproject.toml +1 -1
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_watch_ci.py +210 -6
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_watch_ci_cancelled.py +10 -1
- {pocketshell-0.4.39 → pocketshell-0.4.41}/.gitignore +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/README.md +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/scheduler/README.md +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/agent_card_push.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/agent_log.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/agents.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/cards.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/cli.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/daemon.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/hooks.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/sessions.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/usage.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/__init__.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/data/quse-0.0.11-usage.json +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/data/quse-0.0.9-usage.json +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_agent_card_push.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_agent_log.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_agents.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_cards.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_cards_push_notify.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_cli.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_daemon.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_env.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_github.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_hooks.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_logs.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_push.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_repos.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_resume.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_sessions.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_tree.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_usage.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_usage_capture.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/tests/test_usage_reset.py +0 -0
- {pocketshell-0.4.39 → pocketshell-0.4.41}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.41
|
|
4
4
|
Summary: Unified server-side Python utility for the PocketShell Android client.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
|
|
6
6
|
Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
|
|
@@ -8,7 +8,7 @@ name = "pocketshell"
|
|
|
8
8
|
# scripts/check-pypi-version.sh enforces this; .github/workflows/build.yml
|
|
9
9
|
# runs that check before publishing to PyPI. See
|
|
10
10
|
# tools/pocketshell/README.md ("Release flow") for the bump procedure.
|
|
11
|
-
version = "0.4.
|
|
11
|
+
version = "0.4.41"
|
|
12
12
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -10,10 +10,12 @@ Exit-code contract under test:
|
|
|
10
10
|
|
|
11
11
|
0 green — all required checks passed
|
|
12
12
|
1 failed — a required check GENUINELY failed
|
|
13
|
-
2 hang — no progress past --no-progress-timeout
|
|
13
|
+
2 hang — no progress past --no-progress-timeout
|
|
14
14
|
3 unresolved — run / inputs couldn't be resolved (or gh stayed broken)
|
|
15
15
|
4 superseded — a newer run replaced this one (routine concurrency-cancel)
|
|
16
16
|
5 no_verdict — cancelled, so no verdict was reached (issue #1650)
|
|
17
|
+
6 failed_fast — required check failed while the run is still in flight
|
|
18
|
+
7 wall_timeout — max wall-clock reached while the run is still in flight
|
|
17
19
|
|
|
18
20
|
The load-bearing case is the HANG one: a run whose jobs NEVER change state must
|
|
19
21
|
exit 2, not loop forever. The test drives a virtual clock so a stalled run is
|
|
@@ -33,6 +35,7 @@ import pytest
|
|
|
33
35
|
|
|
34
36
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
35
37
|
_WATCH_CI_PATH = _REPO_ROOT / "scripts" / "watch-ci.py"
|
|
38
|
+
_ONCALL_PROMPT_PATH = _REPO_ROOT / ".claude" / "agents" / "oncall-engineer.md"
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def _load_module():
|
|
@@ -193,6 +196,8 @@ def test_required_failure_exits_1_and_names_job():
|
|
|
193
196
|
outcome = watcher.watch(run_id="123")
|
|
194
197
|
assert outcome.result == wci.RESULT_FAILED
|
|
195
198
|
assert outcome.exit_code == 1
|
|
199
|
+
assert outcome.exit_reason == wci.EXIT_REASON_RUN_COMPLETED
|
|
200
|
+
assert outcome.run_completed is True
|
|
196
201
|
assert "Unit tests" in outcome.failing_jobs
|
|
197
202
|
# Signature captured from the failed-step log.
|
|
198
203
|
assert outcome.signature is not None
|
|
@@ -200,6 +205,30 @@ def test_required_failure_exits_1_and_names_job():
|
|
|
200
205
|
assert outcome.likely_infra is False
|
|
201
206
|
|
|
202
207
|
|
|
208
|
+
def test_required_failure_while_run_in_progress_has_distinct_fail_fast_exit():
|
|
209
|
+
"""Issue #1873: an early required-check failure is not a completed run.
|
|
210
|
+
|
|
211
|
+
The distinct exit code + cause prevent an on-call from treating the
|
|
212
|
+
watcher's exit as evidence that artifacts are final.
|
|
213
|
+
"""
|
|
214
|
+
gh = FakeGh()
|
|
215
|
+
jobs = _all_required_jobs("in_progress", None)
|
|
216
|
+
jobs[0] = _job("Unit tests", "completed", "failure", db_id=99)
|
|
217
|
+
gh.queue_run_state(status="in_progress", conclusion=None, jobs=jobs)
|
|
218
|
+
gh.log_text = "Unit tests\tRun pytest\t2026-07-29T10:00:00Z BUILD FAILED\n"
|
|
219
|
+
watcher, _ = _make_watcher(gh)
|
|
220
|
+
|
|
221
|
+
outcome = watcher.watch(run_id="123")
|
|
222
|
+
|
|
223
|
+
assert outcome.result == wci.RESULT_FAILED
|
|
224
|
+
assert outcome.exit_code == wci.EXIT_REQUIRED_CHECK_FAILED_FAST
|
|
225
|
+
assert outcome.exit_reason == wci.EXIT_REASON_REQUIRED_CHECK_FAILED_FAST
|
|
226
|
+
assert outcome.run_completed is False
|
|
227
|
+
summary = wci.render_human_summary(outcome)
|
|
228
|
+
assert "RUN IS STILL IN FLIGHT" in summary
|
|
229
|
+
assert "artifacts are not final" in summary
|
|
230
|
+
|
|
231
|
+
|
|
203
232
|
def test_cancelled_run_is_not_a_failure():
|
|
204
233
|
"""Issue #1650: `cancelled` is NOT a failure — it is a NON-verdict.
|
|
205
234
|
|
|
@@ -246,6 +275,8 @@ def test_no_progress_stall_exits_2_does_not_loop_forever():
|
|
|
246
275
|
outcome = watcher.watch(run_id="123")
|
|
247
276
|
assert outcome.result == wci.RESULT_HANG
|
|
248
277
|
assert outcome.exit_code == 2
|
|
278
|
+
assert outcome.exit_reason == wci.EXIT_REASON_HANG_DETECTED
|
|
279
|
+
assert outcome.run_completed is False
|
|
249
280
|
assert "no job-state progress" in outcome.reason
|
|
250
281
|
# Proven bounded: it stopped well before the wall-clock cap.
|
|
251
282
|
assert clock.now() < 200.0
|
|
@@ -269,12 +300,11 @@ def test_progress_then_stall_resets_no_progress_window():
|
|
|
269
300
|
assert outcome.exit_code == 2
|
|
270
301
|
|
|
271
302
|
|
|
272
|
-
# ──
|
|
303
|
+
# ── 7 wall-clock timeout ─────────────────────────────────────────────────────
|
|
273
304
|
|
|
274
305
|
|
|
275
|
-
def
|
|
276
|
-
"""A run that
|
|
277
|
-
cap and exits 2 — never waits forever even while 'progressing'."""
|
|
306
|
+
def test_over_wall_clock_exits_7():
|
|
307
|
+
"""A progressing run that never finishes gets the distinct wall-time code."""
|
|
278
308
|
gh = FakeGh()
|
|
279
309
|
# Each poll reports a DIFFERENT job state so the no-progress timer keeps
|
|
280
310
|
# resetting; only the wall-clock cap can stop it.
|
|
@@ -289,11 +319,49 @@ def test_over_wall_clock_exits_2():
|
|
|
289
319
|
)
|
|
290
320
|
outcome = watcher.watch(run_id="123")
|
|
291
321
|
assert outcome.result == wci.RESULT_HANG
|
|
292
|
-
assert outcome.exit_code ==
|
|
322
|
+
assert outcome.exit_code == wci.EXIT_WALL_CLOCK_TIMEOUT
|
|
323
|
+
assert outcome.exit_reason == wci.EXIT_REASON_WALL_CLOCK_TIMEOUT
|
|
293
324
|
assert "wall-clock" in outcome.reason
|
|
294
325
|
assert clock.now() >= 50.0
|
|
295
326
|
|
|
296
327
|
|
|
328
|
+
def test_single_job_rerun_defensively_raises_too_short_no_progress_timeout():
|
|
329
|
+
"""Issue #1873: a one-job rerun has no intermediate transitions.
|
|
330
|
+
|
|
331
|
+
A caller-provided 3000s timeout must not falsely classify a healthy
|
|
332
|
+
52-minute job as hung; detecting the one-job shape raises the effective
|
|
333
|
+
guard to the watcher-safe default.
|
|
334
|
+
"""
|
|
335
|
+
gh = FakeGh()
|
|
336
|
+
single_job = [_job("Unit tests", "in_progress", None, db_id=99)]
|
|
337
|
+
for _ in range(313):
|
|
338
|
+
gh.queue_run_state(
|
|
339
|
+
status="in_progress",
|
|
340
|
+
conclusion=None,
|
|
341
|
+
jobs=single_job,
|
|
342
|
+
)
|
|
343
|
+
gh.queue_run_state(
|
|
344
|
+
status="completed",
|
|
345
|
+
conclusion="success",
|
|
346
|
+
jobs=[_job("Unit tests", "completed", "success", db_id=99)],
|
|
347
|
+
)
|
|
348
|
+
notices: list[str] = []
|
|
349
|
+
watcher, clock = _make_watcher(
|
|
350
|
+
gh,
|
|
351
|
+
interval_s=10.0,
|
|
352
|
+
no_progress_timeout_s=3000.0,
|
|
353
|
+
max_wall_clock_s=10000.0,
|
|
354
|
+
notice=notices.append,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
outcome = watcher.watch(run_id="123")
|
|
358
|
+
|
|
359
|
+
assert clock.now() > 3000.0
|
|
360
|
+
assert outcome.result == wci.RESULT_GREEN
|
|
361
|
+
assert any("single-job rerun" in notice for notice in notices)
|
|
362
|
+
assert any("6000s" in notice for notice in notices)
|
|
363
|
+
|
|
364
|
+
|
|
297
365
|
# ── 3 unresolved ─────────────────────────────────────────────────────────────
|
|
298
366
|
|
|
299
367
|
|
|
@@ -567,6 +635,39 @@ def test_help_documents_flags(capsys):
|
|
|
567
635
|
assert flag in help_text
|
|
568
636
|
|
|
569
637
|
|
|
638
|
+
def test_help_states_exact_wall_clock_exit_and_json_field_relationship():
|
|
639
|
+
"""Reviewer follow-up: public wording is part of the exit contract.
|
|
640
|
+
|
|
641
|
+
Merely asserting that the flags exist let contradictory exit-2/exit-7 help
|
|
642
|
+
stay green. Assert the option and JSON-field contract at their sources so
|
|
643
|
+
argparse wrapping cannot weaken the check.
|
|
644
|
+
"""
|
|
645
|
+
parser = wci.build_arg_parser()
|
|
646
|
+
max_wall_clock = next(
|
|
647
|
+
action
|
|
648
|
+
for action in parser._actions
|
|
649
|
+
if "--max-wall-clock" in action.option_strings
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
assert max_wall_clock.help == (
|
|
653
|
+
"Exit 7 (wall-clock timeout) if the run exceeds this many seconds total."
|
|
654
|
+
)
|
|
655
|
+
assert (
|
|
656
|
+
"The JSON result is the CI semantic outcome; exit_code and exit_reason "
|
|
657
|
+
"identify why watching stopped."
|
|
658
|
+
) in parser.description
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def test_module_header_distinguishes_semantic_result_from_termination_contract():
|
|
662
|
+
"""The module-level public contract must not equate result with exit cause."""
|
|
663
|
+
assert (
|
|
664
|
+
"Final JSON contract: `result` is the semantic CI outcome; `exit_code` "
|
|
665
|
+
"and `exit_reason` identify why the watcher stopped, and `run_completed` "
|
|
666
|
+
"says whether artifacts are final."
|
|
667
|
+
) in wci.__doc__
|
|
668
|
+
assert "Exit-code contract (also the final JSON's `result`)" not in wci.__doc__
|
|
669
|
+
|
|
670
|
+
|
|
570
671
|
def test_main_prints_compact_summary_and_json(monkeypatch, capsys):
|
|
571
672
|
"""main() should print a human summary + a final parseable JSON line and
|
|
572
673
|
return the contract exit code, with stdout staying compact."""
|
|
@@ -589,6 +690,107 @@ def test_main_prints_compact_summary_and_json(monkeypatch, capsys):
|
|
|
589
690
|
assert "required" in payload
|
|
590
691
|
|
|
591
692
|
|
|
693
|
+
def test_main_writes_final_summary_and_json_to_log_file(monkeypatch, tmp_path, capsys):
|
|
694
|
+
"""Issue #1873 owner follow-up: stdout may disappear with its wrapper.
|
|
695
|
+
|
|
696
|
+
The durable log must therefore contain the exact final human summary and
|
|
697
|
+
machine-readable JSON verdict, not only polling heartbeats.
|
|
698
|
+
"""
|
|
699
|
+
gh = FakeGh()
|
|
700
|
+
gh.queue_run_state(status="completed", conclusion="success", jobs=_all_required_jobs())
|
|
701
|
+
monkeypatch.setattr(wci, "GhRunner", lambda *a, **k: gh)
|
|
702
|
+
log_path = tmp_path / "watch.log"
|
|
703
|
+
|
|
704
|
+
rc = wci.main(
|
|
705
|
+
["--run-id", "123", "--quiet", "--log-file", str(log_path)]
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
assert rc == 0
|
|
709
|
+
stdout = capsys.readouterr().out.strip()
|
|
710
|
+
durable = log_path.read_text(encoding="utf-8")
|
|
711
|
+
assert stdout in durable
|
|
712
|
+
payload = json.loads(durable.strip().splitlines()[-1])
|
|
713
|
+
assert payload["result"] == "green"
|
|
714
|
+
assert payload["exit_reason"] == wci.EXIT_REASON_RUN_COMPLETED
|
|
715
|
+
assert payload["run_completed"] is True
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class _LostLauncherStream:
|
|
719
|
+
"""A systemd-run --pipe stream whose launcher-side reader disappeared."""
|
|
720
|
+
|
|
721
|
+
def write(self, _text):
|
|
722
|
+
raise BrokenPipeError("launcher pipe is gone")
|
|
723
|
+
|
|
724
|
+
def flush(self):
|
|
725
|
+
raise BrokenPipeError("launcher pipe is gone")
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def test_main_finishes_durable_contract_after_launcher_streams_disappear(
|
|
729
|
+
monkeypatch, tmp_path
|
|
730
|
+
):
|
|
731
|
+
"""Issue #1873 recurrence: console loss must not abort the durable verdict.
|
|
732
|
+
|
|
733
|
+
The live watcher survived its launching shell, then died with status 120 on
|
|
734
|
+
a later state-change write. Reproduce both lost streams before the first
|
|
735
|
+
notice and require the log-file contract to finish independently.
|
|
736
|
+
"""
|
|
737
|
+
gh = FakeGh()
|
|
738
|
+
gh.queue_run_state(
|
|
739
|
+
status="in_progress",
|
|
740
|
+
conclusion=None,
|
|
741
|
+
jobs=_all_required_jobs("in_progress", None),
|
|
742
|
+
)
|
|
743
|
+
gh.queue_run_state(
|
|
744
|
+
status="completed",
|
|
745
|
+
conclusion="success",
|
|
746
|
+
jobs=_all_required_jobs(),
|
|
747
|
+
)
|
|
748
|
+
monkeypatch.setattr(wci, "GhRunner", lambda *a, **k: gh)
|
|
749
|
+
monkeypatch.setattr(wci.sys, "stdout", _LostLauncherStream())
|
|
750
|
+
monkeypatch.setattr(wci.sys, "stderr", _LostLauncherStream())
|
|
751
|
+
log_path = tmp_path / "watch.log"
|
|
752
|
+
|
|
753
|
+
rc = wci.main(
|
|
754
|
+
[
|
|
755
|
+
"--run-id",
|
|
756
|
+
"123",
|
|
757
|
+
"--interval",
|
|
758
|
+
"0",
|
|
759
|
+
"--log-file",
|
|
760
|
+
str(log_path),
|
|
761
|
+
]
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
assert rc == 0
|
|
765
|
+
payload = json.loads(log_path.read_text(encoding="utf-8").strip().splitlines()[-1])
|
|
766
|
+
assert payload["result"] == "green"
|
|
767
|
+
assert payload["exit_reason"] == wci.EXIT_REASON_RUN_COMPLETED
|
|
768
|
+
assert payload["run_completed"] is True
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def test_oncall_recipe_preserves_verdict_and_requires_completed_confirmation():
|
|
772
|
+
"""Issue #1873: the durable operating instructions must close both traps."""
|
|
773
|
+
prompt = _ONCALL_PROMPT_PATH.read_text(encoding="utf-8")
|
|
774
|
+
|
|
775
|
+
assert "Do not add `--collect`" in prompt
|
|
776
|
+
assert "--log-file" in prompt
|
|
777
|
+
assert "systemctl --user reset-failed" in prompt
|
|
778
|
+
assert "Always confirm\n`status=completed`" in prompt
|
|
779
|
+
assert "required_check_failed_fast" in prompt
|
|
780
|
+
assert "artifacts are not final" in prompt
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def test_oncall_systemd_recipe_does_not_bind_watcher_output_to_launcher_pipe():
|
|
784
|
+
"""Issue #1873 recurrence: the transient service must own its output sink."""
|
|
785
|
+
prompt = _ONCALL_PROMPT_PATH.read_text(encoding="utf-8")
|
|
786
|
+
recipe = prompt.split("```bash", 2)[2].split("```", 1)[0]
|
|
787
|
+
|
|
788
|
+
assert "systemd-run --user" in recipe
|
|
789
|
+
assert "--wait" in recipe
|
|
790
|
+
assert "--pipe" not in recipe
|
|
791
|
+
assert "stdout and stderr go to the unit journal" in prompt
|
|
792
|
+
|
|
793
|
+
|
|
592
794
|
def test_render_human_summary_under_25_lines_on_failure():
|
|
593
795
|
outcome = wci.WatchOutcome(
|
|
594
796
|
result=wci.RESULT_FAILED,
|
|
@@ -601,6 +803,8 @@ def test_render_human_summary_under_25_lines_on_failure():
|
|
|
601
803
|
run_id="123",
|
|
602
804
|
polls=4,
|
|
603
805
|
elapsed_s=42.0,
|
|
806
|
+
exit_reason=wci.EXIT_REASON_RUN_COMPLETED,
|
|
807
|
+
run_completed=True,
|
|
604
808
|
)
|
|
605
809
|
text = wci.render_human_summary(outcome)
|
|
606
810
|
assert text.count("\n") < 25
|
|
@@ -526,7 +526,7 @@ def test_healthy_long_running_emulator_shard_is_not_a_hang():
|
|
|
526
526
|
assert outcome.exit_code == 0
|
|
527
527
|
|
|
528
528
|
|
|
529
|
-
# ── The exit-code contract stays
|
|
529
|
+
# ── The exit-code contract stays complete and distinct ───────────────────────
|
|
530
530
|
|
|
531
531
|
|
|
532
532
|
def test_exit_code_contract_is_complete_and_distinct():
|
|
@@ -537,6 +537,15 @@ def test_exit_code_contract_is_complete_and_distinct():
|
|
|
537
537
|
assert codes[wci.RESULT_UNRESOLVED] == 3
|
|
538
538
|
assert codes[wci.RESULT_SUPERSEDED] == 4
|
|
539
539
|
assert codes[wci.RESULT_NO_VERDICT] == 5
|
|
540
|
+
assert wci.EXIT_REQUIRED_CHECK_FAILED_FAST == 6
|
|
541
|
+
assert wci.EXIT_WALL_CLOCK_TIMEOUT == 7
|
|
542
|
+
assert len(
|
|
543
|
+
{
|
|
544
|
+
*codes.values(),
|
|
545
|
+
wci.EXIT_REQUIRED_CHECK_FAILED_FAST,
|
|
546
|
+
wci.EXIT_WALL_CLOCK_TIMEOUT,
|
|
547
|
+
}
|
|
548
|
+
) == 8
|
|
540
549
|
# Every result must render a headline (no KeyError on a new verdict).
|
|
541
550
|
for result in codes:
|
|
542
551
|
outcome = wci.WatchOutcome(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|