#include "autocorrect.h" #include "history_preview.h" #include "ocr.h" #include "richtext.h" #include "storage.h" #include "diagnostics.h" #include #include #include #include namespace { int passed = 0; void require(bool ok, const char *name) { if (!ok) throw std::runtime_error(name); ++passed; } void rejects(const std::function &f, const char *name) { bool thrown = false; try { { std::map previews; std::set visible; for(int i=0;i<40;++i) previews[std::to_wstring(i)]=i; for(int i=0;i<8;++i) visible.insert(std::to_wstring(i)); ks::trimHistoryPreviews(previews,visible); require(previews.size()==32,"preview cache stays bounded"); for(const auto &id:visible) require(previews.contains(id),"visible preview survives eviction"); for(int i=0;i<40;++i) { auto id=std::to_wstring(i); previews[id]=i; visible.insert(id); } ks::trimHistoryPreviews(previews,visible); require(previews.size()==40,"tall viewport does not thrash visible previews"); visible.clear(); ks::trimHistoryPreviews(previews,visible); require(previews.size()==32,"cache shrinks after leaving tall viewport"); } f(); } catch (...) { thrown = true; } require(thrown, name); } } // namespace int main(int argc, char **argv) { if (argc > 1 && std::string(argv[1]) == "--hidden-start-window") { HWND w = CreateWindowExW(0, L"STATIC", L"KSwitcher visibility regression", WS_POPUP, 0, 0, 1, 1, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); ks::showRequestedWindow(w); bool visible = w && IsWindowVisible(w); if (w) DestroyWindow(w); return visible ? 0 : 1; } try { { ks::DiagnosticJson record{{"attempt", "42"}, {"process", "editor.exe"}, {"command", "Convert / cycle"}, {"source", "English"}, {"target", "Ukrainian"}, {"outcome", "Original text remained"}, {"problem", true}, {"verification", "read-back-via-accessibility"}, {"original", "hello\n\"quoted\""}, {"expected", ks::utf8(L"привіт")}, {"actual", "hello"}, {"context", "copy\npaste"}}; auto encoded = L"2026-10-31 12:00:00 Operation " + ks::wide(record.dump()); auto rows = ks::parseOperationLog(encoded); require(rows.size() == 1 && rows[0].process == L"editor.exe" && rows[0].problem, "structured operation is one filterable row"); require(rows[0].details.find(L"привіт") != std::wstring::npos && rows[0].details.find(L"copy\r\npaste") != std::wstring::npos, "operation details preserve Unicode and normalize multiline text"); record["problem"] = false; record["outcome"] = "Paste verified"; rows = ks::parseOperationLog(encoded + L"\n2026-10-31 12:00:01 Operation " + ks::wide(record.dump())); require(rows.size() == 2 && !rows[1].problem, "verified operations remain distinguishable from problems"); rows = ks::parseOperationLog(L"old incident\n first stage\n second stage\n2026-10-31 Operation {broken"); require(rows.size() == 2 && rows[0].details.find(L"second stage") != std::wstring::npos && rows[1].details.find(L"{broken") != std::wstring::npos, "legacy context is grouped and malformed records remain inspectable"); require(ks::conversionReadbackOutcome(L"new", L"old", L"new") == L"Paste verified", "expected readback verifies paste"); require(ks::conversionReadbackOutcome(L"old", L"old", L"new") == L"Original text remained", "original readback identifies unchanged editor text"); require(ks::conversionReadbackOutcome(L"other", L"old", L"new") == L"Readback differs", "unexpected readback is never reported as success"); } require(ks::diagnosticText(L"word") == L"\"word\" units=4 truncated=0", "diagnostic word preserved"); require(ks::diagnosticText(L"\"\\\n\r\t") == L"\"\\\"\\\\\\u000a\\u000d\\u0009\" units=5 truncated=0", "diagnostic control characters cannot create log lines"); require(ks::diagnosticText(L"\u0457\u200b") == L"\"\\u0457\\u200b\" units=2 truncated=0", "diagnostic unicode and invisible characters preserved exactly"); require(ks::diagnosticText(std::wstring(513, L'a')).find(L"units=513 truncated=1") != std::wstring::npos, "large diagnostic selections are explicitly truncated"); STARTUPINFOW startup{sizeof(startup)}; ks::DiagnosticBuffer diagnostics; require(diagnostics.record(L"successful-attempt", false, 100).empty(), "successful diagnostics stay in memory"); auto incident = diagnostics.record(L"failed-attempt", true, 200); require(incident.find(L"successful-attempt") != std::wstring::npos && incident.find(L"failed-attempt") != std::wstring::npos, "incident includes preceding successful context"); require(diagnostics.record(L"next-failure", true, 300).find(L"successful-attempt") == std::wstring::npos, "flushed context is not duplicated"); diagnostics.record(L"expired-context", false, 400); require(diagnostics.record(L"later-failure", true, 30500).find(L"expired-context") == std::wstring::npos, "context expires after thirty seconds"); diagnostics.record(L"oldest-context", false, 30600); for (int i = 0; i < 40; ++i) diagnostics.record(L"bounded-context", false, 30600 + i); require(diagnostics.record(L"bounded-failure", true, 30700).find(L"oldest-context") == std::wstring::npos, "context buffer has a fixed size"); diagnostics.record(L"disabled-context", false, 30800); diagnostics.clear(); require(diagnostics.record(L"new-failure", true, 30900).find(L"disabled-context") == std::wstring::npos, "clearing diagnostics discards sensitive context"); startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE; PROCESS_INFORMATION child{}; std::wstring command = L"\"" + ks::executable() + L"\" --hidden-start-window"; require(CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &child), "start visibility test with hidden startup state"); DWORD wait = WaitForSingleObject(child.hProcess, 10000), exitCode = 1; GetExitCodeProcess(child.hProcess, &exitCode); CloseHandle(child.hThread); CloseHandle(child.hProcess); require(wait == WAIT_OBJECT_0 && exitCode == 0, "first explicit window request is visible after hidden process launch"); CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); HWND richParent = CreateWindowExW(0, L"STATIC", L"Rich preview test", WS_POPUP, 0, 0, 300, 200, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); HWND rich = ks::createRichText(richParent, 1, false); require(rich != nullptr, "native RichEdit loads without a browser runtime"); auto styled = ks::textClip(L"plain bold italic under\nnext"); std::string html = "Version:1.0\r\n
plain bold " "italic under
next
"; styled.formats.push_back({0, L"HTML Format", ks::Bytes(html.begin(), html.end())}); require(ks::setRichText(rich, ks::richPreview(styled)), "HTML clipboard preview streams into RichEdit"); auto effects = [&](LONG first, LONG last) { CHARRANGE range{first, last}; SendMessageW(rich, EM_EXSETSEL, 0, (LPARAM)&range); CHARFORMAT2W format{}; format.cbSize = sizeof(format); SendMessageW(rich, EM_GETCHARFORMAT, SCF_SELECTION, (LPARAM)&format); return format.dwEffects; }; require((effects(6, 10) & CFE_BOLD) != 0, "HTML bold is displayed as native bold"); require((effects(11, 17) & CFE_ITALIC) != 0, "HTML italic is displayed as native italic"); require((effects(18, 23) & CFE_UNDERLINE) != 0, "HTML underline is displayed natively"); require((effects(0, 5) & (CFE_BOLD | CFE_ITALIC | CFE_UNDERLINE)) == 0, "inline formatting does not leak into plain text"); wchar_t rendered[256]{}; GetWindowTextW(rich, rendered, 256); require(std::wstring(rendered).find(L"\r\nnext") != std::wstring::npos || std::wstring(rendered).find(L"\rnext") != std::wstring::npos, "HTML line breaks are retained"); html = "Привіт & 🌍" ""; styled.formats.back().data = ks::Bytes(html.begin(), html.end()); ks::setRichText(rich, ks::richPreview(styled)); GetWindowTextW(rich, rendered, 256); require(std::wstring(rendered) == L"Привіт & 🌍", "HTML entities and Unicode survive without script text"); require((effects(0, 6) & CFE_BOLD) != 0, "inline CSS bold is rendered"); auto plain = ks::textClip(L"{\\literal} Привіт"); ks::setRichText(rich, ks::richPreview(plain)); GetWindowTextW(rich, rendered, 256); require(std::wstring(rendered) == plain.text(), "plain Unicode and RTF metacharacters are escaped"); std::string rtf = "{\\rtf1\\ansi \\b native}"; plain.formats.push_back({0, L"Rich Text Format", ks::Bytes(rtf.begin(), rtf.end())}); ks::setRichText(rich, ks::richPreview(plain)); require((effects(0, 6) & CFE_BOLD) != 0, "original RTF formatting is preserved"); DestroyWindow(richParent); ks::Layout en = ks::physicalLayout(); en.id = L"en"; ks::Layout ru{nullptr, L"ru", L"Russian", L"ё1234567890-=йцукенгшщзхъфывапролджэячсмитьбю.\\", L"Ё!\"№;%:?*()_+ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,/"}; ks::Layout uk{nullptr, L"uk", L"Ukrainian", L"'1234567890-=йцукенгшщзхїфівапролджєячсмитьбю.\\", L"₴!\"№;%:?*()_+ЙЦУКЕНГШЩЗХЇФІВАПРОЛДЖЄЯЧСМИТЬБЮ,/"}; require(en.normal.size() == ru.normal.size(), "physical maps align"); require(en.normal.size() == uk.normal.size(), "Ukrainian maps align"); require(ks::convert(L"ghbdtn", en, ru) == L"привет", "English to Russian"); require(ks::convert(L"привіт", uk, en) == L"ghbdsn", "Ukrainian to English"); require(ks::convert(L"hello привет", en, ru) == L"руддщ привет", "mixed text preserves target words"); require(ks::convert(L"htdm.dftim", en, ru) == L"ревьюваешь", "punctuation in words"); std::vector layouts{en, ru, uk}; ks::Run suffixRun; suffixRun.keys = L"snipping "; require(suffixRun.matchSuffix(L"зштп ", layouts, en) >= 0, "recent Cyrillic suffix matches physical keys across layouts"); require(suffixRun.matchSuffix(L"ping ", layouts, en) == 0, "recent English suffix matches physical keys"); require(suffixRun.matchSuffix(L"other", layouts, en) < 0 && suffixRun.matchSuffix(L"", layouts, en) < 0 && suffixRun.matchSuffix(L"snipping ", layouts, en) < 0, "suffix recovery rejects unrelated, empty and complete ranges"); suffixRun.overflow = true; require(suffixRun.matchSuffix(L"зштп ", layouts, en) < 0, "suffix recovery rejects overflowed tracking"); suffixRun.overflow = false; suffixRun.keys = L"LinkedIn "; require(suffixRun.matchSuffix(L"лувШт ", layouts, en) >= 0, "ChatGPT partial CamelCase selection matches tracked physical suffix"); require(suffixRun.match(L"ДштлувШт ", layouts, en) >= 0 && suffixRun.match(L"лувШт ", layouts, en) < 0, "CamelCase conversion still requires the complete tracked text after recovery"); ks::AutoCorrector corrector; ks::Settings autoSettings; require(!autoSettings.autoCorrect, "auto-correction defaults off"); autoSettings.cycle = {L"en", L"uk"}; corrector.configure(L"dictionaries", autoSettings.autoLanguages); auto suggested = corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings); require(suggested && suggested->text == L"привіт " && suggested->target == 2, "dictionary corrects Ukrainian and retains trailing space"); suggested = corrector.suggest(L"lsv ", 0, layouts, autoSettings); require(suggested && suggested->text == L"дім " && suggested->target == 2, "three-letter Ukrainian word is corrected"); autoSettings.autoMinLength = 4; require(!corrector.suggest(L"lsv ", 0, layouts, autoSettings), "configured minimum excludes short words"); require(!corrector.suggest(L"(lsv); ", 0, layouts, autoSettings), "punctuation does not count towards minimum length"); autoSettings.autoMinLength = 3; require(!corrector.suggest(L"the ", 0, layouts, autoSettings), "known three-letter English word stays unchanged"); require(!corrector.suggest(L"ls ", 0, layouts, autoSettings), "two-letter input remains below automatic correction minimum"); suggested = corrector.suggest(L"ljvfiyz ", 0, layouts, autoSettings); require(suggested && suggested->text == L"домашня " && suggested->target == 2, "less frequent Ukrainian inflections are eligible for correction"); require(autoSettings.autoMaxRank == 0, "default frequency limit includes entire dictionary"); autoSettings.autoMaxRank = 21944; require(!corrector.suggest(L"ljvfiyz ", 0, layouts, autoSettings), "rank above limit is excluded"); autoSettings.autoMaxRank = 21945; require(corrector.suggest(L"ljvfiyz ", 0, layouts, autoSettings).has_value(), "rank limit is inclusive"); autoSettings.autoMaxRank = 1; require(!corrector.suggest(L"hello ", 0, layouts, autoSettings), "rank limit never disables known-word protection"); autoSettings.autoMaxRank = 0; suggested = corrector.suggest(L"(ljvf ", 0, layouts, autoSettings); require(suggested && suggested->text == L"(дома ", "opening parenthesis is preserved"); for (auto pair : {std::pair{L"(ljvf) ", L"(дома) "}, {L"[ljvf] ", L"[дома] "}, {L"{ljvf} ", L"{дома} "}, {L"«ljvf» ", L"«дома» "}, {L"“ljvf” ", L"“дома” "}, {L"ljvf) ", L"дома) "}, {L"([{ljvf}]), ", L"([{дома}]), "}}) { suggested = corrector.suggest(pair.first, 0, layouts, autoSettings); require(suggested && suggested->text == pair.second, "paired and closing delimiters preserved"); } require(ks::autoClosingSelection(L")] "), "closing-only selection can be extended"); require(!ks::autoClosingSelection(L"word) ") && !ks::autoClosingSelection(L"@ "), "selection extension excludes words and arbitrary punctuation"); require(!corrector.suggest(L"user@(ljvf) ", 0, layouts, autoSettings), "wrapped email suffix is not a word"); autoSettings.autoPrefixChars += L"#"; autoSettings.autoSuffixChars += L"%"; suggested = corrector.suggest(L"#ljvf%; ", 0, layouts, autoSettings); require(suggested && suggested->text == L"#дома%; ", "custom delimiters are preserved"); require(ks::autoClosingSelection(L"%; ", autoSettings), "custom closing selection is supported"); autoSettings.autoPrefixChars.clear(); require(!corrector.suggest(L"(ljvf ", 0, layouts, autoSettings), "removed prefix delimiter is respected"); autoSettings.autoSuffixChars.clear(); require(!corrector.suggest(L"ljvf; ", 0, layouts, autoSettings), "removed suffix delimiter is respected"); autoSettings.autoPrefixChars = ks::Settings{}.autoPrefixChars; autoSettings.autoSuffixChars = ks::Settings{}.autoSuffixChars; ks::Run parenthesized; parenthesized.keys = L"(ljvf "; require(ks::matchAutoInput(parenthesized, L"ljvf ", layouts, en) == 0, "editor selection may omit the opening parenthesis"); require(ks::matchAutoInput(parenthesized, L"(ljvf ", layouts, en) == 0, "editor selection may include the opening parenthesis"); parenthesized.keys = L"OLDljvf "; require(ks::matchAutoInput(parenthesized, L"ljvf ", layouts, en) < 0, "automatic verification never accepts an arbitrary word suffix"); suggested = corrector.suggest(L"Ghbdsn! ", 0, layouts, autoSettings); require(suggested && suggested->text == L"Привіт! ", "title case and punctuation retained"); suggested = corrector.suggest(L"руддщ ", 2, layouts, autoSettings); require(suggested && suggested->text == L"hello ", "dictionary corrects English"); for (auto word : {L"hello ", L"привіт ", L"GHBDSN ", L"ghBdsn ", L"ghbdsn123 ", L"user@ghbdsn ", L"https://ghbdsn ", L"gh ", L"qzxqzxqzx ", L"oldghbdsn "}) require(!corrector.suggest(word, 0, layouts, autoSettings), "unsafe or known word unchanged"); autoSettings.autoNever = {L"ghbdsn"}; require(!corrector.suggest(L"Ghbdsn ", 0, layouts, autoSettings), "personal exception ignores case"); autoSettings.autoNever.clear(); autoSettings.cycle = {L"en"}; require(!corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings), "disabled target layout excluded"); autoSettings.cycle = {L"en", L"uk"}; corrector.reject(L"Ghbdsn; "); require(!corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings), "undo rejection survives punctuation"); require(corrector.memoryBytes() < 3 * 1024 * 1024, "two dictionaries fit within 3 MiB"); require(!ks::autoCorrectionAllowed(nullptr, autoSettings), "missing focused editor fails closed"); ks::AutoCorrector missing; { ks::AutoCorrector supplemental; auto options = autoSettings; options.autoLanguages = {L"en", L"uk", L"ru"}; options.cycle = {L"en", L"uk", L"ru"}; supplemental.configure(L"dictionaries", options.autoLanguages); for (auto [typed, expected] : {std::pair{L"ntcn ", L"тест "}, std::pair{L"vfkj ", L"мало "}}) { auto shared = supplemental.suggest(typed, 0, layouts, options); require(shared && shared->text == expected && shared->target == 2, "identical Russian and Ukrainian spelling uses preferred Ukrainian layout"); } require(!supplemental.suggest(L"lsv ", 0, layouts, options), "different dictionary results remain ambiguous"); auto release = supplemental.suggest(L"релыз ", 1, layouts, options); require(release && release->text == L"реліз " && release->target == 2, "supplemental Ukrainian release word corrects from Russian layout"); require(!supplemental.suggest(L"реліз ", 2, layouts, options), "correct Ukrainian release word stays unchanged"); require(!supplemental.suggest(L"релиз ", 1, layouts, options), "valid Russian release word stays unchanged"); options.autoMaxRank = 50000; require(!supplemental.suggest(L"релыз ", 1, layouts, options), "supplemental vocabulary respects explicit frequency limit"); options.autoWords[L"uk"] = {L"релізик"}; options.autoMaxRank = 1; auto custom = supplemental.suggest(L"релызик ", 1, layouts, options); require(custom && custom->text == L"релізик " && custom->target == 2, "personal word enables correction without editing dictionary files"); require(!supplemental.suggest(L"релізик ", 2, layouts, options), "personal word is protected from correction"); options.autoNever = {L"релызик"}; require(!supplemental.suggest(L"релызик ", 1, layouts, options), "never-correct list takes precedence over personal word"); options.autoNever.clear(); options.autoWords[L"uk"].push_back(L"робить"); auto commaWord = supplemental.suggest(L"hj,bnm ", 0, layouts, options); require(commaWord && commaWord->text == L"робить " && commaWord->target == 2, "personal Ukrainian word accepts a punctuation key that maps to a letter"); require(!supplemental.suggest(L"hj,bnm123 ", 0, layouts, options), "punctuation-key correction still rejects digits"); options.autoMinLength = 1; require(!supplemental.suggest(L"ye ", 0, layouts, options), "known English ye remains protected at minimum length one"); options.cycle = {L"en", L"ru"}; require(!supplemental.suggest(L"релызик ", 1, layouts, options), "personal dictionary respects disabled target layouts"); } rejects([&] { missing.configure(L"missing-dictionaries", {L"en"}); }, "missing dictionary rejected"); require(ks::convertTo(L"ghbdtn", layouts, 1, 0) == L"привет", "hint conversion"); require(ks::convertTo(L"привет", layouts, 1, 1) == L"привет", "same source is unchanged"); require(ks::convertTo(L"?", layouts, 1, 0) == L",", "punctuation conversion"); require(ks::nextConversionLayout(L"рф", layouts, {0, 1, 2}, 1) == 0 && ks::convertTo(L"рф", layouts, 0, 1) == L"ha", "cycle skips unchanged Russian to Ukrainian and wraps to English"); require(ks::nextConversionLayout(L"шььш", layouts, {0, 2, 1}, 2) == 0 && ks::convertTo(L"шььш", layouts, 0, 2) == L"immi", "cycle skips unchanged Ukrainian to Russian"); require(ks::nextConversionLayout(L"і", layouts, {0, 2, 1}, 2) == 1, "cycle keeps the next language when text changes"); require(ks::nextConversionLayout(L"ha", layouts, {0, 1, 2}, 0) == 1, "cycle preserves configured order for changed text"); require(ks::nextConversionLayout(L"рф", layouts, {1, 2}, 1) == 2, "cycle does not use disabled English when all candidates are unchanged"); require(ks::nextConversionLayout(L"123 ", layouts, {0, 1, 2}, 0) == 1, "unchanged digits finish after a bounded cycle"); require(ks::nextConversionLayout(L"рф", layouts, {2, 0}, 1) == 0, "source outside cycle still skips unchanged candidates"); require(ks::cycleCase(L"hello world") == L"Hello World", "title case"); require(ks::cycleCase(L"Hello World") == L"HELLO WORLD", "upper case"); require(ks::cycleCase(L"ПРИВІТ") == L"привіт", "Unicode lower case"); require(ks::transliterate(L"Привет") == L"Privet", "forward transliteration"); require(ks::transliterate(L"Київ") == L"Kyiv", "Ukrainian transliteration"); require(ks::transliterate(L"Shchuka") == L"Щука", "reverse transliteration"); require(ks::parseTemplate(L"a{cursor}b").suffix == L"b", "snippet suffix"); require(ks::parseTemplate(L"{{cursor}}").text == L"{cursor}", "escaped cursor"); require(ks::parseTemplate(L"rider .{enter}").text == L"rider ." && ks::parseTemplate(L"rider .{enter}").enters == 1, "Enter is an action, not inserted text"); require(ks::parseTemplate(L"a{cursor}b{enter}{enter}").suffix == L"b" && ks::parseTemplate(L"a{cursor}b{enter}{enter}").enters == 2, "Enter actions preserve cursor suffix"); require(ks::parseTemplate(L"{{enter}}").text == L"{enter}" && ks::parseTemplate(L"{{enter}}").enters == 0, "escaped Enter is literal"); require(ks::parseTemplate(L"{cursor}{cursor}").cursors == 2, "duplicate cursor detection"); require(ks::Hotkey::parse(L"Ctrl+Alt+Shift+Space")->key == VK_SPACE, "hotkey parse"); require(!ks::Hotkey::parse(L"Ctrl+F25"), "invalid function key"); require(!ks::Hotkey::parse(L"Ctrl+A+B"), "multiple keys rejected"); require(ks::Hotkey::parse(L"")->key == 0, "disabled shortcut"); ks::HotkeyHook hook; std::vector> bindings{ {5, {MOD_WIN, 'V'}}, {1, {MOD_ALT, VK_F12}}, {100, {MOD_CONTROL | MOD_SHIFT, 'U'}}}; require(hook.update('V', true, MOD_WIN, bindings) == 5, "Win+V dispatches configured action"); require(hook.update('V', true, MOD_WIN, bindings) == -1, "held hotkey does not repeat"); require(hook.update('V', false, 0, {}) == -1, "key-up consumed after settings change"); require(hook.update('V', true, MOD_CONTROL, bindings) == 0, "unassigned Ctrl+V unaffected"); require(hook.update('V', true, MOD_WIN | MOD_SHIFT, bindings) == 0, "modifiers match exactly"); require(hook.update('V', true, MOD_WIN, {}) == 0, "disabled binding leaves OS shortcut intact"); require(hook.update(VK_F12, true, MOD_ALT, bindings) == 1, "conversion uses same hook"); require(hook.update('U', true, MOD_CONTROL | MOD_SHIFT, bindings) == 100, "direct layout uses same hook"); require(hook.update(VK_F12, false, 0, bindings) == -1, "independent key release consumed"); require(hook.update('U', false, 0, bindings) == -1, "second key release consumed"); require(hook.update('V', true, MOD_WIN, bindings) == 5, "hotkey activates again after release"); bindings.push_back({2, {MOD_WIN | MOD_SHIFT, 'S'}}); require(hook.update('S', true, MOD_WIN | MOD_SHIFT, bindings) == 2, "Win+Shift+S can replace the Snipping Tool shortcut"); require(hook.update('S', false, 0, bindings) == -1, "Snipping Tool shortcut release consumed"); for (UINT key : {UINT('K'), UINT(VK_F12), UINT(VK_SPACE), UINT(VK_OEM_PLUS), UINT(VK_LEFT), UINT(VK_NUMPAD7)}) { ks::Hotkey recorded{MOD_CONTROL | MOD_ALT | MOD_SHIFT, key}; require(ks::Hotkey::parse(recorded.format()) == recorded, "recorded shortcut roundtrip"); } ks::Run run; run.record(L'g', (HWND)1, (HKL)1, 1); run.record(L'h', (HWND)1, (HKL)1, 2); require(run.match(L"пр", layouts, en) == 1, "recent input verification"); require(run.match(L"xx", layouts, en) == -1, "unrelated selected text rejected"); require(run.match(L"xпр", layouts, en) == -1, "word selection including older text is rejected before replacement"); require(run.match(L"р", layouts, en) == -1, "coalesced Teams selection of only the final character is rejected"); require(run.match(L"пр ", layouts, en) == -1, "word selection including unrecorded whitespace is rejected"); run.record(L'b', (HWND)1, (HKL)1, 2002); require(run.keys == L"b", "pause resets run"); run.record(L'c', (HWND)2, (HKL)1, 2003); require(run.keys == L"c", "window resets run"); run.record(L'd', (HWND)2, (HKL)2, 2004); require(run.keys == L"d", "layout resets run"); run.record(L' ', (HWND)2, (HKL)2, 2005); run.record(L'e', (HWND)2, (HKL)2, 2006); require(run.keys == L"e", "word boundary resets run"); for (int i = 0; i < 513; ++i) run.record(L'x', (HWND)2, (HKL)2, 2007 + i); require(run.overflow && run.keys.empty(), "overflow does not select partial input"); std::map> processes{ {10, {1, L"ms-teams.exe"}}, {20, {10, L"msedgewebview2.exe"}}, {30, {20, L"msedgewebview2.exe"}}, {40, {1, L"msedgewebview2.exe"}}, {50, {10, L"other.exe"}}, {60, {60, L"msedgewebview2.exe"}}}; auto read = [&](DWORD id) -> std::optional> { auto it = processes.find(id); return it == processes.end() ? std::nullopt : std::optional(it->second); }; require(ks::webViewDescendant(30, 10, read), "Teams renderer belongs to host"); require(!ks::webViewDescendant(40, 10, read), "other WebView rejected"); require(!ks::webViewDescendant(50, 10, read), "arbitrary child rejected"); require(!ks::webViewDescendant(60, 10, read), "process cycles rejected"); require(!ks::webViewDescendant(0, 0, read), "unknown process rejected"); auto clip = ks::textClip(L"Привіт 🌍\r\nline 2"); clip.formats.push_back({0, L"HTML Format", {1, 2, 3, 0}}); auto encoded = ks::encodeClip(clip); require(ks::decodeClip(encoded).text() == clip.text(), "clipboard Unicode roundtrip"); require(ks::decodeClip(encoded).formats[1].name == L"HTML Format", "registered format identity"); auto encrypted = ks::protect(encoded); require(encrypted != encoded, "history ciphertext differs"); require(ks::unprotect(encrypted) == encoded, "Windows DPAPI roundtrip"); encrypted[encrypted.size() / 2] ^= 0x40; rejects([&] { ks::unprotect(encrypted); }, "tampering rejected"); encoded.pop_back(); rejects([&] { ks::decodeClip(encoded); }, "truncated clip rejected"); auto temp = std::filesystem::temp_directory_path() / (L"KSwitcher-tests-" + std::to_wstring(GetCurrentProcessId())); std::filesystem::create_directories(temp); { auto folder = temp / L"dictionaries"; std::filesystem::create_directories(folder); auto put = [&](const wchar_t *name, const std::wstring &text) { auto data = ks::utf8(text); ks::atomicWrite(folder / name, ks::Bytes(data.begin(), data.end())); }; put(L"en.txt", L"zoo\t3\nhello\t99\nhello\t1\n"); put(L"uk.txt", L"дім\t3\n"); ks::AutoCorrector edited; edited.configure(folder, {L"en", L"uk"}); auto options = ks::Settings{}; options.cycle = {L"en", L"uk"}; options.autoMaxRank = 1; auto correction = edited.suggest(L"руддщ ", 2, layouts, options); require(correction && correction->text == L"hello ", "unsorted dictionaries and duplicates load using lowest duplicate rank"); } ks::Pixels pixels{2, 1, {10, 20, 30, 255, 40, 50, 60, 255}}; ks::Clip firstImage{{ks::imageDib(pixels), {0, L"Ole Private Data", {1, 2, 3}}}}; auto thumbnail = ks::decodeImage(firstImage, 1, 1); require(thumbnail.width == 1 && thumbnail.height == 1 && thumbnail.bgra.size() == 4, "thumbnail decoding limits the resident pixel buffer"); thumbnail = ks::decodeImage(firstImage, 208, 128); require(thumbnail.width == 2 && thumbnail.height == 1, "thumbnail decoding does not upscale small images"); auto secondImage = firstImage; secondImage.formats[1].data = {4, 5, 6}; secondImage.formats.push_back({0, L"DataObject", {9, 8, 7}}); require(ks::sameClipboardImage(firstImage, secondImage), "OLE metadata does not duplicate an image"); std::reverse(secondImage.formats.begin(), secondImage.formats.end()); require(ks::sameClipboardImage(firstImage, secondImage), "format ordering does not duplicate an image"); auto recoded = firstImage; recoded.formats[0].data[24] = 1; // Different bitmap resolution metadata; identical pixels. require(ks::sameClipboardImage(firstImage, recoded), "decoded image ignores resolution metadata"); pixels.bgra[0]++; ks::Clip changedImage{{ks::imageDib(pixels)}}; require(!ks::sameClipboardImage(firstImage, changedImage), "a changed pixel remains a distinct image"); auto withText = firstImage; withText.formats.push_back(ks::textClip(L"caption").formats.front()); require(!ks::sameClipboardImage(firstImage, withText), "image deduplication preserves accompanying text"); auto withFiles = firstImage; withFiles.formats.push_back({CF_HDROP, L"", {1}}); require(!ks::sameClipboardImage(firstImage, withFiles), "image deduplication preserves file copies"); { ks::History history(temp / L"images"); ks::Settings defaults; history.add(firstImage, L"SnippingTool", defaults); history.pin(history.entries().front().id); history.add(secondImage, L"SnippingTool", defaults); require(history.entries().size() == 1 && history.entries().front().pinned, "Snipping Tool republish retains one pinned history entry"); history.add(changedImage, L"SnippingTool", defaults); require(history.entries().size() == 2, "a new screenshot is retained"); } ks::Settings settings; settings.writeLog = false; settings.elevatedAutostart = true; settings.autoCorrect = true; settings.autoMaxRank = 10000; settings.autoMinLength = 5; settings.autoPrefixChars = L"(#"; settings.autoSuffixChars = L");%"; settings.autoNever = {L"myWord"}; settings.autoWords = {{L"uk", {L"релізик"}}, {L"en", {L"kswitcher"}}}; settings.autoExcluded = {L"Code.exe"}; settings.cycle = {L"en", L"uk"}; settings.snippets = {{L";address", L"Line 1\r\n{cursor}Line 2"}}; ks::saveSettings(temp / L"settings.json", settings); auto restored = ks::loadSettings(temp / L"settings.json", {}); require(restored.autoWords == settings.autoWords, "personal dictionaries survive settings roundtrip"); auto invalidWords = settings; invalidWords.autoWords[L"uk"] = {L"hello"}; rejects([&] { ks::validateSettings(invalidWords); }, "personal dictionary rejects wrong language letters"); require(ks::encodeSettings(ks::decodeSettings(ks::encodeSettings(settings), {})) == ks::encodeSettings(settings), "configurator snapshot survives serialization without changing the concurrency token"); rejects([&] { ks::decodeSettings(ks::Bytes{'{', '}'}, {}); }, "configurator rejects missing settings format"); require(!restored.writeLog, "logging setting persists when disabled"); require(ks::loadSettings(temp / L"defaults.json", {}).writeLog, "logging defaults on for existing diagnostic installations"); require(restored.snippets[0].text == settings.snippets[0].text, "settings snippet roundtrip"); require(restored.cycle == settings.cycle, "layout order roundtrip"); require(restored.elevatedAutostart, "elevated autostart setting roundtrip"); require(restored.autoMaxRank == 10000, "frequency rank limit roundtrip"); require(restored.autoMinLength == 5, "minimum word length roundtrip"); require(ks::loadSettings(temp / L"defaults.json", {}).autoMinLength == 3, "missing minimum length defaults to three"); for (int badLength : {0, 33}) { auto invalidLength = settings; invalidLength.autoMinLength = badLength; rejects([&] { ks::validateSettings(invalidLength); }, "invalid minimum word length rejected"); } require(restored.autoPrefixChars == settings.autoPrefixChars && restored.autoSuffixChars == settings.autoSuffixChars, "custom punctuation settings roundtrip"); auto invalidCharacters = settings; invalidCharacters.autoPrefixChars = L"a"; rejects([&] { ks::validateSettings(invalidCharacters); }, "letters cannot be treated as word delimiters"); require(ks::loadSettings(temp / L"defaults.json", {}).autoMaxRank == 0, "missing frequency limit defaults to all words"); auto invalidRank = settings; invalidRank.autoMaxRank = -1; rejects([&] { ks::validateSettings(invalidRank); }, "negative frequency limit rejected"); require(restored.autoCorrect && restored.autoLanguages == settings.autoLanguages && restored.autoNever == settings.autoNever && restored.autoExcluded == settings.autoExcluded, "auto-correction settings roundtrip"); require(!ks::loadSettings(temp / L"defaults.json", {}).elevatedAutostart, "elevated autostart is opt-in for existing and default settings"); auto invalid = settings; auto captureSettings = settings; captureSettings.screenshot = L"Ctrl+Shift+F9"; require(!captureSettings.screenshotAutoOpenEditor,"notification-first capture is the default"); captureSettings.screenshotAutoOpenEditor=true; require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor, "explicit automatic editor preference survives settings roundtrip"); captureSettings.screenshotAutoOpenEditor=false; require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor, "notification-first preference survives settings roundtrip"); require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshot == captureSettings.screenshot, "screen capture shortcut roundtrip"); captureSettings.screenshot = captureSettings.clipboard; rejects([&] { ks::validateSettings(captureSettings); }, "screen capture shortcut duplicates rejected"); captureSettings.screenshot.clear(); ks::validateSettings(captureSettings); require(ks::loadSettings(temp / L"defaults.json", {}).screenshot == L"Win+Shift+S", "screen capture default for existing settings"); invalid.undo = invalid.toggle; rejects([&] { ks::validateSettings(invalid); }, "duplicate shortcut rejected"); invalid = settings; invalid.snippets[0].text = L"{cursor}{cursor}"; rejects([&] { ks::validateSettings(invalid); }, "invalid snippet rejected"); { ks::History history(temp / L"history"); history.add(clip, L"Test", settings); history.add(clip, L"Test", settings); require(history.entries().size() == 1, "history consecutive deduplication"); auto id = history.entries()[0].id; history.pin(id); require(history.entries()[0].pinned, "pin persisted in memory"); require(history.search(L"line 2").size() == 1, "full text search"); require(history.load(id).text() == clip.text(), "history payload roundtrip"); } { ks::History history(temp / L"history"); require(history.entries().size() == 1 && history.entries()[0].pinned, "history index reload"); rejects([&] { history.load(L"../other"); }, "history path traversal rejected"); history.clear(); require(history.entries().empty(), "clear history"); } { ks::History history(temp / L"search"); history.add(ks::textClip(std::wstring(260, L'x') + L" hidden needle"), L"Test", settings); history.add(ks::textClip(L"first\nsecond\tthird"), L"Test", settings); require(history.search(L"hidden needle").size() == 1, "search beyond truncated preview"); require(history.search(L"first\nsecond").size() == 1, "search retains original newlines"); require(history.search(L"second\tthird").size() == 1, "search retains original tabs"); require(history.search(L"absent").empty(), "short previews reject unmatched queries"); require(history.search(L"FIRST").size() == 1, "short preview search ignores case"); } { auto folder = temp / L"ocr-history"; ks::History history(folder); ks::Clip image{{ks::imageDib(ks::Pixels{2, 2, ks::Bytes(16, 255)})}}; history.add(image, L"OCR test", settings); auto id = history.entries().front().id; require(history.entries().front().ocrState == 1, "new images await background OCR"); require(ks::History(folder).entries().front().ocrState == 1, "pending OCR survives restart"); auto original = ks::encodeClip(history.load(id)); auto beforeBytes = history.entries().front().bytes; require(history.saveOcr(id, L"Розпізнаний текст HELLO", settings), "save image OCR"); require(history.entries().front().bytes > beforeBytes, "OCR storage counts against history size"); require(ks::encodeClip(history.load(id)) == original, "OCR preserves original image formats"); require(!history.saveOcr(id, L"replacement", settings), "completed OCR is not overwritten"); ks::History restored(folder); require(restored.search(L"розпізнаний").size() == 1, "persisted Ukrainian OCR is searchable"); require(restored.ocrText(id) == L"Розпізнаний текст HELLO", "OCR text survives restart"); auto raw = ks::readFile(folder / (id + L".ocr"), 2 * 1024 * 1024); require(std::string(raw.begin(), raw.end()).find("HELLO") == std::string::npos, "OCR sidecar is encrypted"); restored.remove(id); require(!std::filesystem::exists(folder / (id + L".ocr")), "deleting image removes OCR sidecar"); require(!restored.saveOcr(id, L"late result", settings), "late OCR cannot resurrect deleted image"); restored.add(image, L"OCR test", settings); id = restored.entries().front().id; restored.saveOcr(id, L"", settings); require(restored.ocrText(id).has_value() && restored.ocrText(id)->empty(), "empty OCR is a completed result"); restored.clear(); require(!std::filesystem::exists(folder / (id + L".ocr")), "clear removes OCR sidecars"); restored.add(image, L"OCR test", settings); id = restored.entries().front().id; restored.saveOcr(id, std::nullopt, settings); require(ks::History(folder).entries().front().ocrState == 3, "failed OCR is not retried on every search or restart"); restored.saveOcr(id, L"manual retry", settings); require(restored.search(L"manual retry").size() == 1, "explicit retry can recover failed OCR"); restored.add(ks::textClip(L"newer"), L"test", settings); auto limited = settings; limited.maxItems = 1; restored.add(ks::textClip(L"newest"), L"test", limited); require(!std::filesystem::exists(folder / (id + L".ocr")), "pruning image removes OCR sidecar"); } { ks::Settings options; require(!options.useTesseract, "Tesseract is opt-in by default"); options.useTesseract = true; options.tesseractPath = L"C:\\Program Files\\Tesseract-OCR\\tesseract.exe"; options.tesseractLanguages = L"ukr+eng+rus"; auto restored = ks::decodeSettings(ks::encodeSettings(options), {}); require(restored.useTesseract && restored.tesseractPath == options.tesseractPath && restored.tesseractLanguages == options.tesseractLanguages, "OCR settings roundtrip"); options.tesseractLanguages = L"ukr+../eng"; rejects([&] { ks::validateSettings(options); }, "OCR language paths rejected"); options.tesseractLanguages = L"ukr++eng"; rejects([&] { ks::validateSettings(options); }, "empty OCR language rejected"); options.tesseractLanguages = L"ukr+eng"; options.tesseractPath = L"C:\\missing-tesseract\\tesseract.exe"; rejects([&] { ks::validateTesseract(options); }, "missing optional Tesseract reports clear failure"); options.useTesseract = false; ks::validateTesseract(options); require(true, "Windows OCR works without optional installation"); } // Test-owned directory only; never touch the user's clipboard or stored history. std::filesystem::remove_all(temp); CoUninitialize(); std::cout << "Passed " << passed << " native regression checks\n"; return 0; } catch (const std::exception &e) { std::cerr << "FAILED: " << e.what() << '\n'; return 1; } }