Files
voice-cat/tests/test_tcp_loopback.cpp
Talon 63f457fc54 feat(M1): TCP/TLS control plane -- auth, channels, ephemeral text
Implements the full M1 milestone. Two clients authenticate over TLS 1.3
(guest + Argon2id password) and exchange channel + private text messages
through a real server. All five ctest --preset m1-dev tests pass in ~1 s.

Key components added:
- vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3)
- FrameCodec feed+emit, encode/decode_envelope, protobuf codegen
- TcpServerConn with blocking TLS handshake thread + tls_read_loop
- TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client)
- WorkerPool (3 threads, used for Argon2id)
- Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin
- ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint
- ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated
- SessionRegistry: channel tree, user map, text routing, broadcast
- vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect
- voicecat-admin CLI: account add/reset/del/list
- test_m1_integration: M1 exit criterion, verified green

Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope
was adding the [4-byte len] prefix, then TcpServerConn::send_frame added
a second one, causing the client to parse [len][proto] as protobuf (silent
failure). Fixed by serializing raw protobuf bytes in send_envelope and
letting send_frame apply the single length prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:48:44 +02:00

106 lines
3.5 KiB
C++

/*
* test_tcp_loopback — in-process TCP acceptor + client, sends 10 frames.
* Runs only under m1-dev (requires Asio).
*/
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <mutex>
#include <thread>
#include <vector>
#include "net/transport.h"
using namespace voicecat::net;
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::printf("FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
int main() {
constexpr int kFrameCount = 10;
constexpr uint16_t kPort = 19850;
std::mutex mtx;
std::condition_variable cv;
std::vector<std::vector<uint8_t>> received;
std::atomic<bool> server_connected{false};
// Server io_context + acceptor.
asio::io_context server_io;
auto work = asio::make_work_guard(server_io);
std::thread server_thread([&] { server_io.run(); });
TcpAcceptor acceptor(server_io, kPort, [&](asio::ip::tcp::socket sock) {
TcpChannelCallbacks cbs;
cbs.on_frame = [&](std::vector<uint8_t> frame) {
std::lock_guard<std::mutex> lk(mtx);
received.push_back(std::move(frame));
cv.notify_all();
};
cbs.on_connected = [&] { server_connected.store(true); };
auto conn = std::make_shared<TcpServerConn>(std::move(sock), std::move(cbs));
return conn;
});
acceptor.start();
// Client.
std::atomic<bool> client_connected{false};
TcpChannelCallbacks client_cbs;
client_cbs.on_connected = [&] { client_connected.store(true); };
client_cbs.on_connect_error = [](std::error_code ec) {
std::printf("connect error: %s\n", ec.message().c_str());
};
TcpControlChannel client(std::move(client_cbs));
client.async_connect("127.0.0.1", kPort);
// Wait for connection.
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!client_connected.load() &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
CHECK(client_connected.load());
// Send kFrameCount distinct frames.
for (int i = 0; i < kFrameCount; ++i) {
std::vector<uint8_t> payload = {static_cast<uint8_t>(i), 0xAB, 0xCD};
client.send_frame(payload);
}
// Wait for all frames to arrive on the server side.
{
std::unique_lock<std::mutex> lk(mtx);
bool ok = cv.wait_for(lk, std::chrono::seconds(5),
[&] { return static_cast<int>(received.size()) >= kFrameCount; });
CHECK(ok);
}
CHECK(static_cast<int>(received.size()) == kFrameCount);
for (int i = 0; i < kFrameCount && i < static_cast<int>(received.size()); ++i) {
CHECK(received[i].size() == 3);
if (!received[i].empty()) CHECK(received[i][0] == static_cast<uint8_t>(i));
}
// Clean up.
client.close();
acceptor.stop();
work.reset();
server_io.stop();
server_thread.join();
if (g_failures == 0) {
std::printf("tcp_loopback: all checks passed\n");
return 0;
}
std::printf("tcp_loopback: %d failure(s)\n", g_failures);
return 1;
}