cj-tauri 2.0.0 → 2.1.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.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,205 +1,205 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.plugins.notification.*
5
- import std.collection.*
6
- import stdx.encoding.json.*
7
- import std.unittest.*
8
- import std.unittest.testmacro.*
9
-
10
- /*
11
- * notification 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
- * + 「宿主已装配」时的转交(FakeHost 记痕)。
13
- *
14
- * 真通知落在 C 桥(Linux 走 freedesktop Notifications,Windows 桩阶段未实现),
15
- * 归实机验证;这里只锁入口逻辑。body 可省是契约的一部分,单独覆盖。
16
- */
17
-
18
- /** 取一条「已装配宿主」的命令处理器 */
19
- func notifCmdWithHost(host: FakeHost): CommandHandler {
20
- let c = NotificationCommands()
21
- c.attachHost(host)
22
- return c
23
- }
24
-
25
- func notifErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
26
- try {
27
- handler.handle(cmd, args, IpcContext("main", IpcHub()))
28
- return ""
29
- } catch (e: CommandException) {
30
- return e.message
31
- }
32
- }
33
-
34
- /** 造 { title, body } */
35
- func notifArgs(title: String, body: JsonValue): JsonObject {
36
- var o = JsonObject()
37
- o.put("title", JsonString(title))
38
- o.put("body", body)
39
- return o
40
- }
41
-
42
- @Test
43
- func testNotificationPluginSurface(): Unit {
44
- let p = NotificationPlugin()
45
- @Expect(p.name(), "notification")
46
- let names = pluginCommandNames(p)
47
- @Expect(names.size, 1)
48
- @Expect(names[0], "notification:notify")
49
- }
50
-
51
- @Test
52
- func testNotificationPluginShimTargetsItsOwnNamespace(): Unit {
53
- let js = NotificationPlugin().jsShim()
54
- @Expect(js.contains("t.notification"), true)
55
- @Expect(js.contains("'notification:notify'"), true)
56
- }
57
-
58
- @Test
59
- func testNotificationPluginDeclaresPermissionSets(): Unit {
60
- let sets = pluginPermissionSets(NotificationPlugin())
61
- @Expect(sets.size, 1)
62
- let d = sets.get("notification:default").getOrThrow()
63
- @Expect(d.size, 1)
64
- @Expect(d[0], "notification:notify")
65
- }
66
-
67
- @Test
68
- func testNotificationPermissionSetGatesCommand(): Unit {
69
- let out = ArrayList<String>()
70
- let reg = CapabilityRegistry()
71
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"n\",\"permissions\":[\"notification:default\"]}"))
72
- let hub = hubWithRegistry(reg, out)
73
- registerPluginCommands(hub, NotificationPlugin())
74
- registerPluginPermissionSets(reg, NotificationPlugin())
75
-
76
- // 集内命令放行:走到「宿主未装配」的业务错误,而不是「未授权」
77
- hub.handleRawMessage(
78
- "{\"type\":\"invoke\",\"id\":51,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
79
- waitSink(out, 1)
80
- @Expect(out.size, 1)
81
- @Expect(strField(out[0], "error").contains("command not allowed"), false)
82
- }
83
-
84
- @Test
85
- func testNotificationWithoutPermissionIsDenied(): Unit {
86
- let out = ArrayList<String>()
87
- let reg = CapabilityRegistry()
88
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"empty\",\"permissions\":[]}"))
89
- let hub = hubWithRegistry(reg, out)
90
- registerPluginCommands(hub, NotificationPlugin())
91
- registerPluginPermissionSets(reg, NotificationPlugin())
92
-
93
- hub.handleRawMessage(
94
- "{\"type\":\"invoke\",\"id\":52,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
95
- waitSink(out, 1)
96
- @Expect(strField(out[0], "error"), "command not allowed: notification:notify")
97
- }
98
-
99
- @Test
100
- func testNotificationRequiresTitle(): Unit {
101
- let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
102
- let ctx = IpcContext("main", IpcHub())
103
- var threw = false
104
- try {
105
- cmd.handle("notification:notify", JsonObject(), ctx)
106
- } catch (e: CommandException) {
107
- threw = true
108
- @Expect(e.message.contains("missing argument \"title\""), true)
109
- }
110
- @Expect(threw, true)
111
- }
112
-
113
- @Test
114
- func testNotificationRejectsNonStringTitle(): Unit {
115
- var args = JsonObject()
116
- args.put("title", JsonBool(true))
117
- let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
118
- var threw = false
119
- try {
120
- cmd.handle("notification:notify", args, IpcContext("main", IpcHub()))
121
- } catch (e: CommandException) {
122
- threw = true
123
- @Expect(e.message.contains("must be a string"), true)
124
- }
125
- @Expect(threw, true)
126
- }
127
-
128
- @Test
129
- func testNotificationWithoutHostIsReadableError(): Unit {
130
- @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:notify",
131
- notifArgs("hi", JsonString("body"))).contains("宿主未装配"), true)
132
- }
133
-
134
- @Test
135
- func testNotificationRejectsUnknownCommand(): Unit {
136
- @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:nope",
137
- JsonObject()).contains("unknown notification command"), true)
138
- }
139
-
140
- @Test
141
- func testNotificationBodyIsOptional(): Unit {
142
- let host = FakeHost("main")
143
- let cmd = notifCmdWithHost(host)
144
-
145
- // 只给 title:body 按空串
146
- var onlyTitle = JsonObject()
147
- onlyTitle.put("title", JsonString("标题"))
148
- let r1 = cmd.handle("notification:notify", onlyTitle, IpcContext("main", IpcHub()))
149
- match (r1.kind()) {
150
- case JsonKind.JsNull => ()
151
- case _ => @Expect(false, true)
152
- }
153
- @Expect(host.notifiedArgs, "标题|")
154
-
155
- // body 显式 null:同样按空串(前端 o.body 传 undefined 时序列化可能落成 null)
156
- cmd.handle("notification:notify", notifArgs("标题2", JsonNull()), IpcContext("main", IpcHub()))
157
- @Expect(host.notifiedArgs, "标题2|")
158
-
159
- // body 有值:原样转交
160
- cmd.handle("notification:notify", notifArgs("标题3", JsonString("正文")), IpcContext("main", IpcHub()))
161
- @Expect(host.notifiedArgs, "标题3|正文")
162
- }
163
-
164
- /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
165
- @Test
166
- func testNotificationRoutesToTheWindowThatAsked(): Unit {
167
- // 与 dialog / clipboard 同一条契约:报文里的 window 决定交给哪个宿主
168
- let mainHost = FakeHost("main")
169
- let secondHost = FakeHost("second")
170
- let cmds = NotificationCommands()
171
- cmds.attachHost(mainHost)
172
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
173
-
174
- cmds.handle("notification:notify", notifArgs("second 的通知", JsonString("正文")),
175
- IpcContext("second", IpcHub()))
176
- @Expect(secondHost.notifiedArgs, "second 的通知|正文")
177
- // 关键断言:另一扇窗的宿主一点都没被碰到
178
- @Expect(mainHost.notifiedArgs, "")
179
-
180
- // 反向也成立
181
- cmds.handle("notification:notify", notifArgs("main 的通知", JsonNull()), IpcContext("main", IpcHub()))
182
- @Expect(mainHost.notifiedArgs, "main 的通知|")
183
- @Expect(secondHost.notifiedArgs, "second 的通知|正文")
184
- }
185
-
186
- @Test
187
- func testNotificationReportsUnknownWindowInsteadOfFallingBack(): Unit {
188
- let mainHost = FakeHost("main")
189
- let secondHost = FakeHost("second")
190
- let cmds = NotificationCommands()
191
- cmds.attachHost(mainHost)
192
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
193
-
194
- var threw = false
195
- try {
196
- cmds.handle("notification:notify", notifArgs("谁的", JsonNull()), IpcContext("ghost", IpcHub()))
197
- } catch (e: CommandException) {
198
- threw = true
199
- @Expect(e.message.contains("ghost"), true)
200
- }
201
- @Expect(threw, true)
202
- // 报错之后两个宿主都没被写过:不回落单槽
203
- @Expect(mainHost.notifiedArgs, "")
204
- @Expect(secondHost.notifiedArgs, "")
205
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.notification.*
5
+ import std.collection.*
6
+ import stdx.encoding.json.*
7
+ import std.unittest.*
8
+ import std.unittest.testmacro.*
9
+
10
+ /*
11
+ * notification 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
+ * + 「宿主已装配」时的转交(FakeHost 记痕)。
13
+ *
14
+ * 真通知落在 C 桥(Linux 走 freedesktop Notifications,Windows 桩阶段未实现),
15
+ * 归实机验证;这里只锁入口逻辑。body 可省是契约的一部分,单独覆盖。
16
+ */
17
+
18
+ /** 取一条「已装配宿主」的命令处理器 */
19
+ func notifCmdWithHost(host: FakeHost): CommandHandler {
20
+ let c = NotificationCommands()
21
+ c.attachHost(host)
22
+ return c
23
+ }
24
+
25
+ func notifErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
26
+ try {
27
+ handler.handle(cmd, args, IpcContext("main", IpcHub()))
28
+ return ""
29
+ } catch (e: CommandException) {
30
+ return e.message
31
+ }
32
+ }
33
+
34
+ /** 造 { title, body } */
35
+ func notifArgs(title: String, body: JsonValue): JsonObject {
36
+ var o = JsonObject()
37
+ o.put("title", JsonString(title))
38
+ o.put("body", body)
39
+ return o
40
+ }
41
+
42
+ @Test
43
+ func testNotificationPluginSurface(): Unit {
44
+ let p = NotificationPlugin()
45
+ @Expect(p.name(), "notification")
46
+ let names = pluginCommandNames(p)
47
+ @Expect(names.size, 1)
48
+ @Expect(names[0], "notification:notify")
49
+ }
50
+
51
+ @Test
52
+ func testNotificationPluginShimTargetsItsOwnNamespace(): Unit {
53
+ let js = NotificationPlugin().jsShim()
54
+ @Expect(js.contains("t.notification"), true)
55
+ @Expect(js.contains("'notification:notify'"), true)
56
+ }
57
+
58
+ @Test
59
+ func testNotificationPluginDeclaresPermissionSets(): Unit {
60
+ let sets = pluginPermissionSets(NotificationPlugin())
61
+ @Expect(sets.size, 1)
62
+ let d = sets.get("notification:default").getOrThrow()
63
+ @Expect(d.size, 1)
64
+ @Expect(d[0], "notification:notify")
65
+ }
66
+
67
+ @Test
68
+ func testNotificationPermissionSetGatesCommand(): Unit {
69
+ let out = ArrayList<String>()
70
+ let reg = CapabilityRegistry()
71
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"n\",\"permissions\":[\"notification:default\"]}"))
72
+ let hub = hubWithRegistry(reg, out)
73
+ registerPluginCommands(hub, NotificationPlugin())
74
+ registerPluginPermissionSets(reg, NotificationPlugin())
75
+
76
+ // 集内命令放行:走到「宿主未装配」的业务错误,而不是「未授权」
77
+ hub.handleRawMessage(
78
+ "{\"type\":\"invoke\",\"id\":51,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
79
+ waitSink(out, 1)
80
+ @Expect(out.size, 1)
81
+ @Expect(strField(out[0], "error").contains("command not allowed"), false)
82
+ }
83
+
84
+ @Test
85
+ func testNotificationWithoutPermissionIsDenied(): Unit {
86
+ let out = ArrayList<String>()
87
+ let reg = CapabilityRegistry()
88
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"empty\",\"permissions\":[]}"))
89
+ let hub = hubWithRegistry(reg, out)
90
+ registerPluginCommands(hub, NotificationPlugin())
91
+ registerPluginPermissionSets(reg, NotificationPlugin())
92
+
93
+ hub.handleRawMessage(
94
+ "{\"type\":\"invoke\",\"id\":52,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
95
+ waitSink(out, 1)
96
+ @Expect(strField(out[0], "error"), "command not allowed: notification:notify")
97
+ }
98
+
99
+ @Test
100
+ func testNotificationRequiresTitle(): Unit {
101
+ let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
102
+ let ctx = IpcContext("main", IpcHub())
103
+ var threw = false
104
+ try {
105
+ cmd.handle("notification:notify", JsonObject(), ctx)
106
+ } catch (e: CommandException) {
107
+ threw = true
108
+ @Expect(e.message.contains("missing argument \"title\""), true)
109
+ }
110
+ @Expect(threw, true)
111
+ }
112
+
113
+ @Test
114
+ func testNotificationRejectsNonStringTitle(): Unit {
115
+ var args = JsonObject()
116
+ args.put("title", JsonBool(true))
117
+ let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
118
+ var threw = false
119
+ try {
120
+ cmd.handle("notification:notify", args, IpcContext("main", IpcHub()))
121
+ } catch (e: CommandException) {
122
+ threw = true
123
+ @Expect(e.message.contains("must be a string"), true)
124
+ }
125
+ @Expect(threw, true)
126
+ }
127
+
128
+ @Test
129
+ func testNotificationWithoutHostIsReadableError(): Unit {
130
+ @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:notify",
131
+ notifArgs("hi", JsonString("body"))).contains("宿主未装配"), true)
132
+ }
133
+
134
+ @Test
135
+ func testNotificationRejectsUnknownCommand(): Unit {
136
+ @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:nope",
137
+ JsonObject()).contains("unknown notification command"), true)
138
+ }
139
+
140
+ @Test
141
+ func testNotificationBodyIsOptional(): Unit {
142
+ let host = FakeHost("main")
143
+ let cmd = notifCmdWithHost(host)
144
+
145
+ // 只给 title:body 按空串
146
+ var onlyTitle = JsonObject()
147
+ onlyTitle.put("title", JsonString("标题"))
148
+ let r1 = cmd.handle("notification:notify", onlyTitle, IpcContext("main", IpcHub()))
149
+ match (r1.kind()) {
150
+ case JsonKind.JsNull => ()
151
+ case _ => @Expect(false, true)
152
+ }
153
+ @Expect(host.notifiedArgs, "标题|")
154
+
155
+ // body 显式 null:同样按空串(前端 o.body 传 undefined 时序列化可能落成 null)
156
+ cmd.handle("notification:notify", notifArgs("标题2", JsonNull()), IpcContext("main", IpcHub()))
157
+ @Expect(host.notifiedArgs, "标题2|")
158
+
159
+ // body 有值:原样转交
160
+ cmd.handle("notification:notify", notifArgs("标题3", JsonString("正文")), IpcContext("main", IpcHub()))
161
+ @Expect(host.notifiedArgs, "标题3|正文")
162
+ }
163
+
164
+ /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
165
+ @Test
166
+ func testNotificationRoutesToTheWindowThatAsked(): Unit {
167
+ // 与 dialog / clipboard 同一条契约:报文里的 window 决定交给哪个宿主
168
+ let mainHost = FakeHost("main")
169
+ let secondHost = FakeHost("second")
170
+ let cmds = NotificationCommands()
171
+ cmds.attachHost(mainHost)
172
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
173
+
174
+ cmds.handle("notification:notify", notifArgs("second 的通知", JsonString("正文")),
175
+ IpcContext("second", IpcHub()))
176
+ @Expect(secondHost.notifiedArgs, "second 的通知|正文")
177
+ // 关键断言:另一扇窗的宿主一点都没被碰到
178
+ @Expect(mainHost.notifiedArgs, "")
179
+
180
+ // 反向也成立
181
+ cmds.handle("notification:notify", notifArgs("main 的通知", JsonNull()), IpcContext("main", IpcHub()))
182
+ @Expect(mainHost.notifiedArgs, "main 的通知|")
183
+ @Expect(secondHost.notifiedArgs, "second 的通知|正文")
184
+ }
185
+
186
+ @Test
187
+ func testNotificationReportsUnknownWindowInsteadOfFallingBack(): Unit {
188
+ let mainHost = FakeHost("main")
189
+ let secondHost = FakeHost("second")
190
+ let cmds = NotificationCommands()
191
+ cmds.attachHost(mainHost)
192
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
193
+
194
+ var threw = false
195
+ try {
196
+ cmds.handle("notification:notify", notifArgs("谁的", JsonNull()), IpcContext("ghost", IpcHub()))
197
+ } catch (e: CommandException) {
198
+ threw = true
199
+ @Expect(e.message.contains("ghost"), true)
200
+ }
201
+ @Expect(threw, true)
202
+ // 报错之后两个宿主都没被写过:不回落单槽
203
+ @Expect(mainHost.notifiedArgs, "")
204
+ @Expect(secondHost.notifiedArgs, "")
205
+ }
@@ -267,3 +267,51 @@ func testSerialPortListIsSortedAndWhitelisted(): Unit {
267
267
  i++
268
268
  }
