Assistant vocal local ESP32 : étapes 1 à 15, et 15b écrite

État du projet au premier commit :
- firmware ESP32 : micro I2S, lecture anti-gigue, MQTT (état retenu, testament),
  mains libres, domotique de démonstration, chien de garde, présence du serveur ;
- serveur Python : sessions, VAD, faster-whisper, pyannote, identification,
  droits par profil, domotique, mot de réveil, Piper ;
- broker Mosquitto : configuration, comptes et ACL (non activés en développement) ;
- étape 15b écrite, en attente d'essai : agent MQTT pour machines Linux.

Étapes 1 à 15 validées. 203 tests, pyright sans erreur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Denis Defolie
2026-09-30 17:40:55 +02:00
commit d20976fea2
108 changed files with 15022 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
"""Tests de l'agent Linux, étape 15b : configuration, liste blanche, exécution (sans broker)."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
AGENT_DIR = Path(__file__).resolve().parents[2] / "agent"
if str(AGENT_DIR) not in sys.path:
sys.path.insert(0, str(AGENT_DIR))
from voice_agent import Action, AgentConfig, ConfigError, load_config, run_action, state_payload # noqa: E402
def config(**actions: Action) -> AgentConfig:
return AgentConfig(agent_id="pc-test", host="127.0.0.1", port=1883, username=None, password=None,
topic_prefix="agent", dry_run=False, actions=dict(actions))
def test_example_config_is_valid() -> None:
loaded = load_config(AGENT_DIR / "agent.yaml.example")
assert loaded.agent_id == "pc-bureau" and not loaded.dry_run
assert loaded.actions["shutdown"].command == ("systemctl", "poweroff")
assert loaded.topic("command") == "agent/pc-bureau/command"
@pytest.mark.parametrize(
("text", "message"),
[
("id: PC Bureau\nmqtt: {host: x}\n", "id « PC Bureau » invalide"),
("id: pc\nmqtt: {}\n", "mqtt.host manquant"),
# Une chaîne passerait par un shell : « poweroff; rm -rf / » serait exécutable.
("id: pc\nmqtt: {host: x}\nactions: {off: {command: 'systemctl poweroff'}}\n", "doit être une liste"),
("id: pc\nmqtt: {host: x}\nactions: {off: {command: [x], timeout_s: 0}}\n", "timeout_s"),
],
)
def test_invalid_config_is_explained(tmp_path: Path, text: str, message: str) -> None:
path = tmp_path / "agent.yaml"
path.write_text(text, encoding="utf-8")
with pytest.raises(ConfigError, match=message):
load_config(path)
def test_only_listed_actions_run() -> None:
result = run_action(config(ok=Action(("true",), 5)), "rm", "r1")
assert not result.ok and result.output == "action inconnue : rm"
def test_success_failure_and_output() -> None:
agent = config(ok=Action(("echo", "bonjour"), 5), ko=Action(("false",), 5))
ok = run_action(agent, "ok", "r1")
assert ok.ok and ok.code == 0 and ok.output == "bonjour"
ko = run_action(agent, "ko", "r2")
assert not ko.ok and ko.code == 1
def test_missing_program_and_timeout() -> None:
agent = config(absent=Action(("/nonexistent/prog",), 5), slow=Action(("sleep", "5"), 0.2))
assert run_action(agent, "absent", "r").output == "programme introuvable : /nonexistent/prog"
slow = run_action(agent, "slow", "r")
assert not slow.ok and slow.output == "arretee apres 0.2 s"
def test_dry_run_executes_nothing() -> None:
calls: list[object] = []
agent = AgentConfig(**{**config(off=Action(("systemctl", "poweroff"), 5)).__dict__, "dry_run": True})
result = run_action(agent, "off", "r", runner=lambda *args, **kwargs: calls.append(args)) # type: ignore[arg-type]
assert result.ok and result.output == "essai : systemctl poweroff" and calls == []
def test_state_lists_actions_for_the_server() -> None:
online = json.loads(state_payload(config(b=Action(("x",), 1), a=Action(("y",), 1)), True))
assert online == {"status": "online", "actions": ["a", "b"], "dry_run": False}
assert json.loads(state_payload(config(), False)) == {"status": "offline"}
def test_result_payload() -> None:
result = run_action(config(ok=Action(("true",), 5)), "ok", "w42")
assert json.loads(result.payload()) == {"action": "ok", "id": "w42", "ok": True, "code": 0, "output": ""}