Testing, bundling and the updater
Freddy's app does a lot now. Before he gives it to his team he wants three things: tests that run the real pipeline without a window, an app his colleagues can double-click, and a way to send them the next version.
Tests through the real pipeline
elyra::testing runs the app's commands through the container,
providers, middleware and validation, without opening a window:
use elyra::testing::TestApp;
#[tokio::test]
async fn a_note_needs_a_title() {
let app = TestApp::new(app_for_tests());
let errors = app.invoke_validation_errors("notes_store", (input_without_title,))
.await.expect("a validation bag");
assert!(errors.contains_key("title"));
}
invoke_ok, invoke_err and invoke_err_kind
cover the rest, the last one returning a kind from chapter 3 or 11 such as
offline or queued; assert_emitted checks the
event bus; and chapter 7's app.live(..) waits for a pushed result.
Fakes are Laravel's Queue::fake(), Storage::fake() and
Event::fake(), with Notifier::fake() and
Store::fake() beside them. App::swap puts one in place after
every provider has registered, so the production wiring stays as it is and the fake
still wins:
let app = TestApp::new(
App::new()
.provider(StorageProvider::at(data_dir))
.provider(QueueProvider::new())
.swap(Storage::fake())
.swap(Queue::fake())
.commands(commands![export_all]),
);
app.invoke_ok::<()>("export_all", ()).await;
app.get::<Storage>().assert_exists("exports/train-ideas.md");
Chapter 4's dyn Exporter fakes the same way, with
swap_as. A failed assertion names what did happen and points at
Freddy's test line, not into the framework. For the Laravel half,
Http::fake(HttpFake) answers HTTP from a table, and the backend guide
shows a signed-in app, PusherFake for broadcasting, a write saved with
no network, and a notification.
An app to double-click
rata build builds the frontend and a release binary with it embedded.
rata bundle then packages it for the platform it runs on, from
[bundle] in elyra.toml:
[bundle]
identifier = "com.example.freddynotes"
name = "Freddy Notes"
version = "1.0.0"
deep_link = "freddynotes" # registers freddynotes:// with the OS
| Host | What rata bundle makes |
|---|---|
| macOS | target/release/bundle/Freddy Notes.app, ad-hoc code-signed so it launches locally |
| Linux | a .deb and a portable .tar.gz, with the .desktop entry and icon |
| Windows | a portable folder with the .exe, the icon and a README.txt |
The icon comes from app/public/icon.svg, which rata new
scaffolded, and deep_link is what makes chapter 8's links reach the app.
A bundled app is also what macOS wants for notifications and autostart.
The next version
With the updater feature, the framework wires up the whole flow: a
check, progress events, and a themed update toast in the page with What's
new, Install & restart and Later:
App::new()
.updater(
UpdaterConfig::new(PUBLIC_KEY_B64, "https://notes.example.com/desktop/latest.json",
env!("CARGO_PKG_VERSION"))
.auto_check(true),
)
.allow_frontend(Capability::UpdaterInstall) // enables the toast's install button
The security model mirrors Tauri's: each release is an artifact plus an ed25519
signature, listed in a JSON manifest, and the app ships the matching public key. An
update is only installed after its downloaded bytes verify against that key, so a
compromised release server still cannot push a malicious binary. Manifest and
artifact must be HTTPS, and the download is capped in size. The startup check is off
until auto_check(true), and installing needs a capability that is
opt-in, because it replaces the binary and relaunches.
Two honest notes from the updater page. Generating the keys and signatures and hosting the manifest is Freddy's release pipeline's job. And replacing a code-signed binary invalidates its signature, so an app distributed through Gatekeeper must be re-signed and notarized as part of each release.
Looking back
Freddy started with a notes app in a browser. He now has a compiled desktop app with its own database and screens generated from his models; lists that update themselves; a quick-note window behind a shortcut; settings, a keychain and background jobs; AI in the app and an MCP server for agents; and, as the desktop half of his Laravel app, sign-in with the token in the keychain, changes from the web in his open windows, writes that survive a train ride, and Laravel's notifications on his dock. All of it with the building blocks he knew from Laravel, and none of it with a runtime interpreter.
Where to go from here: the guide has a page for every
part of this course and more, the roadmap says what is
coming, and the changelog says what each release
changed. Upgrading means moving the tag in Cargo.toml and the runtime
tarball in app/package.json together, then rata ai:update
so the coding agent knows the new version too.
What you learned
TestAppruns commands through the real pipeline, andApp::swapputs fakes in place after the providers.rata bundlemakes an ad-hoc signed.app, a.debor a portable folder; Developer ID signing and notarization are the project's own release step.- The updater installs only what verifies against the app's ed25519 key, over HTTPS, with install opt-in.