From f8026945d1732f315f617baef33dc692f561f3c8 Mon Sep 17 00:00:00 2001 From: jrtxio Date: Mon, 19 Oct 2026 13:49:54 +0800 Subject: [PATCH 1/6] test: enforce cross-platform product acceptance --- .github/workflows/acceptance.yml | 107 ++++++++++++++++++ AGENTS.md | 8 +- linux/GeneratedBackend.hpp | 19 ++++ linux/src/main.cpp | 2 + .../Sources/RivetHost/MainWindowView.swift | 3 + racket/autarx/update.rkt | 2 +- racket/autarx/vendor.rkt | 2 +- racket/tests/app-update-test.rkt | 10 +- rivet-acceptance.rktd | 63 +++++++++++ scripts/acceptance/package.rkt | 26 +++++ scripts/e2e/e2e-linux.sh | 31 +++++ scripts/e2e/e2e-macos.sh | 22 ++++ scripts/e2e/e2e-windows.ps1 | 72 ++++++++++++ scripts/e2e/linux-atspi.py | 57 ++++++++++ scripts/e2e/macos-ax.swift | 41 +++++++ windows/GeneratedBackend.hpp | 19 ++++ windows/MainWindow.xaml | 2 + windows/MainWindow.xaml.cpp | 5 +- 18 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/acceptance.yml create mode 100644 rivet-acceptance.rktd create mode 100644 scripts/acceptance/package.rkt create mode 100644 scripts/e2e/e2e-linux.sh create mode 100644 scripts/e2e/e2e-macos.sh create mode 100644 scripts/e2e/e2e-windows.ps1 create mode 100644 scripts/e2e/linux-atspi.py create mode 100644 scripts/e2e/macos-ax.swift diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml new file mode 100644 index 0000000..753cdc4 --- /dev/null +++ b/.github/workflows/acceptance.yml @@ -0,0 +1,107 @@ +name: Product Acceptance + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +env: + RIVET_PIN: de1fb8997539d34ba029064f500f6903b955b5a7 + +jobs: + windows: + name: Windows packaged WinUI acceptance + runs-on: windows-latest + timeout-minutes: 35 + steps: + - uses: actions/checkout@v4 + - uses: Bogdanp/setup-racket@v1.15 + with: + version: '9.3' + architecture: x64 + distribution: full + variant: CS + - name: Install pinned dependencies and link core + shell: pwsh + run: | + raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#$env:RIVET_PIN" crypto + raco pkg install --auto --no-docs --name autarx-core --link racket + - name: Run executable product acceptance + run: raco rivet acceptance --json + - uses: actions/upload-artifact@v4 + if: always() + with: + name: autarx-acceptance-windows + path: .rivet/acceptance/ + if-no-files-found: warn + + macos: + name: macOS packaged SwiftUI acceptance (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + arch: arm64 + - runner: macos-15-intel + arch: x64 + steps: + - uses: actions/checkout@v4 + - uses: Bogdanp/setup-racket@v1.15 + with: + version: '9.3' + architecture: ${{ matrix.arch }} + distribution: full + variant: CS + - name: Install pinned dependencies and link core + run: | + raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}" crypto + raco pkg install --auto --no-docs --name autarx-core --link racket + - name: Run executable product acceptance + run: raco rivet acceptance --json + - uses: actions/upload-artifact@v4 + if: always() + with: + name: autarx-acceptance-macos-${{ matrix.arch }} + path: .rivet/acceptance/ + if-no-files-found: warn + + linux: + name: Linux packaged GTK4 acceptance + runs-on: ubuntu-22.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - uses: Bogdanp/setup-racket@v1.15 + with: + version: '9.3' + architecture: x64 + distribution: full + variant: CS + - name: Install native and accessibility dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake libgtk-4-dev libglib2.0-dev libsecret-1-dev \ + liblz4-dev zlib1g-dev libncurses-dev weston at-spi2-core python3-pyatspi + - name: Install pinned dependencies and link core + run: | + raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}" crypto + raco pkg install --auto --no-docs --name autarx-core --link racket + - name: Remember full Racket command path + run: echo "FULL_RACKET_BIN=$(dirname "$(command -v raco)")" >> "$GITHUB_ENV" + - name: Set up embeddable Racket CS + uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@de1fb8997539d34ba029064f500f6903b955b5a7 + - name: Run executable product acceptance + run: PATH="$FULL_RACKET_BIN:$PATH" dbus-run-session -- raco rivet acceptance --json + - uses: actions/upload-artifact@v4 + if: always() + with: + name: autarx-acceptance-linux + path: .rivet/acceptance/ + if-no-files-found: warn diff --git a/AGENTS.md b/AGENTS.md index 9cbaf6f..54e0587 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,9 @@ racket racket/autarx/backend.rkt # RVT1 transport (GUI/dev) # 5) Large-file benchmark (R0 stop-rule reproduction) racket scripts/gen-bench-ecuc.rkt -o "$TMPDIR/autarx-bench" -n 50000 + +# 6) Current-platform product acceptance (contract → RVT1 → native → package → UI) +raco rivet acceptance --json ``` ## Contracts (read before changing behaviour; change both sides together) @@ -80,5 +83,8 @@ autarx/ - Unknown AUTOSAR element types degrade to `unknown` — never a parse failure. Ambiguity is a first-class outcome, never silently resolved. - Rivet framework gaps go upstream (issue/PR to turinglambdaai/rivet); - this repo pins a commit. + this repo pins immutable commit `de1fb8997539d34ba029064f500f6903b955b5a7`. +- Agent 不得以单元测试、编译或截图单独宣称产品完成;涉及 GUI 的改动必须让 + `raco rivet acceptance --json` 在声明支持的平台通过,并保留原生控件交互与 + 后端结果证据。 - `raco test racket/` must stay green on every change. diff --git a/linux/GeneratedBackend.hpp b/linux/GeneratedBackend.hpp index 8461362..d1b4f05 100644 --- a/linux/GeneratedBackend.hpp +++ b/linux/GeneratedBackend.hpp @@ -1,6 +1,7 @@ // Generated by Rivet. Do not edit by hand. #pragma once +#include #include #include #include @@ -182,40 +183,58 @@ class API { std::future check_updates() { auto rivet_call = backend_.call("check_updates", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateCheck { return detail::decode_UpdateCheck(rivet_call.get()); }); } [[nodiscard]] std::uint64_t check_updates_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("check_updates", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateCheck(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional check_updates_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("check_updates", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateCheck(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future close_workspace() { auto rivet_call = backend_.call("close_workspace", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> bool { return detail::decode_Bool(rivet_call.get()); }); } [[nodiscard]] std::uint64_t close_workspace_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("close_workspace", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Bool(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional close_workspace_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("close_workspace", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Bool(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future communication() { auto rivet_call = backend_.call("communication", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> CommunicationDto { return detail::decode_CommunicationDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t communication_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("communication", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_CommunicationDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional communication_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("communication", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_CommunicationDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future diff_workspaces(std::string before, std::string after) { auto rivet_call = backend_.call("diff_workspaces", rivet::Value::List{detail::encode_String(before), detail::encode_String(after)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t diff_workspaces_async(std::string before, std::string after, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("diff_workspaces", rivet::Value::List{detail::encode_String(before), detail::encode_String(after)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional diff_workspaces_within(std::string before, std::string after, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("diff_workspaces", rivet::Value::List{detail::encode_String(before), detail::encode_String(after)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> find_objects(std::string pattern) { auto rivet_call = backend_.call("find_objects", rivet::Value::List{detail::encode_String(pattern)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_SemanticObjectDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t find_objects_async(std::string pattern, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("find_objects", rivet::Value::List{detail::encode_String(pattern)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_SemanticObjectDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> find_objects_within(std::string pattern, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("find_objects", rivet::Value::List{detail::encode_String(pattern)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_SemanticObjectDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> get_refs(std::string name_or_path) { auto rivet_call = backend_.call("get_refs", rivet::Value::List{detail::encode_String(name_or_path)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_ReferenceDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t get_refs_async(std::string name_or_path, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("get_refs", rivet::Value::List{detail::encode_String(name_or_path)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_ReferenceDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> get_refs_within(std::string name_or_path, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("get_refs", rivet::Value::List{detail::encode_String(name_or_path)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_ReferenceDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future impact_for_ecu(std::string before, std::string after, std::string ecu_name) { auto rivet_call = backend_.call("impact_for_ecu", rivet::Value::List{detail::encode_String(before), detail::encode_String(after), detail::encode_String(ecu_name)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t impact_for_ecu_async(std::string before, std::string after, std::string ecu_name, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("impact_for_ecu", rivet::Value::List{detail::encode_String(before), detail::encode_String(after), detail::encode_String(ecu_name)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional impact_for_ecu_within(std::string before, std::string after, std::string ecu_name, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("impact_for_ecu", rivet::Value::List{detail::encode_String(before), detail::encode_String(after), detail::encode_String(ecu_name)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> list_ecus() { auto rivet_call = backend_.call("list_ecus", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_SemanticObjectDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t list_ecus_async(std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("list_ecus", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_SemanticObjectDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> list_ecus_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("list_ecus", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_SemanticObjectDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> list_objects(std::string kind) { auto rivet_call = backend_.call("list_objects", rivet::Value::List{detail::encode_String(kind)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_SemanticObjectDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t list_objects_async(std::string kind, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("list_objects", rivet::Value::List{detail::encode_String(kind)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_SemanticObjectDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> list_objects_within(std::string kind, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("list_objects", rivet::Value::List{detail::encode_String(kind)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_SemanticObjectDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future open_workspace(std::string path) { auto rivet_call = backend_.call("open_workspace", rivet::Value::List{detail::encode_String(path)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> WorkspaceSummary { return detail::decode_WorkspaceSummary(rivet_call.get()); }); } [[nodiscard]] std::uint64_t open_workspace_async(std::string path, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("open_workspace", rivet::Value::List{detail::encode_String(path)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_WorkspaceSummary(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional open_workspace_within(std::string path, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("open_workspace", rivet::Value::List{detail::encode_String(path)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_WorkspaceSummary(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future patch_apply(std::string workspace, std::string ops_json) { auto rivet_call = backend_.call("patch_apply", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t patch_apply_async(std::string workspace, std::string ops_json, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("patch_apply", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional patch_apply_within(std::string workspace, std::string ops_json, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("patch_apply", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future patch_plan(std::string workspace, std::string ops_json) { auto rivet_call = backend_.call("patch_plan", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t patch_plan_async(std::string workspace, std::string ops_json, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("patch_plan", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional patch_plan_within(std::string workspace, std::string ops_json, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("patch_plan", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future start_download() { auto rivet_call = backend_.call("start_download", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> void { detail::decode_Void(rivet_call.get()); }); } [[nodiscard]] std::uint64_t start_download_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("start_download", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + bool start_download_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("start_download", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); rivet_done->set_value(); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return false; } rivet_future.get(); return true; } std::future trace_from(std::string name_or_path, std::int64_t depth) { auto rivet_call = backend_.call("trace_from", rivet::Value::List{detail::encode_String(name_or_path), detail::encode_Int64(depth)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> TraceDto { return detail::decode_TraceDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t trace_from_async(std::string name_or_path, std::int64_t depth, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("trace_from", rivet::Value::List{detail::encode_String(name_or_path), detail::encode_Int64(depth)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_TraceDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional trace_from_within(std::string name_or_path, std::int64_t depth, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("trace_from", rivet::Value::List{detail::encode_String(name_or_path), detail::encode_Int64(depth)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_TraceDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> unresolved_references() { auto rivet_call = backend_.call("unresolved_references", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_ReferenceDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t unresolved_references_async(std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("unresolved_references", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_ReferenceDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> unresolved_references_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("unresolved_references", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_ReferenceDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_state() { auto rivet_call = backend_.call("update_state", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateState { return detail::decode_UpdateState(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_state_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_state", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateState(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_state_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_state", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateState(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> validate_workspace() { auto rivet_call = backend_.call("validate_workspace", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_String_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t validate_workspace_async(std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("validate_workspace", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_String_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> validate_workspace_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("validate_workspace", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_String_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future workspace_summary() { auto rivet_call = backend_.call("workspace_summary", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> WorkspaceSummary { return detail::decode_WorkspaceSummary(rivet_call.get()); }); } [[nodiscard]] std::uint64_t workspace_summary_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("workspace_summary", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_WorkspaceSummary(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional workspace_summary_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("workspace_summary", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_WorkspaceSummary(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } private: rivet::linux_runtime::Backend& backend_; diff --git a/linux/src/main.cpp b/linux/src/main.cpp index fea417e..5c43e29 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -1721,6 +1721,8 @@ ListPage make_list_page(GtkStack* stack, char const* list_css_class) { GtkWidget* make_nav_row(char const* title, Section section) { auto* row = gtk_list_box_row_new(); + gtk_accessible_update_property(GTK_ACCESSIBLE(row), + GTK_ACCESSIBLE_PROPERTY_LABEL, title, -1); auto* label = gtk_label_new(title); gtk_label_set_xalign(GTK_LABEL(label), 0.0f); gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label); diff --git a/macos-host/Sources/RivetHost/MainWindowView.swift b/macos-host/Sources/RivetHost/MainWindowView.swift index b5ab9bc..78d26ac 100644 --- a/macos-host/Sources/RivetHost/MainWindowView.swift +++ b/macos-host/Sources/RivetHost/MainWindowView.swift @@ -14,6 +14,7 @@ struct MainWindowView: View { Label(section.rawValue, systemImage: icon(for: section)) } .buttonStyle(.plain) + .accessibilityIdentifier("Nav\(section.rawValue)") } .navigationTitle("Autarx") .frame(minWidth: 190) @@ -61,6 +62,7 @@ struct WorkspaceSummaryView: View { Text(row.1).textSelection(.enabled) } } + .accessibilityIdentifier("WorkspaceSummary") } else { ContentUnavailableView( "No workspace", @@ -116,6 +118,7 @@ struct ObjectListView: View { object.absolute_path == selectedPath ? Color.accentColor.opacity(0.15) : Color.clear) } + .accessibilityIdentifier("ObjectList") } .inspector(isPresented: .constant(selectedObject != nil)) { RefInspector(object: selectedObject, refs: detail) diff --git a/racket/autarx/update.rkt b/racket/autarx/update.rkt index fb25e37..5f0ec60 100644 --- a/racket/autarx/update.rkt +++ b/racket/autarx/update.rkt @@ -366,7 +366,7 @@ (define (strip-trailing-slash s) (if (and (> (string-length s) 1) - (char=? (string-ref s (- (string-length s) 1)) #\/)) + (memq (string-ref s (- (string-length s) 1)) '(#\/ #\\))) (substring s 0 (- (string-length s) 1)) s)) diff --git a/racket/autarx/vendor.rkt b/racket/autarx/vendor.rkt index 55aac07..69a7d56 100644 --- a/racket/autarx/vendor.rkt +++ b/racket/autarx/vendor.rkt @@ -84,7 +84,7 @@ (let-values ([(base _dir _) (split-path (string->path full-project-abs))]) (define dir (if (path? base) (path->string base) ".")) (if (and (> (string-length dir) 1) - (char=? (string-ref dir (- (string-length dir) 1)) #\/)) + (memq (string-ref dir (- (string-length dir) 1)) '(#\/ #\\))) (substring dir 0 (- (string-length dir) 1)) dir))) (vendor-invocation diff --git a/racket/tests/app-update-test.rkt b/racket/tests/app-update-test.rkt index c16a310..29842a6 100644 --- a/racket/tests/app-update-test.rkt +++ b/racket/tests/app-update-test.rkt @@ -40,19 +40,21 @@ ;; per-directory read so a packaged regression trips here, not in rivet's ;; launch smoke. (test-case "app-info candidates cover every packaged layout" - (define exe-dir (string->path "/tmp/Autarx.app/Contents/MacOS/")) + (define base (make-temporary-file "autarx-layout-~a" 'directory)) + (define exe-dir (build-path base "Autarx.app" "Contents" "MacOS")) (define dirs (a:app-info-candidate-dirs exe-dir)) (check-equal? (length dirs) 4) ;; macOS bundle: Contents/MacOS → Contents/Resources/app (check-not-false - (member (simplify-path (string->path "/tmp/Autarx.app/Contents/Resources/app")) + (member (simplify-path (build-path base "Autarx.app" "Contents" "Resources" "app")) dirs) "macOS bundle layout missing") ;; stage root: exe beside app/ (check-not-false - (member (build-path (string->path "/tmp/Autarx.app/Contents/MacOS/") "app") + (member (build-path exe-dir "app") dirs) - "stage-root layout missing")) + "stage-root layout missing") + (delete-directory/files base)) (test-case "app-info version reads the packaged bundle layout" (define base (make-temporary-file "autarx-bundle-~a" 'directory)) diff --git a/rivet-acceptance.rktd b/rivet-acceptance.rktd new file mode 100644 index 0000000..2794f58 --- /dev/null +++ b/rivet-acceptance.rktd @@ -0,0 +1,63 @@ +#hasheq( + (version . 1) + (requirements + . (#hasheq((kind . contract) (platforms . (windows macos linux))) + #hasheq((kind . bridge) (platforms . (windows macos linux))) + #hasheq((kind . native-build) (platforms . (windows macos linux))) + #hasheq((kind . package) (platforms . (windows macos linux))) + #hasheq((kind . native-ui) (platforms . (windows macos linux))))) + (suites + . (#hasheq((id . core-contract) + (kind . contract) + (platforms . (windows macos linux)) + (command . ("raco" "test" "racket/")) + (timeout-seconds . 900)) + #hasheq((id . embedded-wire) + (kind . bridge) + (platforms . (windows macos linux)) + (command . ("raco" "test" "racket/tests/rvt1-smoke.rkt")) + (timeout-seconds . 300)) + #hasheq((id . native-host) + (kind . native-build) + (platforms . (windows macos linux)) + (command . ("raco" "rivet" "build")) + (timeout-seconds . 1800)) + #hasheq((id . windows-package) + (kind . package) + (platforms . (windows)) + (command . ("racket" "scripts/acceptance/package.rkt")) + (timeout-seconds . 1800) + (evidence . (".rivet/acceptance/windows/package.json"))) + #hasheq((id . macos-package) + (kind . package) + (platforms . (macos)) + (command . ("racket" "scripts/acceptance/package.rkt")) + (timeout-seconds . 1800) + (evidence . (".rivet/acceptance/macos/package.json"))) + #hasheq((id . linux-package) + (kind . package) + (platforms . (linux)) + (command . ("racket" "scripts/acceptance/package.rkt")) + (timeout-seconds . 1800) + (evidence . (".rivet/acceptance/linux/package.json"))) + #hasheq((id . windows-native-ui) + (kind . native-ui) + (platforms . (windows)) + (command . ("pwsh" "-NoProfile" "-File" "scripts/e2e/e2e-windows.ps1")) + (timeout-seconds . 600) + (evidence . (".rivet/acceptance/windows/interaction-trace.json" + ".rivet/acceptance/windows/accessibility.json"))) + #hasheq((id . macos-native-ui) + (kind . native-ui) + (platforms . (macos)) + (command . ("bash" "scripts/e2e/e2e-macos.sh")) + (timeout-seconds . 600) + (evidence . (".rivet/acceptance/macos/interaction-trace.json" + ".rivet/acceptance/macos/accessibility.json"))) + #hasheq((id . linux-native-ui) + (kind . native-ui) + (platforms . (linux)) + (command . ("bash" "scripts/e2e/e2e-linux.sh")) + (timeout-seconds . 600) + (evidence . (".rivet/acceptance/linux/interaction-trace.json" + ".rivet/acceptance/linux/accessibility.json")))))) diff --git a/scripts/acceptance/package.rkt b/scripts/acceptance/package.rkt new file mode 100644 index 0000000..af51bf3 --- /dev/null +++ b/scripts/acceptance/package.rkt @@ -0,0 +1,26 @@ +#lang racket/base + +(require json racket/file racket/path racket/system) + +(define platform + (case (system-type 'os) + [(windows) "windows"] + [(macosx) "macos"] + [(unix) "linux"] + [else (error 'autarx-acceptance "unsupported platform")])) +(define raco + (or (find-executable-path "raco") + (error 'autarx-acceptance "raco was not found"))) +(unless (system* raco "rivet" "package" "--skip-launch-smoke") + (error 'autarx-acceptance "raco rivet package failed")) +(define output-directory + (build-path (current-directory) ".rivet" "acceptance" platform)) +(make-directory* output-directory) +(call-with-output-file (build-path output-directory "package.json") + #:exists 'truncate/replace + (lambda (output) + (write-json (hash 'platform platform + 'package-verified #t + 'source "raco rivet package --skip-launch-smoke") + output) + (newline output))) diff --git a/scripts/e2e/e2e-linux.sh b/scripts/e2e/e2e-linux.sh new file mode 100644 index 0000000..915c2a2 --- /dev/null +++ b/scripts/e2e/e2e-linux.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +output="$repo/.rivet/acceptance/linux" +executable="$(find "$repo/dist" -type f -name RivetHost -perm -u+x -print -quit)" +fixture="$repo/shared/fixtures/OemDelivery" +mkdir -p "$output" +[[ -n "$executable" && -x "$executable" ]] +export HOME="$output/home-$$" +export AUTARX_OPEN="$fixture" +export XDG_RUNTIME_DIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/autarx-acceptance-runtime-$$" +export WAYLAND_DISPLAY=wayland-autarx-acceptance +export GDK_BACKEND=wayland GTK_A11Y=atspi NO_AT_BRIDGE=0 LANG=C.UTF-8 +unset DISPLAY +mkdir -p "$HOME" "$XDG_RUNTIME_DIR"; chmod 700 "$XDG_RUNTIME_DIR" +weston --backend=headless-backend.so --socket="$WAYLAND_DISPLAY" --idle-time=0 --debug --log="$output/weston.log" & +weston_pid=$!; app_pid=""; atspi_pid="" +stop_bounded() { local pid="$1"; kill -TERM "$pid" 2>/dev/null || true; for _ in $(seq 1 200); do kill -0 "$pid" 2>/dev/null || return 0; sleep 0.05; done; kill -KILL "$pid" 2>/dev/null || true; return 1; } +cleanup() { local status=$?; trap - EXIT; [[ -n "$app_pid" ]] && kill -0 "$app_pid" 2>/dev/null && stop_bounded "$app_pid" || true; [[ -n "$atspi_pid" ]] && kill -0 "$atspi_pid" 2>/dev/null && kill -TERM "$atspi_pid" || true; kill -TERM "$weston_pid" 2>/dev/null || true; wait "$app_pid" "$atspi_pid" "$weston_pid" 2>/dev/null || true; exit "$status"; } +trap cleanup EXIT +for _ in $(seq 1 100); do [[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]] && break; kill -0 "$weston_pid" 2>/dev/null; sleep 0.05; done +[[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]] +launcher="$(command -v at-spi-bus-launcher || true)"; [[ -n "$launcher" ]] || launcher=/usr/libexec/at-spi-bus-launcher +"$launcher" --launch-immediately >"$output/at-spi-bus.log" 2>&1 & atspi_pid=$! +for _ in $(seq 1 100); do gdbus call --session --dest org.a11y.Bus --object-path /org/a11y/bus --method org.a11y.Bus.GetAddress >/dev/null 2>&1 && break; kill -0 "$atspi_pid" 2>/dev/null; sleep 0.05; done +export AT_SPI_BUS_ADDRESS="$(gdbus call --session --dest org.a11y.Bus --object-path /org/a11y/bus --method org.a11y.Bus.GetAddress | sed -E "s/^\('([^']+)',\)$/\1/")" +cd "$repo"; "$executable" >"$output/application.log" 2>&1 & app_pid=$! +/usr/bin/python3 "$repo/scripts/e2e/linux-atspi.py" --output "$output" +stop_bounded "$app_pid" || true; wait "$app_pid" 2>/dev/null || true; app_pid=""; trap - EXIT +kill -TERM "$atspi_pid" "$weston_pid" 2>/dev/null || true; wait "$atspi_pid" "$weston_pid" 2>/dev/null || true diff --git a/scripts/e2e/e2e-macos.sh b/scripts/e2e/e2e-macos.sh new file mode 100644 index 0000000..4b9c145 --- /dev/null +++ b/scripts/e2e/e2e-macos.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +output="$repo/.rivet/acceptance/macos" +app="$repo/dist/autarx.app/Contents/MacOS/autarx" +driver="$output/macos-ax" +home="$output/home-$$" +fixture="$repo/shared/fixtures/OemDelivery" +mkdir -p "$output" "$home" +[[ -x "$app" ]] +swiftc "$repo/scripts/e2e/macos-ax.swift" -o "$driver" -framework AppKit -framework ApplicationServices +HOME="$home" AUTARX_OPEN="$fixture" "$app" >"$output/application.log" 2>&1 & +app_pid=$! +stop_bounded() { local pid="$1"; kill -TERM "$pid" 2>/dev/null || true; for _ in $(seq 1 200); do kill -0 "$pid" 2>/dev/null || return 0; sleep 0.05; done; kill -KILL "$pid" 2>/dev/null || true; return 1; } +cleanup() { local status=$?; trap - EXIT; if kill -0 "$app_pid" 2>/dev/null; then stop_bounded "$app_pid" || true; wait "$app_pid" 2>/dev/null || true; fi; exit "$status"; } +trap cleanup EXIT +"$driver" --pid "$app_pid" --output "$output" +for _ in $(seq 1 200); do kill -0 "$app_pid" 2>/dev/null || break; sleep 0.05; done +if kill -0 "$app_pid" 2>/dev/null; then echo "Autarx did not exit after Quit" >&2; exit 1; fi +wait "$app_pid" +trap - EXIT diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1 new file mode 100644 index 0000000..2ba50e2 --- /dev/null +++ b/scripts/e2e/e2e-windows.ps1 @@ -0,0 +1,72 @@ +$ErrorActionPreference = 'Stop' + +Add-Type -AssemblyName UIAutomationClient +Add-Type -AssemblyName UIAutomationTypes + +$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$output = Join-Path $repo '.rivet\acceptance\windows' +$fixture = (Resolve-Path -LiteralPath (Join-Path $repo 'shared\fixtures\OemDelivery')).Path +$executable = Get-ChildItem -LiteralPath (Join-Path $repo 'dist') -Filter RivetHost.exe -File -Recurse | Select-Object -First 1 +if (-not $executable) { throw 'packaged Autarx executable was not found' } +New-Item -ItemType Directory -Force -Path $output | Out-Null + +function Wait-Until { + param([string]$Description, [scriptblock]$Probe, [int]$TimeoutSeconds = 60) + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + while ([DateTime]::UtcNow -lt $deadline) { + $value = & $Probe + if ($null -ne $value -and $value -ne $false) { return $value } + Start-Sleep -Milliseconds 100 + } + throw "timed out waiting for $Description" +} +function Find-ById($Root, [string]$Id) { + $condition = [System.Windows.Automation.PropertyCondition]::new([System.Windows.Automation.AutomationElement]::AutomationIdProperty, $Id) + $Root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition) +} +function Find-ByName($Root, [string]$Name) { + $condition = [System.Windows.Automation.PropertyCondition]::new([System.Windows.Automation.AutomationElement]::NameProperty, $Name) + $Root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition) +} + +$process = $null +$savedOpen = $env:AUTARX_OPEN +try { + $env:AUTARX_OPEN = $fixture + $process = Start-Process -FilePath $executable.FullName -PassThru -WorkingDirectory $repo ` + -RedirectStandardOutput (Join-Path $output 'application.stdout.log') ` + -RedirectStandardError (Join-Path $output 'application.stderr.log') + $desktop = [System.Windows.Automation.AutomationElement]::RootElement + $window = Wait-Until 'Autarx window' { + $condition = [System.Windows.Automation.PropertyCondition]::new([System.Windows.Automation.AutomationElement]::ProcessIdProperty, $process.Id) + $desktop.FindFirst([System.Windows.Automation.TreeScope]::Children, $condition) + } + [void](Wait-Until 'workspace result from embedded backend' { + $status = Find-ById $window 'StatusText' + if ($status -and $status.Current.Name -match 'objects') { return $true } + return $false + }) + $objects = Wait-Until 'Objects navigation item' { Find-ByName $window 'Objects' } + while ($objects -and $objects.Current.ControlType -ne [System.Windows.Automation.ControlType]::ListItem) { + $objects = [System.Windows.Automation.TreeWalker]::ControlViewWalker.GetParent($objects) + } + if (-not $objects) { throw 'Objects list item was not exposed to UI Automation' } + $selection = $objects.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern) + ([System.Windows.Automation.SelectionItemPattern]$selection).Select() + [void](Wait-Until 'VehicleSpeed object rendered after navigation' { if (Find-ByName $window 'VehicleSpeed') { return $true }; return $false }) + + $nodes = $window.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) + $snapshot = for ($index = 0; $index -lt [Math]::Min($nodes.Count, 500); $index++) { + $node = $nodes[$index] + [ordered]@{ name = $node.Current.Name; controlType = $node.Current.ControlType.ProgrammaticName; automationId = $node.Current.AutomationId } + } + $snapshot | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $output 'accessibility.json') -Encoding utf8 + [ordered]@{ platform = 'windows'; artifact = $executable.FullName; actions = @('select:Objects'); assertions = @('packaged WinUI window activated','fixture opened through RVT1','Objects navigation exposed through UI Automation','VehicleSpeed rendered from embedded backend') } | + ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'interaction-trace.json') -Encoding utf8 + $window.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close() + [void](Wait-Until 'orderly process exit' { $process.Refresh(); if ($process.HasExited) { return $true }; return $false } 15) +} finally { + $env:AUTARX_OPEN = $savedOpen + if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue; $process.WaitForExit() } + if ($process) { $process.Dispose() } +} diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py new file mode 100644 index 0000000..b564801 --- /dev/null +++ b/scripts/e2e/linux-atspi.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import argparse, json, time +from pathlib import Path +import pyatspi + +def children(node): + for index in range(node.childCount): + try: yield node.getChildAtIndex(index) + except Exception: continue +def walk(node): + yield node + for child in children(node): yield from walk(child) +def role(node): + try: return node.getRoleName() + except Exception: return "unknown" +def text(node): + try: + value = node.queryText(); return value.getText(0, value.characterCount) + except Exception: return "" +def contains(root, fragment): return any(fragment in (node.name or "") or fragment in text(node) for node in walk(root)) +def wait_for(description, probe, timeout=60.0): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + value = probe() + if value: return value + time.sleep(0.05) + raise AssertionError(f"timed out waiting for {description}") +def find_application(): + desktop = pyatspi.Registry.getDesktop(0) + for application in children(desktop): + if any(role(node) == "frame" and (node.name or "") == "Autarx" for node in walk(application)): return application + return None +def press(node): + action = node.queryAction() + if action.nActions < 1 or not action.doAction(0): raise AssertionError(f"AT-SPI action failed for {node.name!r}") +def snapshot(node, depth=0): + result = {"name": node.name or "", "role": role(node)}; value = text(node) + if value: result["text"] = value + if depth < 8: result["children"] = [snapshot(child, depth + 1) for child in children(node)] + return result + +def main(): + parser = argparse.ArgumentParser(); parser.add_argument("--output", type=Path, required=True); args = parser.parse_args(); args.output.mkdir(parents=True, exist_ok=True) + application = None + try: + application = wait_for("Autarx application", find_application) + window = wait_for("Autarx frame", lambda: next((node for node in walk(application) if role(node) == "frame"), None)) + wait_for("workspace result from embedded backend", lambda: contains(window, "OemDelivery") and contains(window, "objects")) + objects = wait_for("Objects navigation row", lambda: next((node for node in walk(window) if (node.name or "") == "Objects" and role(node) == "list item"), None)) + press(objects) + wait_for("VehicleSpeed object rendered after navigation", lambda: contains(window, "VehicleSpeed")) + (args.output / "accessibility.json").write_text(json.dumps(snapshot(application), ensure_ascii=False, indent=2), encoding="utf-8") + (args.output / "interaction-trace.json").write_text(json.dumps({"platform":"linux","actions":["press:Objects"],"assertions":["packaged GTK4 frame activated on native Wayland","fixture opened through RVT1","Objects navigation exposed through AT-SPI","VehicleSpeed rendered from embedded backend"]}, indent=2), encoding="utf-8") + except Exception: + if application is not None: (args.output / "accessibility-failure.json").write_text(json.dumps(snapshot(application), ensure_ascii=False, indent=2), encoding="utf-8") + raise +if __name__ == "__main__": main() diff --git a/scripts/e2e/macos-ax.swift b/scripts/e2e/macos-ax.swift new file mode 100644 index 0000000..8d7cca3 --- /dev/null +++ b/scripts/e2e/macos-ax.swift @@ -0,0 +1,41 @@ +import AppKit +import ApplicationServices +import Foundation + +enum DriverError: Error, CustomStringConvertible { + case failure(String) + var description: String { switch self { case .failure(let message): return message } } +} +func attribute(_ element: AXUIElement, _ name: String) -> CFTypeRef? { var value: CFTypeRef?; return AXUIElementCopyAttributeValue(element, name as CFString, &value) == .success ? value : nil } +func stringAttribute(_ element: AXUIElement, _ name: String) -> String { attribute(element, name) as? String ?? "" } +func children(_ element: AXUIElement) -> [AXUIElement] { attribute(element, kAXChildrenAttribute) as? [AXUIElement] ?? [] } +func walk(_ element: AXUIElement) -> [AXUIElement] { [element] + children(element).flatMap(walk) } +func waitFor(_ description: String, timeout: TimeInterval = 60, probe: () -> T?) throws -> T { let deadline = Date().addingTimeInterval(timeout); while Date() < deadline { if let value = probe() { return value }; Thread.sleep(forTimeInterval: 0.05) }; throw DriverError.failure("timed out waiting for \(description)") } +func contains(_ root: AXUIElement, fragment: String) -> Bool { walk(root).contains { element in [kAXTitleAttribute, kAXValueAttribute, kAXDescriptionAttribute].contains { stringAttribute(element, $0).contains(fragment) } } } +func findButton(_ root: AXUIElement, names: Set) -> AXUIElement? { walk(root).first { element in stringAttribute(element, kAXRoleAttribute) == (kAXButtonRole as String) && [kAXTitleAttribute, kAXDescriptionAttribute, kAXIdentifierAttribute].contains { attributeName in names.contains(stringAttribute(element, attributeName)) } } } +func findMenuItem(_ root: AXUIElement, titles: Set) -> AXUIElement? { walk(root).first { stringAttribute($0, kAXRoleAttribute) == (kAXMenuItemRole as String) && titles.contains(stringAttribute($0, kAXTitleAttribute)) } } +func snapshot(_ element: AXUIElement, depth: Int = 0) -> [String: Any] { var result: [String: Any] = ["role": stringAttribute(element, kAXRoleAttribute), "identifier": stringAttribute(element, kAXIdentifierAttribute), "title": stringAttribute(element, kAXTitleAttribute), "label": stringAttribute(element, kAXDescriptionAttribute), "value": stringAttribute(element, kAXValueAttribute)]; if depth < 8 { result["children"] = children(element).map { snapshot($0, depth: depth + 1) } }; return result } +func writeJSON(_ value: Any, to url: URL) throws { try JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted, .sortedKeys]).write(to: url) } + +var diagnosticApplication: AXUIElement? +var diagnosticOutput: URL? +do { + let arguments = CommandLine.arguments + guard let pidIndex = arguments.firstIndex(of: "--pid"), pidIndex + 1 < arguments.count, let pid = Int32(arguments[pidIndex + 1]), let outputIndex = arguments.firstIndex(of: "--output"), outputIndex + 1 < arguments.count else { throw DriverError.failure("usage: macos-ax --pid PID --output DIRECTORY") } + let output = URL(fileURLWithPath: arguments[outputIndex + 1], isDirectory: true); diagnosticOutput = output; try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true) + guard AXIsProcessTrusted() else { throw DriverError.failure("accessibility access is unavailable") } + let application = AXUIElementCreateApplication(pid); diagnosticApplication = application + let window: AXUIElement = try waitFor("Autarx window") { (attribute(application, kAXWindowsAttribute) as? [AXUIElement])?.first } + _ = AXUIElementSetAttributeValue(application, kAXFrontmostAttribute as CFString, kCFBooleanTrue); _ = AXUIElementPerformAction(window, kAXRaiseAction as CFString) + _ = try waitFor("workspace result from embedded backend") { contains(window, fragment: "OemDelivery") ? true : nil } + let objects: AXUIElement = try waitFor("Objects navigation button") { findButton(window, names: ["Objects", "NavObjects"]) } + guard AXUIElementPerformAction(objects, kAXPressAction as CFString) == .success else { throw DriverError.failure("AXPress failed for Objects navigation") } + _ = try waitFor("VehicleSpeed object rendered after navigation") { contains(window, fragment: "VehicleSpeed") ? true : nil } + try writeJSON(snapshot(application), to: output.appendingPathComponent("accessibility.json")) + try writeJSON(["platform": "macos", "actions": ["press:Objects", "press:Quit Autarx"], "assertions": ["packaged SwiftUI window activated", "fixture opened through RVT1", "Objects navigation exposed through Accessibility", "VehicleSpeed rendered from embedded backend"]], to: output.appendingPathComponent("interaction-trace.json")) + let quit: AXUIElement = try waitFor("Quit Autarx menu item") { findMenuItem(application, titles: ["Quit Autarx", "退出 Autarx"]) } + guard AXUIElementPerformAction(quit, kAXPressAction as CFString) == .success else { throw DriverError.failure("AXPress failed for Quit Autarx") } +} catch { + if let application = diagnosticApplication, let output = diagnosticOutput { try? writeJSON(snapshot(application), to: output.appendingPathComponent("accessibility-failure.json")) } + FileHandle.standardError.write(Data("Autarx macOS acceptance failed: \(error)\n".utf8)); exit(1) +} diff --git a/windows/GeneratedBackend.hpp b/windows/GeneratedBackend.hpp index a7f6630..6e686a4 100644 --- a/windows/GeneratedBackend.hpp +++ b/windows/GeneratedBackend.hpp @@ -1,6 +1,7 @@ // Generated by Rivet. Do not edit by hand. #pragma once +#include #include #include #include @@ -182,40 +183,58 @@ class API { std::future check_updates() { auto rivet_call = backend_.call("check_updates", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateCheck { return detail::decode_UpdateCheck(rivet_call.get()); }); } [[nodiscard]] std::uint64_t check_updates_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("check_updates", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateCheck(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional check_updates_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("check_updates", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateCheck(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future close_workspace() { auto rivet_call = backend_.call("close_workspace", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> bool { return detail::decode_Bool(rivet_call.get()); }); } [[nodiscard]] std::uint64_t close_workspace_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("close_workspace", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Bool(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional close_workspace_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("close_workspace", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Bool(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future communication() { auto rivet_call = backend_.call("communication", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> CommunicationDto { return detail::decode_CommunicationDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t communication_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("communication", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_CommunicationDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional communication_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("communication", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_CommunicationDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future diff_workspaces(std::string before, std::string after) { auto rivet_call = backend_.call("diff_workspaces", rivet::Value::List{detail::encode_String(before), detail::encode_String(after)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t diff_workspaces_async(std::string before, std::string after, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("diff_workspaces", rivet::Value::List{detail::encode_String(before), detail::encode_String(after)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional diff_workspaces_within(std::string before, std::string after, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("diff_workspaces", rivet::Value::List{detail::encode_String(before), detail::encode_String(after)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> find_objects(std::string pattern) { auto rivet_call = backend_.call("find_objects", rivet::Value::List{detail::encode_String(pattern)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_SemanticObjectDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t find_objects_async(std::string pattern, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("find_objects", rivet::Value::List{detail::encode_String(pattern)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_SemanticObjectDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> find_objects_within(std::string pattern, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("find_objects", rivet::Value::List{detail::encode_String(pattern)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_SemanticObjectDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> get_refs(std::string name_or_path) { auto rivet_call = backend_.call("get_refs", rivet::Value::List{detail::encode_String(name_or_path)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_ReferenceDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t get_refs_async(std::string name_or_path, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("get_refs", rivet::Value::List{detail::encode_String(name_or_path)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_ReferenceDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> get_refs_within(std::string name_or_path, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("get_refs", rivet::Value::List{detail::encode_String(name_or_path)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_ReferenceDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future impact_for_ecu(std::string before, std::string after, std::string ecu_name) { auto rivet_call = backend_.call("impact_for_ecu", rivet::Value::List{detail::encode_String(before), detail::encode_String(after), detail::encode_String(ecu_name)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t impact_for_ecu_async(std::string before, std::string after, std::string ecu_name, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("impact_for_ecu", rivet::Value::List{detail::encode_String(before), detail::encode_String(after), detail::encode_String(ecu_name)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional impact_for_ecu_within(std::string before, std::string after, std::string ecu_name, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("impact_for_ecu", rivet::Value::List{detail::encode_String(before), detail::encode_String(after), detail::encode_String(ecu_name)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> list_ecus() { auto rivet_call = backend_.call("list_ecus", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_SemanticObjectDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t list_ecus_async(std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("list_ecus", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_SemanticObjectDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> list_ecus_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("list_ecus", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_SemanticObjectDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> list_objects(std::string kind) { auto rivet_call = backend_.call("list_objects", rivet::Value::List{detail::encode_String(kind)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_SemanticObjectDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t list_objects_async(std::string kind, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("list_objects", rivet::Value::List{detail::encode_String(kind)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_SemanticObjectDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> list_objects_within(std::string kind, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("list_objects", rivet::Value::List{detail::encode_String(kind)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_SemanticObjectDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future open_workspace(std::string path) { auto rivet_call = backend_.call("open_workspace", rivet::Value::List{detail::encode_String(path)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> WorkspaceSummary { return detail::decode_WorkspaceSummary(rivet_call.get()); }); } [[nodiscard]] std::uint64_t open_workspace_async(std::string path, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("open_workspace", rivet::Value::List{detail::encode_String(path)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_WorkspaceSummary(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional open_workspace_within(std::string path, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("open_workspace", rivet::Value::List{detail::encode_String(path)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_WorkspaceSummary(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future patch_apply(std::string workspace, std::string ops_json) { auto rivet_call = backend_.call("patch_apply", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t patch_apply_async(std::string workspace, std::string ops_json, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("patch_apply", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional patch_apply_within(std::string workspace, std::string ops_json, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("patch_apply", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future patch_plan(std::string workspace, std::string ops_json) { auto rivet_call = backend_.call("patch_plan", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> DiffDto { return detail::decode_DiffDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t patch_plan_async(std::string workspace, std::string ops_json, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("patch_plan", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_DiffDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional patch_plan_within(std::string workspace, std::string ops_json, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("patch_plan", rivet::Value::List{detail::encode_String(workspace), detail::encode_String(ops_json)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_DiffDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future start_download() { auto rivet_call = backend_.call("start_download", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> void { detail::decode_Void(rivet_call.get()); }); } [[nodiscard]] std::uint64_t start_download_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("start_download", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + bool start_download_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("start_download", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); rivet_done->set_value(); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return false; } rivet_future.get(); return true; } std::future trace_from(std::string name_or_path, std::int64_t depth) { auto rivet_call = backend_.call("trace_from", rivet::Value::List{detail::encode_String(name_or_path), detail::encode_Int64(depth)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> TraceDto { return detail::decode_TraceDto(rivet_call.get()); }); } [[nodiscard]] std::uint64_t trace_from_async(std::string name_or_path, std::int64_t depth, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("trace_from", rivet::Value::List{detail::encode_String(name_or_path), detail::encode_Int64(depth)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_TraceDto(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional trace_from_within(std::string name_or_path, std::int64_t depth, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("trace_from", rivet::Value::List{detail::encode_String(name_or_path), detail::encode_Int64(depth)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_TraceDto(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> unresolved_references() { auto rivet_call = backend_.call("unresolved_references", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_ReferenceDto_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t unresolved_references_async(std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("unresolved_references", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_ReferenceDto_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> unresolved_references_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("unresolved_references", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_ReferenceDto_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_state() { auto rivet_call = backend_.call("update_state", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateState { return detail::decode_UpdateState(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_state_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_state", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateState(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_state_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_state", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateState(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> validate_workspace() { auto rivet_call = backend_.call("validate_workspace", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_String_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t validate_workspace_async(std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("validate_workspace", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_String_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> validate_workspace_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("validate_workspace", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_String_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future workspace_summary() { auto rivet_call = backend_.call("workspace_summary", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> WorkspaceSummary { return detail::decode_WorkspaceSummary(rivet_call.get()); }); } [[nodiscard]] std::uint64_t workspace_summary_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("workspace_summary", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_WorkspaceSummary(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional workspace_summary_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("workspace_summary", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_WorkspaceSummary(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } private: rivet::windows::Backend& backend_; diff --git a/windows/MainWindow.xaml b/windows/MainWindow.xaml index 83c1247..5af6533 100644 --- a/windows/MainWindow.xaml +++ b/windows/MainWindow.xaml @@ -171,6 +171,7 @@ @@ -299,6 +300,7 @@ Date: Mon, 19 Oct 2026 14:02:11 +0800 Subject: [PATCH 2/6] ci: install supported CMake for Linux acceptance --- .github/workflows/acceptance.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 753cdc4..ce79e92 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -89,6 +89,7 @@ jobs: sudo apt-get update sudo apt-get install -y cmake libgtk-4-dev libglib2.0-dev libsecret-1-dev \ liblz4-dev zlib1g-dev libncurses-dev weston at-spi2-core python3-pyatspi + python3 -m pip install --user --disable-pip-version-check cmake==3.31.6 - name: Install pinned dependencies and link core run: | raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}" crypto @@ -98,7 +99,7 @@ jobs: - name: Set up embeddable Racket CS uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@de1fb8997539d34ba029064f500f6903b955b5a7 - name: Run executable product acceptance - run: PATH="$FULL_RACKET_BIN:$PATH" dbus-run-session -- raco rivet acceptance --json + run: PATH="$HOME/.local/bin:$FULL_RACKET_BIN:$PATH" dbus-run-session -- raco rivet acceptance --json - uses: actions/upload-artifact@v4 if: always() with: From 0e81fc36a8eec752fa91fe7a30d197b7f6758090 Mon Sep 17 00:00:00 2001 From: jrtxio Date: Mon, 19 Oct 2026 14:36:31 +0800 Subject: [PATCH 3/6] ci: use Ubuntu 24.04 for GTK acceptance --- .github/workflows/acceptance.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index ce79e92..df6e581 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -74,7 +74,7 @@ jobs: linux: name: Linux packaged GTK4 acceptance - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 40 steps: - uses: actions/checkout@v4 @@ -89,7 +89,6 @@ jobs: sudo apt-get update sudo apt-get install -y cmake libgtk-4-dev libglib2.0-dev libsecret-1-dev \ liblz4-dev zlib1g-dev libncurses-dev weston at-spi2-core python3-pyatspi - python3 -m pip install --user --disable-pip-version-check cmake==3.31.6 - name: Install pinned dependencies and link core run: | raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}" crypto @@ -99,7 +98,7 @@ jobs: - name: Set up embeddable Racket CS uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@de1fb8997539d34ba029064f500f6903b955b5a7 - name: Run executable product acceptance - run: PATH="$HOME/.local/bin:$FULL_RACKET_BIN:$PATH" dbus-run-session -- raco rivet acceptance --json + run: PATH="$FULL_RACKET_BIN:$PATH" dbus-run-session -- raco rivet acceptance --json - uses: actions/upload-artifact@v4 if: always() with: From 2c6f0b9d8d52b90383be102713a4eef933b1630e Mon Sep 17 00:00:00 2001 From: jrtxio Date: Mon, 19 Oct 2026 15:10:20 +0800 Subject: [PATCH 4/6] fix: expose Linux sidebar activation --- linux/src/main.cpp | 11 +++++++++++ scripts/e2e/linux-atspi.py | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 5c43e29..41f0b23 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -1728,6 +1728,17 @@ GtkWidget* make_nav_row(char const* title, Section section) { gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label); g_object_set_data(G_OBJECT(row), "section", GINT_TO_POINTER(static_cast(section))); + // Expose the same navigation behavior through GTK's activate action so + // screen readers and AT-SPI automation can operate the sidebar, not only + // pointer-driven ListBox selection. + g_signal_connect( + row, "activate", + G_CALLBACK(+[](GtkListBoxRow* activated, gpointer) { + auto const selected = static_cast
(GPOINTER_TO_INT( + g_object_get_data(G_OBJECT(activated), "section"))); + select_section(selected); + }), + nullptr); return row; } diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index b564801..e6dfd48 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -32,7 +32,10 @@ def find_application(): return None def press(node): action = node.queryAction() - if action.nActions < 1 or not action.doAction(0): raise AssertionError(f"AT-SPI action failed for {node.name!r}") + if action.nActions < 1: raise AssertionError(f"AT-SPI exposes no action for {node.name!r}") + # GTK queues accessible actions asynchronously and may return False even + # when activation was dispatched. Verify the resulting page instead. + action.doAction(0) def snapshot(node, depth=0): result = {"name": node.name or "", "role": role(node)}; value = text(node) if value: result["text"] = value From d56e4ce86267c6150059f5d25350619195aec583 Mon Sep 17 00:00:00 2001 From: jrtxio Date: Mon, 19 Oct 2026 18:45:17 +0800 Subject: [PATCH 5/6] fix: select Linux navigation through AT-SPI --- scripts/e2e/linux-atspi.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index e6dfd48..6249e75 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -31,11 +31,16 @@ def find_application(): if any(role(node) == "frame" and (node.name or "") == "Autarx" for node in walk(application)): return application return None def press(node): - action = node.queryAction() - if action.nActions < 1: raise AssertionError(f"AT-SPI exposes no action for {node.name!r}") - # GTK queues accessible actions asynchronously and may return False even - # when activation was dispatched. Verify the resulting page instead. - action.doAction(0) + # GtkListBox exposes its semantic navigation through the Selection + # interface. Selecting the child drives the same row-selected signal as + # a pointer click; the optional row action is a second accessibility path. + parent = node.parent + parent.querySelection().selectChild(node.getIndexInParent()) + try: + action = node.queryAction() + if action.nActions > 0: action.doAction(0) + except Exception: + pass def snapshot(node, depth=0): result = {"name": node.name or "", "role": role(node)}; value = text(node) if value: result["text"] = value From 9c8723d0cbe7777ee9f901781f630ac0f4b71184 Mon Sep 17 00:00:00 2001 From: jrtxio Date: Mon, 19 Oct 2026 19:46:32 +0800 Subject: [PATCH 6/6] ci: align acceptance with Rivet v0.12.0 --- .github/workflows/acceptance.yml | 4 ++-- AGENTS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index df6e581..1fcf961 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -10,7 +10,7 @@ permissions: contents: read env: - RIVET_PIN: de1fb8997539d34ba029064f500f6903b955b5a7 + RIVET_PIN: v0.12.0 jobs: windows: @@ -96,7 +96,7 @@ jobs: - name: Remember full Racket command path run: echo "FULL_RACKET_BIN=$(dirname "$(command -v raco)")" >> "$GITHUB_ENV" - name: Set up embeddable Racket CS - uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@de1fb8997539d34ba029064f500f6903b955b5a7 + uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@v0.12.0 - name: Run executable product acceptance run: PATH="$FULL_RACKET_BIN:$PATH" dbus-run-session -- raco rivet acceptance --json - uses: actions/upload-artifact@v4 diff --git a/AGENTS.md b/AGENTS.md index 54e0587..2cbf012 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ autarx/ - Unknown AUTOSAR element types degrade to `unknown` — never a parse failure. Ambiguity is a first-class outcome, never silently resolved. - Rivet framework gaps go upstream (issue/PR to turinglambdaai/rivet); - this repo pins immutable commit `de1fb8997539d34ba029064f500f6903b955b5a7`. + this repo pins the reviewed `v0.12.0` release across CI, acceptance, and release. - Agent 不得以单元测试、编译或截图单独宣称产品完成;涉及 GUI 的改动必须让 `raco rivet acceptance --json` 在声明支持的平台通过,并保留原生控件交互与 后端结果证据。