Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
076df5d
research: add guarded phonebook write profiler
MarcLeinenDE Sep 8, 2026
26bd85e
tests: guard phonebook write profiler
MarcLeinenDE Sep 8, 2026
19c0b53
research: harden phonebook update and move profiler
MarcLeinenDE Sep 10, 2026
05cc5f3
test: cover hardened phonebook profiler source
MarcLeinenDE Sep 10, 2026
6db65aa
test: keep profiler assertions focused
MarcLeinenDE Sep 10, 2026
4977cf7
test: add field-specific phonebook update profiler
MarcLeinenDE Sep 10, 2026
7c647ea
test: cover field-specific phonebook update profiler gate
MarcLeinenDE Sep 10, 2026
1d4b5b3
research: characterize phonebook create baseline before update fields
MarcLeinenDE Sep 10, 2026
4f150c8
test: cover field-specific phonebook profiler
MarcLeinenDE Sep 10, 2026
98e3329
research: fail lifecycle profiler on index-set mismatch
MarcLeinenDE Sep 10, 2026
c2cf178
test: require exact index-set restoration in phonebook lifecycle prof…
MarcLeinenDE Sep 10, 2026
53bc262
test: describe hardened phonebook profiler evidence
MarcLeinenDE Sep 10, 2026
cc1202e
research: retain observed-baseline phonebook field profiler
MarcLeinenDE Sep 10, 2026
12c2db7
test: keep phonebook profiler coverage current
MarcLeinenDE Sep 10, 2026
1c6886d
test: no-op guard refresh
MarcLeinenDE Sep 10, 2026
208f5e6
test: maintain phonebook profiler assertions
MarcLeinenDE Sep 10, 2026
3f23c24
test: stabilize phonebook profiler guards
MarcLeinenDE Sep 10, 2026
ef87b9b
research: add guarded phonebook residue cleanup
MarcLeinenDE Sep 21, 2026
8dc9694
test: guard phonebook residue cleanup helper
MarcLeinenDE Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions examples/check_cleanup_phonebook_residue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

import os
from collections.abc import Mapping
from typing import Any

from nr2301 import NR2301Client


LOCAL_LOCATION = 0
PAGE_CAPACITY = 100
KNOWN_RESIDUE_INDEXES = {3, 4, 5, 6, 7}
KNOWN_SYNTHETIC_MOBILES = {
"5550200001", "5550200002", "5550200003", "5550200004",
"5550300001", "5550300002", "5550300003", "5550300004",
"5550400001",
}


def _as_int(value: Any) -> int | None:
if isinstance(value, bool):
return None
try:
return int(value)
except (TypeError, ValueError):
return None


def _contacts(router: NR2301Client) -> list[Mapping[str, Any]]:
response = router.phonebook.contacts_by_location(
LOCAL_LOCATION,
page_capacity=PAGE_CAPACITY,
page_index=0,
)
values = response.get("contactlist")
if not isinstance(values, list):
raise RuntimeError("getcontactbylocation did not return contactlist as a list")
return [item for item in values if isinstance(item, Mapping)]


def _synthetic_markers(item: Mapping[str, Any]) -> tuple[bool, bool, bool]:
name = item.get("name")
mobile = item.get("mobile")
email = item.get("email")
return (
isinstance(name, str) and name.startswith("SDK-PB-"),
isinstance(mobile, str) and mobile in KNOWN_SYNTHETIC_MOBILES,
isinstance(email, str) and email.endswith("@example.invalid"),
)


def _is_known_residue(item: Mapping[str, Any]) -> bool:
index = _as_int(item.get("index"))
if index not in KNOWN_RESIDUE_INDEXES:
return False
name_marker, mobile_marker, email_marker = _synthetic_markers(item)
return name_marker or mobile_marker or email_marker


def _delete(router: NR2301Client, index: int) -> bool:
router.call(
"phonebook",
"delete_pb",
data={
"delete_pb": {
"location": "0",
"count": "1",
"indexarray": str(index),
}
},
)
return all(_as_int(item.get("index")) != index for item in _contacts(router))


def main() -> None:
password = os.environ.get("NR2301_PASSWORD")
if not password:
raise SystemExit("NR2301_PASSWORD is required")

cleanup_enabled = os.environ.get("NR2301_PHONEBOOK_RESIDUE_CLEANUP") == "1"

with NR2301Client(
os.environ.get("NR2301_URL", "http://zyxel.home"),
username=os.environ.get("NR2301_USERNAME", "admin"),
password=password,
timeout=10.0,
) as router:
router.login()
current = _contacts(router)

indexes = sorted(
index
for item in current
if (index := _as_int(item.get("index"))) is not None
)
classified = []
for item in current:
index = _as_int(item.get("index"))
markers = _synthetic_markers(item)
known = _is_known_residue(item)
classified.append((index, known, markers))
print(
"PHONEBOOK_RESIDUE_ROW"
f" index={index}"
f" known_residue={known}"
f" name_marker={markers[0]}"
f" mobile_marker={markers[1]}"
f" email_marker={markers[2]}"
)

print(f"PHONEBOOK_RESIDUE_COUNT = {len(current)}")
print(f"PHONEBOOK_RESIDUE_INDEXES = {indexes}")

if not current:
print("PHONEBOOK_RESIDUE_CLEANUP = NOT_NEEDED")
return

safe = (
all(index in KNOWN_RESIDUE_INDEXES for index in indexes)
and len(indexes) == len(current)
and all(known for _, known, _ in classified)
)
print(f"PHONEBOOK_RESIDUE_SAFE_TO_CLEAN = {safe}")

if not safe:
raise RuntimeError(
"current local phonebook does not consist exclusively of the known "
"synthetic residue; refusing automatic cleanup"
)

if not cleanup_enabled:
print("PHONEBOOK_RESIDUE_CLEANUP = ARMED_NOT_EXECUTED")
print("Set NR2301_PHONEBOOK_RESIDUE_CLEANUP=1 to remove the classified residue.")
return

for index in indexes:
absent = _delete(router, index)
print(f"PHONEBOOK_RESIDUE_DELETE index={index} absent={absent}")
if not absent:
raise RuntimeError(f"synthetic residue index {index} could not be removed")

final = _contacts(router)
if final:
raise RuntimeError("local phonebook is not empty after residue cleanup")
print("PHONEBOOK_RESIDUE_CLEANUP = PASS")


if __name__ == "__main__":
main()
Loading
Loading