// 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 #include #include #include #include #include #include #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(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(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("secret-value-1")); CHECK(st.history_load("default") == std::vector{"typed line"}); CHECK(st.load_system("default") == std::optional("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{"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(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; }