refactor: move strix/viewer under strix/interface

This commit is contained in:
Ahmed Allam
2026-07-26 13:11:14 -07:00
committed by Ahmed Allam
parent 8157ccba27
commit d1e8225d5f
88 changed files with 87 additions and 81 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
# Node / local-viewer SPA source (the built bundle in
# strix/viewer/static/ is committed and shipped; do not ignore it)
# strix/interface/viewer/static/ is committed and shipped; do not ignore it)
node_modules/
strix/viewer/frontend/node_modules/
strix/viewer/frontend/.vite/
strix/interface/viewer/frontend/node_modules/
strix/interface/viewer/frontend/.vite/
# Python
__pycache__/
+5 -5
View File
@@ -102,16 +102,16 @@ We welcome feature ideas! Please:
## 🖥️ Local viewer SPA
`strix view` serves a prebuilt web UI whose source lives in
`strix/viewer/frontend/` (a Vite + React project) and whose built output is
committed to `strix/viewer/static/` and shipped in the package. End users never
run a JS build. If you change anything under `strix/viewer/frontend/`, rebuild
`strix/interface/viewer/frontend/` (a Vite + React project) and whose built output is
committed to `strix/interface/viewer/static/` and shipped in the package. End users never
run a JS build. If you change anything under `strix/interface/viewer/frontend/`, rebuild
and commit the output:
```bash
make viewer # or: cd strix/viewer/frontend && npm ci && npm run build
make viewer # or: cd strix/interface/viewer/frontend && npm ci && npm run build
```
Commit both the source change and the regenerated `strix/viewer/static/`.
Commit both the source change and the regenerated `strix/interface/viewer/static/`.
## 🤝 Community
+2 -2
View File
@@ -69,8 +69,8 @@ clean:
viewer:
@echo "🖥️ Building the local-viewer SPA..."
cd strix/viewer/frontend && npm ci && npm run build
@echo "✅ Viewer built to strix/viewer/static/ (commit the changes)."
cd strix/interface/viewer/frontend && npm ci && npm run build
@echo "✅ Viewer built to strix/interface/viewer/static/ (commit the changes)."
dev: format lint type-check
@echo "✅ Development cycle complete!"
+6 -6
View File
@@ -79,10 +79,10 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["strix"]
# The prebuilt viewer bundle under strix/viewer/static/ ships automatically
# The prebuilt viewer bundle under strix/interface/viewer/static/ ships automatically
# (hatchling includes non-.py files under the package). The Vite SOURCE lives
# under the package dir too (strix/viewer/frontend/) but must never ship in the wheel.
exclude = ["strix/viewer/frontend", "strix/viewer/frontend/**"]
# under the package dir too (strix/interface/viewer/frontend/) but must never ship in the wheel.
exclude = ["strix/interface/viewer/frontend", "strix/interface/viewer/frontend/**"]
# ============================================================================
# Type Checking Configuration
@@ -222,10 +222,10 @@ ignore = [
"tests/test_codex_streaming.py" = ["N802"]
"tests/test_report_pdf.py" = ["S105", "S106"]
# Stdlib HTTP handler overrides (do_GET/do_POST) and lazy imports that avoid a
# circular dependency with strix.telemetry / strix.viewer.report_pdf.
"strix/viewer/server.py" = ["N802", "PLC0415"]
# circular dependency with strix.telemetry / strix.interface.viewer.report_pdf.
"strix/interface/viewer/server.py" = ["N802", "PLC0415"]
# Lazy telemetry import to avoid importing PostHog before the viewer starts.
"strix/viewer/cli.py" = ["PLC0415"]
"strix/interface/viewer/cli.py" = ["PLC0415"]
# Lazy imports inside functions to avoid circular dependency with
# strix.telemetry / strix.report.dedupe / cvss.
"strix/tools/notes/tools.py" = ["PLC0415", "TC002"]
+7 -7
View File
@@ -26,7 +26,7 @@ for tcss_file in strix_root.rglob('*.tcss'):
datas.append((str(tcss_file), str(rel_path.parent)))
# Prebuilt local-viewer SPA (served by `strix view`).
viewer_static = strix_root / 'viewer' / 'static'
viewer_static = strix_root / 'interface' / 'viewer' / 'static'
for asset in viewer_static.rglob('*'):
if asset.is_file():
rel_path = asset.relative_to(project_root)
@@ -158,12 +158,12 @@ hiddenimports = [
'strix.report.dedupe',
'strix.report.state',
'strix.report.writer',
'strix.viewer',
'strix.viewer.auth',
'strix.viewer.cli',
'strix.viewer.report_pdf',
'strix.viewer.server',
'strix.viewer.transcript',
'strix.interface.viewer',
'strix.interface.viewer.auth',
'strix.interface.viewer.cli',
'strix.interface.viewer.report_pdf',
'strix.interface.viewer.server',
'strix.interface.viewer.transcript',
# PDF report generation + encryption
'reportlab',
+1 -1
View File
@@ -952,7 +952,7 @@ def main() -> None:
# `strix view [<run>]` is a viewer-only subcommand, dispatched before the
# scan argument parser (which requires a target) and before any scan setup.
if len(sys.argv) > 1 and sys.argv[1] == "view":
from strix.viewer.cli import run_view
from strix.interface.viewer.cli import run_view
run_view(sys.argv[2:])
return
+1 -1
View File
@@ -1862,7 +1862,7 @@ class StrixTUIApp(App): # type: ignore[misc]
webbrowser.open(self._viewer_url)
return
try:
from strix.viewer.server import authorized_url, bundle_is_built, serve
from strix.interface.viewer.server import authorized_url, bundle_is_built, serve
if not bundle_is_built():
self._set_viewer_cta("[#eab308]Viewer UI not built[/]")
@@ -6,7 +6,7 @@ directly from the run's on-disk files. No cloud dependency, no file picker.
from __future__ import annotations
from strix.viewer.server import serve
from strix.interface.viewer.server import serve
__all__ = ["serve"]
@@ -4,7 +4,7 @@ The local viewer proxies email verification and encrypted-report delivery to
the Strix relay (``STRIX_APP_URL``). The browser never talks to the relay
directly, and the report password generated locally is never sent to it.
State lives in ``~/.strix/viewer-auth.json`` (0600). ``is_verified`` is a local
State lives in ``~/.strix/interface/viewer-auth.json`` (0600). ``is_verified`` is a local
flag that unlocks browsing the run history list; the relay still enforces token
expiry when a report is actually sent.
"""
@@ -155,7 +155,7 @@ def _post_json(path: str, payload: dict[str, Any], *, timeout: int) -> tuple[int
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 # nosec B310
return response.status, _parse_body(response.read())
except urllib.error.HTTPError as exc:
return exc.code, _parse_body(exc.read())
@@ -16,8 +16,8 @@ from strix.core.paths import (
run_record_path,
runs_base_dir,
)
from strix.viewer.server import authorized_url, bundle_is_built, serve
from strix.viewer.transcript import read_run_summary
from strix.interface.viewer.server import authorized_url, bundle_is_built, serve
from strix.interface.viewer.transcript import read_run_summary
if TYPE_CHECKING:
@@ -58,7 +58,7 @@ def run_view(argv: list[str]) -> None:
if not bundle_is_built():
console.print(
"[bold red]Viewer UI is not built.[/]\n"
"Build it with: [cyan]cd strix/viewer/frontend && npm ci && npm run build[/]"
"Build it with: [cyan]cd strix/interface/viewer/frontend && npm ci && npm run build[/]"
)
raise SystemExit(1)

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -5,7 +5,7 @@ import { fileURLToPath, URL } from "node:url";
// The viewer is served as static files by a stdlib Python server on an
// arbitrary ephemeral port, so all asset URLs must be relative (base: "./").
// The build output is committed at strix/viewer/static and shipped.
// The build output is committed at strix/interface/viewer/static and shipped.
export default defineConfig({
base: "./",
plugins: [react(), tailwindcss()],
@@ -38,7 +38,7 @@ from reportlab.platypus import (
TableStyle,
)
from strix.viewer.transcript import (
from strix.interface.viewer.transcript import (
primary_target,
read_run_summary,
read_vulnerabilities,
@@ -27,8 +27,8 @@ from typing import TYPE_CHECKING, Any
from urllib.parse import parse_qs, unquote, urlencode, urlsplit
from strix.core.paths import run_record_path
from strix.viewer import auth
from strix.viewer.transcript import (
from strix.interface.viewer import auth
from strix.interface.viewer.transcript import (
build_run_state,
primary_target,
read_report_markdown,
@@ -367,7 +367,7 @@ def _make_handler(state: _ViewerState) -> type[BaseHTTPRequestHandler]:
self._send_json(HTTPStatus.CONFLICT, {"error": "run_not_finished"})
return
from strix.viewer.report_pdf import build_encrypted_report
from strix.interface.viewer.report_pdf import build_encrypted_report
pdf_bytes, password, filename = build_encrypted_report(run_dir)
run_name = str(summary.get("run_name") or run_dir.name)

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -44,8 +44,8 @@ def build_run_state(run_dir: Path) -> dict[str, Any]:
Reuses the Textual-free ``TuiLiveView`` projection so the viewer and the TUI
share one parser for ``agents.json`` + ``agents.db`` and never drift.
"""
# Imported lazily so importing strix.viewer does not eagerly pull the TUI.
from strix.interface.tui.live_view import TuiLiveView # noqa: PLC0415
# Imported lazily so importing strix.interface.viewer does not eagerly pull the TUI.
from strix.interface.tui.live_view import TuiLiveView
view = TuiLiveView()
view.hydrate_from_run_dir(run_dir)
+1 -1
View File
@@ -4,13 +4,13 @@ from __future__ import annotations
from pygments.lexers import BashLexer, PythonLexer
from strix.interface.viewer.report_pdf import _strip_code_fence
from strix.report.writer import (
guess_language_name,
parse_fenced_code,
resolve_lexer,
safe_fence,
)
from strix.viewer.report_pdf import _strip_code_fence
def test_parse_fenced_code_extracts_language_and_body() -> None:
+2 -2
View File
@@ -10,7 +10,7 @@ import pytest
from pypdf import PdfReader
from pypdf.errors import WrongPasswordError
from strix.viewer.report_pdf import (
from strix.interface.viewer.report_pdf import (
build_encrypted_report,
encrypt_pdf,
generate_password,
@@ -91,7 +91,7 @@ def test_wrong_password_is_rejected(tmp_path: Path) -> None:
run_dir = _make_run(tmp_path)
encrypted = encrypt_pdf(generate_report_pdf(run_dir), "correct-horse-battery")
with pytest.raises(WrongPasswordError):
PdfReader(BytesIO(encrypted), password="not-the-password")
PdfReader(BytesIO(encrypted), password="not-the-password") # nosec B106
def test_build_encrypted_report(tmp_path: Path) -> None:
+36 -30
View File
@@ -1,4 +1,4 @@
"""Tests for the local run viewer (strix.viewer) and its path helpers."""
"""Tests for the local run viewer (strix.interface.viewer) and its path helpers."""
from __future__ import annotations
@@ -9,8 +9,8 @@ import urllib.request
from typing import TYPE_CHECKING
from strix.core.paths import latest_run_dir, runs_base_dir
from strix.viewer.server import serve
from strix.viewer.transcript import (
from strix.interface.viewer.server import serve
from strix.interface.viewer.transcript import (
build_run_state,
read_report_markdown,
read_run_summary,
@@ -89,7 +89,7 @@ def test_build_run_state_from_agents_json(tmp_path: Path) -> None:
def _get(url: str, *, cookie: str | None = None) -> tuple[int, str, bytes]:
headers = {"Cookie": cookie} if cookie else {}
req = urllib.request.Request(url, headers=headers) # noqa: S310 - localhost test server
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server # nosec B310
return resp.status, resp.headers.get("Content-Type", ""), resp.read()
@@ -100,7 +100,7 @@ def test_server_serves_api_and_static(tmp_path: Path, monkeypatch: pytest.Monkey
(assets / "assets").mkdir(parents=True)
(assets / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
(assets / "assets" / "app.js").write_text("console.log(1)", encoding="utf-8")
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
httpd, url, _ = serve(run_dir, open_browser=False)
try:
@@ -132,7 +132,7 @@ def test_server_event_endpoint_forwards_cta(
assets = tmp_path / "bundle"
assets.mkdir()
(assets / "index.html").write_text("x", encoding="utf-8")
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
seen: list[tuple[str, str | None]] = []
monkeypatch.setattr(
@@ -148,7 +148,7 @@ def test_server_event_endpoint_forwards_cta(
req = urllib.request.Request( # noqa: S310 - localhost test server
f"{url}/api/event", data=body, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req) as resp: # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
assert resp.status == 204
assert seen == [("PR reviews", "sidebar_nav")]
finally:
@@ -163,7 +163,7 @@ def test_server_event_endpoint_forwards_email_funnel(
assets = tmp_path / "bundle"
assets.mkdir()
(assets / "index.html").write_text("x", encoding="utf-8")
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
seen: list[tuple[str, str | None]] = []
monkeypatch.setattr(
@@ -183,7 +183,7 @@ def test_server_event_endpoint_forwards_email_funnel(
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp: # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
assert resp.status == 204
assert seen == expected
finally:
@@ -207,7 +207,7 @@ def test_server_event_endpoint_forwards_agent_steered(
data=json.dumps({"event": "agent_steered"}).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp: # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
assert resp.status == 204
assert seen == [True]
finally:
@@ -222,7 +222,7 @@ def test_feedback_records_telemetry_on_success(
_bundle(tmp_path, monkeypatch)
sent: list[bool] = []
monkeypatch.setattr("strix.viewer.auth.feedback_submit", lambda *_a: None)
monkeypatch.setattr("strix.interface.viewer.auth.feedback_submit", lambda *_a: None)
monkeypatch.setattr(
"strix.telemetry.posthog.viewer_feedback_submitted", lambda: sent.append(True)
)
@@ -257,7 +257,7 @@ def _post(
url + path, data=json.dumps(payload).encode(), headers=headers, method="POST"
)
try:
with urllib.request.urlopen(req) as resp: # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
return resp.status, resp.read()
except urllib.error.HTTPError as exc:
return exc.code, exc.read()
@@ -266,7 +266,7 @@ def _post(
def _session_cookie(url: str, token: str) -> str:
"""Bootstrap a session via the tokened URL and return its ``name=value`` cookie."""
bootstrap = f"{url}/?token={token}"
with urllib.request.urlopen(bootstrap) as resp: # noqa: S310 - localhost test server
with urllib.request.urlopen(bootstrap) as resp: # noqa: S310 - localhost test server # nosec B310
raw = str(resp.headers.get("Set-Cookie", ""))
return raw.split(";", 1)[0]
@@ -275,7 +275,7 @@ def _get_status(url: str, *, cookie: str | None = None) -> int:
headers = {"Cookie": cookie} if cookie else {}
req = urllib.request.Request(url, headers=headers) # noqa: S310 - localhost test server
try:
with urllib.request.urlopen(req) as resp: # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
return int(resp.status)
except urllib.error.HTTPError as exc:
return int(exc.code)
@@ -285,7 +285,7 @@ def _bundle(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
assets = tmp_path / "bundle"
assets.mkdir()
(assets / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
def test_capability_issued_only_for_tokened_bootstrap(
@@ -296,26 +296,26 @@ def test_capability_issued_only_for_tokened_bootstrap(
(assets / "assets").mkdir(parents=True)
(assets / "index.html").write_text("<!doctype html>index", encoding="utf-8")
(assets / "assets" / "app.js").write_text("1", encoding="utf-8")
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
httpd, url, token = serve(run_dir, open_browser=False)
try:
# A bare index load -- all a reachable client can do -- hands out nothing.
with urllib.request.urlopen(url + "/") as resp: # noqa: S310
with urllib.request.urlopen(url + "/") as resp: # noqa: S310 # nosec B310
assert resp.headers.get("Set-Cookie") is None
# A wrong token is likewise refused the capability.
with urllib.request.urlopen(f"{url}/?token=wrong") as resp: # noqa: S310
with urllib.request.urlopen(f"{url}/?token=wrong") as resp: # noqa: S310 # nosec B310
assert resp.headers.get("Set-Cookie") is None
# Only the correct bootstrap token mints the session cookie.
with urllib.request.urlopen(f"{url}/?token={token}") as resp: # noqa: S310
with urllib.request.urlopen(f"{url}/?token={token}") as resp: # noqa: S310 # nosec B310
cookie = str(resp.headers.get("Set-Cookie", ""))
assert "strix_viewer_session=" in cookie
assert "HttpOnly" in cookie and "SameSite=Strict" in cookie
# Static assets never carry it.
with urllib.request.urlopen(url + "/assets/app.js") as resp: # noqa: S310
with urllib.request.urlopen(url + "/assets/app.js") as resp: # noqa: S310 # nosec B310
assert resp.headers.get("Set-Cookie") is None
finally:
httpd.shutdown()
@@ -338,7 +338,7 @@ def test_unauthorized_client_cannot_acquire_capability(
try:
# A direct network client can reach the page but is handed no capability,
# so replaying an empty/guessed cookie cannot steer a live scan.
with urllib.request.urlopen(url + "/") as resp: # noqa: S310
with urllib.request.urlopen(url + "/") as resp: # noqa: S310 # nosec B310
assert resp.headers.get("Set-Cookie") is None
status, _ = _post(
url,
@@ -356,9 +356,11 @@ def test_unauthorized_client_cannot_acquire_capability(
def test_auth_status_reflects_expiry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
run_dir = _make_run(tmp_path, "status", status="running", end_time=None)
_bundle(tmp_path, monkeypatch)
monkeypatch.setattr("strix.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"})
monkeypatch.setattr(
"strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
)
verified = {"value": True}
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: verified["value"])
monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: verified["value"])
httpd, url, token = serve(run_dir, open_browser=False)
try:
@@ -384,7 +386,7 @@ def test_auth_mutations_require_session(tmp_path: Path, monkeypatch: pytest.Monk
run_dir = _make_run(tmp_path, "authmut", status="running", end_time=None)
_bundle(tmp_path, monkeypatch)
forgotten = {"value": False}
monkeypatch.setattr("strix.viewer.auth.forget", lambda: forgotten.update(value=True))
monkeypatch.setattr("strix.interface.viewer.auth.forget", lambda: forgotten.update(value=True))
httpd, url, _ = serve(run_dir, open_browser=False)
try:
@@ -432,7 +434,9 @@ def test_report_send_requires_session_cookie(
_bundle(tmp_path, monkeypatch)
# A verified machine token exists, but that alone must not authorize a caller.
monkeypatch.setattr("strix.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"})
monkeypatch.setattr(
"strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
)
httpd, url, token = serve(run_dir, open_browser=False)
try:
@@ -456,7 +460,9 @@ def test_report_send_rejects_live_run(tmp_path: Path, monkeypatch: pytest.Monkey
# fail closed even for a verified, session-holding caller.
run_dir = _make_run(tmp_path, "live", status="running", end_time=None)
_bundle(tmp_path, monkeypatch)
monkeypatch.setattr("strix.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"})
monkeypatch.setattr(
"strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
)
httpd, url, token = serve(run_dir, open_browser=False)
try:
@@ -475,7 +481,7 @@ def test_historical_run_data_requires_verification(
_bundle(tmp_path, monkeypatch)
verified = {"value": False}
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: verified["value"])
monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: verified["value"])
httpd, url, token = serve(launched, open_browser=False)
try:
@@ -509,12 +515,12 @@ def test_runs_list_requires_session_and_verification(
_make_run(tmp_path, "other", status="completed", end_time="2026-01-01T00:00:00Z")
_bundle(tmp_path, monkeypatch)
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: True)
monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: True)
def _runs(cookie: str | None) -> dict[str, object]:
headers = {"Cookie": cookie} if cookie else {}
req = urllib.request.Request(f"{url}/api/runs", headers=headers) # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server # nosec B310
return dict(json.loads(resp.read()))
httpd, url, token = serve(launched, open_browser=False)
@@ -543,7 +549,7 @@ def test_server_rejects_path_traversal(tmp_path: Path, monkeypatch: pytest.Monke
assets = tmp_path / "bundle"
assets.mkdir()
(assets / "index.html").write_text("<!doctype html>index", encoding="utf-8")
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
httpd, url, _ = serve(run_dir, open_browser=False)
try:
+9 -9
View File
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
import pytest
from strix.viewer import auth
from strix.interface.viewer import auth
def _iso(delta: timedelta) -> str:
@@ -30,7 +30,7 @@ def test_write_read_forget_roundtrip() -> None:
assert auth.read_auth() is None
assert auth.is_verified() is False
auth.write_auth(email="user@example.com", token="tok-123", verified_at=_iso(timedelta(days=30)))
auth.write_auth(email="user@example.com", token="tok-123", verified_at=_iso(timedelta(days=30))) # nosec B106
record = auth.read_auth()
assert record is not None
@@ -47,23 +47,23 @@ def test_write_read_forget_roundtrip() -> None:
def test_is_verified_enforces_expiry() -> None:
# An expired record still reads back, but no longer unlocks history.
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=-1)))
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=-1))) # nosec B106
assert auth.read_auth() is not None
assert auth.is_verified() is False
# A future expiry unlocks it.
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=1)))
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=1))) # nosec B106
assert auth.is_verified() is True
def test_is_verified_fails_closed_when_expiry_absent_or_unparseable() -> None:
# No/blank expiry: fail closed rather than unlocking history forever.
auth.write_auth(email="a@b.com", token="t", verified_at="")
auth.write_auth(email="a@b.com", token="t", verified_at="") # nosec B106
assert auth.read_auth() is not None
assert auth.is_verified() is False
# Garbage expiry likewise requires re-verification.
auth.write_auth(email="a@b.com", token="t", verified_at="not-a-date")
auth.write_auth(email="a@b.com", token="t", verified_at="not-a-date") # nosec B106
assert auth.is_verified() is False
@@ -73,9 +73,9 @@ def test_is_verified_accepts_epoch_expiry() -> None:
past = (datetime.now(UTC) - timedelta(hours=1)).timestamp()
# As a numeric string (how write_auth persists it).
auth.write_auth(email="a@b.com", token="t", verified_at=str(future))
auth.write_auth(email="a@b.com", token="t", verified_at=str(future)) # nosec B106
assert auth.is_verified() is True
auth.write_auth(email="a@b.com", token="t", verified_at=str(past))
auth.write_auth(email="a@b.com", token="t", verified_at=str(past)) # nosec B106
assert auth.is_verified() is False
# As a raw JSON number, if a record is written that way.
@@ -87,7 +87,7 @@ def test_is_verified_accepts_epoch_expiry() -> None:
def test_write_auth_is_0600() -> None:
auth.write_auth(email="a@b.com", token="t", verified_at="")
auth.write_auth(email="a@b.com", token="t", verified_at="") # nosec B106
mode = stat.S_IMODE(auth.AUTH_PATH.stat().st_mode)
assert mode == 0o600
+1 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import json
from typing import TYPE_CHECKING
from strix.viewer.server import build_runs_payload, resolve_run_dir
from strix.interface.viewer.server import build_runs_payload, resolve_run_dir
if TYPE_CHECKING: