补齐桌面壳网络与门禁自检
桌面网络 HostBridge 补成功与失败映射单测 桌面壳配置脚本纳入受控脚本与替身词扫描 原生壳文档改用唯一完整路径清单口径 项目记忆同步桌面壳验收边界
This commit is contained in:
@@ -181,6 +181,7 @@ const desktopShellNavigationSource = fs.readFileSync(
|
||||
);
|
||||
const productionSourceRoots = [
|
||||
new URL('../package.json', import.meta.url),
|
||||
new URL('../scripts/', import.meta.url),
|
||||
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
||||
new URL('../src-tauri/Info.plist', import.meta.url),
|
||||
new URL('../src-tauri/build.rs', import.meta.url),
|
||||
@@ -301,6 +302,9 @@ const expectedDesktopShellRustFiles = [
|
||||
'webview.rs',
|
||||
'window_state.rs',
|
||||
];
|
||||
const expectedDesktopShellScripts = [
|
||||
'check-config.mjs',
|
||||
];
|
||||
|
||||
function extractCargoPackageString(source, key) {
|
||||
const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm'));
|
||||
@@ -576,7 +580,7 @@ function collectProductionSourceFiles(entry) {
|
||||
if (!productionFileExtensions.has(extension)) {
|
||||
return [];
|
||||
}
|
||||
if (path.includes('.test.') || path.endsWith('/scripts/check-config.mjs')) {
|
||||
if (path.includes('.test.')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1197,6 +1201,11 @@ function readDirectoryFileList(directory, label) {
|
||||
}
|
||||
|
||||
function assertDesktopSourceLayout() {
|
||||
assertSameList(
|
||||
readDirectoryFileList(new URL('../scripts/', import.meta.url), 'desktop shell scripts'),
|
||||
expectedDesktopShellScripts,
|
||||
'desktop shell scripts',
|
||||
);
|
||||
assertSameList(
|
||||
readDirectoryEntryList(rustSourceDir, 'desktop shell Rust root entries'),
|
||||
expectedDesktopRustRootEntries,
|
||||
@@ -2009,11 +2018,19 @@ if (
|
||||
}
|
||||
for (const snippet of [
|
||||
'resolve_desktop_host_bridge_network_status',
|
||||
'map_desktop_host_bridge_network_status_result',
|
||||
'resolve_desktop_host_bridge_network_status_payload',
|
||||
'resolve_desktop_host_bridge_network_status_payload_with',
|
||||
'network_status_unavailable_response',
|
||||
'spawn_blocking(resolve_desktop_network_status)',
|
||||
'spawn_blocking(resolver)',
|
||||
'resolve_desktop_host_bridge_network_status_payload_with(resolve_desktop_network_status)',
|
||||
'"isConnected": true',
|
||||
'"connectionType": "unknown"',
|
||||
'"network status unavailable"',
|
||||
'network_status_unavailable_response_is_stable',
|
||||
'network_status_success_response_reports_contract_shape',
|
||||
'network_status_failure_hides_native_error_detail',
|
||||
'private native resolver detail',
|
||||
]) {
|
||||
if (!desktopHostBridgeNetworkSource.includes(snippet)) {
|
||||
throw new Error(`desktop shell network module is missing ${snippet}`);
|
||||
|
||||
@@ -2,19 +2,35 @@ use crate::host_bridge::protocol::{failed, ok, HostBridgeRequest, HostBridgeResp
|
||||
use crate::shell::webview::resolve_desktop_network_status;
|
||||
use serde_json::Value;
|
||||
|
||||
async fn resolve_desktop_host_bridge_network_status_payload() -> Result<Value, String> {
|
||||
tauri::async_runtime::spawn_blocking(resolve_desktop_network_status)
|
||||
fn map_desktop_host_bridge_network_status_result(
|
||||
status: Result<Value, String>,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
match status {
|
||||
Ok(status) => ok(request.id.clone(), status),
|
||||
Err(_) => network_status_unavailable_response(request),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_desktop_host_bridge_network_status_payload_with(
|
||||
resolver: fn() -> Value,
|
||||
) -> Result<Value, String> {
|
||||
tauri::async_runtime::spawn_blocking(resolver)
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
async fn resolve_desktop_host_bridge_network_status_payload() -> Result<Value, String> {
|
||||
resolve_desktop_host_bridge_network_status_payload_with(resolve_desktop_network_status).await
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_desktop_host_bridge_network_status(
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
match resolve_desktop_host_bridge_network_status_payload().await {
|
||||
Ok(status) => ok(request.id.clone(), status),
|
||||
Err(_) => network_status_unavailable_response(request),
|
||||
}
|
||||
map_desktop_host_bridge_network_status_result(
|
||||
resolve_desktop_host_bridge_network_status_payload().await,
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
fn network_status_unavailable_response(request: &HostBridgeRequest) -> HostBridgeResponse {
|
||||
@@ -25,6 +41,7 @@ fn network_status_unavailable_response(request: &HostBridgeRequest) -> HostBridg
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host_bridge::protocol::request;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn network_status_unavailable_response_is_stable() {
|
||||
@@ -35,4 +52,37 @@ mod tests {
|
||||
assert_eq!(error.code, "host_error");
|
||||
assert_eq!(error.message, "network status unavailable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_status_success_response_reports_contract_shape() {
|
||||
let payload = json!({
|
||||
"isConnected": true,
|
||||
"isInternetReachable": true,
|
||||
"connectionType": "unknown",
|
||||
"nativeType": "online",
|
||||
});
|
||||
let response = map_desktop_host_bridge_network_status_result(
|
||||
Ok(payload.clone()),
|
||||
&request("network.status"),
|
||||
);
|
||||
|
||||
assert!(response.ok);
|
||||
assert_eq!(response.id, "request-1");
|
||||
assert_eq!(response.result, Some(payload));
|
||||
assert!(response.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_status_failure_hides_native_error_detail() {
|
||||
let response = map_desktop_host_bridge_network_status_result(
|
||||
Err("private native resolver detail".to_string()),
|
||||
&request("network.status"),
|
||||
);
|
||||
|
||||
assert!(!response.ok);
|
||||
let error = response.error.expect("network status error");
|
||||
assert_eq!(error.code, "host_error");
|
||||
assert_eq!(error.message, "network status unavailable");
|
||||
assert!(!error.message.contains("private native resolver detail"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3078,3 +3078,10 @@
|
||||
- 决策:`openMobileShellExternalNavigation(...)` 只在非法 URL 或系统明确不能打开时返回 `false`,原生 `canOpenURL` / `openURL` 异常必须抛给 `ShellApp` 的 `logMobileShellNavigationFailure(...)` 记录;`scanner.scanQrCode` pending 状态必须使用共享 `HOST_BRIDGE_SCANNER_TIMEOUT_MS` 自动拒绝并清理,成功、取消、失败和测试 reset 都必须清理 timer。
|
||||
- 影响范围:`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/navigation.test.ts`、`apps/mobile-shell/src/shell/ShellApp.test.tsx`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/scanner.test.ts`、`apps/mobile-shell/scripts/check-config.mjs`。
|
||||
- 验证方式:`npm run mobile-shell:test -- src/shell/navigation.test.ts src/shell/ShellApp.test.tsx src/host-bridge/scanner.test.ts src/shell/QrScannerOverlay.test.tsx`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-20 桌面壳门禁脚本与网络状态测试边界
|
||||
|
||||
- 背景:Tauri 桌面壳 `network.status` 已由 `host_bridge/network.rs` 统一包装,但成功响应和底层 resolver 失败映射主要靠字符串门禁;同时桌面单端检查没有把 `apps/desktop-shell/scripts/check-config.mjs` 自身纳入脚本清单和生产替身词扫描。
|
||||
- 决策:`host_bridge/network.rs` 新增可注入映射 helper,Rust 单测直接覆盖 `network.status` 成功 response shape 和 resolver 失败不暴露原生细节;桌面壳单端配置检查登记并扫描 `scripts/check-config.mjs`,根级文档门禁改为只从“结构门禁按完整相对路径”canonical 段反查文件清单,短清单只保留指针文案。
|
||||
- 影响范围:`apps/desktop-shell/src-tauri/src/host_bridge/network.rs`、`apps/desktop-shell/scripts/check-config.mjs`、`scripts/check-native-shells.mjs`、宿主壳方案文档、宿主壳能力统一协议文档。
|
||||
- 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::network shell::network`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -928,9 +928,20 @@ function readDirectoryNameList(directory, label) {
|
||||
}
|
||||
|
||||
function assertShellLayerLayoutDocumented(source, label) {
|
||||
const canonicalMarker = '结构门禁按完整相对路径反查文档和目录';
|
||||
const pointerMarker =
|
||||
'当前 `npm run check:native-shells` 锁定的生产文件清单以本文后续“结构门禁按完整相对路径反查文档和目录”段落为唯一文档口径';
|
||||
if (!source.includes(pointerMarker)) {
|
||||
throw new Error(`${label} must point short shell file lists to the canonical full-path section`);
|
||||
}
|
||||
const canonicalStart = source.indexOf(canonicalMarker);
|
||||
if (canonicalStart < 0) {
|
||||
throw new Error(`${label} missing canonical shell layer layout section`);
|
||||
}
|
||||
const canonicalSource = source.slice(canonicalStart);
|
||||
for (const group of documentedShellLayerGroups) {
|
||||
for (const fileName of group.files) {
|
||||
if (!source.includes(fileName)) {
|
||||
if (!canonicalSource.includes(fileName)) {
|
||||
throw new Error(`${label} missing ${group.label}: ${fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user