Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c6ca9eb
refactor: introduce per-patch lifecycle state machine (types + persis…
eseidel May 4, 2026
6190b50
refactor: add download/install/boot transitions to lifecycle
eseidel May 4, 2026
0b08f18
refactor: replace UpdaterState's PatchManager backend with PatchLifec…
eseidel May 4, 2026
bc13f6b
refactor: cut update_internal over to PatchLifecycle
eseidel May 5, 2026
f55b2e2
refactor: delete patch_manager.rs and download_state.rs
eseidel May 5, 2026
6e396f4
refactor: address self-review feedback on lifecycle PR
eseidel May 5, 2026
0b7f653
refactor: undo two over-corrections from the prior self-review fix
eseidel May 5, 2026
1f21c77
ci: fix warnings-as-errors and cspell
eseidel May 5, 2026
36f25eb
ci: gate PathBuf import to platforms that actually use it
eseidel May 5, 2026
1f761d6
test: cover the gaps surfaced by the coverage report
eseidel May 5, 2026
d23b2d5
ci: add 'keypair' to cspell dictionary
eseidel May 5, 2026
08016a8
test: audit deleted patch_manager tests; restore strict checks
eseidel May 5, 2026
6631c1c
test: port the two patch_manager tests I'd handwaved
eseidel May 5, 2026
75e1ba6
test: port the remaining patch_manager tests with real coverage gaps
eseidel May 5, 2026
96a8db7
refactor: restore separate download directory under OS cache root
eseidel May 5, 2026
612cef0
test: cover the new download_root cleanup paths
eseidel May 5, 2026
e3da960
refactor: targeted wipe + legacy patches_state.json + orphan-download…
eseidel May 5, 2026
30b1a72
test: cover all four branches of cleanup_orphan_downloads
eseidel May 5, 2026
b189390
ci: add 'embedder' to cspell dictionary
eseidel May 5, 2026
d03c4e2
refactor: drop Installed.hash, fix mark_bad ordering, address bdero r…
eseidel May 7, 2026
d4384a0
ci: add 'rollforward' to cspell dictionary
eseidel May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ words:
- dllexport
- dlopen
- downloadables
- EDQUOT
- EACCES
- EDQUOT
- embedder
- embedders
- EOCD
- endtemplate
Expand All @@ -51,6 +52,7 @@ words:
- gclient
- hdpi
- ifdef
- keypair
- libapp
- libc
- libflutter
Expand All @@ -68,7 +70,9 @@ words:
- repr
- reqwest
- rlib
- roundtrips
- rsplit
- rollforward
- rollouts
- RTLD
- rustflags
Expand All @@ -82,9 +86,11 @@ words:
- subosito
- swiftpm
- symbolication
- tombstoned
- ureq
- unbootable
- unbooted
- unparseable
- unreviewable
- unwritable
- usize
Expand Down
99 changes: 99 additions & 0 deletions library/src/c_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,105 @@ mod test {
assert_eq!(shorebird_current_boot_patch_number(), 0);
}

/// Server rolls back patch 1, then later rolls it forward again with
/// the same number and same hash. The pre-lifecycle code added the
/// rolled-back number to `known_bad_patches` permanently for the
/// release, so the rollforward was silently dropped on the device.
/// The lifecycle's `cleanup` is state-aware: a server-driven
/// rollback on a non-`Bad` patch forgets the patch entirely (no
/// tombstone), leaving the number free to be reinstalled.
/// shorebirdtech/shorebird#3728.
#[serial]
#[test]
fn rollforward_after_server_rollback_reinstalls_patch() {
testing_reset_config();
let tmp_dir = TempDir::new().unwrap();

let apk_path = tmp_dir.path().join("base.apk");
write_fake_apk(
apk_path.to_str().unwrap(),
HELLO_TESTS_PATCH.base.as_bytes(),
);
let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so");
let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap());
let c_yaml = c_string("app_id: foo");
assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
free_c_string(c_yaml);
free_parameters(c_params);

// Phase 1: install patch 1 and report a successful launch.
testing_set_network_hooks(
|_url, _request| {
Ok(PatchCheckResponse {
patch_available: true,
patch: Some(crate::Patch {
number: 1,
hash: HELLO_TESTS_PATCH.hash.to_owned(),
download_url: "ignored".to_owned(),
hash_signature: None,
}),
rolled_back_patch_numbers: None,
})
},
|_url, dest: &Path, _resume_from: u64| HELLO_TESTS_PATCH.write_to(dest),
|_url, _event| Ok(()),
);
assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
shorebird_report_launch_start();
shorebird_report_launch_success();
assert_eq!(shorebird_current_boot_patch_number(), 1);

// Phase 2: server rolls patch 1 back with no replacement.
// Phase-1 spawned threads (PatchDownload, PatchInstallSuccess)
// hold a clone of the config from when they were spawned, so they
// hit the old report hook above. Nothing in phase 2 should report
// or download — only a patch-check happens.
testing_set_network_hooks(
|_url, _request| {
Ok(PatchCheckResponse {
patch_available: false,
patch: None,
rolled_back_patch_numbers: Some(vec![1]),
})
},
UNEXPECTED_DOWNLOAD,
UNEXPECTED_REPORT,
);
assert!(!shorebird_check_for_downloadable_update(std::ptr::null()));
assert_eq!(shorebird_next_boot_patch_number(), 0);

// Phase 3: server rolls patch 1 forward — same number, same hash,
// empty rolled_back list (the row's `is_rolled_back` flipped back
// to false on the server). The device must accept this as a normal
// "patch available" response and reinstall.
testing_set_network_hooks(
|_url, _request| {
Ok(PatchCheckResponse {
patch_available: true,
patch: Some(crate::Patch {
number: 1,
hash: HELLO_TESTS_PATCH.hash.to_owned(),
download_url: "ignored".to_owned(),
hash_signature: None,
}),
rolled_back_patch_numbers: Some(vec![]),
})
},
|_url, dest: &Path, _resume_from: u64| HELLO_TESTS_PATCH.write_to(dest),
|_url, _event| Ok(()),
);

// Pre-lifecycle: returns false because patch 1 sat in
// `known_bad_patches` from phase 2's `remove_patch` call.
// Post-lifecycle: phase 2's cleanup forgot patch 1 entirely
// (no Bad tombstone for server-driven rollbacks), so this
// installs cleanly.
assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
assert_eq!(shorebird_next_boot_patch_number(), 1);
}

/// Patch-to-patch rollback: device on patch 2, server rolls back to
/// patch 1 (sends rollback signal AND a downloadable replacement).
/// `check_for_downloadable_update` returns true (replacement available),
Expand Down
Loading
Loading