feat(a2): port leaf PS scripts to ci_orchestrator CLI

Implements Phase A2 of plans/implementation-plan-A-B.md:

- commands/wait.py    -> wait-ready (replaces Wait-VMReady.ps1)

- commands/vm.py      -> vm remove + vm cleanup (replaces Remove-BuildVM.ps1, Cleanup-OrphanedBuildVMs.ps1); cleanup accepts injected VmBackend (Phase C ESXi hook preserved)

- commands/monitor.py -> monitor disk + monitor runner (replaces Watch-DiskSpace.ps1, Watch-RunnerHealth.ps1)

- commands/report.py  -> report job (replaces Get-CIJobSummary.ps1)

Each PS script reduced to a 3-line shim that delegates to the Python CLI and preserves \0.

Tests: 69 pytest cases across new test_commands_*.py modules; ruff clean, mypy --strict clean, coverage 74.5% (>=70 gate).
This commit is contained in:
2026-05-14 17:01:15 +02:00
parent 10da8f4e81
commit 80f6661ad5
17 changed files with 2074 additions and 881 deletions
+6 -5
View File
@@ -12,6 +12,7 @@ import pytest
from click.testing import CliRunner
import ci_orchestrator.__main__ as cli_module
import ci_orchestrator.commands.wait as wait_module
from ci_orchestrator import __version__
from ci_orchestrator.backends.protocol import VmHandle
from ci_orchestrator.credentials import Credential
@@ -54,11 +55,11 @@ class _FakeStore:
def _patch_common(monkeypatch: pytest.MonkeyPatch, backend: _FakeBackend) -> None:
monkeypatch.setattr(cli_module, "load_backend", lambda _cfg: backend)
monkeypatch.setattr(cli_module, "KeyringCredentialStore", lambda: _FakeStore())
monkeypatch.setattr(cli_module, "WinRmTransport", _FakeTransport)
monkeypatch.setattr(cli_module, "SshTransport", _FakeTransport)
monkeypatch.setattr(cli_module.time, "sleep", lambda _s: None)
monkeypatch.setattr(wait_module, "load_backend", lambda _cfg: backend)
monkeypatch.setattr(wait_module, "KeyringCredentialStore", lambda: _FakeStore())
monkeypatch.setattr(wait_module, "WinRmTransport", _FakeTransport)
monkeypatch.setattr(wait_module, "SshTransport", _FakeTransport)
monkeypatch.setattr(wait_module.time, "sleep", lambda _s: None)
def test_help_lists_wait_ready() -> None:
+152
View File
@@ -0,0 +1,152 @@
"""Tests for ``ci_orchestrator monitor disk`` and ``monitor runner``."""
from __future__ import annotations
import json
import shutil
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import pytest
from click.testing import CliRunner
import ci_orchestrator.commands.monitor as mon
from ci_orchestrator.__main__ import cli
@pytest.fixture(autouse=True)
def _no_event_log_or_webhook(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(mon, "_write_windows_event", lambda *_a, **_kw: True)
monkeypatch.setattr(mon, "_post_webhook", lambda *_a, **_kw: True)
# ── monitor disk ───────────────────────────────────────────────────────────
def _fake_usage(free_gb: float, total_gb: float = 1000.0) -> shutil._ntuple_diskusage:
gb = 1024 ** 3
return shutil._ntuple_diskusage(int(total_gb * gb), int((total_gb - free_gb) * gb), int(free_gb * gb))
def test_disk_ok_exits_zero(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(mon.shutil, "disk_usage", lambda _p: _fake_usage(free_gb=200))
result = CliRunner().invoke(cli, ["monitor", "disk", "--min-free-gb", "50"])
assert result.exit_code == 0, result.output
assert "OK" in result.output
def test_disk_alert_exits_one(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(mon.shutil, "disk_usage", lambda _p: _fake_usage(free_gb=10))
result = CliRunner().invoke(cli, ["monitor", "disk", "--min-free-gb", "50"])
assert result.exit_code == 1
assert "below" in result.output or "WARNING" in result.output
def test_disk_drive_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
def _raise(_p: Any) -> Any:
raise FileNotFoundError("nope")
monkeypatch.setattr(mon.shutil, "disk_usage", _raise)
result = CliRunner().invoke(cli, ["monitor", "disk", "--drive-letter", "Z"])
assert result.exit_code == 2
assert "not found" in result.output
def test_disk_json_output_ok(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(mon.shutil, "disk_usage", lambda _p: _fake_usage(free_gb=200))
result = CliRunner().invoke(
cli, ["monitor", "disk", "--min-free-gb", "50", "--json"]
)
assert result.exit_code == 0
payload = json.loads(result.output.strip())
assert payload["status"] == "ok"
assert payload["free_gb"] == 200.0
def test_disk_webhook_invoked_on_alert(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, str]] = []
monkeypatch.setattr(mon, "_post_webhook", lambda url, content, **_kw: calls.append((url, content)) or True)
monkeypatch.setattr(mon.shutil, "disk_usage", lambda _p: _fake_usage(free_gb=10))
result = CliRunner().invoke(
cli,
[
"monitor",
"disk",
"--min-free-gb",
"50",
"--webhook-url",
"https://example.invalid/hook",
],
)
assert result.exit_code == 1
assert calls and calls[0][0] == "https://example.invalid/hook"
# ── monitor runner ─────────────────────────────────────────────────────────
def test_runner_running_exits_zero(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(mon, "_service_status", lambda _s: "Running")
result = CliRunner().invoke(
cli,
["monitor", "runner", "--state-dir", str(tmp_path)],
)
assert result.exit_code == 0
assert "Running" in result.output
def test_runner_not_installed_exits_two(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(mon, "_service_status", lambda _s: None)
result = CliRunner().invoke(
cli, ["monitor", "runner", "--state-dir", str(tmp_path)]
)
assert result.exit_code == 2
assert "not found" in result.output
def test_runner_maintenance_flag_skips(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
flag = tmp_path / "runner-maintenance.flag"
flag.write_text("")
result = CliRunner().invoke(
cli, ["monitor", "runner", "--state-dir", str(tmp_path)]
)
assert result.exit_code == 0
assert "maintenance mode" in result.output
def test_runner_restart_attempted_when_stopped(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
statuses = iter(["Stopped", "Running"])
monkeypatch.setattr(mon, "_service_status", lambda _s: next(statuses))
monkeypatch.setattr(mon, "_restart_service", lambda _s: True)
result = CliRunner().invoke(
cli, ["monitor", "runner", "--state-dir", str(tmp_path)]
)
assert result.exit_code == 0
assert "restarted" in result.output
assert (tmp_path / "runner-restart-log.json").is_file()
def test_runner_rate_limit_exits_one(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# Pre-fill the cooldown log with recent restarts up to the limit.
recent = [datetime.now(tz=UTC).isoformat()] * 3
(tmp_path / "runner-restart-log.json").write_text(json.dumps(recent))
monkeypatch.setattr(mon, "_service_status", lambda _s: "Stopped")
monkeypatch.setattr(mon, "_restart_service", lambda _s: True)
result = CliRunner().invoke(
cli,
[
"monitor",
"runner",
"--state-dir",
str(tmp_path),
"--max-restarts",
"3",
],
)
assert result.exit_code == 1
assert "limit" in result.output
+108
View File
@@ -0,0 +1,108 @@
"""Tests for ``ci_orchestrator report job``."""
from __future__ import annotations
import json
from pathlib import Path
from click.testing import CliRunner
from ci_orchestrator.__main__ import cli
def _write_jsonl(path: Path, events: list[dict[str, object]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(json.dumps(e) for e in events) + "\n", encoding="utf-8")
def _success_events(job_id: str = "run-1") -> list[dict[str, object]]:
return [
{"jobId": job_id, "phase": "job", "status": "start", "ts": "2026-05-14T10:00:00Z"},
{"jobId": job_id, "phase": "phase4.wait-ready", "status": "success", "ts": "2026-05-14T10:01:00Z"},
{
"jobId": job_id,
"phase": "job",
"status": "success",
"ts": "2026-05-14T10:05:00Z",
"data": {"elapsedSec": 305},
},
]
def _failure_events(job_id: str = "run-2") -> list[dict[str, object]]:
return [
{"jobId": job_id, "phase": "job", "status": "start", "ts": "2026-05-14T11:00:00Z"},
{
"jobId": job_id,
"phase": "job",
"status": "failure",
"ts": "2026-05-14T11:00:30Z",
"data": {"elapsedSec": 30, "error": "Clone failed: source not found"},
},
]
def test_report_job_summary_table(tmp_path: Path) -> None:
_write_jsonl(tmp_path / "run-1" / "invoke-ci.jsonl", _success_events("run-1"))
_write_jsonl(tmp_path / "run-2" / "invoke-ci.jsonl", _failure_events("run-2"))
result = CliRunner().invoke(cli, ["report", "job", "--log-dir", str(tmp_path)])
assert result.exit_code == 0, result.output
assert "run-1" in result.output
assert "run-2" in result.output
assert "success" in result.output
assert "FAILED" in result.output
def test_report_job_failed_filter(tmp_path: Path) -> None:
_write_jsonl(tmp_path / "run-1" / "invoke-ci.jsonl", _success_events("run-1"))
_write_jsonl(tmp_path / "run-2" / "invoke-ci.jsonl", _failure_events("run-2"))
result = CliRunner().invoke(
cli, ["report", "job", "--log-dir", str(tmp_path), "--failed"]
)
assert result.exit_code == 0, result.output
assert "run-1" not in result.output
assert "run-2" in result.output
def test_report_job_detail_view(tmp_path: Path) -> None:
_write_jsonl(tmp_path / "run-2" / "invoke-ci.jsonl", _failure_events("run-2"))
result = CliRunner().invoke(
cli,
["report", "job", "--log-dir", str(tmp_path), "--job-id", "run-2"],
)
assert result.exit_code == 0, result.output
assert "Job: run-2" in result.output
assert "Clone failed" in result.output
def test_report_job_missing_log_dir(tmp_path: Path) -> None:
missing = tmp_path / "nope"
result = CliRunner().invoke(cli, ["report", "job", "--log-dir", str(missing)])
assert result.exit_code == 1
assert "not found" in result.output
def test_report_job_no_logs(tmp_path: Path) -> None:
result = CliRunner().invoke(cli, ["report", "job", "--log-dir", str(tmp_path)])
assert result.exit_code == 0
assert "No invoke-ci.jsonl" in result.output
def test_report_job_json_output(tmp_path: Path) -> None:
_write_jsonl(tmp_path / "run-1" / "invoke-ci.jsonl", _success_events("run-1"))
result = CliRunner().invoke(
cli, ["report", "job", "--log-dir", str(tmp_path), "--json"]
)
assert result.exit_code == 0
payload = json.loads(result.output.strip())
assert isinstance(payload, list)
assert payload[0]["jobId"] == "run-1"
assert payload[0]["status"] == "success"
def test_report_job_unknown_id(tmp_path: Path) -> None:
result = CliRunner().invoke(
cli, ["report", "job", "--log-dir", str(tmp_path), "--job-id", "ghost"]
)
assert result.exit_code == 1
assert "No JSONL log" in result.output
+211
View File
@@ -0,0 +1,211 @@
"""Tests for ``ci_orchestrator vm remove`` and ``vm cleanup``.
Migrated from Pester ``tests/Remove-BuildVM.Tests.ps1`` (now removed).
"""
from __future__ import annotations
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
import pytest
from click.testing import CliRunner
import ci_orchestrator.commands.vm as vm_module
from ci_orchestrator.__main__ import cli
from ci_orchestrator.backends.errors import BackendOperationFailed
from ci_orchestrator.backends.protocol import VmHandle
class _FakeBackend:
def __init__(
self,
*,
running_after_stop: bool = False,
delete_fails_first: int = 0,
) -> None:
self.calls: list[tuple[str, Any]] = []
self._running = True
self._running_after_stop = running_after_stop
self._delete_fails_remaining = delete_fails_first
def stop(self, handle: VmHandle, hard: bool = False) -> None:
self.calls.append(("stop", hard))
if not hard or not self._running_after_stop:
self._running = False
def is_running(self, _h: VmHandle) -> bool:
return self._running
def delete(self, _h: VmHandle) -> None:
self.calls.append(("delete", None))
if self._delete_fails_remaining > 0:
self._delete_fails_remaining -= 1
raise BackendOperationFailed("deleteVM", 1, "transient")
@pytest.fixture(autouse=True)
def _no_sleep(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(time, "sleep", lambda _s: None)
monkeypatch.setattr(vm_module.time, "sleep", lambda _s: None)
def _make_clone_dir(tmp_path: Path, name: str = "clone") -> tuple[Path, Path]:
clone_dir = tmp_path / name
clone_dir.mkdir()
vmx = clone_dir / f"{name}.vmx"
vmx.write_text('config.version = "8"', encoding="utf-8")
return clone_dir, vmx
# ── vm remove ──────────────────────────────────────────────────────────────
def test_vm_remove_missing_vmx_returns_without_error(tmp_path: Path) -> None:
"""Pester migration: returns without error when VMX does not exist."""
missing = tmp_path / "ghost" / "ghost.vmx"
result = CliRunner().invoke(cli, ["vm", "remove", "--vmx", str(missing)])
assert result.exit_code == 0, result.output
assert "nothing to remove" in result.output
def test_vm_remove_partial_clone_dir_is_removed(tmp_path: Path) -> None:
"""Pester migration: partial clone dir without VMX is still removed."""
clone_dir, vmx = _make_clone_dir(tmp_path, "partial")
vmx.unlink() # leave the directory but no VMX
result = CliRunner().invoke(cli, ["vm", "remove", "--vmx", str(vmx)])
assert result.exit_code == 0
assert not clone_dir.exists()
def test_vm_remove_happy_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""Pester migration: removes the clone dir when VMX exists and stop+delete succeed."""
clone_dir, vmx = _make_clone_dir(tmp_path, "live")
backend = _FakeBackend()
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
result = CliRunner().invoke(
cli, ["vm", "remove", "--vmx", str(vmx), "--graceful-timeout", "1"]
)
assert result.exit_code == 0, result.output
assert ("delete", None) in backend.calls
assert not clone_dir.exists()
def test_vm_remove_force_skips_soft_stop(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_, vmx = _make_clone_dir(tmp_path, "force")
backend = _FakeBackend()
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
result = CliRunner().invoke(cli, ["vm", "remove", "--vmx", str(vmx), "--force"])
assert result.exit_code == 0, result.output
# No soft stop was issued.
soft_stops = [c for c in backend.calls if c == ("stop", False)]
assert soft_stops == []
hard_stops = [c for c in backend.calls if c == ("stop", True)]
assert hard_stops != []
def test_vm_remove_delete_retry_succeeds(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
clone_dir, vmx = _make_clone_dir(tmp_path, "retry")
backend = _FakeBackend(delete_fails_first=2)
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
result = CliRunner().invoke(cli, ["vm", "remove", "--vmx", str(vmx)])
assert result.exit_code == 0
delete_calls = [c for c in backend.calls if c[0] == "delete"]
assert len(delete_calls) == 3 # 2 failures + 1 success
assert not clone_dir.exists()
# ── vm cleanup ─────────────────────────────────────────────────────────────
def _age_dir(path: Path, hours: float) -> None:
ts = (datetime.now() - timedelta(hours=hours)).timestamp()
import os
os.utime(path, (ts, ts))
def test_vm_cleanup_dry_run_lists_orphans(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
base = tmp_path / "build-vms"
base.mkdir()
fresh, _ = _make_clone_dir(base, "fresh")
old, _ = _make_clone_dir(base, "old")
_age_dir(old, hours=10)
_age_dir(fresh, hours=0)
# Make sure backend isn't constructed (vmrun absent on test host).
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: None)
result = CliRunner().invoke(
cli,
[
"vm",
"cleanup",
"--clone-base-dir",
str(base),
"--max-age-hours",
"4",
"--what-if",
],
)
assert result.exit_code == 0, result.output
assert "would destroy" in result.output
assert old.exists() # dry run did NOT delete
assert fresh.exists()
def test_vm_cleanup_destroys_old_dirs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
base = tmp_path / "build-vms"
base.mkdir()
old, _ = _make_clone_dir(base, "old")
_age_dir(old, hours=10)
backend = _FakeBackend()
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
result = CliRunner().invoke(
cli,
[
"vm",
"cleanup",
"--clone-base-dir",
str(base),
"--max-age-hours",
"4",
],
)
assert result.exit_code == 0, result.output
assert not old.exists()
# Backend was used to stop+delete.
assert any(c[0] == "delete" for c in backend.calls)
def test_vm_cleanup_handles_missing_base_dir(tmp_path: Path) -> None:
missing = tmp_path / "does-not-exist"
result = CliRunner().invoke(
cli, ["vm", "cleanup", "--clone-base-dir", str(missing)]
)
assert result.exit_code == 0
assert "nothing to do" in result.output
def test_vm_cleanup_removes_stale_lock(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
base = tmp_path / "bv"
base.mkdir()
lock = tmp_path / "vm-start.lock"
lock.write_text("x")
_age_dir(lock, hours=1)
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: None)
result = CliRunner().invoke(
cli,
[
"vm",
"cleanup",
"--clone-base-dir",
str(base),
"--lock-file",
str(lock),
],
)
assert result.exit_code == 0
assert not lock.exists()
+189
View File
@@ -0,0 +1,189 @@
"""Tests for ``ci_orchestrator wait-ready``.
Migrated from Pester ``tests/Wait-VMReady.Tests.ps1`` (now removed).
Preserves the negative cases:
* missing/invalid VMX → command surfaces a backend error path
* never-becomes-ready → exit 2 with timeout message
* IP override skips the get_ip polling step
"""
from __future__ import annotations
from typing import Any
import pytest
from click.testing import CliRunner
import ci_orchestrator.commands.wait as wait_module
from ci_orchestrator.__main__ import cli
from ci_orchestrator.backends.protocol import VmHandle
from ci_orchestrator.credentials import Credential
class _FakeBackend:
def __init__(self, *, running: bool = True, ip: str | None = "10.0.0.42") -> None:
self._running = running
self._ip = ip
self.running_calls = 0
def is_running(self, _h: VmHandle) -> bool:
self.running_calls += 1
return self._running
def get_ip(self, _h: VmHandle, timeout: float = 0.0) -> str | None:
return self._ip
class _FakeTransport:
def __init__(self, *_a: Any, **_kw: Any) -> None:
pass
def __enter__(self) -> _FakeTransport:
return self
def __exit__(self, *_e: object) -> None:
pass
def is_ready(self) -> bool:
return True
class _BrokenTransport(_FakeTransport):
def is_ready(self) -> bool:
return False
class _FakeStore:
def get(self, _target: str) -> Credential:
return Credential("user", "pwd")
def _patch(monkeypatch: pytest.MonkeyPatch, backend: _FakeBackend, transport: type = _FakeTransport) -> None:
monkeypatch.setattr(wait_module, "load_backend", lambda _cfg: backend)
monkeypatch.setattr(wait_module, "KeyringCredentialStore", lambda: _FakeStore())
monkeypatch.setattr(wait_module, "WinRmTransport", transport)
monkeypatch.setattr(wait_module, "SshTransport", transport)
monkeypatch.setattr(wait_module.time, "sleep", lambda _s: None)
# ── happy paths ────────────────────────────────────────────────────────────
def test_wait_ready_windows_via_transport_alias(monkeypatch: pytest.MonkeyPatch) -> None:
"""``--transport WinRM`` is a PS-shim alias of ``--guest-os windows``."""
_patch(monkeypatch, _FakeBackend())
result = CliRunner().invoke(
cli,
["wait-ready", "--vmx", "x.vmx", "--transport", "WinRM", "--timeout", "1"],
)
assert result.exit_code == 0, result.output
assert "WinRM ready" in result.output
def test_wait_ready_skip_ping_is_no_op(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch, _FakeBackend())
result = CliRunner().invoke(
cli,
["wait-ready", "--vmx", "x.vmx", "--skip-ping", "--timeout", "1"],
)
assert result.exit_code == 0, result.output
def test_wait_ready_ip_override_skips_get_ip(monkeypatch: pytest.MonkeyPatch) -> None:
"""When --ip-address is provided, the backend is not polled for an IP."""
backend = _FakeBackend(running=True, ip=None) # would time out at IP step
_patch(monkeypatch, backend)
result = CliRunner().invoke(
cli,
[
"wait-ready",
"--vmx",
"x.vmx",
"--ip-address",
"10.0.0.55",
"--timeout",
"1",
],
)
assert result.exit_code == 0, result.output
assert "10.0.0.55" in result.output
def test_wait_ready_linux_via_ssh_alias(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch, _FakeBackend())
result = CliRunner().invoke(
cli,
["wait-ready", "--vmx", "x.vmx", "--transport", "SSH", "--timeout", "1"],
)
assert result.exit_code == 0, result.output
assert "SSH ready" in result.output
# ── failure paths (Pester migration) ───────────────────────────────────────
def test_wait_ready_timeout_when_vm_never_running(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch, _FakeBackend(running=False))
result = CliRunner().invoke(
cli,
[
"wait-ready",
"--vmx",
"x.vmx",
"--timeout",
"0.01",
"--poll-interval",
"0.001",
],
)
assert result.exit_code == 2
assert "timeout waiting for VM" in result.output
def test_wait_ready_timeout_no_ip(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch, _FakeBackend(running=True, ip=None))
result = CliRunner().invoke(
cli,
[
"wait-ready",
"--vmx",
"x.vmx",
"--timeout",
"0.01",
"--poll-interval",
"0.001",
],
)
assert result.exit_code == 3
assert "timeout waiting for guest IP" in result.output
def test_wait_ready_timeout_transport_never_ready(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch, _FakeBackend(), transport=_BrokenTransport)
result = CliRunner().invoke(
cli,
[
"wait-ready",
"--vmx",
"x.vmx",
"--ip-address",
"10.0.0.55",
"--timeout",
"0.01",
"--poll-interval",
"0.001",
],
)
assert result.exit_code == 4
assert "transport to be ready" in result.output
def test_wait_ready_invalid_transport_value(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch, _FakeBackend())
result = CliRunner().invoke(
cli,
["wait-ready", "--vmx", "x.vmx", "--transport", "bogus"],
)
assert result.exit_code != 0
assert "Unknown" in result.output or "bogus" in result.output