Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Smoke

on:
push:
branches:
- main
pull_request:

jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r toptek/requirements-lite.txt
- name: Run smoke checks
run: ./scripts/smoke.sh
10 changes: 10 additions & 0 deletions configs/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ chart:
fps: 12
max_points: 180
price_decimals: 2
lmstudio:
enabled: true
base_url: "http://localhost:1234/v1"
api_key: "lm-studio"
model: "llama-3.1-8b-instruct"
system_prompt: "You are the Autostealth Evolution assistant. Follow V10 ZERO-CONS."
max_tokens: 512
temperature: 0.0
top_p: 1.0
timeout_s: 30
status:
login:
idle: "Awaiting verification"
Expand Down
8 changes: 8 additions & 0 deletions scripts/smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

echo "[smoke] compiling python modules"
python -m compileall toptek >/dev/null

echo "[smoke] running targeted tests"
pytest -q tests/test_config_schema.py tests/test_lmstudio_client.py
168 changes: 160 additions & 8 deletions tests/gui/test_live_tab_wiring.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,176 @@
"""Behavioural wiring tests for the Live tab widget."""

from __future__ import annotations

import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple

import pytest

core_package = pytest.importorskip("toptek.core")
sys.modules.setdefault("core", core_package)

tk = pytest.importorskip("tkinter")
from tkinter import ttk # noqa: E402

from toptek.core import utils # noqa: E402

try: # Prefer dedicated Live tab module when present
from toptek.gui.live_tab import LiveTab # type: ignore
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover - legacy builds without the Live tab
from toptek.gui.widgets import LiveTab # type: ignore # noqa: F401


@pytest.fixture
def tk_root() -> Any:
try:
from toptek.gui.widgets import LiveTab # type: ignore
except (ModuleNotFoundError, ImportError, AttributeError):
LiveTab = None # type: ignore
root = tk.Tk()
except tk.TclError as exc: # pragma: no cover - depends on CI environment
pytest.skip(f"Tk unavailable: {exc}")
root.withdraw()
yield root
root.destroy()


def _paths(base: Path) -> utils.AppPaths:
return utils.AppPaths(root=base, cache=base / "cache", models=base / "models")


def _build_tab(
root: Any,
tmp_path: Path,
configs: Dict[str, Dict[str, object]],
**kwargs: Any,
) -> LiveTab:
notebook = ttk.Notebook(root)
notebook.pack()
return LiveTab(notebook, configs, _paths(tmp_path), **kwargs)


def test_live_tab_metrics_visibility_toggle(tk_root: Any, tmp_path: Path) -> None:
configs: Dict[str, Dict[str, object]] = {"live": {"defaults": {"symbol": "ES"}}}
tab = _build_tab(tk_root, tmp_path, configs)

assert tab.metrics_frame.winfo_manager()

tab.metrics_visible.set(False)
tab._update_metrics_visibility()
assert not tab.metrics_frame.winfo_manager()

tab.metrics_visible.set(True)
tab._update_metrics_visibility()
assert tab.metrics_frame.winfo_manager()


def test_live_tab_compose_request_uses_config_defaults(
tk_root: Any, tmp_path: Path
) -> None:
configs: Dict[str, Dict[str, object]] = {
"live": {
"defaults": {
"account_id": "ACC-1",
"symbol": "MESU4",
"quantity": 3,
"order_type": "LIMIT",
"time_in_force": "GTC",
"route": "LIVE",
"limit_price": 4321.0,
"stop_price": "",
}
}
}
tab = _build_tab(tk_root, tmp_path, configs)

tab.account_var.set("")
tab.symbol_var.set("")
tab.quantity_var.set("")
tab.order_type_var.set("")
tab.tif_var.set("")
tab.route_var.set("")
tab.limit_var.set("")
tab.stop_var.set("")

request = tab.compose_request()

assert request["account_id"] == "ACC-1"
assert request["symbol"] == "MESU4"
assert request["quantity"] == 3
assert request["order_type"] == "LIMIT"
assert request["time_in_force"] == "GTC"
assert request["route"] == "LIVE"
assert request["limit_price"] == 4321.0
assert request["stop_price"] == ""
assert configs["live"]["last_request"] == request
redacted = configs["live"].get("last_request_redacted")
assert redacted
assert redacted["symbol"] == "[REDACTED_TICKER]"
assert redacted["account_id"] == "[REDACTED_TICKER]"
assert tab.request_defaults["symbol"] == "MESU4"


