Files
CyberGril/tests/test_store.py
T
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

111 lines
3.5 KiB
Python
Raw 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.
"""存储层测试。
守的是遗忘的**下限**:accessibility 会衰减,但永不归零。
这是 availability / accessibility 双阈值的实现——
想不起来,但还在,情绪线索能唤起。
"""
from __future__ import annotations
from cybergril.memory import Store
def test_schema_initialises(store: Store) -> None:
rows = store.query(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
names = {r["name"] for r in rows}
for expected in (
"bazi_core",
"identity_change_log",
"state_snapshot",
"life_event",
"mem_episodic",
"mem_semantic",
"mem_relational",
"dyad_state",
"dyad_event",
"projection_gap",
):
assert expected in names, f"缺表 {expected}"
def test_save_and_read_state(store: Store) -> None:
store.save_state(v=-0.4, a=0.6, d=0.3, e=0.5, mood_word="闷闷的")
row = store.last_state()
assert row is not None
assert abs(row["valence"] - (-0.4)) < 1e-9
assert row["mood_word"] == "闷闷的"
def test_add_episodic_and_fts(store: Store) -> None:
eid = store.add_episodic(
role="user",
raw="我今天很难过",
salience=0.85,
affect={"v": -0.7, "a": 0.6, "d": 0.3},
)
assert eid > 0
row = store.query_one("SELECT * FROM mem_episodic WHERE id=?", (eid,))
assert row is not None
assert row["salience"] == 0.85
assert row["accessibility"] == 1.0
def test_decay_never_reaches_zero(store: Store) -> None:
"""最关键的一条:遗忘有下限。"""
store.add_episodic(role="user", raw="一件小事", salience=0.1)
store.decay_accessibility(dt_hours=24 * 365, floor=0.03)
row = store.query_one("SELECT accessibility FROM mem_episodic LIMIT 1")
assert row is not None
assert row["accessibility"] >= 0.03
assert row["accessibility"] < 0.1 # 确实衰减了
def test_high_salience_decays_slower(store: Store) -> None:
store.add_episodic(role="user", raw="重要的事", salience=1.0)
store.add_episodic(role="user", raw="不重要的事", salience=0.0)
store.decay_accessibility(dt_hours=48, floor=0.03)
rows = store.query(
"SELECT salience, accessibility FROM mem_episodic ORDER BY salience DESC"
)
high, low = rows[0], rows[1]
assert high["accessibility"] > low["accessibility"]
def test_reinforce_raises_accessibility(store: Store) -> None:
eid = store.add_episodic(role="user", raw="会被想起的事", salience=0.5)
store.decay_accessibility(dt_hours=100, floor=0.03)
before = store.query_one(
"SELECT accessibility, recall_count FROM mem_episodic WHERE id=?", (eid,)
)
store.reinforce(eid, boost=0.25)
after = store.query_one(
"SELECT accessibility, recall_count FROM mem_episodic WHERE id=?", (eid,)
)
assert after["accessibility"] > before["accessibility"]
assert after["recall_count"] == before["recall_count"] + 1
def test_dyad_persistence(store: Store) -> None:
eid = store.add_dyad_event(kind="hurt", summary="他忘了约定", severity=-0.6)
store.save_dyad(trust=0.4, intimacy=0.5, debt=-0.6, security=0.4, unresolved=[eid])
row = store.last_dyad()
assert row is not None
assert abs(row["debt"] - (-0.6)) < 1e-9
ev = store.query_one("SELECT * FROM dyad_event WHERE id=?", (eid,))
assert ev is not None
assert ev["severity"] < 0
def test_projection_gap_recorded(store: Store) -> None:
store.record_projection_gap("mystery.event", {"detail": "x"})
assert store.projection_gap_count() == 1