1c6c1952e8
Regressions fixed (aa916de simplification):
- firmware/platformio.ini: restore esp32s3_waveshare (pioarduino platform,
lib_deps: ArduinoJson/ESP32_Display_Panel/ESP32-audioI2S/IO_Expander,
BOARD_HAS_PSRAM, I2S pins), restore esp32s3_qemu (extends waveshare +
QEMU_BUILD), fix default_envs=esp32s3_waveshare, keep build_dir=/tmp/kl_pio_build
- .github/workflows/ci.yml: restore firmware-native (112 Unity tests),
firmware-build (esp32s3_waveshare artifact), firmware-sim (Wokwi gated),
hardware-export (KiCad ERC + SVG/PDF/netlist + KiBot + compliance)
- .gitignore: add .kibot-venv/ and .pio-venv/ (prevent committing venvs)
Dataset (HuggingFace clemsail/kill-life-embedded-qa v2):
- generate_hf_dataset.py: rag_query timeout 30s->120s (LLM takes ~45s),
rag_search timeout 15s->30s; resolves intermittent server-busy failures
- 30 entries (10+10+5+5) -- 100% coverage vs 21/40 previously
Firmware tests (112/112):
- 4 suites: test_basic (39), test_modules (32), test_radio_state (26),
test_wifi_state (15); test_logic.cpp now a stub (content moved to test_basic)
Hardware:
- esp32_minimal.kicad_pcb, esp32s3_enclosure.FCStd/.step, gen_pcb.py,
gen_enclosure.py, REGISTRY.md, ERC reports for all design blocks
MCP tools:
- apify_mcp.py: +5 Kill_LIFE tools (fetch_espressif_docs, fetch_kicad_library_info,
fetch_platformio_registry, ingest_to_rag, get_runtime_info)
- mcp_runtime_status.py: fix classify_overall -- accept_degraded respected for
failed checks, task annotations, optional_degraded logic cleaned up
- deploy/cad/docker-compose.yml: path mascarade->mascarade-main
Specs & docs:
- docs/plans/TODO_2026-03-26.md, TODO_2026-03-27.md
- ai-agentic-embedded-base/specs: arch, tasks, intake, spec updates
- docs/playbooks/kicad_happy_hw_bom_forge.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
5.0 KiB
Python
134 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from tools.mcp_runtime_status import classify_overall, derive_blockers, run_check
|
|
|
|
|
|
class McpRuntimeStatusTests(unittest.TestCase):
|
|
class _FakeProc:
|
|
def __init__(self, stdout: str, stderr: str = "", returncode: int = 0):
|
|
self._stdout = stdout.encode("utf-8")
|
|
self._stderr = stderr.encode("utf-8")
|
|
self.returncode = returncode
|
|
|
|
async def communicate(self):
|
|
return self._stdout, self._stderr
|
|
|
|
def test_classify_ready(self):
|
|
results = [
|
|
{"status": "ready", "accept_degraded": False},
|
|
{"status": "ready", "accept_degraded": True},
|
|
]
|
|
self.assertEqual(classify_overall(results, strict=False), "ready")
|
|
|
|
def test_classify_degraded_when_only_soft_blockers_exist(self):
|
|
results = [
|
|
{"status": "ready", "accept_degraded": False},
|
|
{"status": "degraded", "accept_degraded": True},
|
|
]
|
|
self.assertEqual(classify_overall(results, strict=False), "degraded")
|
|
self.assertEqual(classify_overall(results, strict=True), "failed")
|
|
|
|
def test_optional_degraded_path_does_not_block_non_strict_runtime(self):
|
|
results = [
|
|
{"status": "ready", "accept_degraded": False},
|
|
{
|
|
"name": "kicad-host",
|
|
"status": "degraded",
|
|
"accept_degraded": True,
|
|
"optional_degraded": True,
|
|
"task": "K-012",
|
|
"blocked_when": "host_pcbnew_import != ok (optional host-native path)",
|
|
"error": "pcbnew not importable on host runtime",
|
|
},
|
|
]
|
|
self.assertEqual(classify_overall(results, strict=False), "ready")
|
|
self.assertEqual(classify_overall(results, strict=True), "failed")
|
|
self.assertEqual(derive_blockers(results), [])
|
|
|
|
def test_classify_failed_accept_degraded_counts_as_degraded_non_strict(self):
|
|
# accept_degraded=True on a failed check: in non-strict mode counts as degraded (not failed).
|
|
# This matches the kicad/nexar use-case where source is missing but runtime remains usable.
|
|
results = [
|
|
{"status": "ready", "accept_degraded": False},
|
|
{"status": "failed", "accept_degraded": True},
|
|
]
|
|
self.assertEqual(classify_overall(results, strict=False), "degraded")
|
|
self.assertEqual(classify_overall(results, strict=True), "failed")
|
|
|
|
def test_classify_failed_when_hard_failure_no_accept(self):
|
|
# accept_degraded=False on a failed check: always fails regardless of strict mode.
|
|
results = [
|
|
{"status": "ready", "accept_degraded": False},
|
|
{"status": "failed", "accept_degraded": False},
|
|
]
|
|
self.assertEqual(classify_overall(results, strict=False), "failed")
|
|
self.assertEqual(classify_overall(results, strict=True), "failed")
|
|
|
|
def test_derive_blockers_only_from_degraded_task_checks(self):
|
|
results = [
|
|
{
|
|
"name": "kicad-host",
|
|
"status": "degraded",
|
|
"task": "K-012",
|
|
"blocked_when": "host_pcbnew_import != ok",
|
|
"error": "pcbnew not importable on host runtime",
|
|
},
|
|
{
|
|
"name": "nexar-api",
|
|
"status": "ready",
|
|
"task": "K-014",
|
|
"blocked_when": "token missing or demo mode",
|
|
},
|
|
]
|
|
self.assertEqual(
|
|
derive_blockers(results),
|
|
[
|
|
{
|
|
"task": "K-012",
|
|
"check": "kicad-host",
|
|
"reason": "pcbnew not importable on host runtime",
|
|
"condition": "host_pcbnew_import != ok",
|
|
}
|
|
],
|
|
)
|
|
|
|
def test_run_check_marks_external_quota_limit_as_optional_degraded(self):
|
|
spec = {
|
|
"name": "nexar-api",
|
|
"cmd": ["python3", "tools/nexar_mcp_smoke.py", "--json"],
|
|
"accept_degraded": True,
|
|
"task": "K-014",
|
|
"blocked_when": "live Nexar unavailable",
|
|
"optional_degraded_when_live_validation": ("quota_exceeded",),
|
|
}
|
|
stdout = json.dumps(
|
|
{
|
|
"status": "degraded",
|
|
"live_validation": "quota_exceeded",
|
|
"error": "quota exceeded",
|
|
}
|
|
)
|
|
fake_proc = self._FakeProc(stdout=stdout, stderr="", returncode=0)
|
|
|
|
async def fake_spawn(*args, **kwargs):
|
|
return fake_proc
|
|
|
|
with mock.patch("tools.mcp_runtime_status.asyncio.create_subprocess_exec", side_effect=fake_spawn):
|
|
payload = asyncio.run(run_check(spec))
|
|
self.assertTrue(payload["optional_degraded"])
|
|
self.assertEqual(payload["task"], "K-014")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|