Session kxkm-ai 25-26 mars 2026 — 98/98 tâches TODO complétées. Hardware: - KiCad 10.0.0 native avec sym/fp-lib-table locales - Nouveau block SPI (gen_spi_header.py), ERC clean - Module partagé hardware/lib/kicad_gen.py (5 générateurs refactorisés) - Pipeline export: tools/hw/hw_export.sh (ERC + SVG + PDF + netlist) - KiBot 1.8.5 installé, .kibot.yaml configuré Firmware: - Fix I2S driver conflict: migration I2sMic vers nouvelle API i2s_channel_* - Fix WDT risk: yield() dans CompletePushToTalk (voice_controller.cpp) - Fix XSS wifi_manager.cpp: innerHTML → createElement/textContent - Fix null check FwIsValidWavHeader - Dead code supprimé: i2s_audio.cpp.bak, i2s_audio.h Simulation MCU: - QEMU ESP32-S3 v9.2.2 installé (tools/sim/) - Script run_qemu_esp32s3.sh — boot OK vérifié - [env:esp32s3_qemu] dans platformio.ini - Wokwi CI: wokwi.toml + diagram.json + scenario.yaml - SPICE bridge POC: tools/sim/spice_bridge.py (ngspice → ADC/brownout) Compliance: - Profil iot_wifi_eu validé (16 standards, 8 evidence) - 4 evidence remplis: risk_assessment, security_architecture, test_plan_radio_emc, supply_chain_declarations - plan.yaml complété avec données produit réelles CI: - Job hardware-export (KiCad 10 ERC + SVG + PDF + netlist) - Job firmware-sim (Wokwi, conditionnel WOKWI_CLI_TOKEN) - evidence_pack.yml enrichi avec exports hardware Docs & RAG: - specs/02_arch.md complet (481 lignes, 4 ADR, diagrammes) - docs/SIMULATION.md (3 niveaux: native, QEMU, Wokwi) - 6 chunks ingérés dans kb-kicad RAG - Dataset HF: tools/generate_hf_dataset.py + datasets/kb_kicad_qa.jsonl - Rapport analyse: docs/plans/ANALYSIS_REPORT_2026-03-25.md - Recherche OSS: docs/research/oss_similar_projects.md Infra: - ZeroClaw 0.1.7 installé (cargo install) - APIFY_API_KEY configurée, smoke OK - Tests: 39/39 firmware + 26/26 Python stable Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
266 lines
11 KiB
Python
266 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from tools import auto_check_ci_cd
|
|
|
|
|
|
class AutoCheckCiCdTests(unittest.TestCase):
|
|
def sample_evidence_summary(self, lane: str) -> dict | None:
|
|
summaries = {
|
|
"esp": {
|
|
"required_files": [
|
|
"build.result.json",
|
|
"build.stdout.txt",
|
|
"build.stderr.txt",
|
|
],
|
|
"missing": [],
|
|
},
|
|
"linux": {
|
|
"required_files": [
|
|
"test.result.json",
|
|
"test.stdout.txt",
|
|
"test.stderr.txt",
|
|
],
|
|
"missing": ["test.stderr.txt", "artifacts"],
|
|
},
|
|
}
|
|
return summaries.get(lane)
|
|
|
|
def sample_drift_evidence_summary(self, lane: str) -> dict | None:
|
|
summaries = {
|
|
"esp": self.sample_evidence_summary("esp"),
|
|
"linux": {
|
|
"required_files": [
|
|
"test.result.json",
|
|
"test.stdout.txt",
|
|
"test.stderr.txt",
|
|
],
|
|
"artifacts": [
|
|
"firmware/.pio/build/native/program",
|
|
"docs/evidence/linux/test.result.json",
|
|
"docs/evidence/linux/test.stdout.txt",
|
|
],
|
|
"missing": [],
|
|
"status": "ok",
|
|
},
|
|
}
|
|
return summaries.get(lane)
|
|
|
|
def sample_drift_report(self) -> dict:
|
|
report = self.sample_report()
|
|
report["targets"]["linux"] = [
|
|
{
|
|
"command": ["python", "tools/test_firmware.py", "linux"],
|
|
"returncode": 0,
|
|
"stdout": "Tests terminés pour linux",
|
|
"stderr": "",
|
|
},
|
|
{
|
|
"command": ["python", "tools/collect_evidence.py", "linux"],
|
|
"returncode": 0,
|
|
"stdout": f"Evidence pack généré pour linux: {auto_check_ci_cd.ROOT}/docs/evidence/linux",
|
|
"stderr": "",
|
|
},
|
|
{
|
|
"command": ["python", "tools/verify_evidence.py", "linux"],
|
|
"returncode": 1,
|
|
"stdout": "Artifacts manquants pour linux: ['firmware/.pio/build/native/program']",
|
|
"stderr": "",
|
|
},
|
|
]
|
|
return report
|
|
|
|
def sample_report(self) -> dict:
|
|
return {
|
|
"compliance": {
|
|
"command": ["python", "tools/compliance/validate.py", "--strict"],
|
|
"returncode": 0,
|
|
"stdout": "OK: compliance profile validated.",
|
|
"stderr": "",
|
|
},
|
|
"targets": {
|
|
"esp": [
|
|
{
|
|
"command": ["python", "tools/build_firmware.py", "esp"],
|
|
"returncode": 0,
|
|
"stdout": "Build terminé pour esp",
|
|
"stderr": "",
|
|
},
|
|
{
|
|
"command": ["python", "tools/collect_evidence.py", "esp"],
|
|
"returncode": 0,
|
|
"stdout": f"Evidence pack généré pour esp: {auto_check_ci_cd.ROOT}/docs/evidence/esp",
|
|
"stderr": "",
|
|
},
|
|
{
|
|
"command": ["python", "tools/verify_evidence.py", "esp"],
|
|
"returncode": 0,
|
|
"stdout": "Evidence pack trouvé pour esp: ['firmware/.pio/build/esp32s3_arduino/firmware.bin', 'firmware/.pio/build/esp32s3_arduino/firmware.elf', 'firmware/.pio/build/esp32s3_arduino/firmware.map', 'firmware/.pio/build/esp32s3_arduino']",
|
|
"stderr": "",
|
|
},
|
|
],
|
|
"linux": [
|
|
{
|
|
"command": ["python", "tools/test_firmware.py", "linux"],
|
|
"returncode": 1,
|
|
"stdout": "",
|
|
"stderr": "native test failed",
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
def test_render_markdown_summary_contains_lane_table_and_step_details(self):
|
|
with patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_evidence_summary,
|
|
):
|
|
summary = auto_check_ci_cd.render_markdown_summary(self.sample_report())
|
|
self.assertIn("# Kill_LIFE Evidence Pack Summary", summary)
|
|
self.assertIn("| compliance | `0` | ok |", summary)
|
|
self.assertIn("| esp | `0` | ok |", summary)
|
|
self.assertIn("| linux | `1` | failed (1) |", summary)
|
|
self.assertIn("## Focus failures", summary)
|
|
self.assertIn("| linux | `1` | `test_firmware` | native test failed |", summary)
|
|
self.assertIn("## Artifact summary", summary)
|
|
self.assertIn(
|
|
"| esp | ok | `4` | `firmware.bin`, `firmware.elf`, `+2` | `3` | `build.result.json`, `build.stdout.txt`, `build.stderr.txt` | - | - |",
|
|
summary,
|
|
)
|
|
self.assertIn(
|
|
"| linux | failed (1) | `0` | - | `3` | `test.result.json`, `test.stdout.txt`, `test.stderr.txt` | `test.stderr.txt`, `artifacts` | - |",
|
|
summary,
|
|
)
|
|
self.assertIn("## esp", summary)
|
|
self.assertIn("`build_firmware`", summary)
|
|
self.assertIn("Build terminé pour esp", summary)
|
|
self.assertIn("Evidence pack généré pour esp: docs/evidence/esp", summary)
|
|
self.assertIn("Evidence pack trouvé pour esp", summary)
|
|
self.assertNotIn(f"{auto_check_ci_cd.ROOT}/docs/evidence/esp", summary)
|
|
self.assertNotIn("firmware.elf', 'firmware.map'", summary)
|
|
|
|
def test_render_markdown_summary_omits_focus_failures_when_all_green(self):
|
|
report = self.sample_report()
|
|
report["targets"]["linux"][0]["returncode"] = 0
|
|
report["targets"]["linux"][0]["stderr"] = ""
|
|
report["targets"]["linux"][0]["stdout"] = "Tests terminés pour linux"
|
|
|
|
with patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_evidence_summary,
|
|
):
|
|
summary = auto_check_ci_cd.render_markdown_summary(report)
|
|
self.assertNotIn("## Focus failures", summary)
|
|
self.assertIn("| linux | `0` | ok |", summary)
|
|
|
|
def test_render_markdown_summary_marks_verify_drift_when_summary_still_ok(self):
|
|
with patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_drift_evidence_summary,
|
|
):
|
|
summary = auto_check_ci_cd.render_markdown_summary(self.sample_drift_report())
|
|
self.assertIn(
|
|
"| linux | failed (1) | `2` | `test.result.json`, `test.stdout.txt` | `3` | `test.result.json`, `test.stdout.txt`, `test.stderr.txt` | `program` | summary ok |",
|
|
summary,
|
|
)
|
|
|
|
def test_write_step_summary_writes_markdown_when_env_is_set(self):
|
|
report = self.sample_report()
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
summary_path = Path(tmp) / "summary.md"
|
|
markdown_path = Path(tmp) / "ci_cd_audit_summary.md"
|
|
with patch.object(auto_check_ci_cd, "MARKDOWN_REPORT_PATH", markdown_path), patch.dict(
|
|
os.environ, {auto_check_ci_cd.STEP_SUMMARY_ENV: str(summary_path)}, clear=False
|
|
), patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_evidence_summary,
|
|
):
|
|
auto_check_ci_cd.write_markdown_report(report)
|
|
written = auto_check_ci_cd.write_step_summary(report)
|
|
|
|
self.assertEqual(written, summary_path)
|
|
content = summary_path.read_text(encoding="utf-8")
|
|
self.assertIn("Kill_LIFE Evidence Pack Summary", content)
|
|
self.assertIn("Artifact snapshot: `docs/evidence/`", content)
|
|
self.assertEqual(content, markdown_path.read_text(encoding="utf-8"))
|
|
|
|
def test_write_markdown_report_writes_sidecar_in_docs_evidence(self):
|
|
report = self.sample_report()
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
markdown_path = Path(tmp) / "ci_cd_audit_summary.md"
|
|
with patch.object(auto_check_ci_cd, "MARKDOWN_REPORT_PATH", markdown_path), patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_evidence_summary,
|
|
):
|
|
written = auto_check_ci_cd.write_markdown_report(report)
|
|
|
|
self.assertEqual(written, markdown_path)
|
|
content = markdown_path.read_text(encoding="utf-8")
|
|
self.assertIn("# Kill_LIFE Evidence Pack Summary", content)
|
|
self.assertIn("| linux | `1` | failed (1) |", content)
|
|
self.assertIn("## Focus failures", content)
|
|
|
|
def test_compact_repo_paths_keeps_markdown_repo_relative(self):
|
|
signal = f"Evidence pack généré: {auto_check_ci_cd.ROOT}/docs/evidence/linux | prêt"
|
|
compacted = auto_check_ci_cd.markdown_signal(signal)
|
|
self.assertEqual(compacted, "Evidence pack généré: docs/evidence/linux \\| prêt")
|
|
|
|
def test_markdown_signal_compacts_artifact_lists(self):
|
|
signal = (
|
|
"Evidence pack trouvé pour linux: "
|
|
"['firmware/.pio/build/native']"
|
|
)
|
|
compacted = auto_check_ci_cd.markdown_signal(signal)
|
|
self.assertEqual(compacted, "Evidence pack trouvé pour linux")
|
|
|
|
def test_artifact_summary_rows_extract_counts_and_samples(self):
|
|
with patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_evidence_summary,
|
|
):
|
|
rows = auto_check_ci_cd.artifact_summary_rows(self.sample_report())
|
|
self.assertEqual(rows[0]["lane"], "esp")
|
|
self.assertEqual(rows[0]["status"], "ok")
|
|
self.assertEqual(len(rows[0]["source_artifacts"]), 4)
|
|
self.assertEqual(rows[0]["evidence_files"], ["build.result.json", "build.stdout.txt", "build.stderr.txt"])
|
|
self.assertEqual(rows[1]["missing"], ["test.stderr.txt", "artifacts"])
|
|
self.assertEqual(
|
|
auto_check_ci_cd.artifact_summary_sample(rows[0]["source_artifacts"]),
|
|
"`firmware.bin`, `firmware.elf`, `+2`",
|
|
)
|
|
|
|
def test_artifact_summary_rows_marks_drift_when_verify_fails_after_ok_summary(self):
|
|
with patch.object(
|
|
auto_check_ci_cd,
|
|
"load_evidence_summary",
|
|
side_effect=self.sample_drift_evidence_summary,
|
|
):
|
|
rows = auto_check_ci_cd.artifact_summary_rows(self.sample_drift_report())
|
|
self.assertEqual(rows[1]["status"], "failed (1)")
|
|
self.assertEqual(
|
|
rows[1]["source_artifacts"],
|
|
[
|
|
"docs/evidence/linux/test.result.json",
|
|
"docs/evidence/linux/test.stdout.txt",
|
|
],
|
|
)
|
|
self.assertEqual(rows[1]["evidence_files"], ["test.result.json", "test.stdout.txt", "test.stderr.txt"])
|
|
self.assertEqual(rows[1]["missing"], ["firmware/.pio/build/native/program"])
|
|
self.assertEqual(rows[1]["drift"], "summary ok")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|