mirror of
https://github.com/usestrix/strix.git
synced 2026-10-05 02:41:38 +00:00
fix(cli): verify the model before the TUI opens on a direct launch
This commit is contained in:
parent
d218c29ed1
commit
5325009f65
5 changed files with 72 additions and 72 deletions
|
|
@ -392,11 +392,10 @@ def _print_model_connection_error(exc: BaseException, model_name: str) -> None:
|
|||
|
||||
|
||||
def _bootstrap_scan(args: argparse.Namespace) -> None:
|
||||
"""Warm up the model and prepare the run for a non-interactive scan.
|
||||
"""Warm up the model and prepare the run before the interface starts.
|
||||
|
||||
Interactive launches skip this: the model preflight and run preparation
|
||||
happen inside the TUI so the interface paints immediately instead of
|
||||
waiting on a model round trip.
|
||||
Start-screen launches skip this: they verify the model and prepare the
|
||||
run once the user has entered a target.
|
||||
"""
|
||||
set_scan_phase("preflight")
|
||||
try:
|
||||
|
|
@ -483,7 +482,7 @@ def main() -> None:
|
|||
# Everything below imports the scan engine; do not race the warm-up thread.
|
||||
wait_for_import_warmup()
|
||||
|
||||
if args.non_interactive:
|
||||
if args.non_interactive or not args.needs_setup:
|
||||
_bootstrap_scan(args)
|
||||
|
||||
from strix.report.state import get_global_report_state
|
||||
|
|
|
|||
|
|
@ -152,11 +152,6 @@ class TuiController:
|
|||
]
|
||||
self.notify_changed()
|
||||
|
||||
def begin_preparation(self) -> None:
|
||||
"""Mark a directly-launched run as preparing behind the live TUI."""
|
||||
self.scan_state = "preparing"
|
||||
self.notify_changed()
|
||||
|
||||
def fail_preparation(self, detail: str) -> None:
|
||||
self.scan_state = "failed"
|
||||
self.error = detail
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ class GoTuiRuntime:
|
|||
async def check_setup_model(self) -> None:
|
||||
"""Verify the model route as soon as the start screen is up.
|
||||
|
||||
The same round trip a direct launch makes in prepare_and_start, run in
|
||||
the background so the screen paints first and the outcome lands in the
|
||||
The same round trip main() makes before a direct launch, run in the
|
||||
background so the screen paints first and the outcome lands in the
|
||||
setup log before the user has finished typing.
|
||||
"""
|
||||
if not (load_settings().llm.model or "").strip():
|
||||
|
|
@ -162,13 +162,14 @@ class GoTuiRuntime:
|
|||
await preflight_model_connection(model)
|
||||
self.model_verified = True
|
||||
|
||||
def _start_preparation(self) -> asyncio.Task[None]:
|
||||
def _start_preparation(self) -> asyncio.Task[None] | None:
|
||||
"""Kick off the work that runs behind the freshly painted TUI."""
|
||||
if self.controller.setup_mode:
|
||||
self._setup_preflight = asyncio.create_task(self.check_setup_model())
|
||||
return self._setup_preflight
|
||||
self.controller.begin_preparation()
|
||||
return asyncio.create_task(self.prepare_and_start())
|
||||
self.init_run_state()
|
||||
self.start_scan()
|
||||
return None
|
||||
|
||||
async def start_from_setup(self) -> None:
|
||||
candidate = deepcopy(self.args)
|
||||
|
|
@ -208,34 +209,6 @@ class GoTuiRuntime:
|
|||
self.init_run_state()
|
||||
self.start_scan()
|
||||
|
||||
async def prepare_and_start(self) -> None:
|
||||
"""Prepare a directly-launched scan once the TUI is on screen.
|
||||
|
||||
The model round trip and run preparation run here rather than before
|
||||
launch so the interface appears immediately.
|
||||
"""
|
||||
model = (load_settings().llm.model or "").strip()
|
||||
set_scan_phase("preflight")
|
||||
try:
|
||||
await preflight_model_connection(model)
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI scan preparation failed")
|
||||
report_error("model_connection_failed", exc)
|
||||
self.controller.fail_preparation(str(exc))
|
||||
return
|
||||
try:
|
||||
persist_current()
|
||||
prepare_run(self.args)
|
||||
telemetry_start(self.args)
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI scan preparation failed")
|
||||
report_error("scan_preparation_failed", exc)
|
||||
self.controller.fail_preparation(str(exc))
|
||||
return
|
||||
self.controller.scan_state = "running"
|
||||
self.init_run_state()
|
||||
self.start_scan()
|
||||
|
||||
def start_scan(self) -> None:
|
||||
if self.scan_task is None:
|
||||
self.scan_task = asyncio.create_task(self._run_scan())
|
||||
|
|
|
|||
|
|
@ -231,9 +231,9 @@ async def test_runtime_does_not_initialize_or_scan_before_ready(
|
|||
monkeypatch.setattr(go_tui, "launch_tui_process", launch)
|
||||
monkeypatch.setattr(go_tui, "wait_process", wait_process)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
monkeypatch.setattr(go_tui, "persist_current", lambda: None)
|
||||
monkeypatch.setattr(go_tui, "prepare_run", lambda _args: None)
|
||||
monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: None)
|
||||
monkeypatch.setattr(go_tui, "persist_current", lambda: calls.append("persist"))
|
||||
monkeypatch.setattr(go_tui, "prepare_run", lambda _args: calls.append("prepare"))
|
||||
monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: calls.append("telemetry"))
|
||||
monkeypatch.setattr(runtime, "init_run_state", init_state)
|
||||
monkeypatch.setattr(runtime, "start_scan", start_scan)
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ async def test_runtime_does_not_initialize_or_scan_before_ready(
|
|||
},
|
||||
)
|
||||
await asyncio.wait_for(run_task, timeout=2)
|
||||
assert calls == ["preflight", "state", "scan"]
|
||||
assert calls == ["state", "scan"]
|
||||
finally:
|
||||
child.close()
|
||||
if not run_task.done():
|
||||
|
|
@ -355,7 +355,7 @@ async def test_setup_preflights_model_before_starting(
|
|||
await runtime.ensure_model_verified()
|
||||
await runtime.start_from_setup()
|
||||
|
||||
# The same steps, in the same order, as a direct launch's prepare_and_start.
|
||||
# The same steps, in the same order, as main() takes before a direct launch.
|
||||
assert calls == ["preflight", "persist", "targets", "prepare", "telemetry", "state", "scan"]
|
||||
assert runtime.args.scan_mode == "quick"
|
||||
assert runtime.args.instruction == ""
|
||||
|
|
@ -1009,28 +1009,7 @@ def _direct_launch_args() -> argparse.Namespace:
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_and_start_reports_ordinary_connection_failures(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(_direct_launch_args())
|
||||
started: list[str] = []
|
||||
|
||||
async def preflight(_model: str) -> None:
|
||||
raise TimeoutError("connection timed out")
|
||||
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
monkeypatch.setattr(runtime, "start_scan", lambda: started.append("scan"))
|
||||
|
||||
await runtime.prepare_and_start()
|
||||
|
||||
assert started == []
|
||||
assert runtime.controller.setup_mode is False
|
||||
assert runtime.controller.scan_state == "failed"
|
||||
assert "connection timed out" in (runtime.controller.error or "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_and_start_runs_the_scan_after_preparation(
|
||||
async def test_direct_launch_starts_the_prepared_scan_without_a_model_round_trip(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(_direct_launch_args())
|
||||
|
|
@ -1046,9 +1025,10 @@ async def test_prepare_and_start_runs_the_scan_after_preparation(
|
|||
monkeypatch.setattr(runtime, "init_run_state", lambda: order.append("state"))
|
||||
monkeypatch.setattr(runtime, "start_scan", lambda: order.append("scan"))
|
||||
|
||||
await runtime.prepare_and_start()
|
||||
assert runtime._start_preparation() is None
|
||||
|
||||
assert order == ["preflight", "persist", "prepare", "telemetry", "state", "scan"]
|
||||
assert order == ["state", "scan"]
|
||||
assert runtime.controller.setup_mode is False
|
||||
assert runtime.controller.scan_state == "running"
|
||||
|
||||
|
||||
|
|
|
|||
53
tests/test_main_launch.py
Normal file
53
tests/test_main_launch.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pytest
|
||||
|
||||
|
||||
cli_main: Any = importlib.import_module("strix.interface.main")
|
||||
|
||||
|
||||
def _launch(monkeypatch: pytest.MonkeyPatch, *, needs_setup: bool) -> list[str]:
|
||||
calls: list[str] = []
|
||||
args = argparse.Namespace(
|
||||
non_interactive=False,
|
||||
needs_setup=needs_setup,
|
||||
run_name=None,
|
||||
fail_on=None,
|
||||
)
|
||||
|
||||
async def run_tui(_args: argparse.Namespace) -> None:
|
||||
calls.append("tui")
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["strix", "--target", "https://example.com"])
|
||||
monkeypatch.setattr(cli_main, "configure_dependency_logging", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "start_import_warmup", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "parse_arguments", lambda: args)
|
||||
monkeypatch.setattr(cli_main, "start_background_check", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "prompt_update_if_available", lambda _console: False)
|
||||
monkeypatch.setattr(cli_main, "check_docker_installed", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "pull_docker_image", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "validate_environment", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "wait_for_import_warmup", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "_bootstrap_scan", lambda _args: calls.append("bootstrap"))
|
||||
monkeypatch.setattr(cli_main, "run_tui", run_tui)
|
||||
monkeypatch.setattr(cli_main, "notify_update", lambda _console: None)
|
||||
|
||||
cli_main.main()
|
||||
return calls
|
||||
|
||||
|
||||
def test_direct_launch_verifies_the_model_before_the_tui(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert _launch(monkeypatch, needs_setup=False) == ["bootstrap", "tui"]
|
||||
|
||||
|
||||
def test_start_screen_launch_defers_the_model_check_to_the_tui(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
assert _launch(monkeypatch, needs_setup=True) == ["tui"]
|
||||
Loading…
Add table
Reference in a new issue