166 lines
6.0 KiB
Python
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
|