@livx.cc/appwrap 0.54.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/runtime/app/main-page.ts +0 -2
- package/runtime/app/shell/capabilities.manifest.ts +27 -92
- package/runtime/app/shell/config.ts +4 -0
- package/runtime/app/shell/env.ts +8 -1
- package/runtime/app/shell/handlers.ts +2 -2
- package/runtime/tests/bridge-response-delivery.test.ts +10 -1
- package/runtime/tests/env-scheme.test.ts +50 -0
- package/scripts/stage-assets.mjs +1 -1
- package/src/cli.ts +424 -621
- package/src/config.ts +8 -1
- package/src/handlers.ts +1 -1
- package/src/packs.ts +174 -0
- package/src/testing.ts +173 -0
- package/templates/module-pack/README.md +44 -0
- package/templates/module-pack/handler.ts +9 -0
- package/templates/module-pack/manifest.ts +27 -0
- package/templates/module-pack/native-src/__MODULE_NAME__/.gitkeep +0 -0
- package/runtime/app/shell/billing-offer.ts +0 -54
- package/runtime/app/shell/handlers-billing.ts +0 -662
- package/runtime/app/shell/handlers-health.ts +0 -178
- package/runtime/app/shell/handlers-widget.ts +0 -97
- package/runtime/modules-native/billing/App_Resources/iOS/src/AppwrapManageSubscriptions.swift +0 -44
- package/runtime/modules-native/health/App_Resources/Android/src/main/java/cc/livx/appwrap/HealthConnectBridge.kt +0 -74
- package/runtime/modules-native/widget/App_Resources/Android/src/main/java/cc/livx/appwrap/AppwrapWidgetProvider.kt +0 -118
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/drawable/appwrap_badge_bg.xml +0 -9
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/layout/appwrap_widget.xml +0 -123
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_colors.xml +0 -10
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_styles.xml +0 -67
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values-night/appwrap_widget_colors.xml +0 -9
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/xml/appwrap_widget_info.xml +0 -17
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/AppwrapWidget.entitlements +0 -10
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/AppwrapWidget.swift +0 -275
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/Info.plist +0 -25
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/extension.json +0 -9
- package/runtime/modules-native/widget/App_Resources/iOS/src/AppwrapWidgetReload.swift +0 -18
- package/runtime/tests/billing-offer.test.ts +0 -61
- package/runtime-desktop/autotest/index.html +0 -77
- package/runtime-desktop/bridge-shim/build.sh +0 -9
- package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
- package/runtime-desktop/bridge-shim/shim.ts +0 -132
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
- package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
- package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
- package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
- package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
- package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
- package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
- package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
- package/runtime-desktop/src-tauri/Cargo.toml +0 -45
- package/runtime-desktop/src-tauri/build.rs +0 -3
- package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
- package/runtime-desktop/src-tauri/icons/icon.png +0 -0
- package/runtime-desktop/src-tauri/shell_config.json +0 -10
- package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
- package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
- package/runtime-desktop/src-tauri/src/main.rs +0 -1228
- package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
- package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
- package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
- package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
- package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
- package/runtime-desktop/src-tauri/src/server.rs +0 -156
- package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
- package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
- package/src/desktop.ts +0 -300
- package/src/plugin/host.ts +0 -242
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
//! Per-app extensibility: a Bun sidecar running the app's custom TS handlers (`desktop.handlers`).
|
|
2
|
-
//!
|
|
3
|
-
//! When `shell_config.json` carries a `handlers` path, `init()` spawns `bun <path>` with piped stdio at
|
|
4
|
-
//! setup and reads its one-line `ready` announcement (methods it serves). The main bridge router then
|
|
5
|
-
//! forwards any CLAIMED method to the sidecar over NDJSON and blocks on the matching response (per-call
|
|
6
|
-
//! timeout). A single reader thread drains stdout and resolves the pending map by id; stderr is prefixed
|
|
7
|
-
//! through to the shell's stderr.
|
|
8
|
-
//!
|
|
9
|
-
//! Wire protocol (appwrap sidecar protocol v1 — one JSON object per line):
|
|
10
|
-
//! sidecar → shell (spawn): {"kind":"ready","methods":[…]}
|
|
11
|
-
//! shell → sidecar: {"kind":"request","id":"…","method":"…","params":{…}}
|
|
12
|
-
//! sidecar → shell: {"kind":"response","id":"…","ok":true,"data":{…}}
|
|
13
|
-
//! or: {"kind":"response","id":"…","ok":false,"error":{"code":"…","message":"…"}}
|
|
14
|
-
//!
|
|
15
|
-
//! DEAD-SIDECAR SEMANTICS: a crash/EOF fails all in-flight calls with NATIVE_ERROR and marks the sidecar
|
|
16
|
-
//! dead; subsequent calls to a still-CLAIMED method also return NATIVE_ERROR (not UNSUPPORTED). Rationale:
|
|
17
|
-
//! these methods are app-custom — the kit has NO web fallback for them (its hybrid fallback only fires on
|
|
18
|
-
//! UNSUPPORTED, and only for built-in kit modules), so returning UNSUPPORTED would gain no graceful
|
|
19
|
-
//! web-degrade and would dishonestly report "feature absent" when the truth is "the handler backend
|
|
20
|
-
//! crashed". NATIVE_ERROR lets the app detect the failure and degrade on its own terms.
|
|
21
|
-
|
|
22
|
-
use serde_json::{json, Value};
|
|
23
|
-
use std::collections::{HashMap, HashSet};
|
|
24
|
-
use std::io::{BufRead, BufReader, Write};
|
|
25
|
-
use std::process::{Child, ChildStdin, Command, Stdio};
|
|
26
|
-
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
27
|
-
use std::sync::mpsc::{self, Receiver, Sender, SyncSender};
|
|
28
|
-
use std::sync::{Arc, Mutex, OnceLock};
|
|
29
|
-
use std::time::Duration;
|
|
30
|
-
|
|
31
|
-
/// A dynamic-code error the app-custom sidecar can return (kit codes are open-ended app-side, so unlike
|
|
32
|
-
/// the built-in handlers' `&'static str` codes we keep the sidecar's code string verbatim).
|
|
33
|
-
pub type SidecarResult = Result<Value, (String, String)>;
|
|
34
|
-
|
|
35
|
-
const READY_TIMEOUT: Duration = Duration::from_secs(5);
|
|
36
|
-
const CALL_TIMEOUT: Duration = Duration::from_secs(30);
|
|
37
|
-
|
|
38
|
-
struct Sidecar {
|
|
39
|
-
methods: HashSet<String>,
|
|
40
|
-
alive: Arc<AtomicBool>,
|
|
41
|
-
stdin: Mutex<ChildStdin>,
|
|
42
|
-
pending: Arc<Mutex<HashMap<String, Sender<Value>>>>,
|
|
43
|
-
child: Mutex<Child>,
|
|
44
|
-
next_id: AtomicU64,
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static SIDECAR: OnceLock<Sidecar> = OnceLock::new();
|
|
48
|
-
|
|
49
|
-
/// Parse one sidecar stdout line into a routable outcome. Pure — unit-tested.
|
|
50
|
-
enum Line {
|
|
51
|
-
Ready(Vec<String>),
|
|
52
|
-
Response { id: String, value: Value },
|
|
53
|
-
Other,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
fn parse_line(line: &str) -> Line {
|
|
57
|
-
let v: Value = match serde_json::from_str(line) {
|
|
58
|
-
Ok(v) => v,
|
|
59
|
-
Err(_) => return Line::Other,
|
|
60
|
-
};
|
|
61
|
-
match v["kind"].as_str() {
|
|
62
|
-
Some("ready") => {
|
|
63
|
-
let methods = v["methods"]
|
|
64
|
-
.as_array()
|
|
65
|
-
.map(|a| a.iter().filter_map(|m| m.as_str().map(str::to_string)).collect())
|
|
66
|
-
.unwrap_or_default();
|
|
67
|
-
Line::Ready(methods)
|
|
68
|
-
}
|
|
69
|
-
Some("response") => match v["id"].as_str() {
|
|
70
|
-
Some(id) => Line::Response { id: id.to_string(), value: v },
|
|
71
|
-
None => Line::Other,
|
|
72
|
-
},
|
|
73
|
-
_ => Line::Other,
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/// Translate a sidecar `response` envelope into a router result. Pure — unit-tested.
|
|
78
|
-
fn response_outcome(resp: &Value) -> SidecarResult {
|
|
79
|
-
if resp["ok"].as_bool() == Some(true) {
|
|
80
|
-
Ok(resp.get("data").cloned().unwrap_or(Value::Null))
|
|
81
|
-
} else {
|
|
82
|
-
let code = resp["error"]["code"].as_str().unwrap_or("NATIVE_ERROR").to_string();
|
|
83
|
-
let message = resp["error"]["message"].as_str().unwrap_or("handler error").to_string();
|
|
84
|
-
Err((code, message))
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Resolve which Bun binary to spawn: the CLI-stamped absolute runtime (`handlersRuntime` =
|
|
89
|
-
/// `process.execPath` of the bun that built the app) when it still exists, else a bare `bun`
|
|
90
|
-
/// PATH lookup. The stamped path matters for GUI launches: Finder/launchd PATH is
|
|
91
|
-
/// /usr/bin:/bin:/usr/sbin:/sbin — no ~/.bun/bin — so `bun` alone is unresolvable there.
|
|
92
|
-
fn resolve_runtime(stamped: &str) -> String {
|
|
93
|
-
if !stamped.is_empty() && std::path::Path::new(stamped).exists() {
|
|
94
|
-
return stamped.to_string();
|
|
95
|
-
}
|
|
96
|
-
if !stamped.is_empty() {
|
|
97
|
-
eprintln!("[sidecar] stamped runtime missing ({stamped}) — falling back to `bun` (PATH lookup; GUI-launched apps have a minimal launchd PATH and may fail to spawn)");
|
|
98
|
-
} else {
|
|
99
|
-
eprintln!("[sidecar] no stamped runtime — falling back to `bun` (PATH lookup; GUI-launched apps have a minimal launchd PATH and may fail to spawn)");
|
|
100
|
-
}
|
|
101
|
-
"bun".to_string()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// Drop any claimed method that falls under a built-in namespace so an app handler can never shadow
|
|
105
|
-
/// a built-in (e.g. `storage.get`). Pure — unit-tested. Warns with the rejected list.
|
|
106
|
-
fn filter_claims(methods: Vec<String>) -> Vec<String> {
|
|
107
|
-
let (rejected, kept): (Vec<String>, Vec<String>) = methods
|
|
108
|
-
.into_iter()
|
|
109
|
-
.partition(|m| crate::BUILTIN_METHOD_PREFIXES.iter().any(|p| m.starts_with(p)));
|
|
110
|
-
if !rejected.is_empty() {
|
|
111
|
-
eprintln!("[sidecar] rejecting handler method(s) that shadow built-ins: {rejected:?} — rename them outside the built-in namespaces");
|
|
112
|
-
}
|
|
113
|
-
kept
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/// Spawn the Bun sidecar and block (≤5s, bounded by READY_TIMEOUT) on its `ready` line — intentional:
|
|
117
|
-
/// sidecar methods must be claimable before the webview's first invoke. On timeout / malformed / spawn
|
|
118
|
-
/// failure the error is logged and the shell runs WITHOUT a sidecar (the feature degrades to inert).
|
|
119
|
-
/// Idempotent-ish: only the first successful init wins (OnceLock).
|
|
120
|
-
pub fn init(path: &str, runtime: &str) {
|
|
121
|
-
if SIDECAR.get().is_some() {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
let bun = resolve_runtime(runtime);
|
|
125
|
-
let mut child = match Command::new(&bun)
|
|
126
|
-
.arg(path)
|
|
127
|
-
.stdin(Stdio::piped())
|
|
128
|
-
.stdout(Stdio::piped())
|
|
129
|
-
.stderr(Stdio::piped())
|
|
130
|
-
.spawn()
|
|
131
|
-
{
|
|
132
|
-
Ok(c) => c,
|
|
133
|
-
Err(e) => {
|
|
134
|
-
eprintln!("[sidecar] spawn `{bun} {path}` failed: {e} — running without custom handlers");
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
let stdin = child.stdin.take().expect("piped stdin");
|
|
140
|
-
let stdout = child.stdout.take().expect("piped stdout");
|
|
141
|
-
let stderr = child.stderr.take();
|
|
142
|
-
|
|
143
|
-
let alive = Arc::new(AtomicBool::new(true));
|
|
144
|
-
let pending: Arc<Mutex<HashMap<String, Sender<Value>>>> = Arc::new(Mutex::new(HashMap::new()));
|
|
145
|
-
|
|
146
|
-
// stderr → shell stderr (prefixed).
|
|
147
|
-
if let Some(stderr) = stderr {
|
|
148
|
-
std::thread::spawn(move || {
|
|
149
|
-
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
|
|
150
|
-
eprintln!("[sidecar:err] {line}");
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Reader thread: first line → ready (via ready_tx), then resolve responses by id.
|
|
156
|
-
let (ready_tx, ready_rx): (SyncSender<Option<Vec<String>>>, Receiver<Option<Vec<String>>>) =
|
|
157
|
-
mpsc::sync_channel(1);
|
|
158
|
-
let r_alive = alive.clone();
|
|
159
|
-
let r_pending = pending.clone();
|
|
160
|
-
std::thread::spawn(move || {
|
|
161
|
-
let mut reader = BufReader::new(stdout);
|
|
162
|
-
let mut got_ready = false;
|
|
163
|
-
let mut line = String::new();
|
|
164
|
-
loop {
|
|
165
|
-
line.clear();
|
|
166
|
-
match reader.read_line(&mut line) {
|
|
167
|
-
Ok(0) | Err(_) => break, // EOF or read error → sidecar gone
|
|
168
|
-
Ok(_) => {}
|
|
169
|
-
}
|
|
170
|
-
let trimmed = line.trim();
|
|
171
|
-
if trimmed.is_empty() {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
match parse_line(trimmed) {
|
|
175
|
-
Line::Ready(methods) => {
|
|
176
|
-
if !got_ready {
|
|
177
|
-
got_ready = true;
|
|
178
|
-
let _ = ready_tx.send(Some(methods));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
Line::Response { id, value } => {
|
|
182
|
-
if let Some(tx) = r_pending.lock().unwrap().remove(&id) {
|
|
183
|
-
let _ = tx.send(value);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
Line::Other => eprintln!("[sidecar] ignoring unrecognized line: {trimmed}"),
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// Sidecar gone: mark dead and fail every in-flight call (dropping the senders makes the
|
|
190
|
-
// waiting recv_timeout return Disconnected → NATIVE_ERROR immediately).
|
|
191
|
-
r_alive.store(false, Ordering::SeqCst);
|
|
192
|
-
r_pending.lock().unwrap().clear();
|
|
193
|
-
if !got_ready {
|
|
194
|
-
let _ = ready_tx.send(None); // never announced → unblock init with a failure
|
|
195
|
-
}
|
|
196
|
-
eprintln!("[sidecar] reader exited — sidecar marked dead");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
let methods = match ready_rx.recv_timeout(READY_TIMEOUT) {
|
|
200
|
-
Ok(Some(methods)) => filter_claims(methods),
|
|
201
|
-
Ok(None) => {
|
|
202
|
-
eprintln!("[sidecar] exited before announcing `ready` — running without custom handlers");
|
|
203
|
-
let _ = child.kill();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
Err(_) => {
|
|
207
|
-
eprintln!("[sidecar] no `ready` within {READY_TIMEOUT:?} — killing, running without custom handlers");
|
|
208
|
-
let _ = child.kill();
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
eprintln!("[sidecar] ready — {} method(s): {:?}", methods.len(), methods);
|
|
214
|
-
let _ = SIDECAR.set(Sidecar {
|
|
215
|
-
methods: methods.into_iter().collect(),
|
|
216
|
-
alive,
|
|
217
|
-
stdin: Mutex::new(stdin),
|
|
218
|
-
pending,
|
|
219
|
-
child: Mutex::new(child),
|
|
220
|
-
next_id: AtomicU64::new(1),
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/// True if a sidecar exists and ADVERTISED this method (regardless of liveness — a dead sidecar's
|
|
225
|
-
/// methods still route here so they return NATIVE_ERROR, not UNSUPPORTED; see the module note).
|
|
226
|
-
pub fn claims(method: &str) -> bool {
|
|
227
|
-
SIDECAR.get().is_some_and(|s| s.methods.contains(method))
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/// Forward a claimed method to the sidecar and block on its response (≤30s). Called from a bridge
|
|
231
|
-
/// worker thread (already off the main thread), so blocking here is fine.
|
|
232
|
-
pub fn request(method: &str, params: &Value) -> SidecarResult {
|
|
233
|
-
let s = SIDECAR.get().ok_or_else(|| ("NATIVE_ERROR".into(), "no sidecar".into()))?;
|
|
234
|
-
if !s.alive.load(Ordering::SeqCst) {
|
|
235
|
-
return Err(("NATIVE_ERROR".into(), format!("handler sidecar is not running ({method})")));
|
|
236
|
-
}
|
|
237
|
-
let id = format!("sc-{}", s.next_id.fetch_add(1, Ordering::SeqCst));
|
|
238
|
-
let (tx, rx) = mpsc::channel::<Value>();
|
|
239
|
-
s.pending.lock().unwrap().insert(id.clone(), tx);
|
|
240
|
-
|
|
241
|
-
let envelope = json!({ "kind": "request", "id": id, "method": method, "params": params });
|
|
242
|
-
let line = format!("{envelope}\n");
|
|
243
|
-
if let Err(e) = (|| {
|
|
244
|
-
let mut w = s.stdin.lock().unwrap();
|
|
245
|
-
w.write_all(line.as_bytes())?;
|
|
246
|
-
w.flush()
|
|
247
|
-
})() {
|
|
248
|
-
s.pending.lock().unwrap().remove(&id);
|
|
249
|
-
return Err(("NATIVE_ERROR".into(), format!("sidecar write failed: {e}")));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
match rx.recv_timeout(CALL_TIMEOUT) {
|
|
253
|
-
Ok(resp) => response_outcome(&resp),
|
|
254
|
-
Err(mpsc::RecvTimeoutError::Timeout) => {
|
|
255
|
-
s.pending.lock().unwrap().remove(&id);
|
|
256
|
-
Err(("NATIVE_ERROR".into(), format!("sidecar timeout after {CALL_TIMEOUT:?} ({method})")))
|
|
257
|
-
}
|
|
258
|
-
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
|
259
|
-
// Reader thread dropped our sender = sidecar died mid-call.
|
|
260
|
-
Err(("NATIVE_ERROR".into(), format!("handler sidecar died during call ({method})")))
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/// Kill the sidecar child (idempotent). Called on main-window teardown so the Bun process never outlives
|
|
266
|
-
/// the shell. (`SIDECAR` is a `'static` OnceLock, so a Drop guard would never run at process exit — this
|
|
267
|
-
/// explicit call is the reliable teardown; killing an already-dead child is a harmless no-op.)
|
|
268
|
-
pub fn shutdown() {
|
|
269
|
-
if let Some(s) = SIDECAR.get() {
|
|
270
|
-
let _ = s.child.lock().unwrap().kill();
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
#[cfg(test)]
|
|
275
|
-
mod tests {
|
|
276
|
-
use super::*;
|
|
277
|
-
|
|
278
|
-
#[test]
|
|
279
|
-
fn parse_ready_extracts_methods() {
|
|
280
|
-
match parse_line(r#"{"kind":"ready","methods":["a.b","a.c"]}"#) {
|
|
281
|
-
Line::Ready(m) => assert_eq!(m, vec!["a.b".to_string(), "a.c".to_string()]),
|
|
282
|
-
_ => panic!("expected Ready"),
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
#[test]
|
|
287
|
-
fn parse_ready_missing_methods_is_empty() {
|
|
288
|
-
match parse_line(r#"{"kind":"ready"}"#) {
|
|
289
|
-
Line::Ready(m) => assert!(m.is_empty()),
|
|
290
|
-
_ => panic!("expected Ready"),
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
#[test]
|
|
295
|
-
fn parse_response_carries_id_and_value() {
|
|
296
|
-
match parse_line(r#"{"kind":"response","id":"sc-1","ok":true,"data":{"x":1}}"#) {
|
|
297
|
-
Line::Response { id, value } => {
|
|
298
|
-
assert_eq!(id, "sc-1");
|
|
299
|
-
assert_eq!(value["data"]["x"], 1);
|
|
300
|
-
}
|
|
301
|
-
_ => panic!("expected Response"),
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
#[test]
|
|
306
|
-
fn parse_malformed_or_unknown_is_other() {
|
|
307
|
-
assert!(matches!(parse_line("not json"), Line::Other));
|
|
308
|
-
assert!(matches!(parse_line(r#"{"kind":"noise"}"#), Line::Other));
|
|
309
|
-
// response without id can't be routed → Other
|
|
310
|
-
assert!(matches!(parse_line(r#"{"kind":"response","ok":true}"#), Line::Other));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
#[test]
|
|
314
|
-
fn outcome_ok_returns_data() {
|
|
315
|
-
let resp = json!({"kind":"response","id":"x","ok":true,"data":{"n":42}});
|
|
316
|
-
assert_eq!(response_outcome(&resp).unwrap(), json!({"n":42}));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
#[test]
|
|
320
|
-
fn outcome_ok_missing_data_is_null() {
|
|
321
|
-
let resp = json!({"kind":"response","id":"x","ok":true});
|
|
322
|
-
assert_eq!(response_outcome(&resp).unwrap(), Value::Null);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
#[test]
|
|
326
|
-
fn outcome_error_preserves_code_and_message() {
|
|
327
|
-
let resp = json!({"kind":"response","id":"x","ok":false,"error":{"code":"DENIED","message":"nope"}});
|
|
328
|
-
let (code, msg) = response_outcome(&resp).unwrap_err();
|
|
329
|
-
assert_eq!(code, "DENIED");
|
|
330
|
-
assert_eq!(msg, "nope");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
#[test]
|
|
334
|
-
fn outcome_error_defaults_when_fields_absent() {
|
|
335
|
-
let resp = json!({"kind":"response","id":"x","ok":false});
|
|
336
|
-
let (code, msg) = response_outcome(&resp).unwrap_err();
|
|
337
|
-
assert_eq!(code, "NATIVE_ERROR");
|
|
338
|
-
assert_eq!(msg, "handler error");
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/// Pending-map resolution: a response by id wakes exactly the matching waiter.
|
|
342
|
-
#[test]
|
|
343
|
-
fn pending_map_resolves_by_id() {
|
|
344
|
-
let pending: Arc<Mutex<HashMap<String, Sender<Value>>>> = Arc::new(Mutex::new(HashMap::new()));
|
|
345
|
-
let (tx, rx) = mpsc::channel::<Value>();
|
|
346
|
-
pending.lock().unwrap().insert("sc-1".into(), tx);
|
|
347
|
-
|
|
348
|
-
// Simulate the reader draining a response line for sc-1.
|
|
349
|
-
if let Line::Response { id, value } = parse_line(r#"{"kind":"response","id":"sc-1","ok":true,"data":5}"#) {
|
|
350
|
-
if let Some(t) = pending.lock().unwrap().remove(&id) {
|
|
351
|
-
t.send(value).unwrap();
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
let got = rx.recv_timeout(Duration::from_secs(1)).unwrap();
|
|
355
|
-
assert_eq!(response_outcome(&got).unwrap(), json!(5));
|
|
356
|
-
assert!(pending.lock().unwrap().is_empty());
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/// Claims under a built-in namespace are dropped; app-namespaced ones survive.
|
|
360
|
-
#[test]
|
|
361
|
-
fn filter_claims_rejects_builtin_shadowing() {
|
|
362
|
-
let kept = filter_claims(vec![
|
|
363
|
-
"storage.get".into(), // shadows built-in storage.*
|
|
364
|
-
"ui.alert".into(), // shadows built-in ui.*
|
|
365
|
-
"app.handshake".into(), // shadows built-in app.*
|
|
366
|
-
"push.x".into(), // shadows built-in push.*
|
|
367
|
-
"biometrics.available".into(), // shadows built-in biometrics.*
|
|
368
|
-
"myapp.doThing".into(), // fine
|
|
369
|
-
"storagex.get".into(), // NOT under "storage." (prefix includes the dot)
|
|
370
|
-
]);
|
|
371
|
-
assert_eq!(kept, vec!["myapp.doThing".to_string(), "storagex.get".to_string()]);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
#[test]
|
|
375
|
-
fn filter_claims_keeps_all_when_no_shadowing() {
|
|
376
|
-
let kept = filter_claims(vec!["a.b".into(), "a.c".into()]);
|
|
377
|
-
assert_eq!(kept, vec!["a.b".to_string(), "a.c".to_string()]);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/// Runtime resolution: existing stamped path wins; missing/empty falls back to `bun`.
|
|
381
|
-
#[test]
|
|
382
|
-
fn resolve_runtime_prefers_existing_stamped_path() {
|
|
383
|
-
assert_eq!(resolve_runtime("/bin/ls"), "/bin/ls"); // any existing file
|
|
384
|
-
assert_eq!(resolve_runtime("/nonexistent/bun-xyz"), "bun");
|
|
385
|
-
assert_eq!(resolve_runtime(""), "bun");
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/// Dropping the pending sender (sidecar death) makes the waiter observe Disconnected.
|
|
389
|
-
#[test]
|
|
390
|
-
fn dropped_sender_disconnects_waiter() {
|
|
391
|
-
let (tx, rx) = mpsc::channel::<Value>();
|
|
392
|
-
drop(tx);
|
|
393
|
-
assert!(matches!(
|
|
394
|
-
rx.recv_timeout(Duration::from_millis(50)),
|
|
395
|
-
Err(mpsc::RecvTimeoutError::Disconnected)
|
|
396
|
-
));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://schema.tauri.app/config/2",
|
|
3
|
-
"productName": "hello-pwa-desktop",
|
|
4
|
-
"version": "0.1.0",
|
|
5
|
-
"identifier": "cc.livx.hellopwa.desktop",
|
|
6
|
-
"build": {
|
|
7
|
-
"frontendDist": "../../examples/hello-pwa/dist"
|
|
8
|
-
},
|
|
9
|
-
"app": {
|
|
10
|
-
"withGlobalTauri": true,
|
|
11
|
-
"security": { "csp": null }
|
|
12
|
-
},
|
|
13
|
-
"bundle": { "active": false }
|
|
14
|
-
}
|