mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-18 12:44:28 +00:00
466 lines
21 KiB
C++
466 lines
21 KiB
C++
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <helpers/CLICommandUtils.h>
|
|
#include <helpers/DeferredCliCommand.h>
|
|
#include <helpers/RemoteCliReplyCache.h>
|
|
#include <helpers/RemoteCliRequest.h>
|
|
#include <helpers/ReplayResetCommand.h>
|
|
#include <helpers/TempRadioReplyBarrier.h>
|
|
#include <helpers/HostCliBridge.h>
|
|
#include <array>
|
|
|
|
#undef MESH_DEBUG_PRINTLN
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
#ifndef MESH_ENABLE_HOST_CLI
|
|
#define MESH_ENABLE_HOST_CLI 0
|
|
#endif
|
|
#define PAYLOAD_TYPE_REQ 0
|
|
#define PAYLOAD_TYPE_RESPONSE 1
|
|
#define PAYLOAD_TYPE_TXT_MSG 2
|
|
#define TXT_TYPE_PLAIN 0
|
|
#define TXT_TYPE_CLI_DATA 1
|
|
#define TXT_TYPE_CLI_COMMAND 2
|
|
#define SERVER_RESPONSE_DELAY 1
|
|
#define TXT_ACK_DELAY 1
|
|
|
|
namespace mesh {
|
|
struct Packet {
|
|
uint8_t path[MAX_PATH_SIZE] = {};
|
|
uint8_t path_len = 0, radio_profile = 0;
|
|
uint32_t radio_generation = 1;
|
|
bool isRouteFlood() const { return false; }
|
|
uint8_t getPathHashSize() const { return 1; }
|
|
};
|
|
struct Utils {
|
|
template <typename... Args>
|
|
static void sha256(uint8_t* dest, size_t size, Args...) { memset(dest, 0, size); }
|
|
static void toHex(char* dest, const uint8_t* bytes, size_t size) {
|
|
for (size_t i = 0; i < size; ++i) snprintf(dest + i * 2, 3, "%02x", bytes[i]);
|
|
}
|
|
};
|
|
struct Console {
|
|
std::vector<std::string> records;
|
|
void printf(const char*, const char* record) { records.emplace_back(record); }
|
|
};
|
|
static Console console;
|
|
inline Console& usbConsolePort() { return console; }
|
|
}
|
|
struct TransportKey { uint8_t key[32] = {}; };
|
|
struct Region { bool isWildcard() const { return true; } };
|
|
struct ClientInfo {
|
|
struct { uint8_t pub_key[PUB_KEY_SIZE] = {}; } id;
|
|
uint32_t last_timestamp = 0, last_activity = 0;
|
|
uint8_t out_path_len = 1, alt_path_len = 0xff;
|
|
uint8_t out_path[MAX_PATH_SIZE] = {}, alt_path[MAX_PATH_SIZE] = {};
|
|
bool admin = true;
|
|
bool region_mgr = false;
|
|
bool isAdmin() const { return admin; }
|
|
bool isRegionMgr() const { return region_mgr; }
|
|
bool isFilterMgr() const { return false; }
|
|
};
|
|
struct Clock { uint32_t getCurrentTime() const { return 1700000000; } };
|
|
struct ReceiveProfileScope {
|
|
template <typename Mesh> ReceiveProfileScope(Mesh&, uint8_t, uint32_t) {}
|
|
};
|
|
static uint32_t now_millis = 100;
|
|
static uint32_t millis() { return now_millis; }
|
|
|
|
class MyMesh {
|
|
public:
|
|
struct {
|
|
ClientInfo clients[3];
|
|
int num_clients = 3;
|
|
int getNumClients() const { return num_clients; }
|
|
ClientInfo* getClientByIdx(int i) { return &clients[i]; }
|
|
const ClientInfo* getClientByIdx(int i) const { return &clients[i]; }
|
|
void remove(int index) {
|
|
ClientInfo* c = &clients[index];
|
|
#include "acl_delete.inc"
|
|
}
|
|
} acl;
|
|
struct {
|
|
int getTransportKeysFor(const Region&, TransportKey*, int) { return 0; }
|
|
} region_map;
|
|
Region* recv_pkt_region = nullptr;
|
|
mesh::DeferredCliCommand deferred_cli_command;
|
|
mesh::RemoteCliReplyCache remote_cli_reply_cache;
|
|
mesh::TempRadioReplyBarrier temp_radio_reply_barrier;
|
|
struct {
|
|
struct Profiles {
|
|
bool pending = false;
|
|
uint32_t generation = 0;
|
|
uint32_t replyMutationGeneration() const { return generation; }
|
|
bool hasReplyMutation() const { return pending; }
|
|
void beginReplyCommand() {}
|
|
void endReplyCommand() {}
|
|
} profiles;
|
|
Profiles& radioProfiles() { return profiles; }
|
|
} _cli;
|
|
uint32_t primary_radio_mutation_generation = 0, radio_reply_deadline = 0;
|
|
bool radio_reply_secondary = false;
|
|
bool deferred_cli_reply_scoped = false;
|
|
TransportKey deferred_cli_reply_scope;
|
|
int matching_peer_indexes[3] = {0, 1, 2};
|
|
uint8_t reply_data[MAX_PACKET_PAYLOAD + 1] = {};
|
|
Clock clock;
|
|
int executions = 0, acknowledgements = 0;
|
|
bool fail_command = false;
|
|
bool host_cli_waiting = false;
|
|
bool host_cli_claimed = false, host_cli_claim_emit = false;
|
|
uint32_t host_cli_deadline = 0, host_cli_claim_emit_at = 0;
|
|
uint64_t host_cli_nonce = 0, host_cli_claim_challenge = 0;
|
|
struct {
|
|
void sign(uint8_t* signature, const uint8_t*, size_t) {
|
|
memset(signature, 0x5a, SIGNATURE_SIZE);
|
|
}
|
|
} self_id;
|
|
struct RNG {
|
|
void random(uint8_t* bytes, size_t count) { memset(bytes, 0x42, count); }
|
|
} rng;
|
|
int delete_during_command = -1;
|
|
// The hardware-side parser callback is replaced by this accepted-operation
|
|
// seam. Receive normalization, fingerprinting, dispatch, cache admission and
|
|
// exact reply-barrier ownership below remain production code.
|
|
enum class Mutation { None, Primary, Secondary } accepted_mutation = Mutation::None;
|
|
bool queue_accepts = true;
|
|
int cancellations = 0;
|
|
mesh::Packet queued_ack;
|
|
std::vector<std::string> replies;
|
|
std::vector<std::array<uint8_t, PUB_KEY_SIZE>> recipients;
|
|
std::vector<std::array<uint8_t, PUB_KEY_SIZE>> secrets;
|
|
std::vector<ClientInfo> reply_destinations;
|
|
|
|
MyMesh() { now_millis = 100;mesh::console.records.clear();
|
|
acl.clients[0].id.pub_key[0] = 0x12; acl.clients[1].id.pub_key[0] = 0x77;
|
|
acl.clients[2].id.pub_key[0] = 0x88; }
|
|
Clock* getRTCClock() { return &clock; }
|
|
RNG* getRNG() { return &rng; }
|
|
uint32_t futureMillis(uint32_t delay) const { return millis() + delay; }
|
|
bool millisHasNowPassed(uint32_t deadline) const { return int32_t(millis() - deadline) >= 0; }
|
|
int handleRequest(ClientInfo*, uint32_t, uint8_t*, size_t) { return 0; }
|
|
template <typename... Args> mesh::Packet* createPathReturn(Args...) { return nullptr; }
|
|
template <typename... Args> mesh::Packet* createDatagram(Args...) { return nullptr; }
|
|
template <typename... Args> void sendFloodReply(Args...) {}
|
|
mesh::Packet* createAck(uint32_t) { return nullptr; }
|
|
template <typename... Args> void sendClientReply(Args...) { ++acknowledgements; }
|
|
bool sendRemoteCliReply(ClientInfo* client, const uint8_t* secret, uint8_t, uint32_t,
|
|
const char* reply, const TransportKey*, mesh::Packet** queued = nullptr) {
|
|
if (!queue_accepts) return false;
|
|
if (queued) *queued = &queued_ack;
|
|
std::array<uint8_t, PUB_KEY_SIZE> key{}, shared{};
|
|
memcpy(key.data(), client->id.pub_key, key.size());
|
|
memcpy(shared.data(), secret, shared.size());
|
|
recipients.push_back(key);secrets.push_back(shared);
|
|
reply_destinations.push_back(*client);
|
|
replies.emplace_back(reply); return true;
|
|
}
|
|
void scheduleNormalRadio() {}
|
|
void finishRadioReply(bool delivered) {
|
|
if (!delivered) ++cancellations;
|
|
_cli.radioProfiles().pending = false;
|
|
temp_radio_reply_barrier.clear();
|
|
radio_reply_deadline = 0;
|
|
radio_reply_secondary = false;
|
|
}
|
|
void clearDeferredCliCommand();
|
|
bool completeHostCliRequest(const char*);
|
|
bool handleHostCliSerialReply(const char*, char*);
|
|
void handleCommand(uint32_t, ClientInfo* sender, char* command, char* reply, int, uint8_t) {
|
|
++executions;
|
|
#include "normalization.inc"
|
|
if (!fail_command) {
|
|
// Accepted route changes mirror handleClientPathCommand's live ACL edits.
|
|
if (strcmp(command, "set outpath flood") == 0) sender->out_path_len = 0xfe;
|
|
else if (strcmp(command, "set outpath direct") == 0) sender->out_path_len = 0;
|
|
else if (strcmp(command, "set outpath ab") == 0) {
|
|
sender->out_path_len = 1;
|
|
sender->out_path[0] = 0xab;
|
|
} else if (strcmp(command, "set altpath cd") == 0) {
|
|
sender->alt_path_len = 1;
|
|
sender->alt_path[0] = 0xcd;
|
|
}
|
|
}
|
|
if (delete_during_command >= 0) acl.remove(delete_during_command);
|
|
if (!fail_command) {
|
|
if (accepted_mutation == Mutation::Primary) ++primary_radio_mutation_generation;
|
|
else if (accepted_mutation == Mutation::Secondary) {
|
|
++_cli.radioProfiles().generation;
|
|
_cli.radioProfiles().pending = true;
|
|
}
|
|
}
|
|
strcpy(reply, fail_command ? "Err - simulated failure" : "OK");
|
|
}
|
|
void onPeerDataRecv(mesh::Packet*, uint8_t, int, const uint8_t*, uint8_t*, size_t);
|
|
void processDeferredCliCommand();
|
|
|
|
void receive(const char* text, uint32_t timestamp, uint32_t logical_id = 99,
|
|
int sender = 0, bool process = true) {
|
|
uint8_t wire[MAX_PACKET_PAYLOAD + 1] = {};
|
|
uint8_t secret[PUB_KEY_SIZE] = {};
|
|
memcpy(secret, acl.clients[sender].id.pub_key, sizeof(secret));
|
|
memcpy(wire, ×tamp, sizeof(timestamp));
|
|
wire[4] = TXT_TYPE_CLI_COMMAND << 2;
|
|
memcpy(wire + 5, text, strlen(text));
|
|
const size_t size = mesh::RemoteCliRequest::append(wire, sizeof(wire) - 1,
|
|
5, strlen(text), logical_id);
|
|
assert(size != 0);
|
|
mesh::Packet packet;
|
|
onPeerDataRecv(&packet, PAYLOAD_TYPE_TXT_MSG, sender, secret, wire, size);
|
|
if (process) processDeferredCliCommand();
|
|
}
|
|
};
|
|
|
|
#include "production.inc"
|
|
|
|
#if MESH_ENABLE_HOST_CLI
|
|
static void changeHostRequester(MyMesh& value, int change) {
|
|
if (change == 0) value.acl.remove(0);
|
|
if (change == 1) value.acl.remove(1);
|
|
if (change == 2) value.acl.clients[1].id.pub_key[31] = 1;
|
|
if (change == 3) {
|
|
value.acl.clients[1].admin = false;
|
|
value.acl.clients[1].region_mgr = true;
|
|
}
|
|
}
|
|
static void hostReply(MyMesh& value, const char* text, char* reply) {
|
|
char command[160];
|
|
snprintf(command, sizeof(command), "host.reply %08X %016llX %s",
|
|
unsigned(value.deferred_cli_command.request_id),
|
|
static_cast<unsigned long long>(value.host_cli_nonce), text);
|
|
assert(value.handleHostCliSerialReply(command, reply));
|
|
}
|
|
static void hostIdentityLifecycle() {
|
|
for (int change : {0, 1, 2, 3}) {
|
|
// Authorization must still be live when an admitted command reaches USB.
|
|
MyMesh value;value.receive("ab|host test", 101, 99, 1, false);
|
|
changeHostRequester(value, change);value.processDeferredCliCommand();
|
|
assert(value.executions == 0);
|
|
assert(mesh::console.records.size() == (change == 0 ? 1U : 0U));
|
|
assert(value.host_cli_waiting == (change == 0));
|
|
if (change == 3) assert(value.replies[0] == "ab|Err - not permitted");
|
|
}
|
|
for (bool change_after_claim_request : {false, true}) for (int change : {0, 1, 2, 3}) {
|
|
MyMesh value;value.receive("ab|host test", 101, 99, 1);
|
|
assert(value.executions == 0 && value.host_cli_waiting);
|
|
assert(mesh::console.records.size() == 1 && value.host_cli_nonce != 0);
|
|
if (!change_after_claim_request) changeHostRequester(value, change);
|
|
char reply[160] = {};
|
|
hostReply(value, "@claim=0000000000000123", reply);
|
|
assert(!strcmp(reply, "OK - host claim accepted"));
|
|
if (change_after_claim_request) changeHostRequester(value, change);
|
|
now_millis += mesh::HostCliBridge::SERVICE_CLAIM_EMIT_DELAY_MILLIS;
|
|
value.processDeferredCliCommand();
|
|
// Provisional serial acceptance is not permission to execute: no signed
|
|
// CLAIMED record may escape after removal or admin-to-manager downgrade.
|
|
assert(mesh::console.records.size() == (change == 0 ? 2U : 1U));
|
|
assert(value.host_cli_claimed == (change == 0));
|
|
if (change != 0) {
|
|
assert(!value.deferred_cli_command.pending && !value.host_cli_waiting);
|
|
assert(!value.host_cli_claim_emit && value.host_cli_nonce == 0);
|
|
assert(value.replies.empty());
|
|
} else {
|
|
assert(mesh::console.records.back().find("HOSTCLI/1 CLAIMED") != std::string::npos);
|
|
hostReply(value, "OK - service reply", reply);
|
|
assert(!strcmp(reply, "OK - host reply accepted"));
|
|
assert(value.replies.size() == 1 && value.replies[0] == "ab|OK - service reply");
|
|
assert(value.recipients[0][0] == 0x77 && value.secrets[0][0] == 0x77);
|
|
assert(!value.deferred_cli_command.pending && !value.host_cli_waiting);
|
|
value.receive("ab|host test", 102, 99, 0);
|
|
assert(mesh::console.records.size() == 2 && value.replies.size() == 2);
|
|
}
|
|
}
|
|
for (int change : {0, 1, 2, 3}) {
|
|
MyMesh value;value.receive("host test", 101, 99, 1);
|
|
char reply[160] = {};
|
|
hostReply(value, "@claim=0000000000000123", reply);
|
|
now_millis += mesh::HostCliBridge::SERVICE_CLAIM_EMIT_DELAY_MILLIS;
|
|
value.processDeferredCliCommand();assert(value.host_cli_claimed);
|
|
changeHostRequester(value, change);
|
|
hostReply(value, "OK - service reply", reply);
|
|
assert(!strcmp(reply, change == 0 ? "OK - host reply accepted" : "Err - host requester is unavailable"));
|
|
assert(value.replies.size() == (change == 0 ? 1U : 0U));
|
|
if (change == 0) assert(value.recipients[0][0] == 0x77 && value.secrets[0][0] == 0x77);
|
|
assert(!value.deferred_cli_command.pending && !value.host_cli_waiting);
|
|
assert(value.host_cli_nonce == 0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int main() {
|
|
#if MESH_ENABLE_HOST_CLI
|
|
hostIdentityLifecycle();
|
|
#endif
|
|
// Dispatch must follow full identity across ACL movement before execution,
|
|
// and snapshot the reply destination before a handler compacts the ACL.
|
|
for (const int deleted : {0, 1}) {
|
|
MyMesh value;
|
|
value.delete_during_command = deleted;
|
|
value.acl.clients[1].out_path[0] = 0x31;
|
|
value.acl.clients[1].alt_path_len = 1;
|
|
value.acl.clients[1].alt_path[0] = 0x32;
|
|
// A reused slot with the same prefix must not supply the final reply route.
|
|
value.acl.clients[2].id.pub_key[0] = 0x77;
|
|
value.acl.clients[2].id.pub_key[PUB_KEY_SIZE - 1] = 1;
|
|
value.acl.clients[2].out_path[0] = 0x99;
|
|
const char* command = "setperm 12 0";
|
|
value.receive(command, 101, 99, 1);
|
|
assert(value.executions == 1 && value.replies.size() == 1);
|
|
assert(value.recipients[0][0] == 0x77 && value.secrets[0][0] == 0x77);
|
|
assert(value.recipients[0][PUB_KEY_SIZE - 1] == 0);
|
|
assert(value.reply_destinations[0].out_path[0] == 0x31);
|
|
assert(value.reply_destinations[0].alt_path_len == 1);
|
|
assert(value.reply_destinations[0].alt_path[0] == 0x32);
|
|
const auto fp = mesh::RemoteCliReplyCache::fingerprint(command, strlen(command));
|
|
assert(value.remote_cli_reply_cache.matches(value.recipients[0].data(), 99, fp));
|
|
assert(!value.remote_cli_reply_cache.matches(value.acl.clients[1].id.pub_key, 99, fp));
|
|
if (deleted == 0) {
|
|
value.receive(command, 102, 99, 0);
|
|
assert(value.executions == 1 && value.recipients.back()[0] == 0x77);
|
|
}
|
|
}
|
|
// Successful path updates affect the first acknowledgement as well as any
|
|
// cached retry, including when the original sender moves to another slot.
|
|
for (const char* command : {"set outpath flood", "set outpath direct",
|
|
"set outpath ab", "set altpath cd"}) {
|
|
for (const int deleted : {-1, 0}) {
|
|
MyMesh value;
|
|
value.delete_during_command = deleted;
|
|
value.acl.clients[1].out_path[0] = 0x31;
|
|
value.receive(command, 101, 99, 1);
|
|
const int index = deleted == 0 ? 0 : 1;
|
|
assert(value.executions == 1 && value.reply_destinations.size() == 1);
|
|
value.receive(command, 102, 99, index);
|
|
assert(value.executions == 1 && value.reply_destinations.size() == 2);
|
|
const ClientInfo& live = value.acl.clients[index];
|
|
for (const ClientInfo& sent : value.reply_destinations) {
|
|
assert(memcmp(sent.id.pub_key, live.id.pub_key, PUB_KEY_SIZE) == 0);
|
|
assert(sent.out_path_len == live.out_path_len);
|
|
assert(memcmp(sent.out_path, live.out_path, MAX_PATH_SIZE) == 0);
|
|
assert(sent.alt_path_len == live.alt_path_len);
|
|
assert(memcmp(sent.alt_path, live.alt_path, MAX_PATH_SIZE) == 0);
|
|
}
|
|
}
|
|
}
|
|
for (const bool host_completion : {false, true}) {
|
|
for (const int change : {0, 1, 2, 3}) {
|
|
MyMesh value;
|
|
value.receive("get name", 101, 99, 1, false);
|
|
if (change == 0) value.acl.remove(0); // same key, different index
|
|
if (change == 1) value.acl.remove(1); // sender deleted, slot reused
|
|
if (change == 2) value.acl.clients[1].id.pub_key[31] = 1; // prefix collision
|
|
if (change == 3) value.acl.clients[1].admin = false; // role revoked
|
|
if (host_completion) {
|
|
value.host_cli_waiting = true;
|
|
assert(value.completeHostCliRequest("OK - service reply") == (change == 0));
|
|
assert(value.executions == 0);
|
|
} else {
|
|
value.processDeferredCliCommand();
|
|
assert(value.executions == (change == 0 ? 1 : 0));
|
|
}
|
|
assert(value.replies.size() == (change == 0 ? 1U : 0U));
|
|
if (change == 0) {
|
|
assert(value.recipients[0][0] == 0x77 && value.secrets[0][0] == 0x77);
|
|
}
|
|
assert(!value.deferred_cli_command.pending);
|
|
}
|
|
}
|
|
const char* commands[] = {
|
|
"advert", "ADVERT", "advert\r\n", " ab|ADVERT \t\r\n",
|
|
"setperm 1212121212121212121212121212121212121212121212121212121212121212 3",
|
|
"Ab|Setperm 1212121212121212121212121212121212121212121212121212121212121212 3\n",
|
|
};
|
|
for (const auto command : commands) {
|
|
for (const bool fail : {false, true}) {
|
|
MyMesh value;
|
|
value.fail_command = fail;
|
|
value.receive(command, 101);
|
|
assert(value.executions == 1 && value.replies.size() == 1);
|
|
const auto expected = value.replies.front();
|
|
value.receive(command, 101); // exact same-timestamp retry
|
|
value.receive(command, 102); // fresh timestamp, same logical request
|
|
value.receive(command, 100); // delayed retry may replay result only
|
|
assert(value.executions == 1 && value.replies.size() == 4);
|
|
for (const auto& reply : value.replies) assert(reply == expected);
|
|
assert(value.acl.clients[0].last_timestamp == 102);
|
|
value.receive("reboot", 100); // uncached stale text remains blocked
|
|
value.receive("reboot", 102); // equal timestamp cannot run new text
|
|
assert(value.executions == 1 && value.replies.size() == 4);
|
|
value.receive(command, 103, 100); // genuinely new request
|
|
assert(value.executions == 2 && value.replies.size() == 5);
|
|
}
|
|
}
|
|
{
|
|
MyMesh value;
|
|
value.receive("ADVERT\n", 101, 99, 0, false);
|
|
value.receive("ADVERT\n", 102, 99, 0, false);
|
|
assert(value.executions == 0 && value.replies.empty());
|
|
value.processDeferredCliCommand();
|
|
assert(value.executions == 1 && value.replies.size() == 1);
|
|
value.receive("ADVERT\n", 103);
|
|
assert(value.executions == 1 && value.replies.size() == 2);
|
|
}
|
|
{
|
|
MyMesh value;
|
|
value.receive("ADVERT", 101);
|
|
value.receive("ADVERT", 101, 99, 1); // another authenticated identity
|
|
assert(value.executions == 2 && value.replies.size() == 2);
|
|
value.acl.clients[0].admin = false;
|
|
value.receive("ADVERT", 102); // revoked role cannot recover cached replies
|
|
assert(value.executions == 2 && value.replies.size() == 2);
|
|
}
|
|
// Every accepted primary operation owns its ACK irrespective of the user's
|
|
// spelling. These used to be detected by a literal "tempradio " prefix.
|
|
for (const char* command : {"tempradio 910.525,62.5,7,5,10",
|
|
"set tempradio 910.525,62.5,7,5,10",
|
|
"A7|set tempradio 910.525,62.5,7,5,10",
|
|
" A7|TEMPRADIO 910.525,62.5,7,5,10\r\n"}) {
|
|
for (const auto mutation : {MyMesh::Mutation::Primary, MyMesh::Mutation::Secondary}) {
|
|
MyMesh value;value.accepted_mutation = mutation;
|
|
value.receive(command, 101);
|
|
assert(value.executions == 1 && value.replies.size() == 1);
|
|
assert(value.temp_radio_reply_barrier.waiting());
|
|
assert(value.temp_radio_reply_barrier.contains(&value.queued_ack));
|
|
assert(value.radio_reply_deadline == millis() + 300000UL);
|
|
assert(value.radio_reply_secondary == (mutation == MyMesh::Mutation::Secondary));
|
|
value.receive(command, 101);value.receive(command, 102);
|
|
assert(value.executions == 1 && value.replies.size() == 1); // no untracked success copy
|
|
value.receive("advert", 103, 100);
|
|
assert(value.executions == 1 && value.replies.size() == 2);
|
|
assert(value.replies.back().find("pending") != std::string::npos);
|
|
assert(value.temp_radio_reply_barrier.contains(&value.queued_ack));
|
|
value.finishRadioReply(true);
|
|
value.receive(command, 104);
|
|
assert(value.executions == 1 && value.replies.size() == 2); // no post-completion replay
|
|
}
|
|
}
|
|
for (const auto mutation : {MyMesh::Mutation::Primary, MyMesh::Mutation::Secondary}) {
|
|
MyMesh value;value.accepted_mutation = mutation;value.queue_accepts = false;
|
|
const char* command = "A7|set tempradio 910.525,62.5,7,5,10";
|
|
value.receive(command, 101);
|
|
assert(value.executions == 1 && value.replies.empty() && value.cancellations == 1);
|
|
assert(!value.temp_radio_reply_barrier.waiting() && !value._cli.radioProfiles().pending);
|
|
value.queue_accepts = true;
|
|
value.receive(command, 102);
|
|
assert(value.executions == 1 && value.replies.empty()); // canceled success is not replayable
|
|
}
|
|
{
|
|
MyMesh value;value._cli.radioProfiles().pending = true;
|
|
value.receive("set tempradio 910.525,62.5,7,5,10", 101);
|
|
assert(value.executions == 0 && value.replies.size() == 1);
|
|
assert(value.replies.back().find("pending") != std::string::npos);
|
|
assert(value._cli.radioProfiles().pending); // do not replace the old accepted operation
|
|
}
|
|
{
|
|
MyMesh value;value.accepted_mutation = MyMesh::Mutation::Primary;value.fail_command = true;
|
|
value.receive("set tempradio invalid", 101);
|
|
value.receive("set tempradio invalid", 102);
|
|
assert(value.executions == 1 && value.replies.size() == 2);
|
|
assert(!value.temp_radio_reply_barrier.waiting()); // validation errors remain recoverable
|
|
}
|
|
puts("CLI wire identity checks passed");
|
|
}
|