/* * test_depth.cpp — Deep coverage tests for libantheos * * Covers gaps in existing suites: Frame class API, parser tail handling, * parser/SidPool move semantics, parser statistics, SidPool edge cases, * Context session_accept, Context session exhaustion, Context feed edge * cases, Context inbound dispatch for all verb types, word_decode * malformed inputs, DecodedWord operator bool. * * Copyright (c) 2025-2026 Are Bjørby * SPDX-License-Identifier: MIT */ #include "test_common.hpp" #include "antheos.hpp" #include #include #include #include #include #include using namespace antheos; /* ================================================================ * 1. Frame class — value semantics & API * ================================================================ */ static void test_frame_default_empty() { Frame f; ASSERT_TRUE(f.empty()); ASSERT_EQ(f.size(), 0u); ASSERT_TRUE(!static_cast(f)); ASSERT_TRUE(f.begin() == f.end()); } static void test_frame_from_ptr() { const uint8_t data[] = {0x02, 0x03}; Frame f(data, 2); ASSERT_EQ(f.size(), 2u); ASSERT_TRUE(!f.empty()); ASSERT_TRUE(static_cast(f)); ASSERT_EQ(f[0], 0x02); ASSERT_EQ(f[1], 0x03); ASSERT_EQ(f.data()[0], 0x02); } static void test_frame_from_vector() { std::vector v = {0x10, 0x20, 0x30}; Frame f(std::move(v)); ASSERT_EQ(f.size(), 3u); ASSERT_EQ(f[0], 0x10); ASSERT_EQ(f[2], 0x30); } static void test_frame_copy() { const uint8_t data[] = {0x01, 0x02, 0x03}; Frame a(data, 3); Frame b = a; ASSERT_EQ(b.size(), 3u); ASSERT_MEM_EQ(b.data(), data, 3); /* Modify copy, original unchanged */ b.bytes()[0] = 0xFF; ASSERT_EQ(a[0], 0x01); ASSERT_EQ(b[0], 0xFF); } static void test_frame_move() { const uint8_t data[] = {0xAA, 0xBB}; Frame a(data, 2); Frame b = std::move(a); ASSERT_EQ(b.size(), 2u); ASSERT_EQ(b[0], 0xAA); ASSERT_EQ(b[1], 0xBB); /* Source is moved-from — empty */ ASSERT_TRUE(a.empty()); } static void test_frame_iterators() { const uint8_t data[] = {0x10, 0x20, 0x30}; Frame f(data, 3); int count = 0; uint8_t sum = 0; for (auto it = f.begin(); it != f.end(); ++it) { sum += *it; count++; } ASSERT_EQ(count, 3); ASSERT_EQ(sum, 0x60); } static void test_frame_bytes_mutable() { const uint8_t data[] = {0x01}; Frame f(data, 1); f.bytes().push_back(0x02); ASSERT_EQ(f.size(), 2u); ASSERT_EQ(f[1], 0x02); } /* ================================================================ * 2. Parser tail handling * ================================================================ */ static void test_parser_tail_callback() { Parser p; int word_count = 0; int msg_count = 0; size_t tail_len = 0; uint8_t tail_data[64]{}; p.on_word([&](wire::WordType, wire::Radix, wire::Unit, const uint8_t*, size_t) { word_count++; }); p.on_tail([&](const uint8_t* data, size_t len) { tail_len = len; if (len <= sizeof(tail_data)) std::memcpy(tail_data, data, len); }); p.on_message([&](size_t) { msg_count++; }); /* Set tail length before feeding frame */ p.set_tail_length(4); /* Feed: SOM !B "test EOM <4 tail bytes> */ auto f = bus::broadcast("test"); ASSERT_TRUE(f.has_value()); /* Feed frame bytes */ p.feed(f->data(), f->size()); /* After EOM, parser should be in Tail state, no message yet */ ASSERT_EQ(msg_count, 0); /* Feed 4 tail bytes */ const uint8_t tail_in[] = {0xDE, 0xAD, 0xBE, 0xEF}; p.feed(tail_in, 4); /* Now message should be complete */ ASSERT_EQ(msg_count, 1); ASSERT_EQ(tail_len, 4u); ASSERT_MEM_EQ(tail_data, tail_in, 4); } static void test_parser_tail_zero_length() { /* tail_length=0: message completes immediately after EOM */ Parser p; int msg_count = 0; p.on_message([&](size_t) { msg_count++; }); p.set_tail_length(0); auto f = bus::broadcast("x"); ASSERT_TRUE(f.has_value()); p.feed(f->data(), f->size()); ASSERT_EQ(msg_count, 1); } /* ================================================================ * 3. Parser statistics accumulation * ================================================================ */ static void test_parser_stats_accumulate() { Parser p; p.on_word([](wire::WordType, wire::Radix, wire::Unit, const uint8_t*, size_t) {}); p.on_message([](size_t) {}); ASSERT_EQ(p.total_words(), 0u); ASSERT_EQ(p.total_messages(), 0u); ASSERT_EQ(p.parse_errors(), 0u); /* Feed two valid frames */ auto f1 = bus::broadcast("a"); /* 2 words: !B, "a */ auto f2 = bus::ping_all(); /* 1 word: !P */ ASSERT_TRUE(f1.has_value() && f2.has_value()); p.feed(f1->data(), f1->size()); ASSERT_EQ(p.total_words(), 2u); ASSERT_EQ(p.total_messages(), 1u); p.feed(f2->data(), f2->size()); ASSERT_EQ(p.total_words(), 3u); ASSERT_EQ(p.total_messages(), 2u); ASSERT_EQ(p.parse_errors(), 0u); } static void test_parser_error_count() { Parser p; p.on_word([](wire::WordType, wire::Radix, wire::Unit, const uint8_t*, size_t) {}); p.on_message([](size_t) {}); /* Feed invalid: SOM followed by garbage (not SOW or EOM) */ const uint8_t bad[] = {wire::SOM, 0x99}; p.feed(bad, 2); ASSERT_EQ(p.parse_errors(), 1u); ASSERT_EQ(p.total_messages(), 0u); /* Recovery: valid frame after error */ auto f = bus::ping_all(); ASSERT_TRUE(f.has_value()); p.feed(f->data(), f->size()); ASSERT_EQ(p.total_messages(), 1u); ASSERT_EQ(p.parse_errors(), 1u); /* error count preserved */ } static void test_parser_stats_survive_reset() { Parser p; p.on_word([](wire::WordType, wire::Radix, wire::Unit, const uint8_t*, size_t) {}); p.on_message([](size_t) {}); auto f = bus::ping_all(); ASSERT_TRUE(f.has_value()); p.feed(f->data(), f->size()); ASSERT_EQ(p.total_words(), 1u); ASSERT_EQ(p.total_messages(), 1u); p.reset(); ASSERT_EQ(static_cast(p.state()), static_cast(ParseState::WaitSom)); /* Lifetime stats preserved across reset */ ASSERT_EQ(p.total_words(), 1u); ASSERT_EQ(p.total_messages(), 1u); } /* ================================================================ * 4. Parser move semantics * ================================================================ */ static void test_parser_move_construct() { Parser a; int msg_count = 0; a.on_message([&](size_t) { msg_count++; }); /* Partially feed */ a.feed(static_cast(wire::SOM)); Parser b = std::move(a); /* Continue feeding on b */ auto f = bus::ping_all(); ASSERT_TRUE(f.has_value()); /* Feed remaining bytes (skip SOM already fed) */ for (size_t i = 1; i < f->size(); i++) b.feed(f->data()[i]); ASSERT_EQ(msg_count, 1); } static void test_parser_move_assign() { Parser a; int count_a = 0; a.on_message([&](size_t) { count_a++; }); Parser b; int count_b = 0; b.on_message([&](size_t) { count_b++; }); b = std::move(a); auto f = bus::ping_all(); ASSERT_TRUE(f.has_value()); b.feed(f->data(), f->size()); ASSERT_EQ(count_a, 1); /* a's callback transferred to b */ ASSERT_EQ(count_b, 0); } /* ================================================================ * 5. SidPool move semantics * ================================================================ */ static void test_sidpool_move_construct() { /* Move-semantics test — uniqueness is not under assertion here, so the * non-conformant escape hatch is the right semantic. */ SidPool a("org", "dev", "iid"); auto sid = a.acquire_unchecked(); ASSERT_TRUE(sid.has_value()); SidPool b = std::move(a); /* b should be functional — can acquire fresh SIDs */ auto sid2 = b.acquire_unchecked(); ASSERT_TRUE(sid2.has_value()); ASSERT_TRUE(sid2->size() >= id::SID_MIN_LEN); ASSERT_TRUE(*sid != *sid2); } static void test_sidpool_move_assign() { /* Move-semantics test — uniqueness is not under assertion here. */ SidPool a("org", "dev", "iid1"); SidPool b("org", "dev", "iid2"); auto sid_a = a.acquire_unchecked(); ASSERT_TRUE(sid_a.has_value()); b = std::move(a); /* b now has a's state — acquire should continue a's counter */ auto sid_b = b.acquire_unchecked(); ASSERT_TRUE(sid_b.has_value()); } /* ================================================================ * 6. SidPool edge cases * ================================================================ */ static void test_sidpool_acquire_unique_null_check() { SidPool pool("org", "dev", "iid"); /* Null collision check returns nullopt */ auto sid = pool.acquire_unique(nullptr); ASSERT_TRUE(!sid.has_value()); } static void test_sidpool_different_identity_different_sids() { /* Identity-driven differentiation test — exercises the FNV-1a hash * divergence between two pools with different IIDs. Uniqueness is not * the property under test here (that's covered by * test_sidpool_acquire_unique_grows_on_collision), so the non-conformant * escape hatch is the honest semantic. */ SidPool pool_a("org", "dev", "iid1"); SidPool pool_b("org", "dev", "iid2"); auto sid_a = pool_a.acquire_unchecked(); auto sid_b = pool_b.acquire_unchecked(); ASSERT_TRUE(sid_a.has_value()); ASSERT_TRUE(sid_b.has_value()); ASSERT_TRUE(*sid_a != *sid_b); } static void test_sidpool_invalid_args() { /* Empty OID */ bool threw = false; try { SidPool pool("", "d", "i"); } catch (const std::invalid_argument&) { threw = true; } ASSERT_TRUE(threw); /* Empty DID */ threw = false; try { SidPool pool("o", "", "i"); } catch (const std::invalid_argument&) { threw = true; } ASSERT_TRUE(threw); /* Empty IID */ threw = false; try { SidPool pool("o", "d", ""); } catch (const std::invalid_argument&) { threw = true; } ASSERT_TRUE(threw); } static void test_sidpool_acquire_unique_grows_on_collision() { /* Spec §11.2 conformance: acquire_unique() MUST never return a SID * present in the active-set, and MUST grow the SID length on collision * until uniqueness is reached. This test forces collisions by claiming * every SID the pool produces at the starting length is "already in * use", then accepting once the length grows. After the grow, the * returned SID is longer than SID_MIN_LEN — the structural proof that * the grow-on-collision algorithm ran. */ SidPool pool("org", "dev", "iid"); const size_t start_len = id::SID_MIN_LEN; std::set active; /* Pre-poison the active set: any SID at the starting length is "taken" * — this forces acquire_unique to grow on the first attempt. */ auto check = [&active, start_len](std::string_view sid) -> bool { if (sid.size() == start_len) return true; /* collision */ return active.count(std::string(sid)) > 0; }; auto sid = pool.acquire_unique(check); ASSERT_TRUE(sid.has_value()); /* Length must have grown past SID_MIN_LEN to escape the poisoned band. */ ASSERT_TRUE(sid->size() > start_len); /* Adding the returned SID to the active set and re-acquiring must * produce a different SID — uniqueness preserved. */ active.insert(*sid); auto sid2 = pool.acquire_unique(check); ASSERT_TRUE(sid2.has_value()); ASSERT_TRUE(*sid != *sid2); } /* ================================================================ * 7. Context session_accept — inbound session handling * ================================================================ */ static void test_context_session_accept_basic() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); int slot = ctx.session_accept("XYZW1234", 5); ASSERT_TRUE(slot >= 0); ASSERT_TRUE(ctx.session_sid(slot) == "XYZW1234"); ASSERT_EQ(ctx.session_mid(slot), 6u); /* inbound_mid + 1 */ ASSERT_TRUE(ctx.session_state(slot) == SessionState::Active); } static void test_context_session_accept_empty_sid() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); int slot = ctx.session_accept("", 0); ASSERT_EQ(slot, -1); } static void test_context_session_accept_call() { /* Accept inbound session then send a call on it */ Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); int slot = ctx.session_accept("MNPQ5678", 10); ASSERT_TRUE(slot >= 0); auto f = ctx.session_call(slot, "TARGET", "hello"); ASSERT_TRUE(f.has_value()); ASSERT_EQ(f->data()[0], wire::SOM); ASSERT_EQ(f->data()[f->size() - 1], wire::EOM); } /* ================================================================ * 8. Context — 32-session exhaustion * ================================================================ */ static void test_context_session_exhaustion() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); /* Open all 32 slots */ int slots[32]; for (int i = 0; i < 32; i++) { slots[i] = ctx.session_open(); ASSERT_TRUE(slots[i] >= 0); } /* 33rd open should fail */ int overflow = ctx.session_open(); ASSERT_EQ(overflow, -1); /* Close one, then open should succeed */ ctx.session_close(slots[0]); int reused = ctx.session_open(); ASSERT_TRUE(reused >= 0); } /* ================================================================ * 9. Context feed edge cases * ================================================================ */ static void test_context_feed_null() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); ASSERT_EQ(ctx.feed(nullptr, 0), 0u); ASSERT_EQ(ctx.feed(nullptr, 100), 0u); } static void test_context_feed_empty() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); uint8_t data[] = {0x00}; ASSERT_EQ(ctx.feed(data, 0), 0u); } /* ================================================================ * 10. Context inbound dispatch — all verb types * ================================================================ */ struct DispatchState { int event_count = 0; std::string last_verb; std::string last_id; std::string last_id2; std::string last_detail; int msg_count = 0; std::string last_msg_verb; std::string last_msg_sid; uint32_t last_msg_mid = 0; std::string last_msg_body; int offer_count = 0; std::string last_offer_bid; std::string last_offer_desc; }; static void wire_ctx(Context& ctx, DispatchState& s) { ctx.on_event([&s](std::string_view verb, std::string_view id, std::string_view id2, std::string_view detail) { s.event_count++; s.last_verb = std::string(verb); s.last_id = std::string(id); s.last_id2 = std::string(id2); s.last_detail = std::string(detail); }); ctx.on_message([&s](std::string_view verb, std::string_view sid, std::string_view, uint32_t mid, std::string_view body) { s.msg_count++; s.last_msg_verb = std::string(verb); s.last_msg_sid = std::string(sid); s.last_msg_mid = mid; s.last_msg_body = std::string(body); }); ctx.on_offer([&s](std::string_view bid, std::string_view desc) { s.offer_count++; s.last_offer_bid = std::string(bid); s.last_offer_desc = std::string(desc); }); } static void test_dispatch_establish() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::establish("AB12"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "E"); ASSERT_TRUE(s.last_id == "AB12"); } static void test_dispatch_conflict() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::conflict("CD34"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "C"); ASSERT_TRUE(s.last_id == "CD34"); } static void test_dispatch_ping() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::ping("EF56"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "P"); ASSERT_TRUE(s.last_id == "EF56"); } static void test_dispatch_discover() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::discover("GH78"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "D"); ASSERT_TRUE(s.last_id == "GH78"); } static void test_dispatch_discover_response() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::discover_response("GH78", "JK9A"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "D"); ASSERT_TRUE(s.last_id == "GH78"); ASSERT_TRUE(s.last_id2 == "JK9A"); } static void test_dispatch_verify() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::verify("LM2B"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "V"); ASSERT_TRUE(s.last_id == "LM2B"); } static void test_dispatch_acknowledge() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::acknowledge("NP3C"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "W"); ASSERT_TRUE(s.last_id == "NP3C"); } static void test_dispatch_exception() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::exception("OVERLOAD"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "X"); ASSERT_TRUE(s.last_detail == "OVERLOAD"); } static void test_dispatch_query() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = service::query("temperature"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "Q"); ASSERT_TRUE(s.last_detail == "temperature"); } static void test_dispatch_offer() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = service::offer("QR4D", "thermo"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.offer_count, 1); ASSERT_TRUE(s.last_offer_bid == "QR4D"); ASSERT_TRUE(s.last_offer_desc == "thermo"); } static void test_dispatch_accept() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = service::accept("ST5E"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "A"); ASSERT_TRUE(s.last_id == "ST5E"); } static void test_dispatch_session_notify() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = session::notify("UV6F", {}, 3, "temp_high"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.msg_count, 1); ASSERT_TRUE(s.last_msg_verb == "N"); ASSERT_TRUE(s.last_msg_sid == "UV6F"); ASSERT_EQ(s.last_msg_mid, 3u); ASSERT_TRUE(s.last_msg_body == "temp_high"); } static void test_dispatch_session_status() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = session::status("WX7G", {}, 1); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "T"); ASSERT_TRUE(s.last_id == "WX7G"); } static void test_dispatch_session_locate() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = session::locate("YZ8H"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "L"); ASSERT_TRUE(s.last_id == "YZ8H"); } static void test_dispatch_session_resume() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = session::resume("A2B3"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "U"); ASSERT_TRUE(s.last_id == "A2B3"); } static void test_dispatch_session_finish() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = session::finish("C4D5"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "F"); ASSERT_TRUE(s.last_id == "C4D5"); } /* ================================================================ * 10b. Auth (Z-verb) dispatch * ================================================================ */ static void test_dispatch_auth_challenge() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::auth_challenge("4T9X2", "deadbeef01234567"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "Z"); ASSERT_TRUE(s.last_id == "4T9X2"); ASSERT_TRUE(s.last_id2.empty()); /* no key_id */ ASSERT_TRUE(s.last_detail == "deadbeef01234567"); /* nonce */ } static void test_dispatch_auth_response() { DispatchState s; Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); wire_ctx(ctx, s); auto f = bus::auth_response("4T9X2", "kid123", "sig456"); ASSERT_TRUE(f.has_value()); ctx.feed(f->data(), f->size()); ASSERT_EQ(s.event_count, 1); ASSERT_TRUE(s.last_verb == "Z"); ASSERT_TRUE(s.last_id == "4T9X2"); ASSERT_TRUE(s.last_id2 == "kid123"); /* key_id */ ASSERT_TRUE(s.last_detail == "sig456"); /* signature */ } /* ================================================================ * 11. word_decode — malformed input rejection * ================================================================ */ static void test_decode_null_input() { auto dw = wire::word_decode(nullptr, 10); ASSERT_TRUE(!dw.has_value()); } static void test_decode_too_short() { uint8_t data[] = {wire::SOW, '"', wire::SOB}; auto dw = wire::word_decode(data, 3); ASSERT_TRUE(!dw.has_value()); } static void test_decode_missing_sow() { uint8_t data[] = {0xFF, '"', wire::SOB, 'A', wire::EOW}; auto dw = wire::word_decode(data, 5); ASSERT_TRUE(!dw.has_value()); } static void test_decode_invalid_word_type() { uint8_t data[] = {wire::SOW, 0x99, wire::SOB, 'A', wire::EOW}; auto dw = wire::word_decode(data, 5); ASSERT_TRUE(!dw.has_value()); } static void test_decode_invalid_radix() { /* Valid word type, but radix flag is invalid */ uint8_t data[] = {wire::SOW, '#', wire::SOR, 0x99, wire::SOB, '1', wire::EOW}; auto dw = wire::word_decode(data, 7); ASSERT_TRUE(!dw.has_value()); } static void test_decode_invalid_unit() { /* Valid word type + valid radix, but unit flag is invalid */ uint8_t data[] = {wire::SOW, '#', wire::SOR, 'D', wire::SOU, 0x99, wire::SOB, '1', wire::EOW}; auto dw = wire::word_decode(data, 9); ASSERT_TRUE(!dw.has_value()); } static void test_decode_missing_sob() { /* After type, expect SOR/SOU/SOB — instead get body byte */ uint8_t data[] = {wire::SOW, '"', 'A', wire::EOW}; auto dw = wire::word_decode(data, 4); ASSERT_TRUE(!dw.has_value()); } static void test_decode_reserved_in_body() { uint8_t data[] = {wire::SOW, '"', wire::SOB, wire::SOM, wire::EOW}; auto dw = wire::word_decode(data, 5); ASSERT_TRUE(!dw.has_value()); } static void test_decode_truncated_radix() { /* SOR present but no radix byte follows (end of input) */ uint8_t data[] = {wire::SOW, '#', wire::SOR}; auto dw = wire::word_decode(data, 3); ASSERT_TRUE(!dw.has_value()); } static void test_decode_truncated_unit() { /* SOU present but no unit byte follows (end of input) */ uint8_t data[] = {wire::SOW, '#', wire::SOR, 'D', wire::SOU}; auto dw = wire::word_decode(data, 5); ASSERT_TRUE(!dw.has_value()); } static void test_decode_no_eow() { /* Body data present but no EOW terminator (end of input) */ uint8_t data[] = {wire::SOW, '"', wire::SOB, 'A', 'B'}; auto dw = wire::word_decode(data, 5); ASSERT_TRUE(!dw.has_value()); } /* ================================================================ * 12. DecodedWord operator bool * ================================================================ */ static void test_decoded_word_bool() { /* Symbol with empty body: operator bool returns true (Symbol is special) */ wire::DecodedWord dw; dw.type = wire::WordType::Symbol; dw.radix = wire::Radix::None; dw.unit = wire::Unit::None; ASSERT_TRUE(static_cast(dw)); /* Text with non-empty body: true */ dw.type = wire::WordType::Text; dw.body = {0x41}; ASSERT_TRUE(static_cast(dw)); /* Text with empty body: false */ dw.body.clear(); ASSERT_TRUE(!static_cast(dw)); } /* ================================================================ * 13. Context session_mid/state/sid for edge slots * ================================================================ */ static void test_context_session_accessors_invalid() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); /* Negative slot */ ASSERT_TRUE(ctx.session_sid(-1).empty()); ASSERT_EQ(ctx.session_mid(-1), 0u); ASSERT_TRUE(ctx.session_state(-1) == SessionState::Idle); /* Slot >= MAX_SESSIONS */ ASSERT_TRUE(ctx.session_sid(32).empty()); ASSERT_EQ(ctx.session_mid(32), 0u); ASSERT_TRUE(ctx.session_state(32) == SessionState::Idle); /* Idle (never opened) slot */ ASSERT_TRUE(ctx.session_sid(0).empty()); ASSERT_EQ(ctx.session_mid(0), 0u); ASSERT_TRUE(ctx.session_state(0) == SessionState::Idle); } static void test_context_session_close_idle() { /* Closing a never-opened slot returns nullopt */ Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); ASSERT_TRUE(!ctx.session_close(0).has_value()); } static void test_context_session_notify_invalid() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); ASSERT_TRUE(!ctx.session_notify(-1, {}, "ev").has_value()); ASSERT_TRUE(!ctx.session_notify(32, {}, "ev").has_value()); ASSERT_TRUE(!ctx.session_notify(0, {}, "ev").has_value()); /* idle */ } static void test_context_session_status_invalid() { Context ctx("ORG", "Dev", "SN001", "ABCDEFGH"); ASSERT_TRUE(!ctx.session_status(-1, {}).has_value()); ASSERT_TRUE(!ctx.session_status(32, {}).has_value()); ASSERT_TRUE(!ctx.session_status(0, {}).has_value()); /* idle */ } /* ================================================================ * 14. word_encode edge cases * ================================================================ */ static void test_encode_null_body_zero_len() { /* null body + 0 length: should succeed (empty body word) */ auto result = wire::word_encode(wire::WordType::Text, wire::Radix::None, wire::Unit::None, nullptr, 0); ASSERT_TRUE(result.has_value()); /* [SOW][WT][SOB][EOW] = 4 bytes */ ASSERT_EQ(result->size(), 4u); } static void test_encode_id_decimal_zero() { auto result = wire::encode_id_decimal(0); ASSERT_TRUE(result.has_value()); /* [SOW]@[SOR]D[SOB]0[EOW] = 7 bytes */ ASSERT_EQ(result->size(), 7u); ASSERT_EQ((*result)[5], '0'); } static void test_encode_id_decimal_max() { auto result = wire::encode_id_decimal(UINT32_MAX); ASSERT_TRUE(result.has_value()); /* Body should be "4294967295" (10 digits) */ ASSERT_EQ(result->size(), 16u); /* SOW @ SOR D SOB + 10 digits + EOW */ } /* ================================================================ * 15. Base-32 encoding edge cases * ================================================================ */ static void test_base32_single_byte() { uint8_t data = 0xFF; auto result = id::base32_encode(&data, 1); ASSERT_TRUE(result.has_value()); /* 1 byte = 8 bits -> ceil(8/5) = 2 chars */ ASSERT_EQ(result->size(), 2u); for (char c : *result) { ASSERT_TRUE(std::strchr(id::BASE32_ALPHABET, c) != nullptr); } } static void test_base32_deterministic() { const uint8_t data[] = {0xCA, 0xFE}; auto a = id::base32_encode(data, 2); auto b = id::base32_encode(data, 2); ASSERT_TRUE(a.has_value() && b.has_value()); ASSERT_TRUE(*a == *b); } static void test_base32_zero_length() { uint8_t data = 0; auto result = id::base32_encode(&data, 0); ASSERT_TRUE(!result.has_value()); } /* ================================================================ * 16. Parser large BLOB tail (dynamic buffer) * ================================================================ */ static void test_parser_large_tail() { Parser p; size_t captured_tail_len = 0; std::vector captured_tail; int msg_count = 0; p.on_word([](wire::WordType, wire::Radix, wire::Unit, const uint8_t*, size_t) {}); p.on_tail([&](const uint8_t* data, size_t len) { captured_tail.assign(data, data + len); captured_tail_len = len; }); p.on_message([&](size_t) { msg_count++; }); /* 100 KB tail — well beyond old 4KB limit */ constexpr size_t TAIL_SIZE = 100000; p.set_tail_length(TAIL_SIZE); auto f = bus::broadcast("hello"); ASSERT_TRUE(f.has_value()); p.feed(f->data(), f->size()); /* Feed tail data (sequential pattern for verification) */ std::vector tail(TAIL_SIZE); for (size_t i = 0; i < TAIL_SIZE; i++) tail[i] = static_cast(i & 0xFF); p.feed(tail.data(), tail.size()); ASSERT_EQ(msg_count, 1); ASSERT_EQ(captured_tail_len, TAIL_SIZE); /* Verify first and last bytes survived intact */ ASSERT_EQ(captured_tail[0], 0); ASSERT_EQ(captured_tail[TAIL_SIZE - 1], static_cast((TAIL_SIZE - 1) & 0xFF)); } /* ================================================================ * 17. Context BLOB tail via word-declared size * ================================================================ */ /* Helper: build a K-verb frame with BLOB word (no tail appended yet) */ static std::vector build_blob_kverb( std::string_view sid, uint32_t mid, std::string_view body, size_t tail_size) { using namespace wire; std::vector frame; frame.push_back(SOM); /* K symbol */ auto sym = word_encode(WordType::Symbol, Radix::None, Unit::None, reinterpret_cast("K"), 1); frame.insert(frame.end(), sym->begin(), sym->end()); /* SID (base32 id) */ auto id = word_encode(WordType::Id, Radix::Base32, Unit::None, reinterpret_cast(sid.data()), sid.size()); frame.insert(frame.end(), id->begin(), id->end()); /* MID (decimal id) */ std::string mid_str = std::to_string(mid); auto midw = word_encode(WordType::Id, Radix::Decimal, Unit::None, reinterpret_cast(mid_str.c_str()), mid_str.size()); frame.insert(frame.end(), midw->begin(), midw->end()); /* Body text */ auto txt = word_encode(WordType::Text, Radix::None, Unit::None, reinterpret_cast(body.data()), body.size()); frame.insert(frame.end(), txt->begin(), txt->end()); /* BLOB word: declares tail_size bytes */ std::string size_str = std::to_string(tail_size); auto blob = word_encode(WordType::Blob, Radix::Decimal, Unit::Byte, reinterpret_cast(size_str.c_str()), size_str.size()); frame.insert(frame.end(), blob->begin(), blob->end()); frame.push_back(EOM); return frame; } /* ANTHEOS-BLOB-RADIX-COVERAGE-GAP — the same frame with the size written in a * CHOSEN radix. The builder above hardcodes Decimal, which is why four of the * five radices §6.3 permits were never carried through this path. */ static std::string size_in_radix(size_t v, int base) { if (v == 0) return "0"; const char* digits = "0123456789abcdefghijklmnopqrstuv"; std::string out; while (v) { out.insert(out.begin(), digits[v % static_cast(base)]); v /= static_cast(base); } return out; } static std::vector build_blob_kverb_radix( std::string_view sid, uint32_t mid, std::string_view body, size_t tail_size, wire::Radix radix, int base) { auto frame = build_blob_kverb(sid, mid, body, tail_size); /* Rebuild only the BLOB word: drop the trailing EOM, strip the decimal * BLOB word, append the radix one, restore EOM. */ using namespace wire; std::string dec = std::to_string(tail_size); auto decimal_word = word_encode(WordType::Blob, Radix::Decimal, Unit::Byte, reinterpret_cast(dec.c_str()), dec.size()); frame.resize(frame.size() - 1 - decimal_word->size()); /* EOM + BLOB word */ std::string s = size_in_radix(tail_size, base); auto blob = word_encode(WordType::Blob, radix, Unit::Byte, reinterpret_cast(s.c_str()), s.size()); frame.insert(frame.end(), blob->begin(), blob->end()); frame.push_back(EOM); return frame; } /* Every radix §6.3 permits must carry a BLOB size through encode → parse → * tail. Base32 (duotrigesimal) is the one that could not: decode_blob_size * answered 0 for it, so the tail was never consumed and its payload was fed to * the state machine as protocol — the frame completed, the callback fired with * no tail, and the parser resynchronised on whatever byte looked like SOM. */ static void test_context_blob_tail_every_radix() { struct { wire::Radix r; int base; const char* name; } cases[] = { {wire::Radix::Binary, 2, "binary"}, {wire::Radix::Octal, 8, "octal"}, {wire::Radix::Decimal, 10, "decimal"}, {wire::Radix::Hex, 16, "hex"}, {wire::Radix::Base32, 32, "duotrigesimal"}, }; constexpr size_t TAIL_SIZE = 300; for (auto& c : cases) { Context ctx("TOID", "TDID", "TIID", "AB"); std::string rx_body; ctx.on_message([&](std::string_view, std::string_view, std::string_view, uint32_t, std::string_view b) { rx_body = std::string(b); }); auto frame = build_blob_kverb_radix("ABCD", 1, "DESCRIBE a photo", TAIL_SIZE, c.r, c.base); for (size_t i = 0; i < TAIL_SIZE; i++) frame.push_back(static_cast(i & 0xFF)); ctx.feed(frame.data(), frame.size()); ASSERT_TRUE(rx_body == "DESCRIBE a photo"); auto [tp, tl] = ctx.last_tail(); (void)tp; ASSERT_EQ(tl, TAIL_SIZE); /* the tail was consumed */ ASSERT_EQ(ctx.blob_size_errors(), 0u); /* and the size was readable */ } } /* A size the decoder cannot read is COUNTED and the tail length is left alone, * rather than silently becoming zero. */ static void test_context_blob_size_undecodable_is_reported() { using namespace wire; Context ctx("TOID", "TDID", "TIID", "AB"); auto frame = build_blob_kverb("ABCD", 1, "X", 10); /* Replace the decimal size body with trailing junk: stoull would stop at * the 'z' and report 10, shifting every following byte. */ std::string dec = std::to_string(10); auto decimal_word = word_encode(WordType::Blob, Radix::Decimal, Unit::Byte, reinterpret_cast(dec.c_str()), dec.size()); frame.resize(frame.size() - 1 - decimal_word->size()); const char* junk = "10z"; auto bad = word_encode(WordType::Blob, Radix::Decimal, Unit::Byte, reinterpret_cast(junk), 3); frame.insert(frame.end(), bad->begin(), bad->end()); frame.push_back(EOM); ctx.feed(frame.data(), frame.size()); ASSERT_EQ(ctx.blob_size_errors(), 1u); } /* ANTHEOS-CONTEXT-FEED-SWALLOWS-PARSE-ERRORS — a malformed feed is observable * through the public Context API. It was not: feed discarded the ParseState and * returned len unconditionally, and Context exposed none of the parser's error * surface while owning the parser privately. */ static void test_context_reports_parse_errors() { using namespace wire; Context ctx("TOID", "TDID", "TIID", "AB"); ASSERT_EQ(ctx.parse_errors(), 0u); /* A frame abandoned mid-head: SOM, a word start, then a second SOM. */ const uint8_t bad[] = {SOM, SOW, static_cast(wire::WordType::Text), SOB, 'A', SOM}; ctx.feed(bad, sizeof bad); ASSERT_TRUE(ctx.parse_errors() >= 1u); } static void test_context_blob_tail() { Context ctx("TOID", "TDID", "TIID", "AB"); std::string rx_body; auto [tail_ptr, tail_len] = ctx.last_tail(); ASSERT_EQ(tail_len, 0u); /* initially empty */ ctx.on_message([&](std::string_view, std::string_view, std::string_view, uint32_t, std::string_view body) { rx_body = std::string(body); }); /* Build K-verb with 500 bytes of BLOB tail */ constexpr size_t TAIL_SIZE = 500; auto frame = build_blob_kverb("ABCD", 1, "DESCRIBE a photo", TAIL_SIZE); /* Append tail data */ for (size_t i = 0; i < TAIL_SIZE; i++) frame.push_back(static_cast(i & 0xFF)); ctx.feed(frame.data(), frame.size()); /* Message body should be received normally */ ASSERT_TRUE(rx_body == "DESCRIBE a photo"); /* BLOB tail should be captured */ auto [tptr, tlen] = ctx.last_tail(); ASSERT_EQ(tlen, TAIL_SIZE); ASSERT_EQ(tptr[0], 0); ASSERT_EQ(tptr[TAIL_SIZE - 1], static_cast((TAIL_SIZE - 1) & 0xFF)); } static void test_context_blob_tail_large() { Context ctx("TOID", "TDID", "TIID", "CD"); std::string rx_body; ctx.on_message([&](std::string_view, std::string_view, std::string_view, uint32_t, std::string_view body) { rx_body = std::string(body); }); /* 80 KB tail — typical JPEG from OV5640 */ constexpr size_t TAIL_SIZE = 81920; auto frame = build_blob_kverb("EFGH", 3, "DESCRIBE what you see", TAIL_SIZE); for (size_t i = 0; i < TAIL_SIZE; i++) frame.push_back(static_cast(i & 0xFF)); ctx.feed(frame.data(), frame.size()); ASSERT_TRUE(rx_body == "DESCRIBE what you see"); auto [tptr, tlen] = ctx.last_tail(); ASSERT_EQ(tlen, TAIL_SIZE); /* Spot-check mid-tail and end */ ASSERT_EQ(tptr[1000], static_cast(1000 & 0xFF)); ASSERT_EQ(tptr[TAIL_SIZE - 1], static_cast((TAIL_SIZE - 1) & 0xFF)); } /* ANTHEOS-SCRATCH-IDS-SILENT-DROP — a message carrying more IDs than the * scratch holds is distinguishable from one that fit. The surplus branch did * nothing at all — no error, no counter, no state change — so a five-ID message * was dispatched from the first four as though that were all of them. Nothing * this library BUILDS trips it (verify_response is the widest at three), and a * peer is not bound by what this library builds. */ static void test_context_surplus_ids_are_counted() { using namespace wire; Context ctx("TOID", "TDID", "TIID", "AB"); ASSERT_EQ(ctx.dropped_ids(), 0u); std::vector frame; frame.push_back(SOM); auto sym = word_encode(WordType::Symbol, Radix::None, Unit::None, reinterpret_cast("K"), 1); frame.insert(frame.end(), sym->begin(), sym->end()); /* Six base32 IDs — two past MAX_SCRATCH_IDS. */ for (int i = 0; i < 6; i++) { std::string id = "IDAA" + std::to_string(i); auto w = word_encode(WordType::Id, Radix::Base32, Unit::None, reinterpret_cast(id.data()), id.size()); frame.insert(frame.end(), w->begin(), w->end()); } frame.push_back(EOM); ctx.feed(frame.data(), frame.size()); ASSERT_EQ(ctx.dropped_ids(), 2u); /* the two that did not fit */ } static void test_context_blob_tail_cleared_on_next() { Context ctx("TOID", "TDID", "TIID", "EF"); int msg_count = 0; ctx.on_message([&](std::string_view, std::string_view, std::string_view, uint32_t, std::string_view) { msg_count++; }); /* First message with BLOB tail */ auto frame1 = build_blob_kverb("WXYZ", 1, "WITH BLOB", 10); for (int i = 0; i < 10; i++) frame1.push_back(static_cast(0xAA)); ctx.feed(frame1.data(), frame1.size()); auto [t1ptr, t1len] = ctx.last_tail(); ASSERT_EQ(t1len, 10u); /* Second message WITHOUT BLOB tail — last_tail should be cleared */ auto f2 = session::call("WXYZ", {}, 3, "NO BLOB"); ASSERT_TRUE(f2.has_value()); ctx.feed(f2->data(), f2->size()); auto [t2ptr, t2len] = ctx.last_tail(); ASSERT_EQ(t2len, 0u); /* cleared by the new message */ ASSERT_EQ(msg_count, 2); } /* ================================================================ * Suite entry point * ================================================================ */ void test_depth_run(int& out_run, int& out_passed) { std::printf("\n[depth -- coverage gap tests]\n"); /* 1. Frame class */ TEST(test_context_surplus_ids_are_counted); TEST(test_frame_default_empty); TEST(test_frame_from_ptr); TEST(test_frame_from_vector); TEST(test_frame_copy); TEST(test_frame_move); TEST(test_frame_iterators); TEST(test_frame_bytes_mutable); /* 2. Parser tail handling */ TEST(test_parser_tail_callback); TEST(test_parser_tail_zero_length); /* 3. Parser statistics */ TEST(test_parser_stats_accumulate); TEST(test_parser_error_count); TEST(test_parser_stats_survive_reset); /* 4. Parser move semantics */ TEST(test_parser_move_construct); TEST(test_parser_move_assign); /* 5. SidPool move semantics */ TEST(test_sidpool_move_construct); TEST(test_sidpool_move_assign); /* 6. SidPool edge cases */ TEST(test_sidpool_acquire_unique_null_check); TEST(test_sidpool_different_identity_different_sids); TEST(test_sidpool_invalid_args); TEST(test_sidpool_acquire_unique_grows_on_collision); /* 7. Context session_accept */ TEST(test_context_session_accept_basic); TEST(test_context_session_accept_empty_sid); TEST(test_context_session_accept_call); /* 8. Context session exhaustion */ TEST(test_context_session_exhaustion); /* 9. Context feed edge cases */ TEST(test_context_feed_null); TEST(test_context_feed_empty); /* 10. Context inbound dispatch */ TEST(test_dispatch_establish); TEST(test_dispatch_conflict); TEST(test_dispatch_ping); TEST(test_dispatch_discover); TEST(test_dispatch_discover_response); TEST(test_dispatch_verify); TEST(test_dispatch_acknowledge); TEST(test_dispatch_exception); TEST(test_dispatch_query); TEST(test_dispatch_offer); TEST(test_dispatch_accept); TEST(test_dispatch_session_notify); TEST(test_dispatch_session_status); TEST(test_dispatch_session_locate); TEST(test_dispatch_session_resume); TEST(test_dispatch_session_finish); /* 10b. Auth (Z-verb) dispatch */ TEST(test_dispatch_auth_challenge); TEST(test_dispatch_auth_response); /* 11. word_decode malformed */ TEST(test_decode_null_input); TEST(test_decode_too_short); TEST(test_decode_missing_sow); TEST(test_decode_invalid_word_type); TEST(test_decode_invalid_radix); TEST(test_decode_invalid_unit); TEST(test_decode_missing_sob); TEST(test_decode_reserved_in_body); TEST(test_decode_truncated_radix); TEST(test_decode_truncated_unit); TEST(test_decode_no_eow); /* 12. DecodedWord operator bool */ TEST(test_decoded_word_bool); /* 13. Context session accessors */ TEST(test_context_session_accessors_invalid); TEST(test_context_session_close_idle); TEST(test_context_session_notify_invalid); TEST(test_context_session_status_invalid); /* 14. word_encode edge cases */ TEST(test_encode_null_body_zero_len); TEST(test_encode_id_decimal_zero); TEST(test_encode_id_decimal_max); /* 15. Base-32 encoding edge cases */ TEST(test_base32_single_byte); TEST(test_base32_deterministic); TEST(test_base32_zero_length); /* 16. Parser large BLOB tail */ TEST(test_parser_large_tail); /* 17. Context BLOB tail */ TEST(test_context_blob_tail_every_radix); TEST(test_context_blob_size_undecodable_is_reported); TEST(test_context_reports_parse_errors); TEST(test_context_blob_tail); TEST(test_context_blob_tail_large); TEST(test_context_blob_tail_cleared_on_next); out_run = tests_run; out_passed = tests_passed; }