269
269
  }
270
+
271
+ /* ===== ohos 分支的可桌面验证面(serial_ohos.cj) =====
272
+ * 真正的 open/read/write 走 ohos 专属 FFI(@When 门禁),桌面上编译不到——那部分归真机取证
273
+ * (issue #5:USB 转串口接 OTG、DDK 权限)。下面锁的是 ohos 与桌面**共用同一份代码**的纯函数:
274
+ * 波特率 → termios 常量映射(不带门禁,专为可测升 public)与设备白名单的 /dev/bus 路径。
275
+ */
276
+
277
+ @Test
278
+ func testSerialOhosBaudConstMatchesDesktopTiers(): Unit {
279
+ // 与 SERIAL_BAUDS 同一套档位:桌面允许的每一档,ohos 侧都必须有对应的 termios 常量
280
+ let tiers: Array<Int32> = [300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
281
+ 230400, 460800, 500000, 921600, 1000000]
282
+ for (b in tiers) {
283
+ @Expect(serialOhosBaudConst(b).isSome(), true)
284
+ }
285
+ // 关键取值抽查(asm-generic:B38400 以下连续,往上 0x1000+n)
286
+ @Expect(serialOhosBaudConst(115200).getOrThrow(), 0x1002)
287
+ @Expect(serialOhosBaudConst(9600).getOrThrow(), 0xd)
288
+ @Expect(serialOhosBaudConst(1000000).getOrThrow(), 0x1008)
289
+ // 桌面已拒的档位,ohos 侧同样不收(两层拦同一套名单,不会出现桌面放行、ohos 拒)
290
+ @Expect(serialOhosBaudConst(14400).isNone(), true)
291
+ @Expect(serialOhosBaudConst(4000000).isNone(), true)
292
+ }
293
+
294
+ @Test
295
+ func testSerialPathLooksAllowedCoversOhosDevicePaths(): Unit {
296
+ // 鸿蒙特有的 USB 串口落点必须过白名单(ohos 侧 open 的边界就靠它)
297
+ @Expect(serialPathLooksAllowed("/dev/bus/ttyUSB0"), true)
298
+ @Expect(serialPathLooksAllowed("/dev/bus/ttyACM0"), true)
299
+ @Expect(serialPathLooksAllowed("/dev/bus/ttyS1"), true)
300
+ // 穿越与任意路径照旧拒
301
+ @Expect(serialPathLooksAllowed("/dev/bus/../ttyUSB0"), false)
302
+ @Expect(serialPathLooksAllowed("/data/local/ttyUSB0"), false)
303
+ }
304
+
305
+ @Test
306
+ func testSerialPluginDescribesAllThreeHosts(): Unit {
307
+ let d = SerialPlugin().describe()
308
+ let hosts = d.supportedHosts
309
+ @Expect(hosts.size, 3)
310
+ var hasArkweb = false
311
+ for (h in hosts) {
312
+ if (h == "arkweb") {
313
+ hasArkweb = true
314
+ }
315
+ }
316
+ @Expect(hasArkweb, true)
317
+ }