Files
jarca-ledger-mcp/tests/test_streamlit_browser_evidence.py
T

230 lines
9.1 KiB
Python

from __future__ import annotations
import json
import ssl
import sys
from pathlib import Path
from typing import Any
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import streamlit_browser_evidence as evidence
SECRET_VALUES = ("synthetic-password", "a" * 64, "cookie-value")
SURFACE_STYLE = {
"backgroundImage": "linear-gradient(rgb(1, 2, 3), rgb(4, 5, 6))",
"backgroundColor": "rgb(255, 255, 255)",
"visibility": "visible",
"display": "block",
"opacity": "1",
"rendered": True,
}
def surface_records() -> dict[str, dict[str, object]]:
return {
"stAppViewContainer": dict(SURFACE_STYLE),
"stSidebar": dict(SURFACE_STYLE),
}
def test_redact_secrets_removes_values_from_nested_manifest() -> None:
payload = {"url": "https://127.0.0.1/?token=cookie-value", "nested": ["synthetic-password", {"key": "a" * 64}]}
redacted = evidence.redact_secrets(payload, SECRET_VALUES)
serialized = json.dumps(redacted)
assert all(secret not in serialized for secret in SECRET_VALUES)
assert redacted["nested"][0] == "[REDACTED]"
def test_transition_requires_synthetic_fixture_database(tmp_path: Path) -> None:
with pytest.raises(SystemExit, match="fixture.sqlite3"):
evidence.assert_synthetic_db_path(tmp_path / "operating.sqlite3")
def test_validate_transition_manifest_requires_observations() -> None:
manifest = {
"mode": "transition",
"success": True,
"observations": {"frames": [], "navigation_events": []},
}
with pytest.raises(ValueError, match="frames"):
evidence.validate_transition_manifest(manifest)
def complete_manifest() -> dict[str, Any]:
return {
"mode": "transition",
"success": True,
"observations": {
"frames": [
{
"at_ms": 0,
"selected_menu": "요약",
"body_text_hash": "hash",
"loading": False,
"prior_body_visible": True,
"surfaces": surface_records(),
},
{
"at_ms": 100,
"selected_menu": "분류 규칙",
"body_text_hash": "hash2",
"loading": True,
"prior_body_visible": False,
"surfaces": surface_records(),
},
],
"document_navigation_events": [],
"history_events": [{"type": "pushState", "url": "/?page=rules"}],
"stable_shell": {
"document_marker": {"pre": "doc-1", "post": "doc-1"},
"sidebar_marker": {"pre": "side-1", "post": "side-1"},
"background": {"pre": "rgb(255, 255, 255)", "post": "rgb(255, 255, 255)"},
"surfaces": {"pre": surface_records(), "post": surface_records()},
},
"loading_observed": True,
"prior_body_removed": True,
"target_page": "분류 규칙",
"final_agreement": True,
},
"runtime": {
"viewport": {"width": 1440, "height": 1400},
"interval_ms": 50,
"host_architecture": "aarch64",
"browser_user_agent": "Synthetic Browser",
},
"source_revision": evidence.current_git_revision(),
"source_revision_kind": "git-head-base",
"source_worktree_dirty": evidence.git_worktree_dirty(("dashboard/app.py", "dashboard/common.py")),
"source_files": evidence.source_file_digests(("dashboard/app.py", "dashboard/common.py")),
"evidence_command": ["scripts/streamlit_browser_evidence.py", "--mode", "transition"],
"command": {"exit_code": 0, "teardown": "complete"},
"limitations": [],
}
def test_validate_transition_manifest_accepts_complete_secret_free_shape() -> None:
evidence.validate_transition_manifest(complete_manifest())
def test_wait_for_http_uses_verified_tls_by_default_and_fixture_context_opt_in() -> None:
response = type("Response", (), {"status": 200, "__enter__": lambda self: self, "__exit__": lambda *args: None})()
with patch.object(evidence.urllib.request, "urlopen", return_value=response) as urlopen:
evidence.wait_for_http("https://example.test")
assert urlopen.call_args.kwargs["context"] is None
fixture_context = ssl._create_unverified_context()
with patch.object(evidence.urllib.request, "urlopen", return_value=response) as urlopen:
evidence.wait_for_http("https://127.0.0.1", ssl_context=fixture_context)
assert urlopen.call_args.kwargs["context"] is fixture_context
def test_validate_transition_manifest_rejects_fabricated_navigation_event() -> None:
manifest = complete_manifest()
manifest["observations"]["document_navigation_events"] = [{"type": "load", "url": "/"}]
with pytest.raises(ValueError, match="document navigation"):
evidence.validate_transition_manifest(manifest)
def test_validate_transition_manifest_requires_observed_loading_without_prior_body() -> None:
no_loading = complete_manifest()
no_loading["observations"]["loading_observed"] = False
with pytest.raises(ValueError, match="loading"):
evidence.validate_transition_manifest(no_loading)
stale_body = complete_manifest()
stale_body["observations"]["frames"][1]["prior_body_visible"] = True
with pytest.raises(ValueError, match="prior body"):
evidence.validate_transition_manifest(stale_body)
def test_validate_transition_manifest_requires_pre_post_shell_identity() -> None:
manifest = complete_manifest()
manifest["observations"]["stable_shell"] = {"sidebar": True, "background": "#fff"}
with pytest.raises(ValueError, match="shell"):
evidence.validate_transition_manifest(manifest)
def test_validate_transition_manifest_rejects_unselected_target_body_agreement() -> None:
manifest = complete_manifest()
manifest["observations"]["final_agreement"] = False
with pytest.raises(ValueError, match="agreement"):
evidence.validate_transition_manifest(manifest)
def test_validate_transition_manifest_rejects_attestation_before_teardown() -> None:
manifest = complete_manifest()
manifest["command"] = {"exit_code": 0, "teardown": "running"}
with pytest.raises(ValueError, match="teardown"):
evidence.validate_transition_manifest(manifest)
def test_validate_transition_manifest_requires_exact_source_and_runtime_identity() -> None:
manifest = complete_manifest()
manifest.pop("source_files")
with pytest.raises(ValueError, match="source files"):
evidence.validate_transition_manifest(manifest)
manifest = complete_manifest()
manifest["runtime"].pop("browser_user_agent")
with pytest.raises(ValueError, match="runtime identity"):
evidence.validate_transition_manifest(manifest)
manifest = complete_manifest()
manifest["source_files"]["dashboard/app.py"] = "0" * 64
with pytest.raises(ValueError, match="source file digests"):
evidence.validate_transition_manifest(manifest)
def test_transition_manifest_rejects_secret_text() -> None:
manifest = {"mode": "transition", "success": False, "error": "synthetic-password"}
with pytest.raises(ValueError, match="secret"):
evidence.validate_transition_manifest(manifest, SECRET_VALUES)
def test_validate_transition_manifest_requires_surface_style_evidence_on_every_frame() -> None:
manifest = complete_manifest()
manifest["observations"]["frames"][1]["surfaces"].pop("stSidebar")
with pytest.raises(ValueError, match="surface"):
evidence.validate_transition_manifest(manifest)
def test_validate_transition_manifest_requires_stable_surface_style_evidence() -> None:
manifest = complete_manifest()
manifest["observations"]["stable_shell"]["surfaces"]["post"]["stSidebar"]["opacity"] = "0"
with pytest.raises(ValueError, match="surface"):
evidence.validate_transition_manifest(manifest)
@pytest.mark.parametrize(
("field", "value"),
(
("backgroundImage", "none"),
("backgroundColor", ""),
("visibility", "hidden"),
("display", "none"),
("opacity", "0"),
("opacity", "NaN"),
("rendered", False),
),
)
def test_validate_transition_manifest_rejects_non_visible_surface_styles(field: str, value: object) -> None:
manifest = complete_manifest()
observations = manifest["observations"]
for frame in observations["frames"]:
frame["surfaces"]["stAppViewContainer"][field] = value
observations["stable_shell"]["surfaces"]["pre"]["stAppViewContainer"][field] = value
observations["stable_shell"]["surfaces"]["post"]["stAppViewContainer"][field] = value
with pytest.raises(ValueError, match="surface"):
evidence.validate_transition_manifest(manifest)
def test_validate_transition_manifest_rejects_missing_stable_shell_markers() -> None:
manifest = complete_manifest()
manifest["observations"]["stable_shell"]["document_marker"] = {"pre": None, "post": None}
with pytest.raises(ValueError, match="shell"):
evidence.validate_transition_manifest(manifest)