Attribution (from prior session, now committed): - llm_attributor: mechanical narration evidence, unnamed-role rules, wider context (12->20 lines, 8->40 utterances), uncertain flag, verify pass with 20-item batches and invented-name gate - reattribute_all: --min-id/--max-id, --dry-run, --no-verify, --verify-rounds, qa_accepted skip guard, per-chapter elapsed times - test_multipass_attribution: 19 tests Web banner: - voice_maintenance module: pid-liveness-checked marker file so a crashed run self-heals the banner away - routes/base.html: amber banner on every page while the LLM re-attribution run has Qwen3-TTS offline - reattribute_all: sets banner before switching models, clears in finally (also on failure) - test_voice_maintenance: 8 tests qwen3-tts submodule: UmbraAudio Voice Studio branding in web demo (icon data-uri + favicon logo).
76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""Tests for the web-UI voice-maintenance banner marker.
|
|
|
|
The marker is a small JSON file whose ``pid`` field gates the banner: a
|
|
run that died without clearing the file must not leave the banner up
|
|
forever, so ``active_voice_maintenance`` checks the owner is still alive.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from umbraaudio import voice_maintenance as vm
|
|
|
|
|
|
@pytest.fixture
|
|
def marker(tmp_path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
path = tmp_path / "voice_maintenance.json"
|
|
monkeypatch.setattr(vm, "MAINTENANCE_FILE", path)
|
|
return path
|
|
|
|
|
|
def test_set_writes_marker_with_live_pid(marker: Path) -> None:
|
|
vm.set_voice_maintenance("analysis in progress")
|
|
assert marker.exists()
|
|
data = json.loads(marker.read_text())
|
|
assert data["pid"] == os.getpid()
|
|
assert "analysis" in data["reason"]
|
|
|
|
|
|
def test_active_returns_marker_while_owner_alive(marker: Path) -> None:
|
|
vm.set_voice_maintenance("analysis in progress")
|
|
active = vm.active_voice_maintenance()
|
|
assert active is not None
|
|
assert active["pid"] == os.getpid()
|
|
|
|
|
|
def test_active_none_when_no_marker(marker: Path) -> None:
|
|
assert vm.active_voice_maintenance() is None
|
|
|
|
|
|
def test_clear_removes_marker(marker: Path) -> None:
|
|
vm.set_voice_maintenance("analysis in progress")
|
|
vm.clear_voice_maintenance()
|
|
assert not marker.exists()
|
|
assert vm.active_voice_maintenance() is None
|
|
|
|
|
|
def test_clear_missing_marker_is_idempotent(marker: Path) -> None:
|
|
vm.clear_voice_maintenance()
|
|
vm.clear_voice_maintenance()
|
|
|
|
|
|
def test_active_none_when_owner_dead(marker: Path) -> None:
|
|
marker.write_text(
|
|
json.dumps({"pid": 2**24, "started_at": 1, "reason": "dead owner"})
|
|
)
|
|
assert vm.active_voice_maintenance() is None
|
|
|
|
|
|
def test_active_none_on_corrupt_marker(marker: Path) -> None:
|
|
marker.write_text("{not json")
|
|
assert vm.active_voice_maintenance() is None
|
|
|
|
|
|
def test_active_none_on_bad_pid_type(marker: Path) -> None:
|
|
marker.write_text(
|
|
json.dumps({"pid": "not-an-int", "started_at": 1, "reason": "bad"})
|
|
)
|
|
assert vm.active_voice_maintenance() is None
|