Release v0.6.28: typo correction, partial sync and selection diagnostics

This commit is contained in:
2026-11-02 11:19:13 +02:00
parent 738c81dce2
commit 55993c0f90
23 changed files with 676 additions and 76 deletions
+79 -1
View File
@@ -54,6 +54,10 @@ int main()
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;
@@ -84,9 +88,49 @@ int main()
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");
rejects([&] { mergeSyncSettings({}, bytes(left), base); }, "first sync needs explicit choice");
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;
@@ -173,6 +217,40 @@ int main()
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<std::wstring>{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");