flashgate 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {flashgate-0.4.0 → flashgate-0.4.2}/PKG-INFO +3 -3
  2. {flashgate-0.4.0 → flashgate-0.4.2}/README.md +2 -2
  3. flashgate-0.4.2/flashgate/__init__.py +13 -0
  4. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/cli.py +43 -21
  5. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/swdsig.py +27 -3
  6. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/PKG-INFO +3 -3
  7. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/SOURCES.txt +1 -0
  8. {flashgate-0.4.0 → flashgate-0.4.2}/pyproject.toml +1 -1
  9. flashgate-0.4.2/tests/test_cli.py +148 -0
  10. {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_swdsig.py +22 -3
  11. flashgate-0.4.0/flashgate/__init__.py +0 -8
  12. {flashgate-0.4.0 → flashgate-0.4.2}/LICENSE +0 -0
  13. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/__main__.py +0 -0
  14. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/board.py +0 -0
  15. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/flasher.py +0 -0
  16. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/gatestate.py +0 -0
  17. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/mcp_server.py +0 -0
  18. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/probes.py +0 -0
  19. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/serialmon.py +0 -0
  20. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/sttools.py +0 -0
  21. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/dependency_links.txt +0 -0
  22. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/entry_points.txt +0 -0
  23. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/requires.txt +0 -0
  24. {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/top_level.txt +0 -0
  25. {flashgate-0.4.0 → flashgate-0.4.2}/setup.cfg +0 -0
  26. {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_board.py +0 -0
  27. {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_gatestate.py +0 -0
  28. {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_probes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashgate
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -79,8 +79,8 @@ fresh clone verifies out of the box once wired up.
79
79
  | 2 | flash failed |
80
80
  | 3 | board stayed silent (no banner / no signature within timeout) |
81
81
  | 4 | error string seen on serial (HardFault, assertion) |
82
- | 5 | on-board identity ≠ repo state |
83
- | 6 | environment error (no ST-Link / serial / tools) |
82
+ | 5 | on-board identity ≠ repo state (git sha or board name) |
83
+ | 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
84
84
  | 7 | functional probe failed |
85
85
 
86
86
  ## The Stop hook
@@ -63,8 +63,8 @@ fresh clone verifies out of the box once wired up.
63
63
  | 2 | flash failed |
64
64
  | 3 | board stayed silent (no banner / no signature within timeout) |
65
65
  | 4 | error string seen on serial (HardFault, assertion) |
66
- | 5 | on-board identity ≠ repo state |
67
- | 6 | environment error (no ST-Link / serial / tools) |
66
+ | 5 | on-board identity ≠ repo state (git sha or board name) |
67
+ | 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
68
68
  | 7 | functional probe failed |
69
69
 
70
70
  ## The Stop hook
@@ -0,0 +1,13 @@
1
+ """flashgate: hardware-in-the-loop verification gate for coding agents.
2
+
3
+ The agent may not claim firmware work is done until the board itself
4
+ says so: build -> flash -> boot banner over serial, with exit codes
5
+ that an agent harness (Stop hook) can enforce.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
9
+
10
+ try:
11
+ __version__ = _pkg_version("flashgate") # pyproject.toml is the single source
12
+ except PackageNotFoundError: # running from a raw source tree
13
+ __version__ = "0.0.0"
@@ -260,8 +260,10 @@ def cmd_verify(board: Board, probe_names: list[str] | None, evidence: str | None
260
260
 
261
261
  def _verify_swd(board: Board, probe_names: list[str] | None) -> int:
262
262
  """Boot gate through the ST-Link alone: fixed-address RAM signature.
263
- No serial cable needed; functional probes are skipped (they need the
264
- console) unless a probe run is explicitly requested and a port exists."""
263
+ No serial cable needed. Probes explicitly requested via --probe /
264
+ --all-probes (as the Stop hook does) MUST run: if the console is
265
+ missing or unusable, verify FAILS (exit 6) — a check that didn't
266
+ happen must never count as a pass."""
265
267
  print(_cyan(f"[verify] {board.name}: build -> flash -> SWD signature"))
266
268
  rc = _build(board)
267
269
  if rc != EXIT_OK:
@@ -287,6 +289,9 @@ def _verify_swd(board: Board, probe_names: list[str] | None) -> int:
287
289
  board.flash_connect, board.sig_address, board.sig_size,
288
290
  timeout_s=board.banner_timeout_s)
289
291
  if info is None:
292
+ if "not supported" in err:
293
+ print(_red(f"[verify] SIGNATURE LAYOUT MISMATCH: {err}"))
294
+ return EXIT_ENV
290
295
  print(_red(f"[verify] TIMEOUT: board never published its SWD signature ({err})"))
291
296
  return EXIT_BANNER_TIMEOUT
292
297
 
@@ -300,26 +305,46 @@ def _verify_swd(board: Board, probe_names: list[str] | None) -> int:
300
305
  return EXIT_SHA_MISMATCH
301
306
 
302
307
  if probe_names is not None:
303
- port, _ = _console_port(board)
308
+ port, why = _console_port(board)
304
309
  if port is None:
305
- print(_yellow("[verify] probes skipped: no console serial in swd mode"))
306
- else:
307
- try:
308
- conn = serialmon.open_flush(port, board.baudrate)
309
- except serial.SerialException as exc:
310
- print(_yellow(f"[verify] probes skipped: cannot open {port} ({exc})"))
311
- return EXIT_OK
312
- try:
313
- return _run_probes(board, None if probe_names == ["all"] else probe_names, conn)
314
- finally:
315
- conn.close()
316
- return EXIT_OK
310
+ print(_red("[verify] probes were explicitly requested but no console serial "
311
+ f"is available ({why}) — failing rather than passing unverified "
312
+ "(drop --probe/--all-probes, or connect the console UART)"))
313
+ return EXIT_ENV
314
+ try:
315
+ conn = serialmon.open_flush(port, board.baudrate)
316
+ except serial.SerialException as exc:
317
+ print(_red(f"[verify] probes were explicitly requested but {port} cannot be "
318
+ f"opened ({exc}) — failing rather than passing unverified"))
319
+ return EXIT_ENV
320
+ try:
321
+ return _run_probes(board, None if probe_names == ["all"] else probe_names, conn)
322
+ finally:
323
+ conn.close()
317
324
 
318
325
  print(_green(f"[verify] PASS — the board's RAM itself confirms the firmware booted "
319
326
  f"(git={info['git']}), no serial cable involved"))
320
327
  return EXIT_OK
321
328
 
322
329
 
330
+ def _check_banner_identity(board: Board, info: dict) -> int | None:
331
+ """Compare the banner's identity fields against the board profile.
332
+ Returns an exit code on contradiction, None when consistent."""
333
+ got_board = info.get("board")
334
+ if got_board and got_board != board.name:
335
+ print(_red(f"[verify] BOARD MISMATCH: banner says board={got_board}, "
336
+ f"profile expects {board.name} — the verdict would not be "
337
+ "about the board you configured"))
338
+ return EXIT_SHA_MISMATCH
339
+ expected = board.head_sha()
340
+ got = info.get("git")
341
+ if expected and got and expected != got:
342
+ print(_red(f"[verify] SHA MISMATCH: board runs {got}, repo HEAD is {expected} "
343
+ "(rebuild after committing?)"))
344
+ return EXIT_SHA_MISMATCH
345
+ return None
346
+
347
+
323
348
  def _verify_uart(board: Board, probe_names: list[str] | None) -> int:
324
349
  all_probes = probe_names == ["all"]
325
350
  title = "[verify] {b}: build -> flash -> boot banner" + (" -> probes" if probe_names is not None else "")
@@ -365,12 +390,9 @@ def _verify_uart(board: Board, probe_names: list[str] | None) -> int:
365
390
  print(_green(f"[verify] banner OK: board={info.get('board')} git={info.get('git')} "
366
391
  f"build={info.get('build')} rtos={info.get('rtos')}"))
367
392
 
368
- expected = board.head_sha()
369
- got = info.get("git")
370
- if expected and got and expected != got:
371
- print(_red(f"[verify] SHA MISMATCH: board runs {got}, repo HEAD is {expected} "
372
- "(rebuild after committing?)"))
373
- return EXIT_SHA_MISMATCH
393
+ mismatch = _check_banner_identity(board, info)
394
+ if mismatch is not None:
395
+ return mismatch
374
396
 
375
397
  if probe_names is not None:
376
398
  return _run_probes(board, None if all_probes else probe_names, conn)
@@ -18,6 +18,7 @@ from .flasher import FLASH_ATTEMPTS # reuse attempt count semantics
18
18
  from .sttools import augmented_env, find_cubeprogrammer
19
19
 
20
20
  SIG_MAGIC = 0xF1A5C0DE
21
+ SIG_VERSION = 1 # the only signature layout this tool decodes
21
22
  READ_TIMEOUT_S = 30
22
23
 
23
24
 
@@ -25,6 +26,13 @@ class SwdError(Exception):
25
26
  pass
26
27
 
27
28
 
29
+ class SignatureLayoutError(SwdError):
30
+ """CRC-valid signature whose layout version this tool cannot decode.
31
+
32
+ Waiting longer cannot fix a firmware/tool generation mismatch, so
33
+ callers should fail fast instead of polling to the timeout."""
34
+
35
+
28
36
  def read_ram(connect: str, address: int, size: int) -> bytes | None:
29
37
  """One CubeProgrammer memory read (HotPlug: does not halt the core)."""
30
38
  cli = find_cubeprogrammer()
@@ -49,7 +57,11 @@ def read_ram(connect: str, address: int, size: int) -> bytes | None:
49
57
 
50
58
 
51
59
  def parse_signature(buf: bytes) -> dict | None:
52
- """Validate magic + CRC and decode the identity. None = not (yet) valid."""
60
+ """Validate magic, layout version and CRC; decode the identity.
61
+
62
+ None = not (yet) valid (retry). SignatureLayoutError = a complete,
63
+ CRC-valid signature from a layout generation this tool refuses to
64
+ interpret — treating it as evidence would be a lie."""
53
65
  if len(buf) < 64:
54
66
  return None
55
67
  magic, version, flags = struct.unpack_from("<IHH", buf, 0)
@@ -58,6 +70,11 @@ def parse_signature(buf: bytes) -> dict | None:
58
70
  crc_field = struct.unpack_from("<I", buf, 0x30)[0]
59
71
  if zlib.crc32(buf[:0x30]) & 0xFFFFFFFF != crc_field:
60
72
  return None # raced the boot-time write: retry
73
+ if version != SIG_VERSION:
74
+ raise SignatureLayoutError(
75
+ f"signature layout version {version} not supported "
76
+ f"(this tool decodes version {SIG_VERSION} only — "
77
+ "update flashgate or the firmware's bsp_signature)")
61
78
  git = buf[0x08:0x18].split(b"\x00", 1)[0].decode("ascii", errors="replace")
62
79
  build = buf[0x18:0x30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
63
80
  return {"version": version, "flags": flags, "git": git, "build": build}
@@ -66,7 +83,11 @@ def parse_signature(buf: bytes) -> dict | None:
66
83
  def wait_for_signature(
67
84
  connect: str, address: int, size: int = 64, timeout_s: float = 15.0
68
85
  ) -> tuple[dict | None, str]:
69
- """Poll the signature until valid or timeout. Returns (info, last_error)."""
86
+ """Poll the signature until valid or timeout. Returns (info, last_error).
87
+
88
+ A SignatureLayoutError aborts the poll immediately (returned as
89
+ (None, message)): an alien layout version is a permanent firmware/tool
90
+ mismatch, not something retrying can resolve."""
70
91
  deadline = time.monotonic() + timeout_s
71
92
  last = "no valid signature yet"
72
93
  while time.monotonic() < deadline:
@@ -76,7 +97,10 @@ def wait_for_signature(
76
97
  last = str(exc)
77
98
  time.sleep(1.0)
78
99
  continue
79
- info = parse_signature(buf or b"")
100
+ try:
101
+ info = parse_signature(buf or b"")
102
+ except SignatureLayoutError as exc:
103
+ return None, str(exc)
80
104
  if info is not None:
81
105
  return info, ""
82
106
  time.sleep(0.5)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashgate
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -79,8 +79,8 @@ fresh clone verifies out of the box once wired up.
79
79
  | 2 | flash failed |
80
80
  | 3 | board stayed silent (no banner / no signature within timeout) |
81
81
  | 4 | error string seen on serial (HardFault, assertion) |
82
- | 5 | on-board identity ≠ repo state |
83
- | 6 | environment error (no ST-Link / serial / tools) |
82
+ | 5 | on-board identity ≠ repo state (git sha or board name) |
83
+ | 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
84
84
  | 7 | functional probe failed |
85
85
 
86
86
  ## The Stop hook
@@ -19,6 +19,7 @@ flashgate.egg-info/entry_points.txt
19
19
  flashgate.egg-info/requires.txt
20
20
  flashgate.egg-info/top_level.txt
21
21
  tests/test_board.py
22
+ tests/test_cli.py
22
23
  tests/test_gatestate.py
23
24
  tests/test_probes.py
24
25
  tests/test_swdsig.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashgate"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,148 @@
1
+ """cli-level fail-closed guards: a check that cannot run must fail, never pass.
2
+
3
+ Regression coverage for the false-green found in 0.4.1: --all-probes with
4
+ no console serial in swd mode used to print a yellow "skipped" and return 0.
5
+ """
6
+
7
+ from types import SimpleNamespace
8
+
9
+ import pytest
10
+ import serial
11
+
12
+ from flashgate import cli
13
+ from flashgate.board import Board, load_board
14
+
15
+
16
+ BODY = """
17
+ board: test-board
18
+ mcu: STM32F999
19
+ description: unit test fixture
20
+ firmware:
21
+ dir: fw
22
+ build: ninja -C build
23
+ artifact: build/fw.bin
24
+ serial:
25
+ baudrate: 9600
26
+ banner: 'BOOT board={board} git={git}'
27
+ """
28
+
29
+
30
+ def make_board(tmp_path) -> Board:
31
+ fw = tmp_path / "fw"
32
+ fw.mkdir(exist_ok=True)
33
+ p = tmp_path / "board.yaml"
34
+ p.write_text(BODY, encoding="utf-8")
35
+ return load_board(p)
36
+
37
+
38
+ @pytest.fixture
39
+ def happy_swd(monkeypatch):
40
+ """Everything up to (but excluding) the probe stage succeeds: build,
41
+ flash, wipe, start, and a valid v1 signature answers immediately."""
42
+ monkeypatch.setattr(cli, "_build", lambda board: cli.EXIT_OK)
43
+ monkeypatch.setattr(cli.flasher, "flash",
44
+ lambda *a, **k: SimpleNamespace(ok=True, detail=""))
45
+ monkeypatch.setattr(cli.flasher, "write32", lambda *a, **k: True)
46
+ monkeypatch.setattr(cli.flasher, "start_app", lambda *a, **k: True)
47
+ monkeypatch.setattr(cli.swdsig, "wait_for_signature",
48
+ lambda *a, **k: ({"version": 1, "flags": 0,
49
+ "git": "x", "build": "y"}, ""))
50
+ monkeypatch.setattr(Board, "head_sha", lambda self: None)
51
+
52
+
53
+ class TestSwdProbeGuard:
54
+ def test_probes_required_no_serial_fails(self, tmp_path, happy_swd, monkeypatch):
55
+ board = make_board(tmp_path)
56
+ monkeypatch.setattr(cli, "_console_port", lambda b: (None, "no serial found"))
57
+ assert cli._verify_swd(board, ["all"]) == cli.EXIT_ENV
58
+
59
+ def test_probes_required_port_unopenable_fails(self, tmp_path, happy_swd, monkeypatch):
60
+ board = make_board(tmp_path)
61
+ monkeypatch.setattr(cli, "_console_port", lambda b: ("COM3", "hint"))
62
+
63
+ def boom(*a, **k):
64
+ raise serial.SerialException("access denied")
65
+ monkeypatch.setattr(cli.serialmon, "open_flush", boom)
66
+ assert cli._verify_swd(board, ["all"]) == cli.EXIT_ENV
67
+
68
+ def test_probes_required_run_when_serial_ok(self, tmp_path, happy_swd, monkeypatch):
69
+ board = make_board(tmp_path)
70
+ monkeypatch.setattr(cli, "_console_port", lambda b: ("COM3", "hint"))
71
+ monkeypatch.setattr(cli.serialmon, "open_flush",
72
+ lambda *a, **k: SimpleNamespace(close=lambda: None))
73
+ seen = {}
74
+ def run_probes(board, names, conn):
75
+ seen["names"] = names
76
+ return cli.EXIT_OK
77
+ monkeypatch.setattr(cli, "_run_probes", run_probes)
78
+ assert cli._verify_swd(board, ["all"]) == cli.EXIT_OK
79
+ assert seen["names"] is None # "all" expanded, not literal
80
+
81
+ def test_no_probes_requested_still_passes_without_serial(self, tmp_path, happy_swd, monkeypatch):
82
+ board = make_board(tmp_path)
83
+ monkeypatch.setattr(cli, "_console_port", lambda b: (None, "no serial"))
84
+ assert cli._verify_swd(board, None) == cli.EXIT_OK
85
+
86
+
87
+ class TestSignatureErrors:
88
+ def test_layout_mismatch_fails_fast_as_env_error(self, tmp_path, happy_swd, monkeypatch):
89
+ board = make_board(tmp_path)
90
+ monkeypatch.setattr(
91
+ cli.swdsig, "wait_for_signature",
92
+ lambda *a, **k: (None, "signature layout version 3 not supported "
93
+ "(this tool decodes version 1 only)"))
94
+ assert cli._verify_swd(board, None) == cli.EXIT_ENV
95
+
96
+ def test_plain_timeout_still_exit_3(self, tmp_path, happy_swd, monkeypatch):
97
+ board = make_board(tmp_path)
98
+ monkeypatch.setattr(cli.swdsig, "wait_for_signature",
99
+ lambda *a, **k: (None, "no valid signature yet"))
100
+ assert cli._verify_swd(board, None) == cli.EXIT_BANNER_TIMEOUT
101
+
102
+
103
+ class TestBannerIdentity:
104
+ def test_board_mismatch_detected(self, tmp_path, monkeypatch):
105
+ board = make_board(tmp_path)
106
+ monkeypatch.setattr(Board, "head_sha", lambda self: None)
107
+ rc = cli._check_banner_identity(board, {"board": "other-board", "git": "x"})
108
+ assert rc == cli.EXIT_SHA_MISMATCH
109
+
110
+ def test_sha_mismatch_detected(self, tmp_path, monkeypatch):
111
+ board = make_board(tmp_path)
112
+ monkeypatch.setattr(Board, "head_sha", lambda self: "abc1234")
113
+ rc = cli._check_banner_identity(board, {"board": "test-board", "git": "deadbee"})
114
+ assert rc == cli.EXIT_SHA_MISMATCH
115
+
116
+ def test_consistent_identity_passes(self, tmp_path, monkeypatch):
117
+ board = make_board(tmp_path)
118
+ monkeypatch.setattr(Board, "head_sha", lambda self: "abc1234")
119
+ assert cli._check_banner_identity(
120
+ board, {"board": "test-board", "git": "abc1234"}) is None
121
+
122
+
123
+ class TestAutoRouting:
124
+ def test_auto_picks_swd_without_serial(self, tmp_path, monkeypatch):
125
+ board = make_board(tmp_path)
126
+ seen = {}
127
+ monkeypatch.setattr(cli, "_console_port", lambda b: (None, "none"))
128
+ def fake_swd(b, p):
129
+ seen["mode"] = "swd"; return cli.EXIT_OK
130
+ def fake_uart(b, p):
131
+ seen["mode"] = "uart"; return cli.EXIT_OK
132
+ monkeypatch.setattr(cli, "_verify_swd", fake_swd)
133
+ monkeypatch.setattr(cli, "_verify_uart", fake_uart)
134
+ assert cli.cmd_verify(board, None, None) == cli.EXIT_OK
135
+ assert seen["mode"] == "swd"
136
+
137
+ def test_auto_picks_uart_with_serial(self, tmp_path, monkeypatch):
138
+ board = make_board(tmp_path)
139
+ seen = {}
140
+ monkeypatch.setattr(cli, "_console_port", lambda b: ("COM3", "hint"))
141
+ def fake_swd(b, p):
142
+ seen["mode"] = "swd"; return cli.EXIT_OK
143
+ def fake_uart(b, p):
144
+ seen["mode"] = "uart"; return cli.EXIT_OK
145
+ monkeypatch.setattr(cli, "_verify_swd", fake_swd)
146
+ monkeypatch.setattr(cli, "_verify_uart", fake_uart)
147
+ assert cli.cmd_verify(board, None, None) == cli.EXIT_OK
148
+ assert seen["mode"] == "uart"
@@ -3,13 +3,16 @@
3
3
  import struct
4
4
  import zlib
5
5
 
6
- from flashgate.swdsig import SIG_MAGIC, parse_signature
6
+ import pytest
7
7
 
8
+ from flashgate.swdsig import SIG_MAGIC, SignatureLayoutError, parse_signature
8
9
 
9
- def build_sig(git=b"abc1234", build=b"2026-08-31T08:00:00Z", magic=SIG_MAGIC) -> bytes:
10
+
11
+ def build_sig(git=b"abc1234", build=b"2026-08-31T08:00:00Z", magic=SIG_MAGIC,
12
+ version=1) -> bytes:
10
13
  buf = bytearray(64)
11
14
  struct.pack_into("<I", buf, 0x00, magic)
12
- struct.pack_into("<H", buf, 0x04, 1)
15
+ struct.pack_into("<H", buf, 0x04, version)
13
16
  struct.pack_into("<H", buf, 0x06, 1)
14
17
  buf[0x08:0x08 + len(git)] = git
15
18
  buf[0x18:0x18 + len(build)] = build
@@ -42,3 +45,19 @@ class TestParseSignature:
42
45
 
43
46
  def test_short_buffer_rejected(self):
44
47
  assert parse_signature(build_sig()[:48]) is None
48
+
49
+ def test_unknown_layout_version_rejected(self):
50
+ # CRC-valid, magic-valid — but a layout generation this tool
51
+ # refuses to decode must never be treated as evidence.
52
+ with pytest.raises(SignatureLayoutError, match="version 2"):
53
+ parse_signature(build_sig(version=2))
54
+
55
+ def test_wait_for_signature_fails_fast_on_unknown_version(self, monkeypatch):
56
+ # An alien layout is permanent: the poll must return immediately
57
+ # with the reason instead of spinning to the timeout.
58
+ from flashgate import swdsig
59
+
60
+ monkeypatch.setattr(swdsig, "read_ram", lambda *a, **k: build_sig(version=9))
61
+ info, err = swdsig.wait_for_signature("port=SWD", 0x2001FF00, 64, timeout_s=20)
62
+ assert info is None
63
+ assert "version 9 not supported" in err
@@ -1,8 +0,0 @@
1
- """flashgate: hardware-in-the-loop verification gate for coding agents.
2
-
3
- The agent may not claim firmware work is done until the board itself
4
- says so: build -> flash -> boot banner over serial, with exit codes
5
- that an agent harness (Stop hook) can enforce.
6
- """
7
-
8
- __version__ = "0.3.0"
File without changes
File without changes
File without changes
File without changes
File without changes