Files
tai/taicpp/tests/foundation_test.cpp
T
retoorandClaude Sonnet 5.5 4583265122 taicpp foundation: vault, settings, debug log, sessions keyed by shell/pane, REPL, plexer
Full port of the Store (17 tables, FTS5, per-profile slices, sessions, turn state,
bots, settings, MCP registry), DebugLog, SETTINGS_CATALOG and boot_settings,
ShellSession (as ShellContext), Conversation persistence, Repl/Boot, and the
PlexerController family with FlowValidator/PlexerFlow. Verified by replaying 643
operations through tai.py and the C++ code (results, tables, FTS, schema, files
identical) and by restarting the REPL under per-pane shells and checking the vault.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 02:02:31 +02:00

247 lines
11 KiB
C++

// Unit tests for the foundation area that need no python oracle: hash
// vectors, UTF-8 helpers, the seal-enabled store paths (with a stand-in
// SealPort), boot settings, directory defaults, plexer detection, line input.
// The behaviour parity with tai.py lives in tests/foundation_parity.py.
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <regex>
#include <sstream>
#include "config/boot_settings.h"
#include "config/config.h"
#include "plexer/plexer.h"
#include "repl/interrupt.h"
#include "repl/line_input.h"
#include "repl/repl.h"
#include "session/conversation.h"
#include "session/dir_defaults.h"
#include "session/md5.h"
#include "session/shell_context.h"
#include "store/clock.h"
#include "store/json.h"
#include "store/sha256.h"
#include "store/store.h"
#include "store/utf8.h"
#include "store/vault_files.h"
using namespace detz;
namespace fs = std::filesystem;
static int g_checks = 0, g_fail = 0;
#define CHECK(cond) do { g_checks++; if (!(cond)) { g_fail++; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } } while (0)
// A reversible stand-in for the vault crypto: enough to exercise every sealed code path of Store.
class FakeSeal : public store::SealPort {
public:
bool enabled() const override { return true; }
std::string lock(const std::string& t) const override {
if (is_sealed(t)) return t;
std::string out = kPrefix;
for (unsigned char c : t) { char b[4]; std::snprintf(b, sizeof b, "%02x", static_cast<unsigned>(c ^ 0x5a)); out += b; }
return out;
}
std::string unlock(const std::string& t) const override {
if (!is_sealed(t)) throw store::SealError("not sealed");
std::string out;
for (size_t i = 5; i + 1 < t.size(); i += 2) out += static_cast<char>(std::stoi(t.substr(i, 2), nullptr, 16) ^ 0x5a);
return out;
}
};
static Config temp_config(const std::string& profile = "default") {
fs::path home = fs::temp_directory_path() / ("detz-ft-" + store::Clock::uuid_hex().substr(0, 8));
fs::create_directories(home / "profiles");
Config c = Config::FromEnv();
c.home = home.string(); c.profilesDir = (home / "profiles").string(); c.audioDir = (home / "audio").string();
c.dbPath = (home / "memory.db").string(); c.historyPath = (home / "history").string(); c.profile = profile;
return c;
}
static void test_hashes() {
CHECK(store::Sha256::hex("") == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
CHECK(store::Sha256::hex("abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
CHECK(store::Sha256::hex(std::string(1000, 'a')) == "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3");
CHECK(session::Md5::hex("") == "d41d8cd98f00b204e9800998ecf8427e");
CHECK(session::Md5::hex("abc") == "900150983cd24fb0d6963f7d28e17f72");
CHECK(session::Md5::hex("1700000000.0").size() == 32);
}
static void test_utf8_and_json() {
using store::Utf8;
CHECK(Utf8::length("h\xc3\xa9llo \xf0\x9f\x98\x80") == 7);
CHECK(Utf8::slice("a\xc3\xa9z", 1, 2) == "\xc3\xa9");
CHECK(Utf8::suffix("ab\xc3\xa9", 2) == "b\xc3\xa9");
CHECK(Utf8::strip("\xc2\xa0 x \xe2\x80\x83") == "x");
auto sp = Utf8::split_first(" cmd the rest ");
CHECK(sp.first == "cmd" && sp.second == "the rest ");
CHECK(Utf8::sanitize("a\xff" "b") == "a\xef\xbf\xbd" "b");
CHECK(store::Json::float_repr(0.1) == "0.1" && store::Json::float_repr(1e16) == "1e+16" && store::Json::float_repr(100.0) == "100.0");
store::Json j = store::Json::loads("{\"b\": [1, 2.5, \"\\u00e9\"], \"a\": null}");
CHECK(j.dumps() == "{\"b\": [1, 2.5, \"\\u00e9\"], \"a\": null}");
CHECK(j.dumps(true) == "{\"a\": null, \"b\": [1, 2.5, \"\\u00e9\"]}");
bool threw = false;
try { store::Json::loads("{\"a\":"); } catch (const store::JsonError&) { threw = true; }
CHECK(threw);
CHECK(std::regex_match(store::Clock::now_iso(), std::regex("\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\+00:00")));
CHECK(std::regex_match(store::Clock::uuid_hex(), std::regex("[0-9a-f]{32}")));
}
static void test_sealed_store() {
Config cfg = temp_config();
store::NullSeal plain;
{
store::Store st(cfg, plain);
st.log_event("default", "user", "say", "plain fox event");
st.save_secret("k", "secret-value-1");
st.save_session("default", {store::Json::loads("{\"role\": \"user\", \"content\": \"hi\"}")}, "main", "ctx");
st.save_system("default", "sys text");
st.history_add("default", "main", "typed line");
st.close();
}
FakeSeal seal;
{
store::Store st(cfg, seal); // upgrades the plaintext vault in place
auto raw = st.db().execute("SELECT text FROM events").rows;
CHECK(!raw.empty() && store::SealPort::is_sealed(raw[0][0].text()));
CHECK(store::SealPort::is_sealed(st.db().execute("SELECT value FROM secrets").rows[0][0].text()));
CHECK(st.load_secret("k") == std::optional<std::string>("secret-value-1"));
CHECK(st.history_load("default") == std::vector<std::string>{"typed line"});
CHECK(st.load_system("default") == std::optional<std::string>("sys text"));
auto msgs = st.load_session("default", "main", "ctx");
CHECK(msgs.size() == 1 && msgs[0].get_string("content") == "hi");
{ // seal_files() only upgrades *.sys.md, *.session.json and *.bots.json, so a ctx-scoped file stays plain until it is saved again (as in tai.py)
std::ifstream f(st.session_path("default", "ctx"));
std::stringstream ss; ss << f.rdbuf();
CHECK(!store::SealPort::is_sealed(ss.str()));
}
st.save_session("default", msgs, "main", "ctx");
{
std::ifstream f(st.session_path("default", "ctx"));
std::stringstream ss; ss << f.rdbuf();
CHECK(store::SealPort::is_sealed(ss.str()));
CHECK(st.load_session("default", "main", "ctx").size() == 1);
}
{
std::ifstream f(st.profile_path("default"));
std::stringstream ss; ss << f.rdbuf();
CHECK(store::SealPort::is_sealed(ss.str())); // default.sys.md was upgraded in place
}
auto hits = st.search_events("default", "fox");
CHECK(hits.size() == 1 && hits[0].text == "plain fox event"); // through the in-memory index
st.log_event("default", "user", "say", "second fox event");
CHECK(st.search_events("default", "fox").size() == 2);
CHECK(st.fts_search("fox", {"event"}).size() == 2);
st.close();
}
{
bool refused = false;
try { store::Store st(cfg, plain); } catch (const store::SealError& e) { refused = std::string(e.what()).find("sealed") != std::string::npos; }
CHECK(refused); // a sealed vault without the seal is refused loudly
}
}
static void test_boot_settings_and_dir_defaults() {
::unsetenv("TAI_THEME"); ::unsetenv("TAI_VOICE"); ::unsetenv("TAI_MUSE_BIN"); ::unsetenv("MUSE_BIN");
Config cfg = temp_config();
store::NullSeal seal;
store::Store st(cfg, seal);
st.upsert_settings({{"voice", store::Json("vault-voice")}, {"theme", store::Json("nord")}, {"muse_bin", store::Json("/opt/muse")}});
auto applied = BootSettings::Boot(cfg, st);
CHECK(cfg.voice == "vault-voice");
bool has_voice = false, has_theme = false;
for (auto& a : applied) { has_voice |= a == "voice"; has_theme |= a == "theme"; }
CHECK(has_voice && has_theme);
CHECK(std::string(std::getenv("TAI_THEME")) == "nord" && std::string(std::getenv("MUSE_BIN")) == "/opt/muse");
// env wins: a second boot with the env already exported changes nothing
::setenv("TAI_VOICE", "env-voice", 1);
Config again = cfg;
again.voice = "x";
auto second = again.ApplyStoreSettings(st);
bool voice_again = false;
for (auto& a : second) voice_again |= a == "voice";
CHECK(!voice_again && again.voice == "x");
::unsetenv("TAI_VOICE"); ::unsetenv("TAI_THEME"); ::unsetenv("MUSE_BIN");
session::Conversation conv(st, cfg, true, false, false, "ctxdd");
st.save_bots("default", store::Json::loads("{\"helper\": {\"nicknames\": []}}"));
st.save_dir_defaults(session::ShellContext::dir_key(), "prov", "mod", "helper");
std::string pinned_provider, pinned_model;
session::DirDefaults::Hooks hooks;
hooks.offered_providers = [] { return std::set<std::string>{"prov"}; };
hooks.pin_provider = [&](const std::string& p) { pinned_provider = p; };
hooks.pin_model = [&](const std::string& m) { pinned_model = m; };
std::string line = session::DirDefaults::apply(conv, hooks);
CHECK(pinned_provider == "prov" && pinned_model == "mod" && conv.bot() == "helper");
CHECK(line == "dir defaults for " + session::ShellContext::dir_key() + ": provider=prov model=mod bot=helper");
}
static void test_conversation_turns() {
Config cfg = temp_config();
store::NullSeal seal;
store::Store st(cfg, seal);
session::Conversation conv(st, cfg, true, false, false, "ctxturn");
CHECK(conv.messages().size() == 1 && conv.messages()[0].get_string("role") == "system");
conv.begin_turn("do the thing");
conv.add_message("user", "do the thing");
conv.checkpoint();
auto open = st.load_turn_state("default", "main", "ctxturn");
CHECK(open.find("open") && open.find("open")->as_bool() && open.get_string("goal") == "do the thing");
conv.add_message("assistant", "done");
conv.close_turn();
CHECK(!st.load_turn_state("default", "main", "ctxturn").find("open")->as_bool());
session::Conversation fresh(st, cfg, true, false, false, "ctxturn");
CHECK(fresh.messages().size() == 3); // system + the two restored messages
session::Conversation other(st, cfg, true, false, false, "ctxother");
CHECK(other.messages().size() == 1);
session::Conversation volatile_run(st, cfg, false, false, false, "ctxturn");
CHECK(volatile_run.messages().size() == 1); // persist=false never restores
CHECK(conv.switch_bot("nobody", true).rfind("unknown bot", 0) == 0);
}
static void test_plexer_detection() {
::unsetenv("TAI_PLEXER_SOCKET"); ::unsetenv("TMUX_PANE"); ::unsetenv("TAI_PLEXER_PANE");
CHECK(plexer::PlexerControllerFactory::detect() == nullptr);
::setenv("TMUX_PANE", "%4", 1);
auto t = plexer::PlexerControllerFactory::detect();
CHECK(t && t->kind() == "tmux");
::setenv("TAI_PLEXER_SOCKET", "/tmp/nonexistent.sock", 1);
::setenv("TAI_PLEXER_PANE", "9", 1);
auto n = plexer::PlexerControllerFactory::detect();
CHECK(n && n->kind() == "native" && dynamic_cast<plexer::NativePlexerController*>(n.get())->pane() == "9");
::unsetenv("TAI_PLEXER_SOCKET"); ::unsetenv("TMUX_PANE"); ::unsetenv("TAI_PLEXER_PANE");
CHECK(plexer::Plexer::shlex_quote("a b") == "'a b'" && plexer::Plexer::shlex_quote("") == "''" && plexer::Plexer::shlex_quote("it's") == "'it'\"'\"'s'");
}
static void test_line_input() {
int fds[2];
CHECK(::pipe(fds) == 0);
const char data[] = "first\r\nsecond\nlast-no-newline";
CHECK(::write(fds[1], data, sizeof data - 1) > 0);
::close(fds[1]);
repl::LineInput in(fds[0]);
std::string line;
CHECK(in.read("", line) == repl::LineInput::Status::Line && line == "first");
CHECK(in.read("", line) == repl::LineInput::Status::Line && line == "second");
CHECK(in.read("", line) == repl::LineInput::Status::Line && line == "last-no-newline");
CHECK(in.read("", line) == repl::LineInput::Status::Eof);
repl::Interrupt::raise();
int fds2[2];
CHECK(::pipe(fds2) == 0);
repl::LineInput quiet(fds2[0]);
CHECK(quiet.read("", line) == repl::LineInput::Status::Interrupted && !repl::Interrupt::requested());
::close(fds[0]); ::close(fds2[0]); ::close(fds2[1]);
}
int main() {
test_hashes();
test_utf8_and_json();
test_sealed_store();
test_boot_settings_and_dir_defaults();
test_conversation_turns();
test_plexer_detection();
test_line_input();
std::printf("%d checks, %d failures\n", g_checks, g_fail);
return g_fail ? 1 : 0;
}