Add the task log: recent tasks with status, Enter shows the log
CI / pytest and ruff (pull_request) Successful in 4m10s
CI / pytest and ruff (push) Successful in 4m32s

l on the main screen opens "Tasks", the recent cluster tasks newest
first. l on the info screen shows only that guest's tasks. Columns:
start, duration (or "running"), node, type, VMID ("-" for node
tasks), user and status. A failed task gets a "!" in the leading
column. Enter opens the task's log in a plain-text widget, so the
output is never parsed as markup.

`task list -o json` has no start or end times, so the list comes from
`/cluster/tasks`. The upid is validated (it must start with "UPID:")
before it goes in as a positional argument.

Review fixes:
- A finished task without a start time no longer crashes the app; its
  duration shows "-".
- "running" now means there is no endtime. A finished task with an
  empty status shows "unknown" and is flagged.

Read-only live check: 25 tasks listed, and the log of the newest
(vzdelsnapshot 104) ends with TASK OK.

Closes #10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #25.
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-03 00:03:58 +01:00
1 parent eec65e0585
commit 1ed5e5069b
6 files changed
+546

No files matched your search

+45
View File
@@ -1295,3 +1295,48 @@ def test_take_snapshot_refuses_description_that_looks_like_an_option(fake):
{"vmid": 104, "node": "mainframe", "type": "lxc"}, "snap1", "-y"
)
assert fake.calls == []
# --- issue #10: task log (spec: "Task log (issue #10)" > cli.py) ------------------------
UPID = "UPID:mainframe:0001:00A:68F:qmstart:108:root@pam:"
def test_tasks_argv_is_cluster_tasks_json(fake): # tasks(): api get /cluster/tasks
fake.result = ("[]", "", 0)
assert cli.tasks() == []
assert fake.argv == ["cv4pve-cli", "api", "get", "/cluster/tasks", "-o", "json"]
def test_tasks_newest_first_including_running_without_endtime(
fake,
): # tasks(): newest first
raw = [
{"upid": "a", "starttime": 100, "endtime": 150, "status": "OK"},
{"upid": "c", "starttime": 300}, # running: no endtime, no status
{"upid": "b", "starttime": 200, "endtime": None},
]
fake.result = (json.dumps(raw), "", 0)
assert [t["upid"] for t in cli.tasks()] == ["c", "b", "a"]
def test_tasks_failure_raises_clierror(monkeypatch): # failure surfaces as CliError
FakeRun(monkeypatch, "Error: boom", returncode=1)
with pytest.raises(cli.CliError):
cli.tasks()
def test_task_log_argv_and_returns_text(fake): # task_log(): task log <upid>
fake.result = ("starting\nTASK OK\n", "", 0)
out = cli.task_log(UPID)
assert fake.argv == ["cv4pve-cli", "task", "log", UPID]
assert "TASK OK" in out
@pytest.mark.parametrize("bad", ["-y", "--help", "", "108", "x UPID:a"])
def test_task_log_rejects_non_upid_without_calling_subprocess(
fake, bad
): # ValueError unless UPID:
with pytest.raises(ValueError):
cli.task_log(bad)
assert fake.calls == []