Technician tool v0.5

Imported from the Google Drive folder 004-technician_tool/v0.5.
This commit is contained in:
kkallas committed 2026-09-26 18:30:51 +02:00
1 parent 72adfe1cec
commit f5b32e1554
21 files changed
+2036 -442

No files matched your search

+85 -1
View File
@@ -52,7 +52,7 @@ def test_modified_image_is_refused(tmp_path):
@pytest.mark.parametrize("patch,msg", [
((0, 0x00), "not a merged"),
((0, 0x00), "not an ESP32 firmware"),
((12, 0x05), "not ESP32-S3"),
])
def test_wrong_images_are_rejected(tmp_path, patch, msg):
@@ -160,3 +160,87 @@ def test_runner_reports_connect_failure_with_hint(image, tmp_path):
res, *_ = _run(plan, cmd)
assert not res.ok and res.failed_step == 0
assert "BOOT" in res.hint
# ------------------------------------------------- developer builds (v0.5)
from kota_tech.flash.image import FirmwareLibrary, app_image_length # noqa: E402
def _app_only_file(image, tmp_path, name="kota-sensorboard.bin"):
p = tmp_path / name
p.write_bytes(image.app_bytes()) # what a developer build/ folder contains
return p
def test_integrity_check_on_factory_image(image):
d = image.data
assert app_image_length(d, 0) > 0 # bootloader
assert app_image_length(d, 0x40000) == len(d) - 0x40000 # app ends exactly at file end
@pytest.mark.parametrize("damage,msg", [
(lambda b: b[:len(b) // 2], "truncated"),
(lambda b: b[:0x40000 + 5000] + bytes([b[0x40000 + 5000] ^ 1]) + b[0x40000 + 5001:], "corrupted"),
])
def test_corrupted_or_truncated_images_refused(tmp_path, damage, msg):
p = tmp_path / "broken.bin"
p.write_bytes(damage(FW.read_bytes()))
with pytest.raises(ImageError, match=msg):
inspect_image(p, origin="developer")
def test_import_app_only_developer_build(image, tmp_path):
lib = FirmwareLibrary(tmp_path / "lib")
src = _app_only_file(image, tmp_path)
img = lib.import_file(src, reference=image)
assert img.is_developer and img.kind == "app"
assert img.partition("ota_0").offset == 0x40000 # layout from the factory image
assert img.app_bytes() == image.app_bytes()
listed, problems = lib.images(image)
assert [i.sha256 for i in listed] == [img.sha256] and problems == []
assert lib.import_file(src, reference=image).sha256 == img.sha256 # re-import: no duplicate
assert len(lib.images(image)[0]) == 1
def test_app_only_build_cannot_be_flashed_full(image, tmp_path):
lib = FirmwareLibrary(tmp_path / "lib")
img = lib.import_file(_app_only_file(image, tmp_path), reference=image)
plan = build_plan(img, Mode.APP_ONLY, "COM5", tmp_path / "w")
assert [s.kind for s in plan.steps] == ["read_pt", "write"]
with pytest.raises(ValueError, match="app only"):
build_plan(img, Mode.FULL, "COM5", tmp_path / "w")
def test_import_merged_developer_build(image, tmp_path):
lib = FirmwareLibrary(tmp_path / "lib")
src = tmp_path / "dev_full.bin"
src.write_bytes(image.data)
img = lib.import_file(src, reference=image)
assert img.kind == "merged" and img.is_developer
assert [s.kind for s in build_plan(img, Mode.FULL, "COM5", tmp_path / "w").steps] == ["write"]
def test_library_detects_file_changed_after_import(image, tmp_path):
lib = FirmwareLibrary(tmp_path / "lib")
img = lib.import_file(_app_only_file(image, tmp_path), reference=image)
stored = tmp_path / "lib" / img.path.name
data = bytearray(stored.read_bytes())
data[-40] ^= 1
stored.write_bytes(data)
listed, problems = lib.images(image)
assert listed == [] and problems
def test_library_remove(image, tmp_path):
lib = FirmwareLibrary(tmp_path / "lib")
img = lib.import_file(_app_only_file(image, tmp_path), reference=image)
lib.remove(img.sha256)
assert lib.images(image) == ([], []) and not img.path.exists()
def test_non_firmware_file_refused(tmp_path, image):
p = tmp_path / "notes.bin"
p.write_bytes(b"hello firmware team")
with pytest.raises(ImageError, match="not an ESP32 firmware"):
FirmwareLibrary(tmp_path / "lib").import_file(p, reference=image)
+159
View File
@@ -0,0 +1,159 @@
"""SD provisioning: ENV.TXT (firmware-exact parsing), disk discovery parsers,
the elevated Windows format script, and write/check on a stand-in card."""
import plistlib
import pytest
from kota_tech.sd.disks import MAX_CARD_BYTES, Disk, parse_macos, parse_windows
from kota_tech.sd.envfile import EnvConfig, parse_env
from kota_tech.sd.provision import check_card, windows_format_script, write_env
URL = "https://influx.kota-app.com/api/v2/write?bucket=sensorbox&org=kota"
TOKEN = "T" * 86 + "==" # shape of an InfluxDB token, not a real one
def cfg(**kw):
base = dict(influx_url=URL, influx_token=TOKEN)
base.update(kw)
return EnvConfig(**base)
# ------------------------------------------------------------------ ENV.TXT
def test_render_matches_the_factory_file_format():
raw = cfg().render()
assert raw == f"INFLUX_TOKEN={TOKEN}\nINFLUX_URL={URL}\n".encode()
assert not raw.startswith(b"\xef\xbb\xbf") and b"\r" not in raw
assert parse_env(raw).ok
def test_wifi_only_written_when_both_set():
raw = cfg(ssid="Site WiFi", password="correct horse").render().decode()
assert "SSID=Site WiFi\n" in raw and "PASSWORD=correct horse\n" in raw
assert any("both SSID and PASSWORD" in i.text for i in cfg(ssid="x").problems())
@pytest.mark.parametrize("kw,fragment", [
(dict(influx_url="influx.kota-app.com"), "https://"),
(dict(influx_url="https://influx.kota-app.com/"), "write URL"),
(dict(influx_token=""), "INFLUX_TOKEN is empty"),
(dict(influx_token="abc def" * 5), "spaces"),
(dict(influx_token=TOKEN + " "), "space"),
(dict(ssid="s" * 33, password="12345678"), "32 bytes"),
(dict(ssid="net", password="short"), "8-63"),
])
def test_invalid_settings_are_errors(kw, fragment):
errors = [i.text for i in cfg(**kw).problems() if i.level == "error"]
assert any(fragment in e for e in errors), errors
def test_parser_mirrors_firmware():
raw = b"\xef\xbb\xbfINFLUX_TOKEN=abc\r\nINFLUX_URL = x\nINFLUX_URL=u1\nINFLUX_URL=u2\nHTTP_SERVER=true\n"
chk = parse_env(raw)
texts = " ".join(i.text for i in chk.issues)
assert "BOM" in texts # first key unreadable
assert "spaces around the key 'INFLUX_URL'" in texts
assert chk.values["INFLUX_URL"] == "u1" # first match wins
assert "appears twice" in texts and "setup mode" in texts
assert "INFLUX_TOKEN is missing" in texts # hidden behind the BOM
assert not chk.ok
def test_masking():
chk = parse_env(cfg(ssid="net", password="secretpassword").render())
m = chk.masked()
assert TOKEN not in m["INFLUX_TOKEN"] and m["INFLUX_TOKEN"].startswith("TTTT")
assert "secret" not in m["PASSWORD"]
# ------------------------------------------------------------- disk lists
WIN_JSON = """[{"Number":0,"FriendlyName":"ST500DM002-1BC142","Size":500107862016,"BusType":"SATA",
"Volumes":[{"DriveLetter":"D","FileSystem":"NTFS","Label":"WINRE"}]},
{"Number":4,"FriendlyName":"Mass Storage Device","Size":0,"BusType":"USB","Volumes":[]},
{"Number":5,"FriendlyName":"SDXC Card","Size":31914983424,"BusType":"SD",
"Volumes":{"DriveLetter":"E","FileSystem":"FAT32","Label":"KOTA"}}]"""
def test_parse_windows_keeps_only_card_buses():
disks = parse_windows(WIN_JSON)
assert [d.id for d in disks] == ["4", "5"] # SATA data disk never listed
reader, card = disks
assert not reader.has_media and reader.size_text == "no card"
assert card.volumes[0].mount == "E:\\" and card.volumes[0].filesystem == "FAT32"
def test_parse_windows_single_object_quirk():
one = '{"Number":5,"FriendlyName":"SD","Size":8000000000,"BusType":"USB","Volumes":null}'
assert [d.id for d in parse_windows(one)] == ["5"]
def test_parse_macos_excludes_system_disk():
listing = plistlib.dumps({"AllDisksAndPartitions": [
{"DeviceIdentifier": "disk0", "Size": 500_000_000_000, "Partitions": []},
{"DeviceIdentifier": "disk3", "Size": 994_000_000_000, "Partitions": [], "APFSVolumes": [{"MountPoint": "/"}]},
{"DeviceIdentifier": "disk4", "Size": 15_931_539_456,
"Partitions": [{"DeviceIdentifier": "disk4s1", "MountPoint": "/Volumes/KOTA", "Content": "DOS_FAT_32",
"VolumeName": "KOTA"}]},
]})
infos = {"disk0": plistlib.dumps({"Internal": True, "BusProtocol": "PCI-Express"}),
"disk3": plistlib.dumps({"Internal": True}),
"disk4": plistlib.dumps({"Internal": True, "RemovableMedia": True, "BusProtocol": "Secure Digital",
"MediaName": "SD Card Reader"})}
disks = parse_macos(listing, infos)
assert [d.id for d in disks] == ["/dev/disk4"] and disks[0].bus == "SD"
assert disks[0].volumes[0].mount == "/Volumes/KOTA"
# -------------------------------------------------------- format script
def test_windows_script_rechecks_disk_before_erasing(tmp_path):
disk = Disk("5", "SDXC Card", 63_864_569_856, "SD")
ps = windows_format_script(disk, tmp_path / "r.json")
erase = ps.index("Clear-Disk")
for guard in ("IsSystem -or $d.IsBoot", "'MMC','SD','USB'", "-ne 63864569856"):
assert guard in ps and ps.index(guard) < erase
assert "-PartitionStyle MBR" in ps and "-FileSystem FAT32" in ps
assert str(32 * 1024**3) in ps # FAT32 partition capped at 32 GB
assert "NewFileSystemLabel 'KOTA'" in ps
def test_large_disks_are_not_cards():
assert MAX_CARD_BYTES == 256 * 1024**3
# ----------------------------------------------- write / check on a card
def test_write_env_replaces_variants_and_keeps_buffered_data(tmp_path):
card = tmp_path / "card"
card.mkdir()
(card / "env.txt").write_bytes(b"INFLUX_URL=old\n")
(card / "BUFFER.TXT").write_bytes(b"line1\nline2\n\n")
out = write_env(str(card), cfg())
assert out.ok, out.message
names = sorted(p.name for p in card.iterdir())
assert names == ["BUFFER.TXT", "ENV.TXT"]
rep = check_card(str(card), "FAT32")
assert rep.env.ok and rep.problems == [] and rep.buffered_lines == 2
def test_write_env_refuses_invalid_settings(tmp_path):
assert not write_env(str(tmp_path), cfg(influx_token="")).ok
assert not (tmp_path / "ENV.TXT").exists()
def test_check_card_reports_missing_env_and_wrong_fs(tmp_path):
rep = check_card(str(tmp_path), "exFAT")
texts = " ".join(rep.problems)
assert "needs FAT32" in texts and "No ENV.TXT" in texts
def test_elevated_result_folder_is_not_owner_only(tmp_path, monkeypatch):
# Regression: tempfile.mkdtemp() made the elevated script's result unreadable.
import kota_tech.sd.provision as prov
monkeypatch.setattr(prov, "WORK_ROOT", tmp_path / "tmp")
work = prov._workdir()
assert work.parent == tmp_path / "tmp" and work.is_dir()
(work / "result.json").write_text('{"ok": true}', encoding="utf-8")
assert (work / "result.json").read_text(encoding="utf-8")
+91
View File
@@ -0,0 +1,91 @@
"""Service-system hand-off: status mapping, note, and the #kota-report fragment
(decoded exactly as the v1.1.0 service page does: base64url + UTF-8 JSON)."""
import base64
import json
from pathlib import Path
import pytest
from kota_tech.core.logfile import read_log_file
from kota_tech.core.logline import parse_line
from kota_tech.diag.engine import DiagnosticsEngine
from kota_tech.service.report import (
CONNECTIVITY, HARDWARE, OK, decode_fragment, normalize_mac, report_url, service_note, service_status,
)
FIX = Path(__file__).parent / "fixtures"
def bench():
e = DiagnosticsEngine()
for ln in read_log_file(FIX / "live_boot_A4CB8FC946F8_fw1.0.txt"):
e.feed(ln)
return e
def test_engine_reads_mac_from_boot_log():
assert bench().mac == "A4:CB:8F:C9:46:F8"
def test_bench_box_is_a_connectivity_problem():
e = bench()
assert service_status(e.report()) == CONNECTIVITY
note = service_note(e.report())
assert note.startswith("[KOTA Technician Tool")
assert "not commissioned in Matter" in note and "-> No Internet" in note
assert "Firmware: v1.0" in note
def test_hardware_and_healthy_statuses():
e = DiagnosticsEngine()
for ln in ["ESP-ROM:esp32s3-20210327", "I (1299) app_init: Project name: kota-sensorboard",
"E (3511) MAIN: Failed to initialize RTC: ESP_ERR_TIMEOUT"]:
e.feed(parse_line(ln))
assert service_status(e.report()) == HARDWARE
ok = DiagnosticsEngine()
for ln in ["ESP-ROM:esp32s3-20210327", "I (1299) app_init: Project name: kota-sensorboard",
"I (31000) INFLUX: HTTP request performed, status code: 204"]:
ok.feed(parse_line(ln))
assert service_status(ok.report()) == OK
def test_url_shape_and_payload_roundtrip():
e = bench()
url = report_url(e.mac, e.report(), "https://service.kota-app.com")
assert url.startswith("https://service.kota-app.com/A4CB8FC946F8#kota-report=")
assert "?" not in url # nothing in the query: the fragment is never sent to a server
data = decode_fragment(url)
assert data["v"] == 1 and data["mac"] == "A4CB8FC946F8" and data["status"] == CONNECTIVITY
def test_fragment_decodes_like_the_service_page():
# Mirror of index.html applyToolPrefill: base64url -> pad -> bytes -> UTF-8 -> JSON
e = bench()
e.note_technician("Zażółć gęślą jaźń -> Purple") # non-ASCII survives
url = report_url("a4:cb:8f:c9:46:f8", e.report())
frag = url.split("#kota-report=")[1]
assert set(frag) <= set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") # page regex
padded = frag.replace("-", "+").replace("_", "/")
padded += "==="[(len(padded) + 3) % 4:]
data = json.loads(base64.b64decode(padded).decode("utf-8"))
assert "Zażółć gęślą jaźń" in data["note"]
def test_note_is_limited_and_no_secrets():
e = bench()
for i in range(400):
e.note_technician(f"check {i} " + "x" * 50)
note = service_note(e.report(), limit=4000)
assert len(note) <= 4000
assert "INFLUX_TOKEN" not in note
@pytest.mark.parametrize("bad", ["", "A4CB8F", "zz:cb:8f:c9:46:f8"])
def test_invalid_mac_rejected(bad):
with pytest.raises(ValueError):
report_url(bad, bench().report())
def test_normalize_mac():
assert normalize_mac("a4:cb:8f:c9:46:f8") == "A4CB8FC946F8"