fractus-vorax / tests /test_speak_integration.py
thefinalboss's picture
Fractus-Vorax v1.0.0 — the takeover: sealed CTE brain + ingestion organs + mechanical speech (199 tests, honest floors)
1da7ac7 verified
Raw
History Blame Contribute Delete
8.65 kB
# tests/test_speak_integration.py
"""Plan 6, tâche 4 — l'intégration : ``:speak`` dans le REPL, core_speak v2.
Substrat (mingw, TOUJOURS vert) : ``:speak`` sans noyau attaché → la ligne
honnête ``[PAROLE] noyau non attaché (:core d'abord)`` — jamais de traceback,
le substrat ne touche pas torch.
Torch (se skipe en mingw) : un tiny CteCore + tokenizer char attachés au
REPL → les trois lignes ``[PAROLE]`` (cartes, steering, parole mécanique) ;
``run_core_speak(mode=...)`` sur un tiny checkpoint fake → le dict v2 avec
greedy/mécanique (et steerée avec brain) + le ciel ouvert (routage, têtes).
"""
import pytest
from fractus_vorax.agent.repl import Repl
from fractus_vorax.brain import Brain
from fractus_vorax.compiler.atoms import Atom
CAPITALS = [
Atom("what is the capital of france", "paris", "cap.csv:2"),
Atom("what is the capital of spain", "madrid", "cap.csv:3"),
Atom("what is the capital of japan", "tokyo", "cap.csv:4"),
Atom("what is the capital of italy", "rome", "cap.csv:5"),
]
DEGRADE = "[PAROLE] noyau non attaché (:core d'abord)"
def _repl(tmp_path) -> Repl:
brain = Brain(D=2048)
brain.ingest_source("capitals", CAPITALS)
brain.save(tmp_path / "brain")
return Repl(tmp_path / "brain", D=2048)
class _Tok128:
"""Tokenizer char-level sur le vocab tiny 128 (protocole minimal)."""
vocab_size = 128
def encode(self, text: str) -> list[int]:
return [ord(c) % 128 for c in text]
def decode(self, ids: list[int]) -> str:
return "".join(chr(int(i) % 128) for i in ids)
# ---------------------------------------------------------------------------
# :speak — dégradation honnête (substrat, les deux venvs)
# ---------------------------------------------------------------------------
def test_speak_without_core_degrades_honestly(tmp_path):
"""Sans :core : UNE ligne honnête, jamais de traceback — le contrat
mingw du plan (skip-proof : ce test ne touche jamais torch)."""
r = _repl(tmp_path)
assert r.core is None and r.tokenizer is None
out = r.feed(":speak hello")
assert out == [DEGRADE]
# le REPL reste vivant après la dégradation (pas d'état cassé)
assert any("[CARTE]" in l for l in r.feed("what is the capital of japan"))
def test_speak_usage_line_without_text(tmp_path):
r = _repl(tmp_path)
assert r.feed(":speak") == ["[PAROLE] usage: :speak <texte>"]
def test_speak_with_fake_tiny_core(tmp_path):
"""Noeud torch : tiny CteCore + tokenizer char attachés → les TROIS
lignes ``[PAROLE]`` (cartes, steering, parole) — la boucle mécanique
vit dans le REPL, pilotée par les organes."""
torch = pytest.importorskip(
"torch", reason=":speak mécanique nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
r = _repl(tmp_path)
torch.manual_seed(0)
r.core = CteCore(CteCoreConfig(vocab_size=128))
r.tokenizer = _Tok128()
out = r.feed(":speak what is the capital of france")
assert any(l.startswith("[PAROLE] cartes: ") for l in out)
steering = [l for l in out if l.startswith("[PAROLE] steering:")]
assert steering, f"ligne steering absente: {out}"
# les organes steered : le premier token char de ' paris' (l'espace, 32)
assert " x8" in steering[0]
speeches = [l for l in out if l.startswith('[PAROLE] "')]
assert speeches, f"ligne parole absente: {out}"
# déterminisme : même seed (7, interne au :speak) => même parole
again = r.feed(":speak what is the capital of france")
assert [l for l in again if l.startswith('[PAROLE] "')] == speeches
def test_speak_tiny_core_failure_is_honest(tmp_path):
"""Un noyau qui échoue (tokenizer incohérent avec le vocab) → ligne
``[PAROLE] indisponible: ...``, jamais de traceback."""
torch = pytest.importorskip(
"torch", reason=":speak mécanique nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
r = _repl(tmp_path)
torch.manual_seed(0)
r.core = CteCore(CteCoreConfig(vocab_size=8)) # vocab minuscule
r.tokenizer = _Tok128() # ids jusqu'à 127 -> hors vocab
out = r.feed(":speak what is the capital of france")
assert any(l.startswith("[PAROLE] indisponible:") for l in out)
# ---------------------------------------------------------------------------
# core_speak v2 — run function sur un tiny checkpoint fake (torch)
# ---------------------------------------------------------------------------
def _tiny_ckpt(tmp_path):
torch = pytest.importorskip("torch")
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
# vocab BPE complet (50257) : le harnais tokenize en vrai GPT-2 — les
# ids doivent vivre dans le vocab du noyau pour que forward passe.
torch.manual_seed(0)
core = CteCore(CteCoreConfig(vocab_size=50257))
ckpt = tmp_path / "tiny_cte.pt"
torch.save({"model_state": core.state_dict()}, ckpt)
return ckpt
def _bpe_or_skip():
from fractus_vorax.model.bpe_tokenizer import bpe_available
if not bpe_available():
pytest.skip("tokenizers indisponible")
try:
from fractus_vorax.model.bpe_tokenizer import Gpt2BpeTokenizer
Gpt2BpeTokenizer()
except Exception as exc: # cache HF vide + réseau injoignable
pytest.skip(f"tokenizer GPT-2 indisponible: {exc}")
def test_core_speak_mechanic_mode_tiny(tmp_path):
"""mode mechanic : baseline greedy + parole mécanique par question, ciel
ouvert (routage top-2/couche + têtes) sur la première question."""
_bpe_or_skip()
from bench.core_speak import run_core_speak
result = run_core_speak(
_tiny_ckpt(tmp_path),
["what is the capital of france", "what is the capital of spain"],
brain=None,
max_new_tokens=3,
mode="mechanic",
)
assert result["mode"] == "mechanic"
assert len(result["runs"]) == 2
for run in result["runs"]:
assert isinstance(run["greedy_output"], str)
assert isinstance(run["mechanic_output"], str)
assert isinstance(run["mechanic_diags"], list) and run["mechanic_diags"]
assert run["mechanic_answer_token"] is False # sans brain -> pas d'attendu
# ciel ouvert : top-2 experts par couche (2 couches sur le tiny), têtes lues
assert set(result["routing"]) == {"layer_0", "layer_1"}
for entries in result["routing"].values():
assert len(entries) == 2 and abs(sum(w for _, w in entries) - 1.0) < 1e-5
conf_sal = result["head_readout"]
assert 0.0 <= conf_sal["confidence"] < 1.0 and 0.0 <= conf_sal["salience"] < 1.0
assert result["answer_token_rate_steered"] is None # pas de steered en mechanic
def test_core_speak_steered_mode_tiny(tmp_path):
"""mode steered (exige brain) : les TROIS générations par question —
greedy attracteur, mécanique non-steerée, mécanique steerée (même seed,
bias organique) — et les deux taux de tokens-réponse mesurés."""
_bpe_or_skip()
from bench.core_speak import run_core_speak
brain = Brain(D=2048)
brain.ingest_source("capitals", CAPITALS)
brain.save(tmp_path / "brain")
result = run_core_speak(
_tiny_ckpt(tmp_path),
["what is the capital of france", "what is the capital of spain"],
brain=tmp_path / "brain",
max_new_tokens=3,
mode="steered",
)
assert result["mode"] == "steered"
for run in result["runs"]:
for key in ("greedy_output", "mechanic_output", "steered_output"):
assert isinstance(run[key], str)
assert run["bias_tokens"], "steering organique vide alors que le brain sait"
assert isinstance(run["steered_answer_token"], bool)
assert run["expected_answer"] in ("paris", "madrid")
assert isinstance(run["answer_token"], int)
assert isinstance(result["answer_token_rate_mechanic"], float)
assert isinstance(result["answer_token_rate_steered"], float)
assert result["verdict"] in ("word salad", "answers present")
def test_core_speak_steered_without_brain_is_refused(tmp_path):
"""steered sans brain : précondition refusée proprement (ValueError du
run, exit 1 du CLI) — pas d'organes, pas de steering."""
_bpe_or_skip()
from bench.core_speak import main, run_core_speak
ckpt = _tiny_ckpt(tmp_path)
with pytest.raises(ValueError):
run_core_speak(ckpt, ["q"], brain=None, max_new_tokens=2, mode="steered")
code = main(["--ckpt", str(ckpt), "--mode", "steered"])
assert code == 1