Files
UmbraAudio/tests/test_speaker_confidence_approval.py
2026-10-18 19:01:04 -07:00

166 lines
6.0 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Tests for the reviewer-facing behaviour of speaker identification confidence.
The specific regression: a reviewer pressed Approve on a speaker, the badge
stayed, and the button looked broken. ``speaker_confidence`` never read
``voice_status``, so no amount of approving could clear it. Its own docstring
claimed an approved voice was a positive signal, so this restores the stated
behaviour rather than adding a new one.
"""
from __future__ import annotations
import json
from pathlib import Path
from umbraaudio.analysis.speaker_confidence import (
assess_speakers,
load_speaker_assessments,
)
CLEAR = "clear"
NEEDS_REVIEW = "needs_review"
# A speaker the heuristic will always doubt: a role-only name, no dialogue.
ROLE_ONLY = {
"primary_name": "Advisor",
"demographics": {},
"vocal_traits": [],
"description": "Created from unresolved name token 'Advisor'",
"aliases": ["Advisor"],
}
def _stats(**over) -> dict:
base = {
"Advisor": {"utterances": 1, "needs_review": 0, "mean_confidence": 0.5}
}
base.update(over)
return base
def test_role_only_speaker_is_flagged_before_approval() -> None:
out = assess_speakers([dict(ROLE_ONLY)], _stats())
assert out[0].status == NEEDS_REVIEW
assert out[0].reviewed is False
def test_approval_clears_the_badge() -> None:
"""The bug: Approve recorded a decision the assessment ignored."""
approved = dict(ROLE_ONLY, voice_status="approved")
out = assess_speakers([approved], _stats())
assert out[0].status == CLEAR
assert out[0].reviewed is True
def test_approval_keeps_the_reasons_visible() -> None:
"""Clearing the badge must not discard the underlying facts."""
approved = dict(ROLE_ONLY, voice_status="approved")
out = assess_speakers([approved], _stats())
assert out[0].reasons, "reasons should survive an approval"
assert any("utterance" in r for r in out[0].reasons)
def test_approval_is_matched_case_insensitively() -> None:
approved = dict(ROLE_ONLY, voice_status="Approved")
assert assess_speakers([approved], _stats())[0].status == CLEAR
def test_pending_voice_status_does_not_clear() -> None:
pending = dict(ROLE_ONLY, voice_status="pending")
assert assess_speakers([pending], _stats())[0].status == NEEDS_REVIEW
def test_rejected_voice_status_does_not_clear() -> None:
rejected = dict(ROLE_ONLY, voice_status="rejected")
assert assess_speakers([rejected], _stats())[0].status == NEEDS_REVIEW
def test_unreviewed_speaker_is_unaffected() -> None:
"""Sanity: speakers without a voice_status behave exactly as before."""
out = assess_speakers([dict(ROLE_ONLY)], _stats())
assert out[0].status == NEEDS_REVIEW
def test_already_clear_speaker_stays_clear_after_approval() -> None:
solid = {
"primary_name": "Adric",
"demographics": {"gender": "male"},
"vocal_traits": ["measured"],
"description": "The palace factor",
"aliases": [],
}
stats = {"Adric": {"utterances": 400, "needs_review": 0, "mean_confidence": 0.9}}
out = assess_speakers([dict(solid, voice_status="approved")], stats)
assert out[0].status == CLEAR
assert out[0].reviewed is True
# ------------------------------------------------------------- via the loader
def test_load_speaker_assessments_exposes_reviewed(tmp_path: Path) -> None:
(tmp_path / "speakers.json").write_text(
json.dumps({"speakers": [dict(ROLE_ONLY, voice_status="approved")]})
)
got = load_speaker_assessments(tmp_path)
assert got["Advisor"].reviewed is True
assert got["Advisor"].status == CLEAR
def test_missing_speakers_file_returns_empty(tmp_path: Path) -> None:
assert load_speaker_assessments(tmp_path) == {}
def test_ledger_overrides_stale_speakers_json(tmp_path: Path) -> None:
"""The exact "Advisor" regression: two sources of truth disagreed.
The Approve button writes ``voice_status.json``. The speaker records in
``speakers.json`` kept a stale inline ``voice_status`` that the web layer
overwrites on read. Reading the stale copy left an approved speaker flagged
even though the reviewer had accepted it.
"""
(tmp_path / "speakers.json").write_text(
json.dumps({"speakers": [dict(ROLE_ONLY, voice_status="pending")]})
)
(tmp_path / "voice_status.json").write_text(json.dumps({"Advisor": "approved"}))
got = load_speaker_assessments(tmp_path)["Advisor"]
assert got.status == CLEAR, "ledger approval must win over the stale inline copy"
assert got.reviewed is True
def test_ledger_pending_does_not_clear(tmp_path: Path) -> None:
(tmp_path / "speakers.json").write_text(
json.dumps({"speakers": [dict(ROLE_ONLY, voice_status="approved")]})
)
(tmp_path / "voice_status.json").write_text(json.dumps({"Advisor": "pending"}))
assert load_speaker_assessments(tmp_path)["Advisor"].status == NEEDS_REVIEW
def test_corrupt_ledger_does_not_break_the_page(tmp_path: Path) -> None:
(tmp_path / "speakers.json").write_text(json.dumps({"speakers": [ROLE_ONLY]}))
(tmp_path / "voice_status.json").write_text("{not json")
got = load_speaker_assessments(tmp_path)
assert got["Advisor"].status == NEEDS_REVIEW
def test_ledger_that_is_a_list_is_ignored(tmp_path: Path) -> None:
(tmp_path / "speakers.json").write_text(json.dumps({"speakers": [ROLE_ONLY]}))
(tmp_path / "voice_status.json").write_text('["approved"]')
assert load_speaker_assessments(tmp_path)["Advisor"].status == NEEDS_REVIEW
def test_assess_speakers_accepts_ledger_directly() -> None:
out = assess_speakers([dict(ROLE_ONLY)], _stats(), {"Advisor": "approved"})
assert out[0].status == CLEAR
def test_absent_ledger_key_falls_back_to_inline() -> None:
"""No ledger entry for this speaker: the inline value still applies."""
out = assess_speakers(
[dict(ROLE_ONLY, voice_status="approved")], _stats(), {"Someone Else": "approved"}
)
assert out[0].status == CLEAR