* refactor: split Linux egui host from shared backend * fix: keep CI contracts portable across runners * fix(ci): preserve CRLF while removing qwen dependency * fix(ci): regenerate target lockfile and install ripgrep * fix(ci): gate desktop-only selection voice module * ci: limit native build parallelism on cross-platform runners * fix(host): expose blocking task seam to coordinator * fix(settings): avoid macOS default hotkey collision * docs: record cross-platform CI evidence * ci: expose Linux validation package artifact * ci: grant reusable Linux package permissions * refactor: remove legacy coordinator dictation facade * ci: skip unsupported MSI prerelease builds * ci: install minisign only for signed Linux releases * fix: keep test-only coordinator session helper * refactor: move style preview diagnostics into core * fix: import shared preferences type from core * refactor: centralize enabled vocabulary projection * ci: install ripgrep for Linux package gates * test: make source contract line-ending agnostic * fix: keep Linux desktop entry standards compliant * docs: expose core style diagnostics contract * ci: resolve Linux package version in app directory * refactor: centralize ASR vocabulary priority * fix: use shared vocabulary note constant * refactor: keep vocabulary priority inside core * test: retain starting-stop queue coverage * ci: scope Linux updater URL to the current repository * docs: close implementation and artifact evidence * docs: record final CI artifact evidence * fix(android): align updater plugin versions * fix(android): pin updater crate exactly * refactor(core): remove unused type anchor * docs: record final artifact runs * fix(remote-input): preserve beta status fields * fix(tauri): finish beta host merge * fix(asr): pass scoped operation to native qwen adapter * fix(macos): bundle mlx metallib as a resource * test(remote-input): update status wire contract * feat: share Less Computer voice flow with Linux * refactor(core): centralize host-independent runtime policy * refactor(core): complete 2.0 backend consolidation * fix(ci): prepare Sherpa after cache restore * fix(macos): route native model paths through Core store * fix(macos): use ModelStore catalog API * fix(ci): serialize macOS native test build * test(ci): preserve macOS cargo diagnostics * fix(macos): release input source lock before await * test(core): replace legacy hotkey injection gate * test(core): move Codex sandbox contract into Core * fix(core): close final runtime policy gaps * fix(windows): terminate coding agent process trees * fix(core): complete local model migration * fix(credentials): align Core LLM model account * fix(core): restore 1.x parity across shared backend * fix(android): complete shared backend stubs * fix: close cross-platform migration review gaps * fix: complete migration lifecycles and platform compatibility * docs: define desktop 2.0 acceptance and Linux handoff * fix: close desktop parity and session lifecycle gaps * fix: finish voice lifecycle and desktop feedback parity * fix: close desktop cancellation and feedback race gaps * fix: honor proxy settings and reject stale session effects * fix: dispatch native cleanup on host runtimes and order events * fix: keep optional recording effects best effort * fix: close desktop agent model and QA lifecycle gaps * fix: preserve desktop raw voice and QA turn ownership * fix: finalize desktop dictation and QA feedback
98 lines
5.1 KiB
C++
98 lines
5.1 KiB
C++
// Exercise the real plugin entry points with two in-process native input
|
|
// contexts. No display, user keyboard, clipboard contents or DBus service is
|
|
// required; only the input-context boundary is replaced by a recording client.
|
|
#include "openless.cpp"
|
|
#include <cassert>
|
|
#include <filesystem>
|
|
#include <unistd.h>
|
|
|
|
class RecordingInputContext final : public fcitx::InputContext {
|
|
public:
|
|
explicit RecordingInputContext(fcitx::InputContextManager &manager)
|
|
: InputContext(manager, "openless-contract") { created(); }
|
|
~RecordingInputContext() override { destroy(); }
|
|
const char *frontend() const override { return "contract"; }
|
|
std::vector<std::string> committed;
|
|
protected:
|
|
void commitStringImpl(const std::string &text) override { committed.push_back(text); }
|
|
void deleteSurroundingTextImpl(int, unsigned int) override {}
|
|
void forwardKeyImpl(const fcitx::ForwardKeyEvent &) override {}
|
|
void updatePreeditImpl() override {}
|
|
};
|
|
|
|
namespace fcitx {
|
|
struct OpenLessInputTargetContract {
|
|
static void select(OpenLess &plugin, InputContext &context) { plugin.selectionIc_ = &context; }
|
|
static void type(OpenLess &plugin, InputContext &context) { plugin.savedIc_ = &context; }
|
|
};
|
|
}
|
|
|
|
int main() {
|
|
const auto config = std::filesystem::temp_directory_path() /
|
|
("openless-fcitx-contract-" + std::to_string(getpid()));
|
|
std::filesystem::create_directory(config);
|
|
setenv("XDG_CONFIG_HOME", config.c_str(), 1);
|
|
{
|
|
char name[] = "openless-contract";
|
|
char disabled[] = "--disable=all";
|
|
char *arguments[] = {name, disabled, nullptr};
|
|
fcitx::Instance instance(2, arguments);
|
|
instance.initialize();
|
|
fcitx::OpenLess plugin(&instance);
|
|
RecordingInputContext first(instance.inputContextManager());
|
|
fcitx::OpenLessInputTargetContract::select(plugin, first);
|
|
first.surroundingText().setText("foo foo", 3, 0);
|
|
assert(plugin.captureSelectionTarget("selection") == "foo");
|
|
// Identical text at a different position is a different selection.
|
|
// Comparing only the selected string would corrupt the wrong range.
|
|
first.surroundingText().setCursor(7, 4);
|
|
assert(!plugin.applySelectionTarget("selection", "foo", "replacement"));
|
|
assert(first.committed.empty());
|
|
first.surroundingText().setCursor(3, 3);
|
|
assert(!plugin.applySelectionTarget("selection", "foo", "replacement"));
|
|
// Changing unselected context or invalidating the native snapshot must
|
|
// also invalidate Apply, even while PRIMARY still contains "foo".
|
|
first.surroundingText().setText("foo bar", 3, 0);
|
|
assert(!plugin.applySelectionTarget("selection", "foo", "replacement"));
|
|
first.surroundingText().invalidate();
|
|
assert(!plugin.applySelectionTarget("selection", "foo", "replacement"));
|
|
first.surroundingText().setText("foo foo", 3, 0);
|
|
// The QA -> preview handoff moves the captured values, never the live
|
|
// SurroundingText object. The old ticket must become unusable.
|
|
assert(plugin.rekeySelectionTarget("selection", "preview"));
|
|
assert(!plugin.applySelectionTarget("selection", "foo", "replacement"));
|
|
assert(plugin.applySelectionTarget("preview", "foo", "replacement"));
|
|
assert(first.committed == std::vector<std::string>{"replacement"});
|
|
// A client reports its new surrounding text after commit. Undo still
|
|
// uses the captured offsets after the ticket has been transferred.
|
|
first.surroundingText().setText("replacement foo", 11, 11);
|
|
assert(plugin.revertSelectionTarget("preview"));
|
|
assert(first.committed.back() == "foo");
|
|
assert(!plugin.revertSelectionTarget("preview"));
|
|
|
|
RecordingInputContext second(instance.inputContextManager());
|
|
fcitx::OpenLessInputTargetContract::type(plugin, first);
|
|
assert(plugin.captureDictationTarget("dictation"));
|
|
fcitx::OpenLessInputTargetContract::type(plugin, second);
|
|
assert(plugin.commitDictationTarget("dictation", "original target"));
|
|
assert(first.committed.back() == "original target");
|
|
assert(second.committed.empty());
|
|
assert(plugin.cancelDictationTarget("dictation"));
|
|
assert(!plugin.commitDictationTarget("dictation", "late write"));
|
|
|
|
{
|
|
RecordingInputContext destroyed(instance.inputContextManager());
|
|
destroyed.surroundingText().setText("original", 8, 0);
|
|
fcitx::OpenLessInputTargetContract::type(plugin, destroyed);
|
|
fcitx::OpenLessInputTargetContract::select(plugin, destroyed);
|
|
assert(plugin.captureDictationTarget("destroyed-dictation"));
|
|
assert(plugin.captureSelectionTarget("destroyed-selection") == "original");
|
|
}
|
|
// Destruction emits the actual InputContextDestroyed event. Both
|
|
// ticket maps must drop the raw handle before either late write runs.
|
|
assert(!plugin.commitDictationTarget("destroyed-dictation", "late write"));
|
|
assert(!plugin.applySelectionTarget("destroyed-selection", "original", "late write"));
|
|
}
|
|
std::filesystem::remove_all(config);
|
|
}
|