def test_live_tab_submit_order_handles_success_and_error(
tk_root: Any, tmp_path: Path
) -> None:
success_events: List[Tuple[Dict[str, Any], Dict[str, Any]]] = []
error_events: List[Tuple[Dict[str, Any], Exception]] = []

class RecordingClient:
def __init__(self, response: Any) -> None:
self.response = response
self.calls: List[Dict[str, Any]] = []

def place_order(self, payload: Dict[str, Any]) -> Dict[str, Any]:
self.calls.append(payload)
if isinstance(self.response, Exception):
raise self.response
return self.response

configs: Dict[str, Dict[str, object]] = {"live": {}}
client = RecordingClient({"status": "ACCEPTED", "id": "123"})
tab = _build_tab(tk_root, tmp_path, configs, client=client)
tab.register_callbacks(
on_success=lambda payload, response: success_events.append((payload, response)),
on_error=lambda payload, exc: error_events.append((payload, exc)),
)

response = tab.submit_order()
assert response == {"status": "ACCEPTED", "id": "123"}
assert len(client.calls) == 1
assert success_events and success_events[0][1]["id"] == "123"
assert not error_events
assert tab.metrics_state["orders_sent"] == 1
assert tab.status_var.get().startswith("Order ACCEPTED")

failing = RecordingClient(ValueError("route unavailable"))
tab.client = failing
tab.submit_order()
assert error_events and isinstance(error_events[-1][1], ValueError)
assert tab.metrics_state["errors"] == 1
assert tab.status_var.get().startswith("Error:")
metrics = configs["live"].get("metrics")
assert isinstance(metrics, dict)
assert metrics.get("orders_sent") == 1
assert metrics.get("errors") == 1


if LiveTab is None: # pragma: no cover - legacy builds without the Live tab
pytest.skip("Live tab implementation unavailable", allow_module_level=True)
def test_live_tab_refresh_metrics_uses_fetcher(
tk_root: Any, tmp_path: Path
) -> None:
calls: List[int] = []

def metrics_fetcher() -> Dict[str, Any]:
calls.append(1)
return {"latency_ms": 42, "fills": 5}

def test_live_tab_placeholder() -> None: # pragma: no cover - executed when LiveTab exists
pytest.skip("Live tab behaviour tests require the implementation module")
configs: Dict[str, Dict[str, object]] = {"live": {}}
tab = _build_tab(tk_root, tmp_path, configs, metrics_fetcher=metrics_fetcher)
tab.metrics_state["orders_sent"] = 7
metrics = tab.refresh_metrics()

assert calls # fetcher invoked
assert metrics["latency_ms"] == 42
assert metrics["fills"] == 5
buffer = tab.metrics_output.get("1.0", "end-1c")
assert '"latency_ms": 42' in buffer
assert configs["live"]["metrics"]["fills"] == 5
26 changes: 26 additions & 0 deletions tests/test_config_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from pathlib import Path

from toptek.core import ui_config


def test_ui_config_includes_lmstudio_defaults(tmp_path: Path) -> None:
config_path = tmp_path / "ui.yml"
config_path.write_text("{}\n", encoding="utf-8")

cfg = ui_config.load_ui_config(config_path, env={})

assert cfg.lmstudio.enabled is True
assert cfg.lmstudio.base_url == "http://localhost:1234/v1"
assert cfg.lmstudio.model == "llama-3.1-8b-instruct"
assert cfg.lmstudio.max_tokens == 512
assert cfg.as_dict()["lmstudio"]["temperature"] == 0.0


def test_repository_ui_config_matches_schema() -> None:
project_cfg = ui_config.load_ui_config(Path("configs/ui.yml"), env={})
lmstudio = project_cfg.lmstudio
assert lmstudio.enabled is True
assert lmstudio.timeout_s == 30
assert "Autostealth Evolution" in lmstudio.system_prompt
Loading