Release v0.6.53: synchronize chat language profiles

This commit is contained in:
agrynco committed 2026-11-06 12:39:00 +02:00
1 parent ede4ce47c0
commit 727dc96908
11 files changed
+115 -22

No files matched your search

+73
View File
@@ -70,6 +70,79 @@ int main()
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);
{
const std::wstring chat=L"telegram\nРобота / ~test";
const auto chatKey=utf8(chat);
Settings chats=local;
chats.chatCycles[chat]={L"win:00000422",L"win:00000409"};
chats.chatCycles[L"telegram-web\nРобота / ~test"]={L"win:00000409"};
chats.chatCycles[L"ms-teams\nTeam"]={L"win:00000422"};
chats.chatCycles[L"viber\nSaved inactive"]={L"win:00000409"};
auto shared=sharedSyncSettings(chats);
require(restoreSyncSettings(shared,local,{},true).chatCycles==chats.chatCycles,
"sync transfers all chat applications, Unicode names and ordered layout IDs even when not installed");
auto legacy=j; legacy.erase("chatCycles");
require(restoreSyncSettings(bytes(legacy),chats,{},true).chatCycles==chats.chatCycles,
"legacy profile without chat field preserves existing profiles");
require(restoreSyncSettings(base,chats,{},true).chatCycles.empty(),
"explicit empty chat map clears synchronized profiles");
auto l=j,r=j;
l["chatCycles"][chatKey]=Json::array({"win:00000422","win:00000409"});
r["chatCycles"]["ms-teams\nOther"]=Json::array({"win:00000409"});
auto first=mergeSyncProfile({},bytes(l),bytes(r));
require(first.conflicts.empty() && parse(first.local)["chatCycles"].size()==2 && first.local==first.remote,
"first sync unions profiles for different chats");
auto ancestor=first.local;
l=r=parse(ancestor);
l["chatCycles"][chatKey]=Json::array({"win:00000409","win:00000422"});
r["chatCycles"]["ms-teams\nOther"]=Json::array({"win:00000422"});
auto independent=mergeSyncProfile(ancestor,bytes(l),bytes(r));
require(independent.conflicts.empty() && parse(independent.local)["chatCycles"]==
Json({{chatKey,l["chatCycles"][chatKey]},{"ms-teams\nOther",r["chatCycles"]["ms-teams\nOther"]}}),
"independent chat edits merge without losing language order");
r["chatCycles"][chatKey]=Json::array({"win:00000422"});
auto conflict=mergeSyncProfile(ancestor,bytes(l),bytes(r));
require(conflict.conflicts.size()==1 && conflict.conflicts[0].key.find("~1")!=std::string::npos &&
conflict.conflicts[0].label.find(L"Chat languages: telegram / ")==0,
"same-chat edit conflict has readable name and escaped pointer");
auto again=mergeSyncProfile(conflict.base,conflict.local,conflict.remote);
require(again.conflicts.size()==1,"chat conflict survives persisted merge ancestor");
auto resolved=mergeSyncProfile(again.base,again.local,again.remote,{{again.conflicts[0],false}});
require(resolved.conflicts.empty() && parse(resolved.local)["chatCycles"][chatKey]==r["chatCycles"][chatKey],
"choose remote languages for one chat");
l=r=parse(ancestor); l["chatCycles"].erase(chatKey);
auto removed=mergeSyncProfile(ancestor,bytes(l),bytes(r));
require(removed.conflicts.empty() && !restoreSyncSettings(removed.local,local,{},true).chatCycles.contains(chat),
"chat deletion propagates and merged profile remains loadable");
r["chatCycles"][chatKey]=Json::array({"win:00000409"});
auto deletion=mergeSyncProfile(ancestor,bytes(l),bytes(r));
require(deletion.conflicts.size()==1,"chat deletion versus edit requires a choice");
auto erase=mergeSyncProfile(deletion.base,deletion.local,deletion.remote,{{deletion.conflicts[0],true}});
require(erase.conflicts.empty() && !parse(erase.remote)["chatCycles"].contains(chatKey),
"choosing deletion removes chat without leaving null entries");
auto stale=mergeSyncProfile(ancestor,bytes(l),bytes(legacy));
require(stale.conflicts.empty() && parse(stale.local)["chatCycles"]==l["chatCycles"],
"older remote missing chat field cannot delete or resurrect local chat profiles");
auto validate=[&](const Bytes &value){restoreSyncSettings(value,chats,{},true);};
FakeProvider provider; provider.remote={1,bytes(legacy)};
auto download=synchronize(provider,{},shared,SyncAction::Download,validate,{},true);
require(provider.writes==0 && restoreSyncSettings(download.settings,local,{},true).chatCycles==chats.chatCycles,
"download from older server profile preserves chat settings");
provider.remote={1,base};
download=synchronize(provider,{},shared,SyncAction::Download,validate,{},true);
require(restoreSyncSettings(download.settings,local,{},true).chatCycles.empty(),
"download explicit empty chat map removes local profiles");
provider.remote={1,bytes(legacy)};
auto upload=synchronize(provider,{},shared,SyncAction::Upload,validate,{},true);
require(provider.writes==1 && restoreSyncSettings(provider.remote.settings,local,{},true).chatCycles==chats.chatCycles,
"upload publishes chat profiles while keeping PC layout settings separate");
auto invalid=j; invalid["chatCycles"][chatKey]=Json::array();
rejects([&]{restoreSyncSettings(bytes(invalid),local,{},true);},"reject empty synchronized chat cycle");
invalid["chatCycles"][chatKey]=Json::array({"not-a-layout"});
rejects([&]{restoreSyncSettings(bytes(invalid),local,{},true);},"reject invalid synchronized layout ID");
invalid["chatCycles"]=Json::object({{"unknown\nChat",Json::array({"win:00000409"})}});
rejects([&]{restoreSyncSettings(bytes(invalid),local,{},true);},"reject unknown synchronized chat application");
}
{
Settings scoped = local;
scoped.snippets.push_back({L"test", L"Only Teams", L"teams-scope", {L"ms-teams"}});