#include "sync.h" #include "json.hpp" #include using namespace ks; using Json = nlohmann::json; namespace { int checks{}; void require(bool condition, const char *message) { if (!condition) throw std::runtime_error(message); ++checks; } template void rejects(F f, const char *message) { bool rejected = false; try { f(); } catch (const std::exception &) { rejected = true; } require(rejected, message); } Bytes bytes(const Json &j) { auto s = j.dump(); return {s.begin(), s.end()}; } Json parse(const Bytes &b) { return Json::parse(b.begin(), b.end()); } struct FakeProvider : SyncProvider { SyncDocument remote; int writes{}; bool race{}; SyncDocument read() override { return remote; } uint64_t write(const Bytes &b, uint64_t expected) override { if (race) ++remote.revision; if (expected != remote.revision) throw std::runtime_error("CAS conflict"); ++writes; remote = {remote.revision + 1, b}; return remote.revision; } }; } int main() { auto root = std::filesystem::temp_directory_path() / (L"ks-sync-test-" + std::to_wstring(GetCurrentProcessId())); try { std::filesystem::create_directories(root); require(!syncVisible(root), "hidden without opt-in file"); require(!loadSyncConfig(root).enabled, "disabled by default"); SyncConfig config; config.server = L"https://bao.example.com"; config.username = L"person"; config.enabled = true; validateSyncConfig(config); for (const auto *server : {L"http://bao.example.com", L"https://user:pass@bao.example.com", L"https://bao.example.com/x", L"https://bao.example.com?x=1"}) { auto invalid = config; invalid.server = server; rejects([&] { validateSyncConfig(invalid); }, "reject unsafe endpoint"); } auto invalid = config; invalid.path = L"../other"; rejects([&] { validateSyncConfig(invalid); }, "reject traversal"); invalid = config; invalid.provider = L"unknown"; rejects([&] { validateSyncConfig(invalid); }, "reject unsupported provider"); saveSyncConfig(root, config); require(syncVisible(root) && loadSyncConfig(root) == config, "connection round trip"); auto partialConfig = config; partialConfig.keepLocalLayouts = true; saveSyncConfig(root, partialConfig); require(loadSyncConfig(root).keepLocalLayouts, "partial sync mode survives restart"); saveSyncConfig(root, config); auto rawConfig = parse(readFile(root / L"sync.json", 16384)); require(!rawConfig.contains("password") && !rawConfig.contains("token"), "no secrets in config"); Settings local; local.autostart = true; local.elevatedAutostart = true; local.tesseractPath = L"C:\\private\\tesseract.exe"; local.history = false; local.snippets.push_back({L"test", L"private snippet"}); auto base = sharedSyncSettings(local); Settings ordered = local, reordered = local; ordered.directIds = {L"00000409", L"00000422"}; ordered.direct = {L"Ctrl+1", L"Ctrl+2"}; reordered.directIds = {L"00000422", L"00000409"}; reordered.direct = {L"Ctrl+2", L"Ctrl+1"}; require(sharedSyncSettings(ordered) == sharedSyncSettings(reordered), "layout enumeration does not cause sync churn"); auto j = parse(base); require(!j.contains("startWithSystem") && !j.contains("tesseractPath") && !j.contains("historyEnabled"), "local-only fields excluded"); auto restored = restoreSyncSettings(base, local, {}); require(restored.autostart && restored.elevatedAutostart && !restored.history && restored.tesseractPath == local.tesseractPath, "preserve device settings"); auto hostile = j; hostile["startWithSystem"] = false; rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject injected device setting"); hostile = j; hostile["schemaVersion"] = 2; rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject future schema"); hostile = j; hostile["layouts"] = Json::array({"00000422"}); rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "missing layouts block destructive normalization"); auto left = j, right = j; left["convertLastWord"] = false; right["switchSystemLayout"] = false; auto merged = parse(mergeSyncSettings(base, bytes(left), bytes(right))); require(merged["convertLastWord"] == false && merged["switchSystemLayout"] == false, "merge independent edits"); left = right = j; left["autoCorrectMinLength"] = 4; right["autoCorrectMinLength"] = 5; rejects([&] { mergeSyncSettings(base, bytes(left), bytes(right)); }, "same field conflict"); left = right = j; left["snippets"].push_back({{"trigger", "aaa"}, {"text", "one"}}); right["snippets"].push_back({{"trigger", "bbb"}, {"text", "two"}}); require(parse(mergeSyncSettings(base, bytes(left), bytes(right)))["snippets"].size() == 3, "independent snippet additions merge"); require(parse(mergeSyncSettings({}, bytes(left), base))["snippets"].size() == 2, "first sync merges independent additions without choosing a whole profile"); require(parse(mergeSyncSettings({}, base, base)) == j, "identical first sync safe"); { auto l = j, r = j; l["autoCorrectMinLength"] = 4; r["autoCorrectMinLength"] = 5; l["autoCorrectNever"].push_back("localword"); r["autoCorrectNever"].push_back("remoteword"); auto plan = mergeSyncProfile({}, bytes(l), bytes(r)); require(plan.conflicts.size() == 1 && parse(plan.local)["autoCorrectNever"].size() == 2 && parse(plan.remote)["autoCorrectNever"].size() == 2, "first sync merges lists while preserving scalar conflicts"); require(parse(plan.local)["autoCorrectMinLength"] == 4 && parse(plan.remote)["autoCorrectMinLength"] == 5, "first sync never guesses between different scalar values"); auto again = mergeSyncProfile(plan.base, plan.local, plan.remote); require(again.conflicts.size() == 1, "first-sync conflicts survive saving the ancestor"); auto resolved = mergeSyncProfile(plan.base, plan.local, plan.remote, {{plan.conflicts[0], false}}); require(resolved.conflicts.empty() && parse(resolved.local)["autoCorrectMinLength"] == 5, "first-sync conflict can be resolved independently"); } require(SyncIntervalMs == 600000, "background interval is ten minutes"); { auto l = j, r = j; l["autoCorrectReplacements"]["helo"] = "hello"; r["autoCorrectReplacements"]["wrold"] = "world"; auto paired = mergeSyncSettings(base, bytes(l), bytes(r)); require(parse(paired)["autoCorrectReplacements"].size() == 2, "independent replacement pairs merge"); l = r = parse(paired); l["autoCorrectReplacements"].erase("helo"); r["autoCorrectReplacements"]["wrold"] = "worlds"; auto removed = mergeSyncSettings(paired, bytes(l), bytes(r)); auto restored = restoreSyncSettings(removed, local, {}); require(restored.autoReplacements.size() == 1 && restored.autoReplacements.at(L"wrold") == L"worlds", "replacement deletion merges with an independent edit and remains loadable"); r["autoCorrectReplacements"]["helo"] = "help"; require(mergeSyncProfile(paired, bytes(l), bytes(r)).conflicts.size() == 1, "replacement deletion versus edit is a conflict"); auto legacy = j; for (auto key : {"autoCorrectReplace", "autoCorrectTypos", "autoCorrectReplacements"}) legacy.erase(key); auto migrated = restoreSyncSettings(bytes(legacy), local, {}); require(!migrated.autoReplace && !migrated.autoTypos && migrated.autoReplacements.empty(), "older synchronized profiles default new typo modes off"); } { auto l = j, r = j; l["autoCorrectMinLength"] = 4; r["autoCorrectMinLength"] = 5; l["convertLastWord"] = false; r["switchSystemLayout"] = false; auto plan = mergeSyncProfile(base, bytes(l), bytes(r)); require(plan.conflicts.size() == 1, "one conflict reported independently"); require(parse(plan.local)["switchSystemLayout"] == false && parse(plan.remote)["convertLastWord"] == false, "nonconflicting changes reach both copies"); require(parse(plan.local)["autoCorrectMinLength"] == 4 && parse(plan.remote)["autoCorrectMinLength"] == 5, "unresolved scalar retains both values"); require(parse(plan.base)["autoCorrectMinLength"] == j["autoCorrectMinLength"], "ancestor retained for unresolved setting"); saveSyncBase(root, config, plan.base); auto again = mergeSyncProfile(loadSyncBase(root, config), plan.local, plan.remote); require(again.conflicts.size() == 1, "conflict survives restart checkpoint"); auto choice = SyncChoice{plan.conflicts.front(), true}; auto resolved = mergeSyncProfile(plan.base, plan.local, plan.remote, {choice}); require(resolved.conflicts.empty() && parse(resolved.remote)["autoCorrectMinLength"] == 4, "resolve only selected setting"); require(parse(resolved.remote)["switchSystemLayout"] == false, "choice preserves independent server change"); auto changed = parse(plan.remote); changed["autoCorrectMinLength"] = 6; require(mergeSyncProfile(plan.base, plan.local, bytes(changed), {choice}).conflicts.size() == 1, "stale conflict choice never overwrites new server value"); FakeProvider partial; partial.remote = {7, bytes(r)}; auto result = synchronize(partial, base, bytes(l), SyncAction::Automatic); require(result.success && result.conflicts.size() == 1 && partial.writes == 1, "partial synchronization commits independent fields with CAS"); auto encoded = encodeSyncChoices({choice}); require(decodeSyncChoices(encoded).front().conflict.key == choice.conflict.key, "choice IPC round trip"); } { auto id = j["snippets"][0]["id"].get(); auto l = j, r = j; l["snippets"][0]["trigger"] = "renamed"; r["snippets"][0]["text"] = "edited remotely"; auto merged = parse(mergeSyncSettings(base, bytes(l), bytes(r))); require(merged["snippets"][0]["id"] == id && merged["snippets"][0]["trigger"] == "renamed" && merged["snippets"][0]["text"] == "edited remotely", "stable ID merges rename and text edit"); l = r = j; l["snippets"] = Json::array(); l["snippetTombstones"] = Json::array({id}); auto deletion = mergeSyncProfile(base, bytes(l), bytes(r)); require(deletion.conflicts.empty() && parse(deletion.remote)["snippets"].empty(), "delete beats unchanged snippet"); require(parse(deletion.remote)["snippetTombstones"] == Json::array({id}), "deletion marker retained"); auto offline = mergeSyncProfile(base, bytes(r), deletion.remote); require(offline.conflicts.empty() && parse(offline.local)["snippets"].empty(), "offline copy cannot resurrect deletion"); r["snippets"][0]["text"] = "offline edit"; auto conflict = mergeSyncProfile(base, bytes(l), bytes(r)); require(conflict.conflicts.size() == 1, "delete versus edit is one snippet conflict"); auto remove = mergeSyncProfile(conflict.base, conflict.local, conflict.remote, {{conflict.conflicts[0], true}}); require(remove.conflicts.empty() && parse(remove.remote)["snippets"].empty(), "choose deletion"); auto keep = mergeSyncProfile(conflict.base, conflict.local, conflict.remote, {{conflict.conflicts[0], false}}); require(keep.conflicts.empty() && parse(keep.local)["snippetTombstones"].empty() && parse(keep.local)["snippets"][0]["text"] == "offline edit", "choose edited snippet removes tombstone"); require(mergeSyncProfile(base, bytes(l), bytes(l)).conflicts.empty(), "both delete converges"); auto legacy = j; legacy.erase("snippetTombstones"); legacy["snippets"][0].erase("id"); auto migrated = parse(mergeSyncSettings(bytes(legacy), base, bytes(legacy))); require(migrated["snippets"][0]["id"] == id, "legacy profiles migrate to identical IDs"); auto roundtrip = decodeSettings(encodeSettings(restored), {}); require(snippetId(roundtrip.snippets.front()) == wide(id), "ID survives settings serialization"); require(newSnippetId() != newSnippetId(), "new snippet IDs are unique"); } { auto l = j, r = j; l["snippets"].push_back({{"id", "new-a"}, {"trigger", "same"}, {"text", "A"}}); r["snippets"].push_back({{"id", "new-b"}, {"trigger", "same"}, {"text", "B"}}); auto collision = mergeSyncProfile(base, bytes(l), bytes(r)); require(collision.conflicts.size() == 2, "duplicate new triggers expose individual choices"); auto resolved = mergeSyncProfile(collision.base, collision.local, collision.remote, {{collision.conflicts[0], true}, {collision.conflicts[1], true}}); require(resolved.conflicts.empty() && parse(resolved.remote)["snippets"].size() == 2, "choose one of colliding additions"); l = r = j; l["toggleHotkey"] = "Ctrl+Alt+F11"; r["caseHotkey"] = "Ctrl+Alt+F11"; require(mergeSyncProfile(base, bytes(l), bytes(r)).conflicts.size() == 2, "cross-field shortcut collisions exposed"); l = r = j; l["excludedApps"].push_back("local-app"); r["excludedApps"].push_back("remote-app"); require(parse(mergeSyncSettings(base, bytes(l), bytes(r)))["excludedApps"].size() == j["excludedApps"].size() + 2, "independent exclusion edits merge"); } FakeProvider provider; rejects([&] { synchronize(provider, {}, base, SyncAction::Automatic); }, "empty server never auto-populated"); auto result = synchronize(provider, {}, base, SyncAction::Upload); require(result.revision == 1 && provider.writes == 1, "initial upload CAS zero"); result = synchronize(provider, base, base, SyncAction::Automatic); require(provider.writes == 1 && result.revision == 1, "no-op avoids versions"); result = synchronize(provider, base, bytes(left), SyncAction::Download); require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes"); provider.race = true; rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected"); require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote"); provider.race = false; { auto remoteProfile = j; remoteProfile["layouts"] = Json::array({"win:00000422"}); remoteProfile["layoutHotkeys"] = Json::array({{{"layoutId", "win:00000422"}, {"hotkey", "Ctrl+Alt+9"}}}); remoteProfile["autoCorrectNever"].push_back("remoteword"); FakeProvider partial; partial.remote = {3, bytes(remoteProfile)}; auto validate = [&](const Bytes &b) { restoreSyncSettings(b, local, {}, true); }; auto result = synchronize(partial, {}, base, SyncAction::Automatic, validate, {}, true); auto restored = restoreSyncSettings(result.settings, local, {}, true); require(restored.cycle == local.cycle && restored.direct == local.direct && restored.autoNever == std::vector{L"remoteword"}, "partial sync restores other settings and keeps local layout configuration"); auto next = sharedSyncSettings(restored); result = synchronize(partial, result.base, next, SyncAction::Automatic, validate, {}, true); require(parse(partial.remote.settings)["layouts"] == remoteProfile["layouts"] && parse(partial.remote.settings)["layoutHotkeys"] == remoteProfile["layoutHotkeys"], "repeated partial sync never deletes unavailable server layouts or shortcuts"); auto changed = parse(next); changed["autoCorrectMinLength"] = 7; result = synchronize(partial, result.base, bytes(changed), SyncAction::Upload, validate, {}, true); require(parse(partial.remote.settings)["autoCorrectMinLength"] == 7 && parse(partial.remote.settings)["layoutHotkeys"] == remoteProfile["layoutHotkeys"], "explicit partial upload preserves server layouts too"); auto downloaded = synchronize(partial, result.base, base, SyncAction::Download, validate, {}, true); auto downloadedSettings = restoreSyncSettings(downloaded.settings, local, {}, true); require(downloadedSettings.autoMinLength == 7 && downloadedSettings.cycle == local.cycle && downloadedSettings.direct == local.direct, "partial Download applies other settings without replacing local layouts"); try { restoreSyncSettings(bytes(remoteProfile), local, {}); require(false, "full sync must identify missing layouts"); } catch (const std::runtime_error &e) { std::string message = e.what(); require(message.find("win:00000422") != message.npos && message.find("Keep this PC's layouts") != message.npos, "missing-layout error identifies IDs and offers partial sync"); } } rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload, [](const Bytes &) { throw std::runtime_error("invalid"); }); }, "validate before upload"); require(provider.writes == 1, "invalid data not uploaded"); saveSyncBase(root, config, base); require(parse(loadSyncBase(root, config)) == j, "encrypted baseline round trip"); auto other = config; other.path = L"other/settings"; require(loadSyncBase(root, other).empty(), "baseline bound to connection"); auto encrypted = readFile(root / L"sync-base.bin", 1024 * 1024); std::string disk(encrypted.begin(), encrypted.end()); require(disk.find("private snippet") == disk.npos, "baseline encrypted on disk"); // Exercise real Credential Manager with an isolated test target, never a user endpoint. config.username = L"ks-sync-test-" + std::to_wstring(GetCurrentProcessId()); saveSyncPassword(config, L"test-only-password"); int logins{}, revokes{}, reads{}, writes{}; unsigned readStatus = 200, writeStatus = 200; bool deleted = false; auto transport = [&](const std::wstring &method, const std::wstring &path, const std::wstring &token, const Bytes &body) -> SyncHttpResponse { if (path == L"/v1/auth/userpass/login/" + config.username) { require(method == L"POST" && token.empty(), "login route and no bearer token"); require(parse(body)["password"] == "test-only-password", "password sourced from credential manager"); ++logins; return {200, bytes({{"auth", {{"client_token", "test-token"}}}})}; } require(token == L"test-token", "authenticated requests use returned token"); if (path == L"/v1/auth/token/revoke-self") { require(method == L"POST", "revoke method"); ++revokes; return {204, {}}; } require(path == L"/v1/kswitcher/data/personal/settings", "KV v2 data route"); if (method == L"GET") { ++reads; if (readStatus != 200 && !deleted) return {readStatus, bytes({{"errors", Json::array()}})}; return {readStatus, bytes({{"data", { {"metadata", {{"version", 7}, {"deletion_time", deleted ? "2026-01-01" : ""}, {"destroyed", false}}}, {"data", {{"syncSchema", 1}, {"settings", j}}}}}})}; } require(method == L"POST", "write method"); auto request = parse(body); require(request["options"]["cas"] == 7, "write includes exact CAS revision"); require(request["data"]["syncSchema"] == 2 && request["data"]["settings"] == j, "versioned settings envelope"); ++writes; return {writeStatus, bytes({{"data", {{"version", 8}}}})}; }; { auto bao = makeSyncProvider(config, transport); auto doc = bao->read(); require(doc.revision == 7 && parse(doc.settings) == j, "read protocol response"); require(bao->write(base, 7) == 8, "write protocol response"); readStatus = 404; require(bao->read().revision == 0, "missing profile"); deleted = true; rejects([&] { bao->read(); }, "soft deleted is not treated as absent"); deleted = false; readStatus = 403; rejects([&] { bao->read(); }, "permission denied is not absent"); readStatus = 302; rejects([&] { bao->read(); }, "redirect response rejected"); writeStatus = 400; rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated"); } require(logins == 1 && revokes == 1 && reads == 5 && writes == 2, "temporary login lifecycle"); forgetSyncPassword(config); forgetSyncPassword(config); ++checks; std::filesystem::remove_all(root); std::cout << checks << " sync checks passed\n"; return 0; } catch (const std::exception &e) { std::cerr << e.what() << "\n"; return 1; } }