Files
CyberGril/tests/test_contracts.py
mobai 1012d8bb79 chore: project foundation
Contracts (the shared language):
- percept / state / context / utterance / render / audio / capabilities
- Capability model instead of vendor schemas (see below)

Providers (capability-based abstraction):
- LLM + Embedding: OpenAI-compatible shape IS the standard, adopted directly
- ASR: internal contract is an event stream (partial -> final), because
  /v1/audio/transcriptions has no partial and dialogue needs it for
  endpointing. OpenAI adapter degrades to supports_partial=False.
- TTS: internal contract carries tags/emotion/style, because
  /v1/audio/speech has no paralinguistics and [laughter]/[breath] are
  the core of feeling alive. Unsupported providers are stripped AND
  reported via Degradation -- silent degradation is forbidden.
- Avatar: no de-facto standard, custom RenderCmd protocol driven by State.

Core:
- mood: dynamical system, slow variables, negativity bias (grudge),
  numbing after prolonged negativity
- bazi: four pillars -> numeric vector; 'ming' is data, not prose
- projection: phi, system event -> in-world statement; uncovered events
  are counted as projection gaps

Storage:
- SQLite schema, three memory layers, dyad ledger
- accessibility floor 0.03 (not 0): forgettable but still there

Config split:
- models.yaml is the ONLY place model versions live
- character.yaml holds world rules / emotion params, no version numbers

58 tests pass; ruff clean.
2026-10-19 20:08:52 +08:00

118 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""契约层测试。
这些测试守的是"接口形状",不是实现细节。
接口一变,它们就该红——因为所有层都依赖这些形状。
"""
from __future__ import annotations
from cybergril.contracts import (
Capabilities,
Degradation,
Expression,
Modality,
ParaEmotion,
ParaEvent,
Paralinguistics,
Percept,
SpeechTag,
State,
Streaming,
Utterance,
)
def test_state_clamp_bounds() -> None:
s = State(v=5.0, a=3.0, d=-1.0, e=2.0)
s.clamp()
assert -1.0 <= s.v <= 1.0
assert 0.0 <= s.a <= 1.0
assert 0.0 <= s.d <= 1.0
assert 0.0 <= s.e <= 1.0
def test_state_energy_band() -> None:
assert State(e=0.1).energy_band() == "exhausted"
assert State(e=0.3).energy_band() == "tired"
assert State(e=0.5).energy_band() == "normal"
assert State(e=0.9).energy_band() == "energized"
def test_utterance_strip_tags_removes_inline() -> None:
u = Utterance(
text="真的吗[laughter]……我、我先想想[breath]",
speech_tags=(SpeechTag.LAUGHTER, SpeechTag.BREATH),
)
stripped = u.strip_tags()
assert "[laughter]" not in stripped.text
assert "[breath]" not in stripped.text
assert stripped.speech_tags == ()
# 原文不变(frozen)
assert "[laughter]" in u.text
def test_utterance_tag_density() -> None:
u = Utterance(text="好" * 100, speech_tags=(SpeechTag.LAUGHTER,))
assert abs(u.tag_density() - 0.01) < 1e-9
def test_capabilities_tag_support() -> None:
caps = Capabilities(
modality=Modality.AUDIO,
streaming=Streaming.CHUNKED,
speech_tags=frozenset({"laughter", "breath"}),
)
assert caps.tags_supported({"laughter"})
assert not caps.tags_supported({"laughter", "sigh"})
def test_capabilities_can() -> None:
caps = Capabilities(modality=Modality.TEXT, emotion_control=True)
assert caps.can("emotion_control")
assert not caps.can("voice_clone")
def test_degradation_readable() -> None:
d = Degradation(
capability="speech_tags",
requested=("sigh",),
reason="provider 不支持",
suggestion="换 provider",
)
text = str(d)
assert "degraded" in text
assert "speech_tags" in text
assert "换 provider" in text
def test_paralinguistics_distress() -> None:
calm = Paralinguistics(emotion=ParaEmotion.HAPPY)
assert not calm.is_distressed()
crying = Paralinguistics(
emotion=ParaEmotion.SAD, events=frozenset({ParaEvent.CRY})
)
assert crying.is_distressed()
assert crying.is_negative()
def test_percept_factories() -> None:
p = Percept.asr_final(ts=1000, text="你好", confidence=0.9)
assert p.kind.value == "asr_final"
assert p.text() == "你好"
assert p.confidence == 0.9
sys_p = Percept.system(ts=0, event="net.error")
assert sys_p.system_event() == "net.error"
def test_expression_from_state() -> None:
e = Expression.from_state(v=0.8, a=0.9, d=0.2, e=0.1)
assert e.happy > 0
assert e.tired > 0.8
assert e.surprise > 0
e2 = Expression.from_state(v=0.0, a=0.3, d=0.8, e=0.9)
assert e2.happy == 0.0
assert e2.sad == 0.0