#include "autocorrect.h" #include "ocr.h" #include "richtext.h" #include "storage.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 { 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 { 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::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; 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::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"{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); 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.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(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; 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"); } // 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; } }