100% Coverage

This commit is contained in:
Renn F
2026-05-06 21:02:31 +02:00
parent 64c48356d0
commit 9aa30fb945
106 changed files with 28143 additions and 423 deletions
+229
View File
@@ -0,0 +1,229 @@
"""roboco.models.journal coverage — entry factory functions.
Covers all create_*_entry helpers: happy paths, journal_id required guard,
and optional-field branches (how_applied, source, resolution, help_needed).
"""
from __future__ import annotations
from uuid import uuid4
import pytest
from roboco.models.base import JournalEntryType
from roboco.models.journal import (
DecisionLogParams,
GeneralEntryParams,
LearningEntryParams,
StruggleEntryParams,
TaskReflectionParams,
create_decision_log,
create_general_entry,
create_learning_entry,
create_struggle_entry,
create_task_reflection,
)
# ---------------------------------------------------------------------------
# create_task_reflection
# ---------------------------------------------------------------------------
def test_create_task_reflection_happy_path() -> None:
journal_id = uuid4()
task_id = uuid4()
entry = create_task_reflection(
TaskReflectionParams(
task_id=task_id,
title="Reflection",
what_done="Implemented X",
what_learned="Y is tricky",
what_struggled="Z timing",
next_steps=["Add tests", "Refactor"],
journal_id=journal_id,
)
)
assert entry.type == JournalEntryType.TASK_REFLECTION
assert entry.journal_id == journal_id
assert entry.task_id == task_id
assert "Implemented X" in entry.content
assert "[ ] Add tests" in entry.content
def test_create_task_reflection_requires_journal_id() -> None:
with pytest.raises(ValueError, match="journal_id is required"):
create_task_reflection(
TaskReflectionParams(
task_id=uuid4(),
title="t",
what_done="d",
what_learned="l",
what_struggled="s",
next_steps=[],
)
)
# ---------------------------------------------------------------------------
# create_decision_log
# ---------------------------------------------------------------------------
def test_create_decision_log_happy_path() -> None:
journal_id = uuid4()
entry = create_decision_log(
DecisionLogParams(
title="Pick framework",
context="We need a web framework",
options=[
{"name": "FastAPI", "pros": "fast", "cons": "newer"},
{"name": "Django", "pros": "batteries", "cons": "heavy"},
],
chosen="FastAPI",
rationale="async-first",
consequences=["Need uvicorn", "Async DB"],
journal_id=journal_id,
)
)
assert entry.type == JournalEntryType.DECISION_LOG
assert "Option 1: FastAPI" in entry.content
assert "Option 2: Django" in entry.content
assert "Chose **FastAPI**" in entry.content
def test_create_decision_log_handles_missing_option_keys() -> None:
"""Options without name/pros/cons should default to placeholders."""
entry = create_decision_log(
DecisionLogParams(
title="Pick",
context="ctx",
options=[{}, {}],
chosen="X",
rationale="r",
consequences=[],
journal_id=uuid4(),
)
)
assert "Option 1" in entry.content
assert "N/A" in entry.content
def test_create_decision_log_requires_journal_id() -> None:
with pytest.raises(ValueError, match="journal_id is required"):
create_decision_log(
DecisionLogParams(
title="t",
context="c",
options=[],
chosen="x",
rationale="r",
consequences=[],
)
)
# ---------------------------------------------------------------------------
# create_learning_entry
# ---------------------------------------------------------------------------
def test_create_learning_entry_with_all_optionals() -> None:
entry = create_learning_entry(
LearningEntryParams(
title="Learned async",
what_learned="async/await",
how_applied="Used in service layer",
source="Real World Python book",
journal_id=uuid4(),
)
)
assert entry.type == JournalEntryType.LEARNING
assert "How I Applied It" in entry.content
assert "## Source" in entry.content
assert entry.sentiment == "positive"
def test_create_learning_entry_without_optionals() -> None:
entry = create_learning_entry(
LearningEntryParams(
title="L",
what_learned="basic",
journal_id=uuid4(),
)
)
assert "How I Applied It" not in entry.content
assert "## Source" not in entry.content
def test_create_learning_entry_requires_journal_id() -> None:
with pytest.raises(ValueError, match="journal_id is required"):
create_learning_entry(LearningEntryParams(title="t", what_learned="w"))
# ---------------------------------------------------------------------------
# create_struggle_entry
# ---------------------------------------------------------------------------
def test_create_struggle_entry_with_resolution_and_help() -> None:
entry = create_struggle_entry(
StruggleEntryParams(
title="Bug",
what_struggled="race condition",
attempted_solutions=["lock", "queue"],
resolution="Used asyncio.Lock",
help_needed="None - solved",
journal_id=uuid4(),
)
)
assert entry.type == JournalEntryType.STRUGGLE
assert "## Resolution" in entry.content
assert "## Help Needed" in entry.content
assert entry.sentiment == "frustrated"
def test_create_struggle_entry_without_optionals() -> None:
entry = create_struggle_entry(
StruggleEntryParams(
title="x",
what_struggled="y",
attempted_solutions=["a"],
journal_id=uuid4(),
)
)
assert "## Resolution" not in entry.content
assert "## Help Needed" not in entry.content
def test_create_struggle_entry_requires_journal_id() -> None:
with pytest.raises(ValueError, match="journal_id is required"):
create_struggle_entry(
StruggleEntryParams(
title="t",
what_struggled="x",
attempted_solutions=[],
)
)
# ---------------------------------------------------------------------------
# create_general_entry
# ---------------------------------------------------------------------------
def test_create_general_entry_happy_path() -> None:
entry = create_general_entry(
GeneralEntryParams(
title="General note",
content="Just thinking",
is_private=True,
journal_id=uuid4(),
)
)
assert entry.type == JournalEntryType.GENERAL
assert entry.is_private is True
assert entry.content == "Just thinking"
def test_create_general_entry_requires_journal_id() -> None:
with pytest.raises(ValueError, match="journal_id is required"):
create_general_entry(GeneralEntryParams(title="t", content="c"))
+120
View File
@@ -0,0 +1,120 @@
"""roboco.models.llm coverage — TOON config and metrics dataclasses.
Covers ToonConfig defaults, EncodedBlock __str__, LLMUsage totals, and
ToonMetrics record/reset/savings/fallback rate computations.
"""
from __future__ import annotations
from datetime import UTC, datetime
from roboco.models.llm import (
EncodedBlock,
LLMUsage,
ToonConfig,
ToonMetrics,
)
_BIG_JSON = 100
_BIG_TOON = 60
_INPUT = 100
_OUTPUT = 50
_CACHE_CREATE = 10
_CACHE_READ = 20
_FALLBACK_PERCENT_HALF = 50.0
_TWO_DECODES = 2
def test_toon_config_defaults() -> None:
cfg = ToonConfig()
assert cfg.delimiter == ","
assert cfg.indent > 0
assert cfg.include_length is True
def test_encoded_block_str_includes_label_and_content() -> None:
blk = EncodedBlock(content="data", label="Section")
s = str(blk)
assert "Section" in s
assert "data" in s
def test_llm_usage_total_tokens() -> None:
usage = LLMUsage(input_tokens=_INPUT, output_tokens=_OUTPUT)
assert usage.total_tokens == _INPUT + _OUTPUT
def test_llm_usage_total_input_with_cache() -> None:
usage = LLMUsage(
input_tokens=_INPUT,
cache_creation_input_tokens=_CACHE_CREATE,
cache_read_input_tokens=_CACHE_READ,
)
assert usage.total_input_with_cache == _INPUT + _CACHE_CREATE + _CACHE_READ
def test_toon_metrics_initial_state() -> None:
m = ToonMetrics()
assert m.json_chars == 0
assert m.toon_chars == 0
assert m.encode_count == 0
assert m.decode_count == 0
assert m.savings_percent == 0.0 # zero json_chars guard
assert m.fallback_rate == 0.0 # zero decode_count guard
def test_toon_metrics_record_encode() -> None:
m = ToonMetrics()
m.record_encode(json_chars=_BIG_JSON, toon_chars=_BIG_TOON)
assert m.json_chars == _BIG_JSON
assert m.toon_chars == _BIG_TOON
assert m.encode_count == 1
# 40% reduction from 100 -> 60.
expected_savings = (1 - _BIG_TOON / _BIG_JSON) * 100
assert m.savings_percent == expected_savings
def test_toon_metrics_record_decode_no_fallback() -> None:
m = ToonMetrics()
m.record_decode(used_fallback=False)
assert m.decode_count == 1
assert m.decode_fallback_count == 0
assert m.fallback_rate == 0.0
def test_toon_metrics_record_decode_with_fallback() -> None:
m = ToonMetrics()
m.record_decode(used_fallback=False)
m.record_decode(used_fallback=True)
assert m.decode_count == _TWO_DECODES
assert m.decode_fallback_count == 1
assert m.fallback_rate == _FALLBACK_PERCENT_HALF
def test_toon_metrics_to_dict_shape() -> None:
m = ToonMetrics()
m.record_encode(_BIG_JSON, _BIG_TOON)
m.record_decode(used_fallback=True)
out = m.to_dict()
assert out["json_chars"] == _BIG_JSON
assert out["toon_chars"] == _BIG_TOON
assert out["encode_count"] == 1
assert out["decode_count"] == 1
assert "started_at" in out
def test_toon_metrics_reset_clears_counters() -> None:
m = ToonMetrics()
m.record_encode(_BIG_JSON, _BIG_TOON)
m.record_decode(used_fallback=True)
before_reset = m.started_at
m.reset()
assert m.json_chars == 0
assert m.toon_chars == 0
assert m.encode_count == 0
assert m.decode_count == 0
assert m.decode_fallback_count == 0
# started_at is refreshed after reset.
assert isinstance(m.started_at, datetime)
assert m.started_at >= before_reset
assert m.started_at.tzinfo == UTC
+317
View File
@@ -0,0 +1,317 @@
"""Coverage for misc model methods, factories, and helper functions.
Targets the long-tail of small-percent gaps in roboco.models.* — convenience
properties, factory functions, lookup helpers, and __post_init__ branches.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from uuid import uuid4
from roboco.models import AgentRole, Team
from roboco.models.a2a import (
A2AConversation,
A2AConversationStatus,
A2ATaskState,
a2a_state_to_task_status,
task_status_to_a2a_state,
)
from roboco.models.agents import AgentConfig
from roboco.models.base import ModelProvider
from roboco.models.channel import (
create_announcements_channel,
create_cell_channel,
create_cross_cell_channel,
)
from roboco.models.handoff import HandoffParams, create_handoff
from roboco.models.llm_catalog import (
MODEL_CATALOG,
_build_anthropic_entries,
provider_type_for_model,
)
from roboco.models.permissions import (
AgentContext,
PermissionLevel,
_build_role_levels,
)
from roboco.models.runtime import (
AgentInstance,
OrchestratorAgentConfig,
OrchestratorAgentState,
)
# ---------------------------------------------------------------------------
# AgentConfig convenience properties (roboco/models/agents.py 63, 68, 73, 78)
# ---------------------------------------------------------------------------
def test_agent_config_convenience_properties() -> None:
cfg = AgentConfig(
name="Test",
slug="test",
role=AgentRole.DEVELOPER,
team=Team.BACKEND,
system_prompt="hello",
)
# provider, model, temperature, max_tokens come from model_config_data
assert cfg.provider == cfg.model_config_data.provider
assert cfg.model == cfg.model_config_data.name
assert cfg.temperature == cfg.model_config_data.temperature
assert cfg.max_tokens == cfg.model_config_data.max_tokens
# ---------------------------------------------------------------------------
# AgentInstance.__post_init__ — explicit None id branch (runtime.py 76-77)
# ---------------------------------------------------------------------------
def test_agent_instance_post_init_assigns_uuid_when_falsy() -> None:
# Forcing an empty UUID(int=0) is falsy → the post_init triggers re-assign.
inst = AgentInstance.__new__(AgentInstance)
# Fill required dataclass fields explicitly so __post_init__ runs cleanly.
inst.id = None # type: ignore[assignment]
inst.agent_id = "be-dev-1"
inst.state = OrchestratorAgentState.OFFLINE
inst.container_id = None
inst.config = None
inst.started_at = None
inst.last_activity = None
inst.current_task_id = None
inst.error_count = 0
inst.waiting_for = None
inst.waiting_context = {}
inst.__post_init__()
assert inst.id is not None
def test_agent_instance_default_factory_assigns_id() -> None:
inst = AgentInstance(agent_id="be-dev-1")
assert inst.id is not None
def test_orchestrator_agent_config_defaults() -> None:
cfg = OrchestratorAgentConfig(
agent_id="be-dev-1",
blueprint_path=Path("/tmp/blueprint"),
)
assert cfg.model == "sonnet"
assert cfg.provider_type == "anthropic"
# ---------------------------------------------------------------------------
# llm_catalog: catalog skip when MODEL_MAP missing entry (line 56)
# ---------------------------------------------------------------------------
def test_build_anthropic_entries_skips_missing_models() -> None:
# Patch MODEL_MAP to be missing one entry → loop hits the `continue` branch.
with patch(
"roboco.models.llm_catalog.MODEL_MAP",
{"opus": "claude-opus-4-6"}, # sonnet+haiku missing
):
entries = _build_anthropic_entries()
# Only one entry survives — opus.
assert len(entries) == 1
assert entries[0].model_name == "opus"
def test_provider_type_for_model_known() -> None:
# Pick the first catalog entry deterministically.
sample = MODEL_CATALOG[0]
assert provider_type_for_model(sample.model_name) == sample.provider_type
def test_provider_type_for_model_unknown_returns_none() -> None:
assert provider_type_for_model("nonexistent-model-x") is None
# ---------------------------------------------------------------------------
# permissions._build_role_levels — exception branch (lines 35-36)
# ---------------------------------------------------------------------------
def test_build_role_levels_skips_invalid_role() -> None:
"""Bad role string should be silently swallowed in the except branch."""
with patch(
"roboco.models.permissions.ROLE_PERMISSION_LEVELS",
{"not_a_role": "CEO", "ceo": "CEO"},
):
result = _build_role_levels()
# 'not_a_role' silently dropped; ceo retained.
assert AgentRole.CEO in result
assert result[AgentRole.CEO] == PermissionLevel.CEO
def test_build_role_levels_skips_invalid_level() -> None:
with patch(
"roboco.models.permissions.ROLE_PERMISSION_LEVELS",
{"ceo": "NOT_A_LEVEL"},
):
result = _build_role_levels()
assert result == {}
# ---------------------------------------------------------------------------
# Channel factories — create_cell_channel, create_cross_cell_channel,
# create_announcements_channel (channel.py 98, 116-117, 135)
# ---------------------------------------------------------------------------
def test_create_cell_channel() -> None:
members = [uuid4(), uuid4()]
auditor_id = uuid4()
ch = create_cell_channel("backend", members, auditor_id)
assert ch.name == "#backend-cell"
assert ch.slug == "backend-cell"
assert ch.members == members
assert ch.silent_observers == [auditor_id]
def test_create_cross_cell_channel() -> None:
members = [uuid4(), uuid4()]
main_pm = uuid4()
auditor = uuid4()
ch = create_cross_cell_channel("dev-all", members, main_pm, auditor)
assert ch.name == "#dev-all"
# main_pm joins the member list.
assert main_pm in ch.members
assert auditor in ch.silent_observers
def test_create_announcements_channel() -> None:
agents = [uuid4() for _ in range(3)]
board = [uuid4(), uuid4()]
main_pm = uuid4()
auditor = uuid4()
ch = create_announcements_channel(agents, board, main_pm, auditor)
assert ch.slug == "announcements"
# Writers = board + main_pm.
assert main_pm in ch.writers
for b in board:
assert b in ch.writers
# ---------------------------------------------------------------------------
# A2A helpers — A2AConversation methods + a2a_state_to_task_status fallback
# ---------------------------------------------------------------------------
def test_a2a_conversation_other_agent() -> None:
conv = A2AConversation(agent_a="be-dev-1", agent_b="fe-dev-1")
assert conv.other_agent("be-dev-1") == "fe-dev-1"
assert conv.other_agent("fe-dev-1") == "be-dev-1"
def test_a2a_conversation_my_unread() -> None:
_UNREAD_A = 2
_UNREAD_B = 5
conv = A2AConversation(
agent_a="be-dev-1",
agent_b="fe-dev-1",
unread_by_a=_UNREAD_A,
unread_by_b=_UNREAD_B,
)
assert conv.my_unread("be-dev-1") == _UNREAD_A
assert conv.my_unread("fe-dev-1") == _UNREAD_B
def test_a2a_conversation_is_participant() -> None:
conv = A2AConversation(agent_a="be-dev-1", agent_b="fe-dev-1")
assert conv.is_participant("be-dev-1") is True
assert conv.is_participant("fe-dev-1") is True
assert conv.is_participant("ux-dev-1") is False
def test_a2a_state_to_task_status_unknown_returns_pending() -> None:
# Default fallback for any unknown state is "pending".
class _FakeState:
pass
fake = _FakeState()
assert a2a_state_to_task_status(fake) == "pending" # type: ignore[arg-type]
def test_a2a_state_to_task_status_known() -> None:
assert a2a_state_to_task_status(A2ATaskState.SUBMITTED) == "pending"
assert a2a_state_to_task_status(A2ATaskState.WORKING) == "in_progress"
assert a2a_state_to_task_status(A2ATaskState.COMPLETED) == "completed"
assert a2a_state_to_task_status(A2ATaskState.FAILED) == "cancelled"
assert a2a_state_to_task_status(A2ATaskState.INPUT_REQUIRED) == "blocked"
def test_task_status_to_a2a_state_known() -> None:
assert task_status_to_a2a_state("pending") == A2ATaskState.SUBMITTED
assert task_status_to_a2a_state("in_progress") == A2ATaskState.WORKING
assert task_status_to_a2a_state("completed") == A2ATaskState.COMPLETED
assert task_status_to_a2a_state("cancelled") == A2ATaskState.CANCELLED
assert task_status_to_a2a_state("blocked") == A2ATaskState.INPUT_REQUIRED
def test_task_status_to_a2a_state_unknown_returns_working() -> None:
assert task_status_to_a2a_state("garbage_status") == A2ATaskState.WORKING
# ---------------------------------------------------------------------------
# AgentContext.level — line 79
# ---------------------------------------------------------------------------
def test_agent_context_level_returns_role_level() -> None:
ctx = AgentContext(agent_id=uuid4(), role=AgentRole.CEO)
assert ctx.level == PermissionLevel.CEO
def test_agent_context_level_falls_back_when_role_missing() -> None:
"""Edge: build an AgentContext for a role removed from ROLE_LEVELS."""
ctx = AgentContext(agent_id=uuid4(), role=AgentRole.DEVELOPER)
# Default fallback returns CELL_MEMBER even when not present.
assert ctx.level in PermissionLevel
# ---------------------------------------------------------------------------
# Conversation status enum + closed/paused round-trip
# ---------------------------------------------------------------------------
def test_a2a_conversation_status_values() -> None:
assert A2AConversationStatus.ACTIVE == "active"
assert A2AConversationStatus.PAUSED == "paused"
assert A2AConversationStatus.CLOSED == "closed"
# ---------------------------------------------------------------------------
# Handoff factory (models/handoff.py 202-208)
# ---------------------------------------------------------------------------
def test_create_handoff_includes_changelog_required_doc() -> None:
task_id = uuid4()
handoff = create_handoff(
HandoffParams(
task_id=task_id,
summary="Built feature X",
commits=[{"sha": "abc123", "message": "init"}],
dev_notes_location="/notes/x",
new_functionality=["new login"],
modified_behavior=["redirect"],
breaking_changes=["remove old endpoint"],
)
)
assert handoff.task_id == task_id
assert handoff.summary == "Built feature X"
# required_docs always includes a changelog item.
doc_types = [d.doc_type for d in handoff.required_docs]
assert "changelog" in doc_types
assert handoff.new_functionality == ["new login"]
# ---------------------------------------------------------------------------
# Sanity: ModelProvider import is reachable
# ---------------------------------------------------------------------------
def test_model_provider_enum_has_anthropic() -> None:
assert ModelProvider.ANTHROPIC == "anthropic"
@@ -0,0 +1,152 @@
"""Notification model factory function coverage."""
from __future__ import annotations
from uuid import uuid4
from roboco.models import NotificationPriority, NotificationType
from roboco.models.notification import (
create_alert,
create_blocker_escalation,
create_broadcast,
create_documentation_request,
create_priority_change,
create_review_request,
create_task_assignment,
)
def test_create_task_assignment() -> None:
pm_id = uuid4()
agent_id = uuid4()
task_id = uuid4()
n = create_task_assignment(
from_pm=pm_id,
to_agent=agent_id,
task_id=task_id,
task_title="Build feature X",
)
assert n.type == NotificationType.TASK_ASSIGNMENT
assert n.from_agent == pm_id
assert n.to_agents == [agent_id]
assert n.related_task_id == task_id
assert "Build feature X" in n.subject
def test_create_blocker_escalation() -> None:
from_pm = uuid4()
to_pm = uuid4()
task_id = uuid4()
n = create_blocker_escalation(
from_pm=from_pm,
to_pm=to_pm,
task_id=task_id,
blocker_description="DB unreachable",
)
assert n.type == NotificationType.BLOCKER_ESCALATION
assert n.priority == NotificationPriority.HIGH
assert n.body == "DB unreachable"
assert n.related_task_id == task_id
def test_create_review_request() -> None:
from_pm = uuid4()
to_qa = uuid4()
task_id = uuid4()
n = create_review_request(
from_pm=from_pm,
to_qa=to_qa,
task_id=task_id,
task_title="Refactor login",
)
assert n.type == NotificationType.REVIEW_REQUEST
assert "Refactor login" in n.subject
assert n.to_agents == [to_qa]
def test_create_documentation_request() -> None:
from_pm = uuid4()
to_doc = uuid4()
task_id = uuid4()
n = create_documentation_request(
from_pm=from_pm,
to_documenter=to_doc,
task_id=task_id,
task_title="API endpoint",
)
assert n.type == NotificationType.DOCUMENTATION_REQUEST
assert "API endpoint" in n.subject
assert "needs documentation" in n.body
def test_create_priority_change_p0_marks_urgent() -> None:
sender = uuid4()
recipients = [uuid4()]
task_id = uuid4()
n = create_priority_change(
from_agent=sender,
to_agents=recipients,
task_id=task_id,
task_title="Critical task",
new_priority=0,
)
assert n.type == NotificationType.PRIORITY_CHANGE
assert n.priority == NotificationPriority.URGENT
assert "P0" in n.body
def test_create_priority_change_high_label() -> None:
sender = uuid4()
recipients = [uuid4()]
task_id = uuid4()
n = create_priority_change(
from_agent=sender,
to_agents=recipients,
task_id=task_id,
task_title="P1 task",
new_priority=1,
)
assert n.priority == NotificationPriority.HIGH
assert "P1" in n.body
def test_create_priority_change_unknown_priority_uses_fallback_label() -> None:
sender = uuid4()
recipients = [uuid4()]
task_id = uuid4()
n = create_priority_change(
from_agent=sender,
to_agents=recipients,
task_id=task_id,
task_title="Custom",
new_priority=99,
)
# Body falls through to f"P{new_priority}".
assert "P99" in n.body
def test_create_alert() -> None:
sender = uuid4()
recipients = [uuid4(), uuid4()]
n = create_alert(
from_agent=sender,
to_agents=recipients,
subject="System down",
body="rebooting now",
)
assert n.type == NotificationType.ALERT
assert n.priority == NotificationPriority.URGENT
assert n.subject == "System down"
def test_create_broadcast_does_not_require_ack() -> None:
sender = uuid4()
recipients = [uuid4()]
n = create_broadcast(
from_agent=sender,
to_agents=recipients,
subject="All hands",
body="please review",
)
assert n.type == NotificationType.BROADCAST
assert n.requires_ack is False
+18 -7
View File
@@ -37,7 +37,8 @@ def test_clear_returns_content_and_resets() -> None:
def test_char_count() -> None:
buf = _buffer()
buf.append("12345")
assert buf.char_count == 5
_DATA_LEN = 5
assert buf.char_count == _DATA_LEN
def test_age_property_positive() -> None:
@@ -95,14 +96,24 @@ def test_has_sentence_ending_empty_returns_false() -> None:
assert buf._has_sentence_ending() is False
_DEFAULT_MIN_CHARS = 50
_DEFAULT_MAX_CHARS = 5000
_DEFAULT_IDLE_SECONDS = 2.0
_CUSTOM_MIN_CHARS = 10
_CUSTOM_MAX_BUFFERS = 5
def test_transcription_config_defaults() -> None:
cfg = TranscriptionConfig()
assert cfg.min_chars_for_extraction == 50
assert cfg.max_chars_before_flush == 5000
assert cfg.idle_threshold_seconds == 2.0
assert cfg.min_chars_for_extraction == _DEFAULT_MIN_CHARS
assert cfg.max_chars_before_flush == _DEFAULT_MAX_CHARS
assert cfg.idle_threshold_seconds == _DEFAULT_IDLE_SECONDS
def test_transcription_config_custom() -> None:
cfg = TranscriptionConfig(min_chars_for_extraction=10, max_buffers_per_agent=5)
assert cfg.min_chars_for_extraction == 10
assert cfg.max_buffers_per_agent == 5
cfg = TranscriptionConfig(
min_chars_for_extraction=_CUSTOM_MIN_CHARS,
max_buffers_per_agent=_CUSTOM_MAX_BUFFERS,
)
assert cfg.min_chars_for_extraction == _CUSTOM_MIN_CHARS
assert cfg.max_buffers_per_agent == _CUSTOM_MAX_BUFFERS