feat(rawcli): suggest the tag's memory map when completing a page argument

Typing the page/block argument of a READ/WRITE now pops the identified tag's memory landmarks
as completions, each labelled with its datasheet role, so you pick a location by meaning:

  READ(      -> 0x00  UID / serial number
                0x03  Capability Container (CC)
                0x04  user memory
                0xE3  CFG0 — MIRROR / MIRROR_PAGE / AUTH0
                0xE5  PWD (32-bit password)  ...
  READ(0xE   -> filters to the E-page landmarks

memory.landmark_pages() derives the notable pages from the same _LAYOUTS the hint/annotation use
(NTAG21x / Ultralight EV1 landmarks; T5577 blocks 0-7). The completer matches the partial in
0x-, bare-hex, or decimal form and inserts canonical 0xNN. A comma ends the page argument, so
data args get no page suggestions; tags with no known layout (MIFARE Classic) offer nothing.

Hardware-verified: completer run against a real identified NTAG216 lists 0x00..0xE6 with roles.
Tests for the map, partial filtering, second-arg guard, T5577 blocks, and the no-layout case.
1269 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-15 16:24:45 -07:00
parent 260cb8eb9e
commit b6fdb7bca4
3 changed files with 87 additions and 0 deletions

View File

@@ -504,6 +504,43 @@ class TestCompleter:
cs = self._complete(s, "01100000") # 0x60 in binary
assert any(c.text == "GET_VERSION(" for c in cs)
def test_page_arg_lists_memory_map(self):
# typing the page argument suggests the tag's memory landmarks with their roles
s = RawSession()
s.protocol, s.transponder = "hf14a", "NTAG216"
cs = self._complete(s, "READ(")
by_text = {c.text: c.display_meta_text for c in cs}
assert by_text["0x00"] == "UID / serial number"
assert by_text["0x03"] == "Capability Container (CC)"
assert by_text["0x04"] == "user memory"
assert "PWD" in by_text["0xE5"]
def test_page_arg_filters_by_partial(self):
s = RawSession()
s.protocol, s.transponder = "hf14a", "NTAG216"
texts = [c.text for c in self._complete(s, "READ(0xE")]
assert texts == ["0xE2", "0xE3", "0xE4", "0xE5", "0xE6"] # only the E-page landmarks
# a decimal/bare-hex partial matches too
assert [c.text for c in self._complete(s, "READ(4")] == ["0x04"]
def test_page_arg_second_arg_untouched(self):
# a comma ends the page argument — the data argument gets no memory suggestions
s = RawSession()
s.protocol, s.transponder = "hf14a", "NTAG216"
assert self._complete(s, "WRITE(0x04, ") == []
def test_page_arg_t5577_blocks(self):
s = RawSession()
s.protocol, s.transponder = "lf", "T5577 / ATA5577"
by_text = {c.text: c.display_meta_text for c in self._complete(s, "READ(")}
assert "config" in by_text["0x00"]
assert "password" in by_text["0x07"]
def test_page_arg_none_without_layout(self):
s = RawSession()
s.protocol, s.transponder = "hf14a", "MIFARE Classic 1K" # no page layout
assert self._complete(s, "READ(") == []
class TestMemoryHint:
def _sess(self, tp="NTAG213"):