diff --git a/.github/workflows/phone.yml b/.github/workflows/phone.yml index 07e74ba7..b9ee3a90 100644 --- a/.github/workflows/phone.yml +++ b/.github/workflows/phone.yml @@ -109,6 +109,11 @@ jobs: env: # Runs the shell's real-kernel relay test instead of skipping it. OCTOS_SHELL_TEST_KERNEL: ${{ runner.temp }}/octos-kernel/target/release/octos + # A Splash entry's 64 ms budget is wall-clock time. With ~1,000 + # tests in one process, an entry waits on shared locks and page + # faults, and the budget counts that against the script. These + # tests check behaviour, not time (makepad#109). + MAKEPAD_SPLASH_BUDGET_MS: "10000" run: | # octosense-app-hub-app is App Hub's shared shell crate, a git # dependency with dev-dependencies: cargo cannot test it from here; diff --git a/crates/shell/src/host_tools/script_apps.rs b/crates/shell/src/host_tools/script_apps.rs index 9c7edaf8..fb0fdebb 100644 --- a/crates/shell/src/host_tools/script_apps.rs +++ b/crates/shell/src/host_tools/script_apps.rs @@ -832,8 +832,16 @@ pub(crate) mod tests { request.args = json!({"connection":current,"operation":"notes.list"}); let (r,sent) = reply();exec.execute(request,r); let output = sent.lock().unwrap(); - assert_eq!(output[0]["error"]["kind"],"account_scope", "{method}"); - assert!(output[0]["error"]["message"].as_str().unwrap().contains("account changed"),"{}",output[0]); + if output[0]["error"]["kind"] == "api_unavailable" { + // A host without the backend API (auth is macOS and Android + // only) refuses it before the account check, once any test + // has registered the auth catalog in this process. + assert!(octosense_appstore::host_api::methods().iter() + .any(|api| api.name == method && !api.supports(octosense_appstore::host_api::platform())),"{}",output[0]); + } else { + assert_eq!(output[0]["error"]["kind"],"account_scope", "{method}"); + assert!(output[0]["error"]["message"].as_str().unwrap().contains("account changed"),"{}",output[0]); + } assert!(!is_waiting(&format!("c-{alias}")),"stale peer must not reach a backend service"); } std::fs::remove_dir_all(host_dir).unwrap(); diff --git a/docs/local-ci.md b/docs/local-ci.md index c42b8540..6a0f13a9 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -73,7 +73,7 @@ tools/ci-local.sh --only all --linux-host --offload --list # what runs where | --- | --- | --- | | desktop / `desktop` | Compile the desktop; desktop tools and scripts | The shell-source, single-graph and native-apps checks | | desktop / `native-host-api` | All of it (a real macOS host) | Nothing | -| phone / `home` | Apple icons and asset catalogs; compile Home; Home's and App Hub's tests (three fail on Linux, see `JOBS`) | The graph checks, the octos kernel, the two-lane scenario | +| phone / `home` | Apple icons and asset catalogs; compile Home | The graph checks, the octos kernel, Home's and App Hub's tests, the two-lane scenario | | apps / `apps` | Nothing | All of it | Each part prepares the sources itself (the job's `python3 tools/setup.py`). The host's part runs as `@linux`; its steps join the table under the job, marked `(linux)`. `--check-drift` fails when a name in `mac_steps` stops matching a step. diff --git a/runtime-patches.lock.json b/runtime-patches.lock.json index d3d55b9d..7582e00e 100644 --- a/runtime-patches.lock.json +++ b/runtime-patches.lock.json @@ -4,7 +4,7 @@ "base_revision": "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", "patch": "tools/runtime-patches/makepad-settings.patch", "sha256": "b17352ebf59e50ab3db6ff1cb825c4e006bcdff47d83522e372b8fe585e57570", - "tree": "4fb9744e3d9525c725dedcb1100c14451d2bb118", + "tree": "6105f0025e748857a620e3c368c6d572a2493555", "product_fixes": [ "Settings integration: Android IME synchronization, accessibility and reduced motion, GLES program-cache validation, obscured-gesture cancellation and scroll-position observation.", "Contained-app isolation and secret-input restrictions come from the pinned upstream runtime; the Settings overlay preserves those gates.", @@ -26,7 +26,8 @@ "WebReader refuses unsupported Linux/Windows platforms before queuing native browser operations instead of reporting success for an invisible view; this does not add embedded browser support.", "Opted-in camera and microphone check OS grants before prompting; background/agent requests retain nonprompt authority through callbacks and cannot use desktop timeout approval. Passive location reads remain available; legacy GPS requests require foreground prompt authority.", "Host request JSON and pre-drain queues are bounded before native allocation; delayed host callbacks retain their nonprompting origin after an agent tool completes or is cancelled.", - "Agent/background prompt denial follows detached timers, tasks, paused continuations, widget callbacks and HTTP/WebSocket callbacks, including native device helpers. A fresh foreground entry retains its own authority; scoped denial restores on exit or unwind." + "Agent/background prompt denial follows detached timers, tasks, paused continuations, widget callbacks and HTTP/WebSocket callbacks, including native device helpers. A fresh foreground entry retains its own authority; scoped denial restores on exit or unwind.", + "A Splash isolate entry keeps its 64 ms wall-clock budget unless MAKEPAD_SPLASH_BUDGET_MS sets another (makepad#109). Only the shell test step sets it: ~1,000 tests in one process wait on shared locks and page faults, which a wall-clock budget counts against the script." ], "stacked": [ { @@ -100,6 +101,10 @@ { "patch": "tools/runtime-patches/makepad-async-prompt-provenance.patch", "sha256": "46bb7620ccb4f6a76a08d0333c0249d2619123da2a2649f55294fd4d35f0d905" + }, + { + "patch": "tools/runtime-patches/makepad-splash-budget.patch", + "sha256": "cebcae3327ec955a2b37eb1f3353e97160d702bd4081abb264006fee4ac17ee5" } ] } diff --git a/tools/ci_local.py b/tools/ci_local.py index b0a2e353..2eb72123 100644 --- a/tools/ci_local.py +++ b/tools/ci_local.py @@ -297,17 +297,12 @@ def load_workflow(name): "desktop.yml:desktop": {"mac_steps": ["Compile the desktop with the shared runtime", "Desktop tools and scripts"]}, "desktop.yml:native-host-api": {"macos_only": True}, - # Home's test suite stays here until it passes on Linux: on the first - # offloaded run (8 Oct 2026) 1017 of 1020 shell tests did. Failing were - # host_tools::script_apps::tests::backend_data_aliases_cannot_reuse_a_stale_peer_with_the_current_handle - # (auth.backend.* is not a Linux host API, so the executor answers - # api_unavailable before the account check the test expects) and two - # module_resize_tests::photos_preview_* (the Splash script time budget - # ran out on the host's slower cores). + # Home's tests run on the host too. Their Splash tests rely on the test + # step's MAKEPAD_SPLASH_BUDGET_MS: ~1,000 tests in one process at the + # host's 72 threads stretch a 6 ms script entry past makepad's 64 ms + # wall-clock budget (makepad#109). "phone.yml:home": {"mac_steps": ["Validate launcher icons and Apple asset catalogs", - "Compile Home and its bundled modules", - "Build Home, App Hub admission and runtime policy tests", - "Test Home, App Hub admission and runtime policy"]}, + "Compile Home and its bundled modules"]}, "apps.yml:services": {"linux_only_note": True}, "apps.yml:kernel-security": {"linux_only_note": True}, "apps.yml:apps": {"mac_steps": []}, diff --git a/tools/runtime-patches/makepad-splash-budget.patch b/tools/runtime-patches/makepad-splash-budget.patch new file mode 100644 index 00000000..61445585 --- /dev/null +++ b/tools/runtime-patches/makepad-splash-budget.patch @@ -0,0 +1,39 @@ +--- a/widgets/src/widget_async.rs ++++ b/widgets/src/widget_async.rs +@@ -503,14 +503,31 @@ fn with_splash_prompt_authority(vm: &mut ScriptVm, f: impl FnOnce(&mut Script + f(vm) + } + ++/// How long one entry into a Splash isolate may run: 64 ms, unless ++/// `MAKEPAD_SPLASH_BUDGET_MS` says otherwise. The budget is wall-clock time, ++/// so it also counts time the entry spends waiting for the CPU or a lock. A ++/// test binary running a thousand tests in one process is the case for the ++/// override: there the wall clock says little about the script. ++fn splash_entry_budget() -> std::time::Duration { ++ static BUDGET: std::sync::OnceLock = std::sync::OnceLock::new(); ++ *BUDGET.get_or_init(|| { ++ let ms = std::env::var("MAKEPAD_SPLASH_BUDGET_MS") ++ .ok() ++ .and_then(|value| value.trim().parse::().ok()) ++ .filter(|&ms| ms > 0) ++ .unwrap_or(64); ++ std::time::Duration::from_millis(ms) ++ }) ++} ++ + /// A Splash isolate runs untrusted-ish user script on the UI thread; cap how long any + /// single entry into it may run. + fn with_splash_budget(vm: &mut ScriptVm, f: impl FnOnce(&mut ScriptVm) -> R) -> R { +- let old_budget = vm.bx.run_budget.replace(ScriptRunBudget::from_durations( +- std::time::Duration::from_millis(64), +- std::time::Duration::from_millis(64), +- 512, +- )); ++ let budget = splash_entry_budget(); ++ let old_budget = vm ++ .bx ++ .run_budget ++ .replace(ScriptRunBudget::from_durations(budget, budget, 512)); + let out = with_splash_prompt_authority(vm, f); + vm.bx.run_budget = old_budget; + out