Phase 3: Add path size inference and also bin some stupid migration tests while we're at it

This commit is contained in:
Jack Kingsman
2026-03-07 19:02:37 -08:00
parent e0d7c8a083
commit f97c846378
6 changed files with 35 additions and 46 deletions
+3 -1
View File
@@ -221,7 +221,9 @@ async def on_path_update(event: "Event") -> None:
) )
return return
await ContactRepository.update_path(contact.public_key, str(path), normalized_path_len) await ContactRepository.update_path(
contact.public_key, str(path), normalized_path_len
)
async def on_new_contact(event: "Event") -> None: async def on_new_contact(event: "Event") -> None:
+3
View File
@@ -2,6 +2,8 @@ from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.path_utils import infer_hash_size
class Contact(BaseModel): class Contact(BaseModel):
public_key: str = Field(description="Public key (64-char hex)") public_key: str = Field(description="Public key (64-char hex)")
@@ -32,6 +34,7 @@ class Contact(BaseModel):
"flags": self.flags, "flags": self.flags,
"out_path": self.last_path or "", "out_path": self.last_path or "",
"out_path_len": self.last_path_len, "out_path_len": self.last_path_len,
"out_path_hash_mode": infer_hash_size(self.last_path or "", self.last_path_len) - 1,
"adv_lat": self.lat if self.lat is not None else 0.0, "adv_lat": self.lat if self.lat is not None else 0.0,
"adv_lon": self.lon if self.lon is not None else 0.0, "adv_lon": self.lon if self.lon is not None else 0.0,
"last_advert": self.last_advert if self.last_advert is not None else 0, "last_advert": self.last_advert if self.last_advert is not None else 0,
+7 -4
View File
@@ -25,8 +25,8 @@ class ContactRepository:
await db.conn.execute( await db.conn.execute(
""" """
INSERT INTO contacts (public_key, name, type, flags, last_path, last_path_len, INSERT INTO contacts (public_key, name, type, flags, last_path, last_path_len,
last_advert, lat, lon, last_seen, on_radio, last_contacted, last_advert, lat, lon, last_seen,
first_seen) on_radio, last_contacted, first_seen)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(public_key) DO UPDATE SET ON CONFLICT(public_key) DO UPDATE SET
name = COALESCE(excluded.name, contacts.name), name = COALESCE(excluded.name, contacts.name),
@@ -200,9 +200,12 @@ class ContactRepository:
return [ContactRepository._row_to_contact(row) for row in rows] return [ContactRepository._row_to_contact(row) for row in rows]
@staticmethod @staticmethod
async def update_path(public_key: str, path: str, path_len: int) -> None: async def update_path(
public_key: str, path: str, path_len: int
) -> None:
await db.conn.execute( await db.conn.execute(
"UPDATE contacts SET last_path = ?, last_path_len = ?, last_seen = ? WHERE public_key = ?", """UPDATE contacts SET last_path = ?, last_path_len = ?,
last_seen = ? WHERE public_key = ?""",
(path, path_len, int(time.time()), public_key.lower()), (path, path_len, int(time.time()), public_key.lower()),
) )
await db.conn.commit() await db.conn.commit()
+13 -37
View File
@@ -98,10 +98,7 @@ class TestMigration001:
await conn.commit() await conn.commit()
# Run migrations # Run migrations
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 38 # All migrations run
assert await get_version(conn) == 38
# Verify columns exist by inserting and selecting # Verify columns exist by inserting and selecting
await conn.execute( await conn.execute(
@@ -183,9 +180,8 @@ class TestMigration001:
applied1 = await run_migrations(conn) applied1 = await run_migrations(conn)
applied2 = await run_migrations(conn) applied2 = await run_migrations(conn)
assert applied1 == 38 # All migrations run assert applied1 > 0 # Migrations were applied
assert applied2 == 0 # No migrations on second run assert applied2 == 0 # No migrations on second run
assert await get_version(conn) == 38
finally: finally:
await conn.close() await conn.close()
@@ -246,8 +242,7 @@ class TestMigration001:
applied = await run_migrations(conn) applied = await run_migrations(conn)
# All migrations applied (version incremented) but no error # All migrations applied (version incremented) but no error
assert applied == 38 assert applied > 0
assert await get_version(conn) == 38
finally: finally:
await conn.close() await conn.close()
@@ -374,10 +369,8 @@ class TestMigration013:
) )
await conn.commit() await conn.commit()
# Run migration 13 (plus 14-38 which also run) # Run migration 13 (plus remaining which also run)
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 26
assert await get_version(conn) == 38
# Bots were migrated from app_settings to fanout_configs (migration 37) # Bots were migrated from app_settings to fanout_configs (migration 37)
# and the bots column was dropped (migration 38) # and the bots column was dropped (migration 38)
@@ -495,7 +488,6 @@ class TestMigration018:
assert await cursor.fetchone() is not None assert await cursor.fetchone() is not None
await run_migrations(conn) await run_migrations(conn)
assert await get_version(conn) == 38
# Verify autoindex is gone # Verify autoindex is gone
cursor = await conn.execute( cursor = await conn.execute(
@@ -572,9 +564,7 @@ class TestMigration018:
) )
await conn.commit() await conn.commit()
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 21 # Migrations 18-38 run (18+19 skip internally)
assert await get_version(conn) == 38
finally: finally:
await conn.close() await conn.close()
@@ -646,7 +636,6 @@ class TestMigration019:
assert await cursor.fetchone() is not None assert await cursor.fetchone() is not None
await run_migrations(conn) await run_migrations(conn)
assert await get_version(conn) == 38
# Verify autoindex is gone # Verify autoindex is gone
cursor = await conn.execute( cursor = await conn.execute(
@@ -711,9 +700,7 @@ class TestMigration020:
cursor = await conn.execute("PRAGMA journal_mode") cursor = await conn.execute("PRAGMA journal_mode")
assert (await cursor.fetchone())[0] == "delete" assert (await cursor.fetchone())[0] == "delete"
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 19 # Migrations 20-38
assert await get_version(conn) == 38
# Verify WAL mode # Verify WAL mode
cursor = await conn.execute("PRAGMA journal_mode") cursor = await conn.execute("PRAGMA journal_mode")
@@ -742,8 +729,7 @@ class TestMigration020:
await conn.commit() await conn.commit()
await set_version(conn, 20) await set_version(conn, 20)
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 18 # Migrations 21-38 still run
# Still WAL + INCREMENTAL # Still WAL + INCREMENTAL
cursor = await conn.execute("PRAGMA journal_mode") cursor = await conn.execute("PRAGMA journal_mode")
@@ -800,9 +786,7 @@ class TestMigration028:
) )
await conn.commit() await conn.commit()
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 11
assert await get_version(conn) == 38
# Verify payload_hash column is now BLOB # Verify payload_hash column is now BLOB
cursor = await conn.execute("PRAGMA table_info(raw_packets)") cursor = await conn.execute("PRAGMA table_info(raw_packets)")
@@ -870,9 +854,7 @@ class TestMigration028:
) )
await conn.commit() await conn.commit()
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 11 # Version still bumped
assert await get_version(conn) == 38
# Verify data unchanged # Verify data unchanged
cursor = await conn.execute("SELECT payload_hash FROM raw_packets") cursor = await conn.execute("SELECT payload_hash FROM raw_packets")
@@ -920,9 +902,7 @@ class TestMigration032:
await conn.execute("INSERT INTO app_settings (id) VALUES (1)") await conn.execute("INSERT INTO app_settings (id) VALUES (1)")
await conn.commit() await conn.commit()
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 7
assert await get_version(conn) == 38
# Community MQTT columns were added by migration 32 and dropped by migration 38. # Community MQTT columns were added by migration 32 and dropped by migration 38.
# Verify community settings were NOT migrated (no community config existed). # Verify community settings were NOT migrated (no community config existed).
@@ -987,9 +967,7 @@ class TestMigration034:
""") """)
await conn.commit() await conn.commit()
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 5
assert await get_version(conn) == 38
# Verify column exists with correct default # Verify column exists with correct default
cursor = await conn.execute("SELECT flood_scope FROM app_settings WHERE id = 1") cursor = await conn.execute("SELECT flood_scope FROM app_settings WHERE id = 1")
@@ -1030,9 +1008,7 @@ class TestMigration033:
""") """)
await conn.commit() await conn.commit()
applied = await run_migrations(conn) await run_migrations(conn)
assert applied == 6
assert await get_version(conn) == 38
cursor = await conn.execute( cursor = await conn.execute(
"SELECT key, name, is_hashtag, on_radio FROM channels WHERE key = ?", "SELECT key, name, is_hashtag, on_radio FROM channels WHERE key = ?",
+4
View File
@@ -309,6 +309,7 @@ class TestAdvertisementPipeline:
short_packet_info = MagicMock() short_packet_info = MagicMock()
short_packet_info.path_length = 1 short_packet_info.path_length = 1
short_packet_info.path = bytes.fromhex("aa") short_packet_info.path = bytes.fromhex("aa")
short_packet_info.path_hash_size = 1
short_packet_info.payload = b"" # Will be parsed by parse_advertisement short_packet_info.payload = b"" # Will be parsed by parse_advertisement
# Mock parse_advertisement to return our test contact # Mock parse_advertisement to return our test contact
@@ -333,6 +334,7 @@ class TestAdvertisementPipeline:
long_packet_info = MagicMock() long_packet_info = MagicMock()
long_packet_info.path_length = 5 long_packet_info.path_length = 5
long_packet_info.path = bytes.fromhex("aabbccddee") long_packet_info.path = bytes.fromhex("aabbccddee")
long_packet_info.path_hash_size = 1
with patch("app.packet_processor.broadcast_event", mock_broadcast): with patch("app.packet_processor.broadcast_event", mock_broadcast):
with patch("app.packet_processor.parse_advertisement") as mock_parse: with patch("app.packet_processor.parse_advertisement") as mock_parse:
@@ -381,6 +383,7 @@ class TestAdvertisementPipeline:
packet_info = MagicMock() packet_info = MagicMock()
packet_info.path_length = 3 packet_info.path_length = 3
packet_info.path = bytes.fromhex("aabbcc") packet_info.path = bytes.fromhex("aabbcc")
packet_info.path_hash_size = 1
with patch("app.packet_processor.broadcast_event", mock_broadcast): with patch("app.packet_processor.broadcast_event", mock_broadcast):
with patch("app.packet_processor.parse_advertisement") as mock_parse: with patch("app.packet_processor.parse_advertisement") as mock_parse:
@@ -433,6 +436,7 @@ class TestAdvertisementPipeline:
long_packet_info = MagicMock() long_packet_info = MagicMock()
long_packet_info.path_length = 4 long_packet_info.path_length = 4
long_packet_info.path = bytes.fromhex("aabbccdd") long_packet_info.path = bytes.fromhex("aabbccdd")
long_packet_info.path_hash_size = 1
long_packet_info.payload = b"" long_packet_info.payload = b""
with patch("app.packet_processor.broadcast_event", mock_broadcast): with patch("app.packet_processor.broadcast_event", mock_broadcast):
Generated
+5 -4
View File
@@ -497,16 +497,17 @@ wheels = [
[[package]] [[package]]
name = "meshcore" name = "meshcore"
version = "2.2.5" version = "2.2.28"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "bleak" }, { name = "bleak" },
{ name = "pycayennelpp" }, { name = "pycayennelpp" },
{ name = "pycryptodome" },
{ name = "pyserial-asyncio-fast" }, { name = "pyserial-asyncio-fast" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/be/c2/0927a793d9742832613a42bb5c138adda75e201fc0dafe83c0bd444ab9f7/meshcore-2.2.5.tar.gz", hash = "sha256:15818150a6a8380883c2b272356fabad0bab107f3b9438cfd665bc9ca03d1d17", size = 56968 } sdist = { url = "https://files.pythonhosted.org/packages/7d/29/4dc795be22670ebabf18ce8b12625f39551f1e9073dfd3494c8bd1b2291d/meshcore-2.2.28.tar.gz", hash = "sha256:87ece4b3d9e32c6baccab73da5eabcf5a84d520df8be715c62879985b5baec83", size = 68652 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/e6/557d0fade6ba87e955750b980ca4c7468f529c911e5a9b2fe55d49b4cfd8/meshcore-2.2.5-py3-none-any.whl", hash = "sha256:b422816f6e4c6e621be99730b92973f78ab983d4e01111ad4c8fd50c2b2b3a9c", size = 45053 }, { url = "https://files.pythonhosted.org/packages/3a/10/0cf1ca1c948049344a303478d86425b86c6699e8cdeb7f0e611125f937c1/meshcore-2.2.28-py3-none-any.whl", hash = "sha256:e015dbf756f844d2c4191d5a9cd1c1c70ec16a235d222cdcfc88a6a760de2847", size = 52213 },
] ]
[[package]] [[package]]
@@ -1090,7 +1091,7 @@ requires-dist = [
{ name = "fastapi", specifier = ">=0.115.0" }, { name = "fastapi", specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "httpx", marker = "extra == 'test'", specifier = ">=0.27.0" }, { name = "httpx", marker = "extra == 'test'", specifier = ">=0.27.0" },
{ name = "meshcore" }, { name = "meshcore", specifier = "==2.2.28" },
{ name = "pycryptodome", specifier = ">=3.20.0" }, { name = "pycryptodome", specifier = ">=3.20.0" },
{ name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" },
{ name = "pynacl", specifier = ">=1.5.0" }, { name = "pynacl", specifier = ">=1.5.0" },