mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:17:02 +01:00
./web.sh serves health, the configuration and the photo album on 127.0.0.1:8140 -- the port tool/tagger.py vacates. React 19, Vite 8, TypeScript 6.0.3, ESLint 10 with strictTypeChecked + stylisticTypeChecked + unicorn + sonarjs + perfectionist, Vitest 4 at 100% on all four metrics. No coverage exclude list, including for main.tsx. This repository treats a coverage omit as a suppression, and "the DOM bootstrap is awkward to assert" is the same sentence as "this code is untested" -- so the bootstrap is a shim thin enough to test in jsdom, and the album view holds its file list apart from the album so the update path stopped carrying a "what if it is null" branch that cannot happen. An untestable branch is an untested one. TypeScript stays at 6.0.3: typescript-eslint 8.68.0 declares typescript >=4.8.4 <6.1.0, and the fleet-wide allowlist entry says the compiler waits for the linter. The freshness gate then caught two pins the reference repo was carrying stale -- eslint-plugin-unicorn 73 -> 74 and a floating node-version: "24" in CI -- both bumped rather than allowlisted. Traps the gates found, all of them real: - .gitignore has a bare lib/, which matches at any depth, so web/src/lib/ would have been silently untracked. The source lives in src/api/. - CI's shellcheck glob was `run.sh scripts/*.sh`, so tag.sh was unlinted in CI for its whole life and web.sh would have inherited that. Now ./*.sh. - tsconfig.json is strict JSON, not JSONC: check-json is generic and tsconfig being commentable is a convention, not a requirement. - The personal-data gate refused an invented phone number in a test, which is exactly what it is for. The album's warning that names a command moved with the command, and every path that pointed at tool/tagger.py now points at ./web.sh. The historical mentions stay: they explain why the code is shaped as it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01416TegJ3sa8xL5qGGgrygP
112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Every script under tool/ imports, and the launcher answers --help.
|
|
|
|
This exists because ``tool/tagger.py`` shipped an unconditional ImportError
|
|
and survived two releases with it: commit 48957a1 renamed
|
|
``pack.default_dir`` to ``default_dirs`` and missed the caller. Nothing could
|
|
have caught it. ``tool/`` is outside ``--cov`` in pyproject.toml, mypy ran
|
|
with ``args: [signal_bot, fake_signal]``, and ruff lints these files but
|
|
cannot resolve a name in another module.
|
|
|
|
So this is deliberately the cheapest possible test: import each file and, for
|
|
the one with a CLI, run it. It adds no coverage burden -- ``tests/`` is not
|
|
measured -- and no module-scope ImportError can survive it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from signal_bot.external import run
|
|
|
|
TOOL = Path(__file__).resolve().parent.parent / "tool"
|
|
|
|
|
|
def _modules() -> list[Path]:
|
|
"""Every Python file under tool/, so a new one is covered automatically."""
|
|
return sorted(path for path in TOOL.glob("*.py") if path.name != "__init__.py")
|
|
|
|
|
|
def test_there_is_something_to_check() -> None:
|
|
# Guards against the glob silently matching nothing and the whole file
|
|
# passing vacuously.
|
|
assert _modules()
|
|
|
|
|
|
def _load(stem: str) -> object:
|
|
"""Import one tool/ module by path, the way the parametrised test does."""
|
|
spec = importlib.util.spec_from_file_location(f"_probe_{stem}", TOOL / f"{stem}.py")
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.mark.parametrize("path", _modules(), ids=lambda path: path.name)
|
|
def test_the_module_imports(path: Path) -> None:
|
|
spec = importlib.util.spec_from_file_location(f"_tool_{path.stem}", path)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
# tool/ is a directory of scripts, not a package, and a couple of them
|
|
# import a sibling by bare name. That resolves in real use -- running
|
|
# "python tool/x.py" puts tool/ at sys.path[0] -- so the harness has to
|
|
# reproduce it rather than the scripts having to work around it.
|
|
sys.path.insert(0, str(TOOL))
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
finally:
|
|
sys.path.remove(str(TOOL))
|
|
sys.modules.pop(spec.name, None)
|
|
|
|
|
|
def test_the_launcher_answers_help() -> None:
|
|
# Through signal_bot.external, like every other subprocess here: S603 is
|
|
# suppressed for exactly one file and widening it is what that entry
|
|
# exists to prevent.
|
|
result = run([sys.executable, str(TOOL / "web.py"), "--help"])
|
|
assert result.ok, result.stderr
|
|
for flag in ("--import", "--untagged", "--dry-run"):
|
|
assert flag in result.stdout
|
|
|
|
|
|
def test_the_bootstrap_reports_what_this_interpreter_cannot_import() -> None:
|
|
# The whole decision rests on this: an interpreter that can already
|
|
# import everything must be left alone, or every run pays for a venv it
|
|
# does not need.
|
|
bootstrap = _load("venv_bootstrap")
|
|
assert bootstrap.missing() == []
|
|
assert bootstrap.missing(("definitely_not_a_real_module",)) == ["definitely_not_a_real_module"]
|
|
# A name that is not importable *and* not a valid spec query has to be
|
|
# reported, not raised: find_spec raises ValueError for some of them.
|
|
assert bootstrap.missing((".relative",)) == [".relative"]
|
|
|
|
|
|
def test_the_bootstrap_module_is_stdlib_only() -> None:
|
|
# It runs before .venv exists, so a third-party import here would be the
|
|
# same bug one level down.
|
|
source = (TOOL / "venv_bootstrap.py").read_text(encoding="utf-8")
|
|
for banned in ("import fastapi", "import httpx", "import uvicorn", "from signal_bot"):
|
|
assert banned not in source
|
|
|
|
|
|
def test_the_bootstrap_only_installs_when_the_venv_cannot_already_do_it(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# Measured bug: ensure() tested the *current* interpreter to decide both
|
|
# whether to re-exec and whether to install. The interpreter you typed
|
|
# never grows a fastapi, so every single launch reinstalled the project
|
|
# before re-execing. The two decisions are separate.
|
|
bootstrap = _load("venv_bootstrap")
|
|
bootstrap.VENV_PYTHON = tmp_path / "nonexistent" / "python"
|
|
assert bootstrap.ready() is False
|
|
bootstrap.VENV_PYTHON = Path(sys.executable)
|
|
assert bootstrap.ready(("sys",)) is True
|
|
assert bootstrap.ready(("definitely_not_a_real_module",)) is False
|