yams/tests/mobile/mobile_abi_smoke_test.cpp

969 lines
40 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <yams/api/mobile_bindings.h>
#include "tests/common/fixture_manager.h"
#include "tests/common/search_corpus_presets.h"
#include "tests/integration/daemon/test_daemon_harness.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <random>
#include <string>
#include <thread>
#include <nlohmann/json.hpp>
namespace {
std::string make_unique_temp_dir() {
auto base = std::filesystem::temp_directory_path();
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_int_distribution<std::uint64_t> dist;
std::filesystem::path candidate;
do {
candidate = base / ("yams-mobile-abi-" + std::to_string(dist(rng)));
} while (std::filesystem::exists(candidate));
std::filesystem::create_directories(candidate);
return candidate.string();
}
struct TempDirGuard {
explicit TempDirGuard(std::string path) : path(std::move(path)) {}
~TempDirGuard() {
if (!path.empty()) {
std::error_code ec;
std::filesystem::remove_all(path, ec);
}
}
std::string path;
};
bool wait_for_document(yams_mobile_context_t* ctx, const std::string& hash,
std::chrono::milliseconds timeout) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
yams_mobile_document_get_request getReq{};
getReq.header = yams_mobile_request_header_default();
getReq.document_hash = hash.c_str();
getReq.metadata_only = 1;
yams_mobile_document_get_result_t* result = nullptr;
const auto status = yams_mobile_get_document(ctx, &getReq, &result);
if (status == YAMS_MOBILE_STATUS_OK) {
yams_mobile_document_get_result_destroy(result);
return true;
}
if (result != nullptr)
yams_mobile_document_get_result_destroy(result);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return false;
}
bool wait_for_corpus_status_documents(yams_mobile_context_t* ctx, std::uint64_t minDocuments,
std::chrono::milliseconds timeout) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
yams_mobile_vector_status_request statusReq{};
statusReq.header = yams_mobile_request_header_default();
yams_mobile_vector_status_result_t* result = nullptr;
const auto status = yams_mobile_get_vector_status(ctx, &statusReq, &result);
if (status == YAMS_MOBILE_STATUS_OK && result != nullptr) {
const auto payload = yams_mobile_vector_status_result_json(result);
if (payload.data != nullptr) {
const auto parsed = nlohmann::json::parse(std::string(payload.data, payload.length),
nullptr, false);
if (!parsed.is_discarded() && parsed.value("documents", 0ULL) >= minDocuments) {
yams_mobile_vector_status_result_destroy(result);
return true;
}
}
}
if (result != nullptr)
yams_mobile_vector_status_result_destroy(result);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return false;
}
bool store_document_with_retry(yams_mobile_context_t* ctx,
const yams_mobile_document_store_request& request,
yams_mobile_string_view* outHash,
std::chrono::milliseconds timeout) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
const auto status = yams_mobile_store_document(ctx, &request, outHash);
if (status == YAMS_MOBILE_STATUS_OK) {
return true;
}
if (status != YAMS_MOBILE_STATUS_INTERNAL_ERROR &&
status != YAMS_MOBILE_STATUS_UNAVAILABLE) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
bool wait_for_search_results(yams_mobile_context_t* ctx, const char* query,
std::chrono::milliseconds timeout) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
yams_mobile_search_request searchReq{};
searchReq.header = yams_mobile_request_header_default();
searchReq.query = query;
searchReq.limit = 10;
yams_mobile_search_result_t* result = nullptr;
const auto status = yams_mobile_search_execute(ctx, &searchReq, &result);
if (status == YAMS_MOBILE_STATUS_OK && result != nullptr) {
const auto payload = yams_mobile_search_result_json(result);
if (payload.data != nullptr) {
const auto parsed = nlohmann::json::parse(std::string(payload.data, payload.length),
nullptr, false);
if (!parsed.is_discarded() && parsed.value("total", 0ULL) >= 1U) {
yams_mobile_search_result_destroy(result);
return true;
}
}
}
if (result != nullptr)
yams_mobile_search_result_destroy(result);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
void require_stable_update_payload(const nlohmann::json& parsed, const std::string& expectedHash) {
REQUIRE(parsed.contains("success"));
REQUIRE(parsed.contains("hash"));
REQUIRE(parsed.contains("contentUpdated"));
REQUIRE(parsed.contains("metadataUpdated"));
REQUIRE(parsed.contains("tagsUpdated"));
REQUIRE(parsed.contains("updatesApplied"));
REQUIRE(parsed.contains("tagsAdded"));
REQUIRE(parsed.contains("tagsRemoved"));
CHECK(parsed.at("hash").template get<std::string>() == expectedHash);
CHECK(parsed.at("success").template get<bool>());
CHECK(parsed.at("metadataUpdated").template get<bool>());
CHECK(parsed.at("updatesApplied").template get<std::uint64_t>() == 1U);
CHECK(parsed.at("tagsUpdated").template get<bool>());
CHECK(parsed.at("tagsAdded").template get<std::uint64_t>() == 1U);
CHECK(parsed.at("tagsRemoved").template get<std::uint64_t>() == 0U);
}
} // namespace
TEST_CASE("Mobile ABI version matches header macros", "[mobile][abi]") {
const auto version = yams_mobile_get_version();
CHECK(version.major == YAMS_MOBILE_API_VERSION_MAJOR);
CHECK(version.minor == YAMS_MOBILE_API_VERSION_MINOR);
CHECK(version.patch == YAMS_MOBILE_API_VERSION_PATCH);
}
TEST_CASE("Mobile corpus document round trip works in embedded mode", "[mobile][abi]") {
const auto workingDir = make_unique_temp_dir();
const auto cacheDir = workingDir + "/cache";
TempDirGuard guard(workingDir);
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.cache_directory = cacheDir.c_str();
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams::test::FixtureManager fixtures;
auto spec = yams::test::mobileSearchCorpusSpec();
auto corpus = fixtures.createSearchCorpus(spec);
REQUIRE_FALSE(corpus.fixtures.empty());
const auto documentPath = corpus.fixtures.front().path.string();
const char* tags[] = {"mobile", "fixture", nullptr};
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
store.tags = tags;
store.tag_count = 2;
store.sync_now = 1;
yams_mobile_string_view stored_hash{};
REQUIRE(store_document_with_retry(ctx, store, &stored_hash, std::chrono::seconds(5)));
INFO(yams_mobile_last_error_message());
REQUIRE(stored_hash.length > 0U);
yams_mobile_list_request listRequest{};
listRequest.header = yams_mobile_request_header_default();
listRequest.limit = 25;
yams_mobile_list_result_t* listResult = nullptr;
REQUIRE(yams_mobile_list_documents(ctx, &listRequest, &listResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(listResult != nullptr);
const auto listJson = yams_mobile_list_result_json(listResult);
REQUIRE(listJson.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(listJson.data, listJson.length));
REQUIRE(parsed.contains("documents"));
const auto& documents = parsed.at("documents");
REQUIRE_FALSE(documents.empty());
const auto hashString = std::string(stored_hash.data, stored_hash.length);
const bool found = std::any_of(documents.begin(), documents.end(), [&](const auto& entry) {
return entry.contains("hash") && entry.at("hash").template get<std::string>() == hashString;
});
CHECK(found);
yams_mobile_list_result_destroy(listResult);
const auto documentName = std::filesystem::path(documentPath).filename().string();
const char* addTags[] = {"smoke-updated", nullptr};
const char* removeTags[] = {"fixture", nullptr};
const char* metadataKeys[] = {"source", nullptr};
const char* metadataValues[] = {"mobile-smoke", nullptr};
yams_mobile_update_request updateReq{};
updateReq.header = yams_mobile_request_header_default();
updateReq.hash = hashString.c_str();
updateReq.add_tags = addTags;
updateReq.add_tag_count = 1;
updateReq.remove_tags = removeTags;
updateReq.remove_tag_count = 1;
updateReq.metadata_keys = metadataKeys;
updateReq.metadata_values = metadataValues;
updateReq.metadata_count = 1;
yams_mobile_update_result_t* updateResult = nullptr;
REQUIRE(yams_mobile_update_document(ctx, &updateReq, &updateResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(updateResult != nullptr);
const auto updateJson = yams_mobile_update_result_json(updateResult);
REQUIRE(updateJson.data != nullptr);
const auto parsedUpdate =
nlohmann::json::parse(std::string(updateJson.data, updateJson.length));
CHECK(parsedUpdate.value("success", false));
yams_mobile_update_result_destroy(updateResult);
yams_mobile_search_request searchRequest{};
searchRequest.header = yams_mobile_request_header_default();
searchRequest.query = "offline";
searchRequest.limit = 10;
yams_mobile_search_result_t* searchResult = nullptr;
CHECK(yams_mobile_search_execute(ctx, &searchRequest, &searchResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
if (searchResult) {
const auto payload = yams_mobile_search_result_json(searchResult);
CHECK(payload.data != nullptr);
yams_mobile_search_result_destroy(searchResult);
}
yams_mobile_delete_request deleteReq{};
deleteReq.header = yams_mobile_request_header_default();
deleteReq.name = documentName.c_str();
deleteReq.dry_run = 0;
yams_mobile_delete_result_t* deleteResult = nullptr;
REQUIRE(yams_mobile_delete_by_name(ctx, &deleteReq, &deleteResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(deleteResult != nullptr);
const auto deleteJson = yams_mobile_delete_result_json(deleteResult);
REQUIRE(deleteJson.data != nullptr);
const auto parsedDelete =
nlohmann::json::parse(std::string(deleteJson.data, deleteJson.length));
REQUIRE(parsedDelete.contains("count"));
CHECK(parsedDelete.at("count").template get<std::size_t>() >= 1U);
yams_mobile_delete_result_destroy(deleteResult);
yams_mobile_document_get_request getAfterDelete{};
getAfterDelete.header = yams_mobile_request_header_default();
getAfterDelete.document_hash = hashString.c_str();
getAfterDelete.metadata_only = 1;
yams_mobile_document_get_result_t* getAfterDeleteResult = nullptr;
CHECK(yams_mobile_get_document(ctx, &getAfterDelete, &getAfterDeleteResult) ==
YAMS_MOBILE_STATUS_NOT_FOUND);
yams_mobile_context_destroy(ctx);
}
TEST_CASE("Mobile daemon mode context fails gracefully when socket is missing", "[mobile][abi]") {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = YAMS_MOBILE_BACKEND_DAEMON;
config.daemon_socket_path = "/tmp/yams_mobile_nonexistent_socket_test.sock";
yams_mobile_context_t* ctx = nullptr;
const auto status = yams_mobile_context_create(&config, &ctx);
CHECK(status != YAMS_MOBILE_STATUS_OK);
CHECK(ctx == nullptr);
const char* err = yams_mobile_last_error_message();
REQUIRE(err != nullptr);
CHECK(std::strlen(err) > 0U);
}
TEST_CASE("Mobile invalid backend mode is rejected", "[mobile][abi]") {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = 99;
yams_mobile_context_t* ctx = nullptr;
const auto status = yams_mobile_context_create(&config, &ctx);
CHECK(status == YAMS_MOBILE_STATUS_INVALID_ARGUMENT);
CHECK(ctx == nullptr);
}
TEST_CASE("Embedded mobile graph query returns corpus graph state", "[mobile][abi]") {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = YAMS_MOBILE_BACKEND_EMBEDDED;
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams::test::FixtureManager fixtures;
auto spec = yams::test::mobileSearchCorpusSpec();
auto corpus = fixtures.createSearchCorpus(spec);
REQUIRE_FALSE(corpus.fixtures.empty());
const auto documentPath = corpus.fixtures.front().path.string();
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
store.sync_now = 1;
yams_mobile_string_view stored_hash{};
REQUIRE(yams_mobile_store_document(ctx, &store, &stored_hash) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(stored_hash.length > 0U);
const auto hashString = std::string(stored_hash.data, stored_hash.length);
yams_mobile_graph_query_request graphReq{};
graphReq.header = yams_mobile_request_header_default();
graphReq.document_hash = hashString.c_str();
graphReq.max_depth = 1;
graphReq.max_results = 10;
yams_mobile_graph_query_result_t* graphResult = nullptr;
REQUIRE(yams_mobile_graph_query(ctx, &graphReq, &graphResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(graphResult != nullptr);
const auto graphJson = yams_mobile_graph_query_result_json(graphResult);
REQUIRE(graphJson.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(graphJson.data, graphJson.length));
CHECK(parsed.contains("kgAvailable"));
CHECK(parsed.at("kgAvailable").template get<bool>());
CHECK(parsed.contains("originNode"));
CHECK(parsed.at("originNode").at("documentHash").template get<std::string>() == hashString);
CHECK(parsed.contains("connectedNodes"));
CHECK(parsed.contains("edges"));
yams_mobile_graph_query_result_destroy(graphResult);
yams_mobile_context_destroy(ctx);
}
TEST_CASE("Embedded mobile corpus status reports readiness without warmup control",
"[mobile][abi]") {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = YAMS_MOBILE_BACKEND_EMBEDDED;
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams::test::FixtureManager fixtures;
auto spec = yams::test::mobileSearchCorpusSpec();
auto corpus = fixtures.createSearchCorpus(spec);
REQUIRE_FALSE(corpus.fixtures.empty());
const auto documentPath = corpus.fixtures.front().path.string();
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
yams_mobile_string_view stored_hash{};
REQUIRE(yams_mobile_store_document(ctx, &store, &stored_hash) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(stored_hash.length > 0U);
yams_mobile_vector_status_request statusReq{};
statusReq.header = yams_mobile_request_header_default();
statusReq.warmup = 1;
yams_mobile_vector_status_result_t* statusResult = nullptr;
REQUIRE(yams_mobile_get_vector_status(ctx, &statusReq, &statusResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(statusResult != nullptr);
const auto statusJson = yams_mobile_vector_status_result_json(statusResult);
REQUIRE(statusJson.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(statusJson.data, statusJson.length));
CHECK(parsed.at("statusType").template get<std::string>() == "corpus");
CHECK(parsed.at("backendMode").template get<std::string>() == "embedded");
CHECK(parsed.at("documents").template get<std::uint64_t>() >= 1U);
CHECK(parsed.at("indexedDocuments").template get<std::uint64_t>() >= 1U);
CHECK(parsed.at("corpusReady").template get<bool>());
CHECK(parsed.at("indexReady").template get<bool>());
CHECK_FALSE(parsed.at("supportsWarmup").template get<bool>());
CHECK(parsed.at("warmupRequested").template get<bool>());
CHECK(parsed.at("warmupIgnored").template get<bool>());
CHECK(parsed.contains("corpusStats"));
CHECK(parsed.at("corpusStats").at("doc_count").template get<std::int64_t>() >= 1);
yams_mobile_vector_status_result_destroy(statusResult);
yams_mobile_context_destroy(ctx);
}
TEST_CASE("Daemon mobile get_document honors include_content flag", "[mobile][abi][daemon]") {
yams::test::DaemonHarness harness({.enableAutoRepair = false});
REQUIRE(harness.start(std::chrono::seconds(10)));
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
const auto documentPath = std::filesystem::path(workingDir) / "daemon-mobile-note.txt";
{
std::ofstream out(documentPath);
REQUIRE(out.is_open());
out << "offline memory for daemon mobile bindings test\n";
}
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = YAMS_MOBILE_BACKEND_DAEMON;
const auto daemonSocketPath = harness.socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
yams_mobile_string_view stored_hash{};
REQUIRE(yams_mobile_store_document(ctx, &store, &stored_hash) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(stored_hash.length > 0U);
const auto hashString = std::string(stored_hash.data, stored_hash.length);
REQUIRE(wait_for_document(ctx, hashString, std::chrono::seconds(5)));
yams_mobile_document_get_request getWithoutContent{};
getWithoutContent.header = yams_mobile_request_header_default();
getWithoutContent.document_hash = hashString.c_str();
getWithoutContent.include_content = 0;
getWithoutContent.include_extracted_text = 0;
yams_mobile_document_get_result_t* noContentResult = nullptr;
REQUIRE(yams_mobile_get_document(ctx, &getWithoutContent, &noContentResult) ==
YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(noContentResult != nullptr);
const auto noContentPayload = yams_mobile_document_get_result_json(noContentResult);
REQUIRE(noContentPayload.data != nullptr);
const auto noContentBody = yams_mobile_document_get_result_content(noContentResult);
CHECK((noContentBody.data == nullptr || noContentBody.length == 0U));
yams_mobile_document_get_result_destroy(noContentResult);
yams_mobile_document_get_request getWithContent{};
getWithContent.header = yams_mobile_request_header_default();
getWithContent.document_hash = hashString.c_str();
getWithContent.include_content = 1;
getWithContent.include_extracted_text = 0;
yams_mobile_document_get_result_t* withContentResult = nullptr;
REQUIRE(yams_mobile_get_document(ctx, &getWithContent, &withContentResult) ==
YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(withContentResult != nullptr);
const auto withContentBody = yams_mobile_document_get_result_content(withContentResult);
REQUIRE(withContentBody.data != nullptr);
CHECK(withContentBody.length > 0U);
const auto withContentString = std::string(withContentBody.data, withContentBody.length);
CHECK(withContentString.find("offline memory for daemon mobile bindings test") !=
std::string::npos);
yams_mobile_document_get_result_destroy(withContentResult);
yams_mobile_context_destroy(ctx);
harness.stop();
}
TEST_CASE("Mobile update_document returns a stable payload shape across backends",
"[mobile][abi][parity]") {
auto run_case = [](yams_mobile_backend_mode backendMode) {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
const auto documentPath = std::filesystem::path(workingDir) / "mobile-update-shape.txt";
{
std::ofstream out(documentPath);
REQUIRE(out.is_open());
out << "stable update payload\n";
}
std::optional<yams::test::DaemonHarness> harness;
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = backendMode;
std::string daemonSocketPath;
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
harness.emplace(yams::test::DaemonHarnessOptions{.enableAutoRepair = false});
REQUIRE(harness->start(std::chrono::seconds(10)));
daemonSocketPath = harness->socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
}
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
yams_mobile_string_view storedHash{};
REQUIRE(store_document_with_retry(ctx, store, &storedHash, std::chrono::seconds(5)));
INFO(yams_mobile_last_error_message());
REQUIRE(storedHash.length > 0U);
const auto hashString = std::string(storedHash.data, storedHash.length);
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
REQUIRE(wait_for_document(ctx, hashString, std::chrono::seconds(5)));
}
const char* addTags[] = {"stable-tag", nullptr};
const char* metadataKeys[] = {"source", nullptr};
const char* metadataValues[] = {"stable-shape-test", nullptr};
yams_mobile_update_request updateReq{};
updateReq.header = yams_mobile_request_header_default();
updateReq.hash = hashString.c_str();
updateReq.add_tags = addTags;
updateReq.add_tag_count = 1;
updateReq.metadata_keys = metadataKeys;
updateReq.metadata_values = metadataValues;
updateReq.metadata_count = 1;
yams_mobile_update_result_t* updateResult = nullptr;
REQUIRE(yams_mobile_update_document(ctx, &updateReq, &updateResult) ==
YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(updateResult != nullptr);
const auto updateJson = yams_mobile_update_result_json(updateResult);
REQUIRE(updateJson.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(updateJson.data, updateJson.length));
require_stable_update_payload(parsed, hashString);
yams_mobile_update_result_destroy(updateResult);
yams_mobile_context_destroy(ctx);
if (harness) {
harness->stop();
}
};
SECTION("embedded") {
run_case(YAMS_MOBILE_BACKEND_EMBEDDED);
}
SECTION("daemon") {
run_case(YAMS_MOBILE_BACKEND_DAEMON);
}
}
TEST_CASE("Mobile search returns a stable payload shape across backends", "[mobile][abi][parity]") {
auto run_case = [](yams_mobile_backend_mode backendMode) {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
const auto documentPath = std::filesystem::path(workingDir) / "mobile-search-shape.txt";
{
std::ofstream out(documentPath);
REQUIRE(out.is_open());
out << "offline search parity test\n";
}
std::optional<yams::test::DaemonHarness> harness;
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = backendMode;
std::string daemonSocketPath;
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
harness.emplace(yams::test::DaemonHarnessOptions{.enableAutoRepair = false});
REQUIRE(harness->start(std::chrono::seconds(10)));
daemonSocketPath = harness->socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
}
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
yams_mobile_string_view storedHash{};
REQUIRE(store_document_with_retry(ctx, store, &storedHash, std::chrono::seconds(5)));
INFO(yams_mobile_last_error_message());
REQUIRE(storedHash.length > 0U);
const auto hashString = std::string(storedHash.data, storedHash.length);
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
REQUIRE(wait_for_document(ctx, hashString, std::chrono::seconds(5)));
}
constexpr const char* kSearchQuery = "mobile-search-shape.txt";
REQUIRE(wait_for_search_results(ctx, kSearchQuery, std::chrono::seconds(30)));
yams_mobile_search_request searchReq{};
searchReq.header = yams_mobile_request_header_default();
searchReq.query = kSearchQuery;
searchReq.limit = 10;
yams_mobile_search_result_t* searchResult = nullptr;
REQUIRE(yams_mobile_search_execute(ctx, &searchReq, &searchResult) ==
YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(searchResult != nullptr);
const auto payload = yams_mobile_search_result_json(searchResult);
REQUIRE(payload.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(payload.data, payload.length));
REQUIRE(parsed.contains("total"));
REQUIRE(parsed.contains("type"));
REQUIRE(parsed.contains("executionTimeMs"));
REQUIRE(parsed.contains("usedHybrid"));
REQUIRE(parsed.contains("results"));
CHECK_FALSE(parsed.at("type").template get<std::string>().empty());
CHECK(parsed.at("usedHybrid").template get<bool>() ==
(parsed.at("type").template get<std::string>() == "hybrid"));
const auto& results = parsed.at("results");
REQUIRE_FALSE(results.empty());
const auto& first = results.front();
CHECK(first.contains("id"));
CHECK(first.contains("hash"));
CHECK(first.contains("title"));
CHECK(first.contains("path"));
CHECK(first.contains("fileName"));
CHECK(first.contains("score"));
CHECK(first.contains("mimeType"));
CHECK(first.contains("fileType"));
CHECK(first.contains("size"));
CHECK(first.contains("created"));
CHECK(first.contains("modified"));
CHECK(first.contains("indexed"));
yams_mobile_search_result_destroy(searchResult);
yams_mobile_context_destroy(ctx);
if (harness) {
harness->stop();
}
};
SECTION("embedded") {
run_case(YAMS_MOBILE_BACKEND_EMBEDDED);
}
SECTION("daemon") {
run_case(YAMS_MOBILE_BACKEND_DAEMON);
}
}
TEST_CASE("Mobile list_documents returns a stable payload shape across backends",
"[mobile][abi][parity]") {
const auto run_case = [](std::uint32_t backendMode) {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
const auto documentPath = std::filesystem::path(workingDir) / "mobile-list-shape.txt";
{
std::ofstream out(documentPath);
REQUIRE(out.is_open());
out << "list parity test\n";
}
std::optional<yams::test::DaemonHarness> harness;
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = backendMode;
std::string daemonSocketPath;
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
harness.emplace(yams::test::DaemonHarnessOptions{.enableAutoRepair = false});
REQUIRE(harness->start(std::chrono::seconds(10)));
daemonSocketPath = harness->socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
}
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
yams_mobile_string_view storedHash{};
REQUIRE(store_document_with_retry(ctx, store, &storedHash, std::chrono::seconds(5)));
INFO(yams_mobile_last_error_message());
REQUIRE(storedHash.length > 0U);
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
const auto hashString = std::string(storedHash.data, storedHash.length);
REQUIRE(wait_for_document(ctx, hashString, std::chrono::seconds(5)));
}
yams_mobile_list_request listReq{};
listReq.header = yams_mobile_request_header_default();
listReq.limit = 10;
yams_mobile_list_result_t* listResult = nullptr;
REQUIRE(yams_mobile_list_documents(ctx, &listReq, &listResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(listResult != nullptr);
const auto payload = yams_mobile_list_result_json(listResult);
REQUIRE(payload.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(payload.data, payload.length));
REQUIRE(parsed.contains("count"));
REQUIRE(parsed.contains("totalFound"));
REQUIRE(parsed.contains("hasMore"));
REQUIRE(parsed.contains("executionTimeMs"));
REQUIRE(parsed.contains("documents"));
const auto& documents = parsed.at("documents");
REQUIRE_FALSE(documents.empty());
const auto& first = documents.front();
CHECK(first.contains("name"));
CHECK(first.contains("fileName"));
CHECK(first.contains("hash"));
CHECK(first.contains("path"));
CHECK(first.contains("extension"));
CHECK(first.contains("size"));
CHECK(first.contains("mimeType"));
CHECK(first.contains("fileType"));
CHECK(first.contains("created"));
CHECK(first.contains("modified"));
CHECK(first.contains("indexed"));
yams_mobile_list_result_destroy(listResult);
yams_mobile_context_destroy(ctx);
if (harness) {
harness->stop();
}
};
SECTION("embedded") {
run_case(YAMS_MOBILE_BACKEND_EMBEDDED);
}
SECTION("daemon") {
run_case(YAMS_MOBILE_BACKEND_DAEMON);
}
}
TEST_CASE("Mobile remove_document returns not_found across backends for missing hashes",
"[mobile][abi][parity]") {
const auto run_case = [](std::uint32_t backendMode) {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
std::optional<yams::test::DaemonHarness> harness;
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = backendMode;
std::string daemonSocketPath;
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
harness.emplace(yams::test::DaemonHarnessOptions{.enableAutoRepair = false});
REQUIRE(harness->start(std::chrono::seconds(10)));
daemonSocketPath = harness->socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
}
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
REQUIRE(ctx != nullptr);
const auto status = yams_mobile_remove_document(
ctx, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
CHECK(status == YAMS_MOBILE_STATUS_NOT_FOUND);
yams_mobile_context_destroy(ctx);
if (harness) {
harness->stop();
}
};
SECTION("embedded") {
run_case(YAMS_MOBILE_BACKEND_EMBEDDED);
}
SECTION("daemon") {
run_case(YAMS_MOBILE_BACKEND_DAEMON);
}
}
TEST_CASE("Mobile delete_by_name returns an empty result across backends when nothing matches",
"[mobile][abi][parity]") {
const auto run_case = [](std::uint32_t backendMode) {
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
std::optional<yams::test::DaemonHarness> harness;
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = backendMode;
std::string daemonSocketPath;
if (backendMode == YAMS_MOBILE_BACKEND_DAEMON) {
harness.emplace(yams::test::DaemonHarnessOptions{.enableAutoRepair = false});
REQUIRE(harness->start(std::chrono::seconds(10)));
daemonSocketPath = harness->socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
}
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
REQUIRE(ctx != nullptr);
yams_mobile_delete_request deleteReq{};
deleteReq.header = yams_mobile_request_header_default();
deleteReq.name = "missing-mobile-delete.txt";
yams_mobile_delete_result_t* deleteResult = nullptr;
REQUIRE(yams_mobile_delete_by_name(ctx, &deleteReq, &deleteResult) ==
YAMS_MOBILE_STATUS_OK);
REQUIRE(deleteResult != nullptr);
const auto payload = yams_mobile_delete_result_json(deleteResult);
REQUIRE(payload.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(payload.data, payload.length));
CHECK(parsed.value("dryRun", true) == false);
CHECK(parsed.value("count", 1ULL) == 0ULL);
REQUIRE(parsed.contains("deleted"));
REQUIRE(parsed.contains("errors"));
CHECK(parsed.at("deleted").empty());
CHECK(parsed.at("errors").empty());
yams_mobile_delete_result_destroy(deleteResult);
yams_mobile_context_destroy(ctx);
if (harness) {
harness->stop();
}
};
SECTION("embedded") {
run_case(YAMS_MOBILE_BACKEND_EMBEDDED);
}
SECTION("daemon") {
run_case(YAMS_MOBILE_BACKEND_DAEMON);
}
}
TEST_CASE("Daemon mobile corpus status reports readiness without warmup control",
"[mobile][abi][daemon]") {
yams::test::DaemonHarness harness({.enableAutoRepair = false});
REQUIRE(harness.start(std::chrono::seconds(10)));
const auto workingDir = make_unique_temp_dir();
TempDirGuard guard(workingDir);
const auto documentPath = std::filesystem::path(workingDir) / "daemon-mobile-status.txt";
{
std::ofstream out(documentPath);
REQUIRE(out.is_open());
out << "daemon corpus status test\n";
}
yams_mobile_context_config config = yams_mobile_context_config_default();
config.working_directory = workingDir.c_str();
config.backend_mode = YAMS_MOBILE_BACKEND_DAEMON;
const auto daemonSocketPath = harness.socketPath().string();
config.daemon_socket_path = daemonSocketPath.c_str();
yams_mobile_context_t* ctx = nullptr;
REQUIRE(yams_mobile_context_create(&config, &ctx) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(ctx != nullptr);
yams_mobile_document_store_request store{};
store.header = yams_mobile_request_header_default();
store.path = documentPath.c_str();
yams_mobile_string_view stored_hash{};
REQUIRE(store_document_with_retry(ctx, store, &stored_hash, std::chrono::seconds(5)));
INFO(yams_mobile_last_error_message());
REQUIRE(stored_hash.length > 0U);
const auto hashString = std::string(stored_hash.data, stored_hash.length);
REQUIRE(wait_for_document(ctx, hashString, std::chrono::seconds(5)));
REQUIRE(wait_for_corpus_status_documents(ctx, 1, std::chrono::seconds(5)));
yams_mobile_vector_status_request statusReq{};
statusReq.header = yams_mobile_request_header_default();
statusReq.warmup = 1;
yams_mobile_vector_status_result_t* statusResult = nullptr;
REQUIRE(yams_mobile_get_vector_status(ctx, &statusReq, &statusResult) == YAMS_MOBILE_STATUS_OK);
INFO(yams_mobile_last_error_message());
REQUIRE(statusResult != nullptr);
const auto statusJson = yams_mobile_vector_status_result_json(statusResult);
REQUIRE(statusJson.data != nullptr);
const auto parsed = nlohmann::json::parse(std::string(statusJson.data, statusJson.length));
CHECK(parsed.at("statusType").template get<std::string>() == "corpus");
CHECK(parsed.at("backendMode").template get<std::string>() == "daemon");
CHECK(parsed.at("documents").template get<std::uint64_t>() >= 1U);
CHECK(parsed.at("indexedDocuments").template get<std::uint64_t>() >= 1U);
CHECK(parsed.at("corpusReady").template get<bool>());
CHECK(parsed.at("indexReady").template get<bool>());
CHECK_FALSE(parsed.at("supportsWarmup").template get<bool>());
CHECK(parsed.at("warmupRequested").template get<bool>());
CHECK(parsed.at("warmupIgnored").template get<bool>());
CHECK(parsed.contains("corpusStats"));
CHECK(parsed.at("corpusStats").at("doc_count").template get<std::int64_t>() >= 1);
yams_mobile_vector_status_result_destroy(statusResult);
yams_mobile_context_destroy(ctx);
harness.stop();
}