Compare commits

...

3 Commits

Author SHA1 Message Date
Bas Nijholt
3d07cbdff0 fix(web): show stderr in console shell sessions (#102)
- Remove `2>/dev/null` from shell command that was suppressing all stderr output
- Command errors like "command not found" are now properly displayed to users
2025-12-21 00:50:58 -08:00
Bas Nijholt
0f67c17281 test: parallel execution and timeout constants (#101)
- Enable `-n auto` for all test commands in justfile (parallel execution)
- Add redis stack to test fixtures (missing stack was causing test failure)
- Replace hardcoded timeouts with constants: `TIMEOUT` (10s) and `SHORT_TIMEOUT` (5s)
- Rename `test-unit` → `test-cli` and `test-browser` → `test-web`
- Skip CLI startup test when running in parallel mode (`-n auto`)
- Update test assertions for 5 stacks (was 4)
2025-12-21 00:48:52 -08:00
Bas Nijholt
bd22a1a55e fix: Reject unknown keys in config with Pydantic strict mode (#100)
Add extra="forbid" to Host and Config models so typos like
`username` instead of `user` raise an error instead of being
silently ignored. Also simplify _parse_hosts to pass dicts
directly to Pydantic instead of manual field extraction.
2025-12-21 00:19:18 -08:00
8 changed files with 266 additions and 248 deletions

View File

@@ -54,7 +54,7 @@ jobs:
run: uv run playwright install chromium --with-deps
- name: Run browser tests
run: uv run pytest -m browser -v --no-cov
run: uv run pytest -m browser -n auto -v
lint:
runs-on: ubuntu-latest

View File

@@ -66,8 +66,8 @@ Use `just` for common tasks. Run `just` to list available commands:
|---------|-------------|
| `just install` | Install dev dependencies |
| `just test` | Run all tests |
| `just test-unit` | Run unit tests (parallel) |
| `just test-browser` | Run browser tests |
| `just test-cli` | Run CLI tests (parallel) |
| `just test-web` | Run web UI tests (parallel) |
| `just lint` | Lint, format, and type check |
| `just web` | Start web UI (port 9001) |
| `just doc` | Build and serve docs (port 9002) |
@@ -78,17 +78,17 @@ Use `just` for common tasks. Run `just` to list available commands:
Run tests with `just test` or `uv run pytest`. Browser tests require Chromium (system-installed or via `playwright install chromium`):
```bash
# Unit tests only (skip browser tests, can parallelize)
# Unit tests only (parallel)
uv run pytest -m "not browser" -n auto
# Browser tests only (run sequentially, no coverage)
uv run pytest -m browser --no-cov
# Browser tests only (parallel)
uv run pytest -m browser -n auto
# All tests
uv run pytest --no-cov
uv run pytest
```
Browser tests are marked with `@pytest.mark.browser`. They use Playwright to test HTMX behavior, JavaScript functionality (sidebar filter, command palette, terminals), and content stability during navigation. Run sequentially (no `-n`) to avoid resource contention.
Browser tests are marked with `@pytest.mark.browser`. They use Playwright to test HTMX behavior, JavaScript functionality (sidebar filter, command palette, terminals), and content stability during navigation.
## Communication Notes

View File

@@ -9,17 +9,17 @@ default:
install:
uv sync --all-extras --dev
# Run all tests (no coverage for speed)
# Run all tests (parallel)
test:
uv run pytest --no-cov
uv run pytest -n auto
# Run unit tests only (parallel, with coverage)
test-unit:
# Run CLI tests only (parallel, with coverage)
test-cli:
uv run pytest -m "not browser" -n auto
# Run browser tests only (sequential, no coverage)
test-browser:
uv run pytest -m browser --no-cov
# Run web UI tests only (parallel)
test-web:
uv run pytest -m browser -n auto
# Lint, format, and type check
lint:

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import getpass
from pathlib import Path
from typing import Any
import yaml
from pydantic import BaseModel, Field, model_validator
@@ -14,7 +15,7 @@ from .paths import config_search_paths, find_config_path
COMPOSE_FILENAMES = ("compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml")
class Host(BaseModel):
class Host(BaseModel, extra="forbid"):
"""SSH host configuration."""
address: str
@@ -22,7 +23,7 @@ class Host(BaseModel):
port: int = 22
class Config(BaseModel):
class Config(BaseModel, extra="forbid"):
"""Main configuration."""
compose_dir: Path = Path("/opt/compose")
@@ -113,7 +114,7 @@ class Config(BaseModel):
return found
def _parse_hosts(raw_hosts: dict[str, str | dict[str, str | int]]) -> dict[str, Host]:
def _parse_hosts(raw_hosts: dict[str, Any]) -> dict[str, Host]:
"""Parse hosts from config, handling both simple and full forms."""
hosts = {}
for name, value in raw_hosts.items():
@@ -122,11 +123,7 @@ def _parse_hosts(raw_hosts: dict[str, str | dict[str, str | int]]) -> dict[str,
hosts[name] = Host(address=value)
else:
# Full form: hostname: {address: ..., user: ..., port: ...}
hosts[name] = Host(
address=str(value.get("address", "")),
user=str(value["user"]) if "user" in value else getpass.getuser(),
port=int(value["port"]) if "port" in value else 22,
)
hosts[name] = Host(**value)
return hosts

View File

@@ -236,8 +236,8 @@ async def _run_shell_session(
await websocket.send_text(f"{RED}Host '{host_name}' not found{RESET}{CRLF}")
return
# Start interactive shell in home directory (avoid login shell to prevent job control warnings)
shell_cmd = "cd ~ && exec bash -i 2>/dev/null || exec sh -i"
# Start interactive shell in home directory
shell_cmd = "cd ~ && exec bash -i || exec sh -i"
if is_local(host):
# Local: use argv list with shell -c to interpret the command

View File

@@ -2,11 +2,14 @@
from __future__ import annotations
import os
import shutil
import subprocess
import sys
import time
import pytest
# Thresholds in seconds, per OS
if sys.platform == "win32":
CLI_STARTUP_THRESHOLD = 2.0
@@ -16,6 +19,10 @@ else: # Linux
CLI_STARTUP_THRESHOLD = 0.25
@pytest.mark.skipif(
"PYTEST_XDIST_WORKER" in os.environ,
reason="Skip in parallel mode due to resource contention",
)
def test_cli_startup_time() -> None:
"""Verify CLI startup time stays within acceptable bounds.

View File

@@ -39,6 +39,15 @@ services:
image: grafana/grafana
""")
# Create a single-service stack for testing service commands
redis_dir = compose_path / "redis"
redis_dir.mkdir()
(redis_dir / "compose.yaml").write_text("""
services:
redis:
image: redis:alpine
""")
return compose_path
@@ -59,6 +68,7 @@ hosts:
stacks:
plex: server-1
grafana: server-2
redis: server-1
""")
# State file must be alongside config file

File diff suppressed because it is too large Load Diff