/**
* Path normalization tests
*
* Tests that $HOME/ is replaced with ~/ in stored text.
* Uses store + load_all to verify normalization through the public API
* (normalize_path is a private static method in sqlite::Backend).
*/
#include "config.h"
#include "embedder.h"
#include "sqlite/backend.h"
#include "test_env.h"
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
static const std::string TEMP_DB = "/tmp/ragger_test_pathnorm.db";
static void cleanup() {
fs::remove(TEMP_DB);
fs::remove(TEMP_DB + "-wal");
fs::remove(TEMP_DB + "-shm");
}
void test_normalize_home_in_stored_text(ragger::Embedder& emb) {
std::println(" test_normalize_home_in_stored_text...");
cleanup();
ragger::sqlite::Backend db(emb, TEMP_DB);
const char* home = std::getenv("HOME");
assert(home != nullptr);
std::string input = std::string(home) + "/Documents/test.md";
db.store(input);
auto all = db.load_all();
assert(all.size() == 1);
assert(all[0].text == "~/Documents/test.md");
db.close();
cleanup();
std::println(" OK");
}
void test_normalize_preserves_non_home_paths(ragger::Embedder& emb) {
std::println(" test_normalize_preserves_non_home_paths...");
cleanup();
ragger::sqlite::Backend db(emb, TEMP_DB);
std::string input = "/usr/local/bin/python";
db.store(input);
auto all = db.load_all();
assert(all.size() == 1);
assert(all[0].text == "/usr/local/bin/python");
db.close();
cleanup();
std::println(" OK");
}
void test_normalize_multiple_occurrences(ragger::Embedder& emb) {
std::println(" test_normalize_multiple_occurrences...");
cleanup();
ragger::sqlite::Backend db(emb, TEMP_DB);
const char* home = std::getenv("HOME");
assert(home != nullptr);
std::string input = "Source: " + std::string(home) + "/a.md and " +
std::string(home) + "/b.md";
db.store(input);
auto all = db.load_all();
assert(all.size() == 1);
// Both occurrences should be replaced
assert(all[0].text == "Source: ~/a.md and ~/b.md");
// Original home path should not appear
assert(all[0].text.find(home) == std::string::npos);
db.close();
cleanup();
std::println(" OK");
}
void test_normalize_partial_match_not_replaced(ragger::Embedder& emb) {
std::println(" test_normalize_partial_match_not_replaced...");
cleanup();
ragger::sqlite::Backend db(emb, TEMP_DB);
const char* home = std::getenv("HOME");
assert(home != nullptr);
// Home path without trailing component â should NOT be replaced
// (normalize_path looks for HOME + "/", not HOME alone)
std::string input = "Path is " + std::string(home);
db.store(input);
auto all = db.load_all();
assert(all.size() == 1);
assert(all[0].text == input); // unchanged
db.close();
cleanup();
std::println(" OK");
}
void test_normalize_embedded_in_longer_text(ragger::Embedder& emb) {
std::println(" test_normalize_embedded_in_longer_text...");
cleanup();
ragger::sqlite::Backend db(emb, TEMP_DB);
const char* home = std::getenv("HOME");
assert(home != nullptr);
std::string input = "I stored notes at " + std::string(home) +
"/Projects/notes.md for future reference.";
db.store(input);
auto all = db.load_all();
assert(all.size() == 1);
assert(all[0].text == "I stored notes at ~/Projects/notes.md for future reference.");
db.close();
cleanup();
std::println(" OK");
}
// PHASE2 Step 5: is_safe_model_repo (C §1.6).
void test_is_safe_model_repo() {
std::println(" test_is_safe_model_repo...");
using ragger::is_safe_model_repo;
for (const char* ok : {"sentence-transformers/all-MiniLM-L6-v2", "a/b",
"a.b.c/d_e-f.g", "A1/B2"})
CHECK(is_safe_model_repo(ok));
for (const char* bad : {"x/../../../tmp/evil", "../a", "a/..", "a//b",
"a/b/c", "/a", "a/", "a\\b/c", "", "a/b c",
".", "./a", "a/.", "..", "a/.."})
CHECK(!is_safe_model_repo(bad));
// Embedded NUL (built with an explicit length so the NUL is inside).
CHECK(!is_safe_model_repo(std::string("a/b\0c", 5)));
// Over-long.
CHECK(!is_safe_model_repo(std::string(150, 'a') + "/" + std::string(60, 'b')));
CHECK(is_safe_model_repo(std::string(100, 'a') + "/" + std::string(99, 'b'))); // 200 total
std::println(" OK");
}
int main() {
TestEnv env("path_normalization"); // must be first: isolates from ~/.ragger
std::println("Running path normalization tests:");
test_is_safe_model_repo(); // pure function â before the model gate
auto cfg = ragger::config();
assert(cfg.normalize_home_path == true);
// Embedder needed for sqlite::Backend store()
auto model_dir = env.model_dir();
if (!std::filesystem::exists(model_dir + "/model.onnx") && !std::filesystem::exists(model_dir + "/onnx/model.onnx")) {
std::cerr << "Skipping path normalization tests: model not found at " << model_dir << "\n";
return 0;
}
ragger::Embedder emb(model_dir);
test_normalize_home_in_stored_text(emb);
test_normalize_preserves_non_home_paths(emb);
test_normalize_multiple_occurrences(emb);
test_normalize_partial_match_not_replaced(emb);
test_normalize_embedded_in_longer_text(emb);
std::println("test_path_normalization: all passed");
return 0;
}