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,243 +1,243 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import std.collection.*
5
- import stdx.encoding.json.*
6
- import std.unittest.*
7
- import std.unittest.testmacro.*
8
-
9
- /*
10
- * 能力安全模型单元测试:默认最小权限是框架的核心承诺,
11
- * 这里锁住「未声明一律拒绝」「声明了才放行」「畸形清单不炸」三条行为。
12
- */
13
-
14
- @Test
15
- func testFromJsonParsesAllFields(): Unit {
16
- let raw = "{\"identifier\":\"default\",\"windows\":[\"main\"],\"commands\":[\"greet\",\"system:ping\"],\"events\":[\"tick\"]}"
17
- let cap = Capability.fromJson(JsonValue.fromStr(raw))
18
- @Expect(cap.isSome(), true)
19
- let c = cap.getOrThrow()
20
- @Expect(c.identifier, "default")
21
- @Expect(c.commands.size, 2)
22
- @Expect(c.canInvoke("greet"), true)
23
- @Expect(c.canInvoke("system:ping"), true)
24
- @Expect(c.canEmit("tick"), true)
25
- @Expect(c.windows.size, 1)
26
- }
27
-
28
- @Test
29
- func testFromJsonDefaultsToDenyAll(): Unit {
30
- // 只给 identifier:清单合法但什么都不放行(默认最小权限)
31
- let cap = Capability.fromJson(JsonValue.fromStr("{\"identifier\":\"empty\"}"))
32
- @Expect(cap.isSome(), true)
33
- let c = cap.getOrThrow()
34
- @Expect(c.canInvoke("greet"), false)
35
- @Expect(c.canEmit("tick"), false)
36
- @Expect(c.commands.size, 0)
37
- @Expect(c.events.size, 0)
38
- }
39
-
40
- @Test
41
- func testFromJsonRejectsInvalidShape(): Unit {
42
- // 顶层不是对象
43
- @Expect(Capability.fromJson(JsonValue.fromStr("[1,2]")).isNone(), true)
44
- // 缺 identifier
45
- @Expect(Capability.fromJson(JsonValue.fromStr("{\"commands\":[\"a\"]}")).isNone(), true)
46
- // identifier 不是字符串
47
- @Expect(Capability.fromJson(JsonValue.fromStr("{\"identifier\":7}")).isNone(), true)
48
- // commands 不是数组:宽容跳过,清单本身仍合法(放行集合为空)
49
- let lenient = Capability.fromJson(JsonValue.fromStr("{\"identifier\":\"x\",\"commands\":\"greet\"}"))
50
- @Expect(lenient.isSome(), true)
51
- @Expect(lenient.getOrThrow().canInvoke("greet"), false)
52
- // 注:非 JSON 文本在 JsonValue.fromStr 处就抛 JsonException,进不到 fromJson;
53
- // 这层防护在调用方——InvokeRequest.parse 与 capability_loader 的 try 里(前者见 ipc 测试)。
54
- }
55
-
56
- @Test
57
- func testFromJsonSkipsNonStringEntries(): Unit {
58
- // 数组里混入数字/对象:跳过而不是整体失败
59
- let raw = "{\"identifier\":\"mixed\",\"commands\":[\"ok\",7,{\"a\":1}],\"events\":[true,\"tick\"]}"
60
- let cap = Capability.fromJson(JsonValue.fromStr(raw))
61
- @Expect(cap.isSome(), true)
62
- let c = cap.getOrThrow()
63
- @Expect(c.canInvoke("ok"), true)
64
- @Expect(c.commands.size, 1)
65
- @Expect(c.canEmit("tick"), true)
66
- @Expect(c.events.size, 1)
67
- }
68
-
69
- @Test
70
- func testRegistryAggregatesCapabilities(): Unit {
71
- let reg = CapabilityRegistry()
72
- let a = Capability("a")
73
- a.allowCommand("greet")
74
- let b = Capability("b")
75
- b.allowCommand("system:ping")
76
- b.allowEvent("tick")
77
- reg.add(a)
78
- reg.add(b)
79
- @Expect(reg.size(), 2)
80
- // 任一清单放行即通过
81
- @Expect(reg.canInvoke("greet"), true)
82
- @Expect(reg.canInvoke("system:ping"), true)
83
- // 都没声明的一律拒绝
84
- @Expect(reg.canInvoke("system:echo"), false)
85
- @Expect(reg.canEmit("tick"), true)
86
- @Expect(reg.canEmit("other"), false)
87
- }
88
-
89
- @Test
90
- func testRegistryEmptyDeniesEverything(): Unit {
91
- let reg = CapabilityRegistry()
92
- @Expect(reg.canInvoke("system:version"), false)
93
- @Expect(reg.canEmit("tick"), false)
94
- @Expect(reg.size(), 0)
95
- }
96
-
97
- @Test
98
- func testRegistryAddFromJson(): Unit {
99
- let reg = CapabilityRegistry()
100
- let ok = reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"x\",\"commands\":[\"greet\"]}"))
101
- @Expect(ok, true)
102
- @Expect(reg.canInvoke("greet"), true)
103
- // 畸形清单:返回 false 且不影响已有能力
104
- let bad = reg.addFromJson(JsonValue.fromStr("{\"nope\":1}"))
105
- @Expect(bad, false)
106
- @Expect(reg.size(), 1)
107
- @Expect(reg.canInvoke("greet"), true)
108
- }
109
-
110
- @Test
111
- func testLoaderToleratesMissingDir(): Unit {
112
- // 目录不存在不是错误:返回 0,注册表保持为空(首次放到没有 capabilities/ 的目录里跑就是这样)
113
- let reg = CapabilityRegistry()
114
- @Expect(loadCapabilitiesFromDir("no/such/capabilities/dir", reg), 0)
115
- @Expect(reg.size(), 0)
116
- }
117
-
118
- /*
119
- * 命名权限集(v1.1):清单里按集名引用,成员展开后与明文同权。
120
- * 三条底线要锁住:集名单独存在不放行、注册了没引用不放行、未知集不生效也不炸。
121
- */
122
-
123
- @Test
124
- func testFromJsonParsesPermissionSets(): Unit {
125
- let cap = Capability.fromJson(JsonValue.fromStr("{\"identifier\":\"default\",\"permissions\":[\"fs:readonly\"]}"))
126
- @Expect(cap.isSome(), true)
127
- let c = cap.getOrThrow()
128
- @Expect(c.permissions.size, 1)
129
- // 集名只是「引用」:没有插件注册同名集时,它一条也不放行
130
- @Expect(c.canInvoke("fs:readText"), false)
131
- }
132
-
133
- @Test
134
- func testPermissionSetExpandsToMembers(): Unit {
135
- let reg = CapabilityRegistry()
136
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"fs:readonly\"]}"))
137
- let members = ArrayList<String>()
138
- members.add("fs:readText")
139
- members.add("fs:exists")
140
- reg.addPermissionSet("fs:readonly", members)
141
- @Expect(reg.canInvoke("fs:readText"), true)
142
- @Expect(reg.canInvoke("fs:exists"), true)
143
- @Expect(reg.canInvoke("fs:writeText"), false) // 集外命令照旧拒绝
144
- @Expect(reg.canInvoke("system:version"), false) // 内置命令的放行规则没变
145
- }
146
-
147
- @Test
148
- func testPermissionSetCoversEventsToo(): Unit {
149
- let reg = CapabilityRegistry()
150
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"demo:watch\"]}"))
151
- let members = ArrayList<String>()
152
- members.add("demo:changed")
153
- reg.addPermissionSet("demo:watch", members)
154
- @Expect(reg.canEmit("demo:changed"), true)
155
- @Expect(reg.canEmit("demo:other"), false)
156
- }
157
-
158
- @Test
159
- func testRegisteredPermissionSetNotReferencedStaysDenied(): Unit {
160
- // 集注册了,但清单没引用它:一条也不放行(默认最小权限不打折)
161
- let reg = CapabilityRegistry()
162
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\"}"))
163
- let members = ArrayList<String>()
164
- members.add("fs:readText")
165
- reg.addPermissionSet("fs:readonly", members)
166
- @Expect(reg.canInvoke("fs:readText"), false)
167
- }
168
-
169
- @Test
170
- func testPermissionSetWithoutProviderIsInert(): Unit {
171
- let reg = CapabilityRegistry()
172
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"nope:none\"],\"commands\":[\"greet\"]}"))
173
- @Expect(reg.canInvoke("greet"), true) // 明文命令不受影响
174
- @Expect(reg.canInvoke("nope:thing"), false) // 未知集不生效、也不炸
175
- @Expect(reg.declaredPermissionSets().size, 1) // 但要能被列出来,供启动提示
176
- @Expect(reg.hasPermissionSet("nope:none"), false)
177
- }
178
-
179
- @Test
180
- func testPlainNameAndPermissionSetCoexist(): Unit {
181
- // 两种写法混用:并集生效(旧清单可以逐条迁移)
182
- let reg = CapabilityRegistry()
183
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"commands\":[\"fs:exists\"],\"permissions\":[\"fs:readonly\"]}"))
184
- let members = ArrayList<String>()
185
- members.add("fs:readText")
186
- reg.addPermissionSet("fs:readonly", members)
187
- @Expect(reg.canInvoke("fs:exists"), true)
188
- @Expect(reg.canInvoke("fs:readText"), true)
189
- @Expect(reg.canInvoke("fs:writeText"), false)
190
- }
191
-
192
- /*
193
- * 白名单的**运行期加白**(D-021):与清单同权、先 deny 后 allow、只收具体值。
194
- * 这三条正是它敢开「运行期」这个口子的前提——缺一条就等于把防线交给页面。
195
- */
196
-
197
- @Test
198
- func testRuntimeAllowExtendsManifestScope(): Unit {
199
- let reg = CapabilityRegistry()
200
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"scopes\":{\"http\":{\"allow\":[\"api.a.com\"]}}}"))
201
- @Expect(reg.scopeAllowsValue("http", "api.a.com"), true)
202
- @Expect(reg.scopeAllowsValue("http", "api.b.com"), false)
203
- // 叠加一条后:新旧都在,没加的照旧拒
204
- @Expect(reg.addRuntimeScopeAllow("http", "api.b.com"), true)
205
- @Expect(reg.scopeAllowsValue("http", "api.b.com"), true)
206
- @Expect(reg.scopeAllowsValue("http", "api.a.com"), true)
207
- @Expect(reg.scopeAllowsValue("http", "api.evil.com"), false)
208
- }
209
-
210
- @Test
211
- func testRuntimeAllowCannotBeatDeny(): Unit {
212
- // deny 命中即拒:加白掀不掉它(运行期条目走的仍是「先 deny 后 allow」同一条判定)
213
- let reg = CapabilityRegistry()
214
- reg.addFromJson(JsonValue.fromStr(
215
- "{\"identifier\":\"d\",\"scopes\":{\"http\":{\"allow\":[\"api.a.com\"],\"deny\":[\"api.a.com\"]}}}"))
216
- @Expect(reg.scopeAllowsValue("http", "api.a.com"), false)
217
- @Expect(reg.addRuntimeScopeAllow("http", "api.a.com"), true)
218
- @Expect(reg.scopeAllowsValue("http", "api.a.com"), false)
219
- }
220
-
221
- @Test
222
- func testRuntimeAllowRefusesGlobPattern(): Unit {
223
- // 页面可控字符串里带一个通配就等于全放行:注册中心必须当场拒,且名单一条都不多
224
- let reg = CapabilityRegistry()
225
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"scopes\":{\"http\":{\"allow\":[\"api.a.com\"]}}}"))
226
- @Expect(reg.addRuntimeScopeAllow("http", "*"), false)
227
- @Expect(reg.addRuntimeScopeAllow("http", "api.*.com"), false)
228
- @Expect(reg.addRuntimeScopeAllow("http", "api.a.com\n"), false)
229
- @Expect(reg.addRuntimeScopeAllow("http", ""), false)
230
- @Expect(reg.scopeAllowsValue("http", "api.evil.com"), false)
231
- @Expect(reg.scopeForPlugin("http").getOrThrow().allow.size, 1)
232
- }
233
-
234
- @Test
235
- func testRuntimeAllowIgnoredWhenManifestHasNoScope(): Unit {
236
- // 清单没给该插件写 scope:调用方按旧行为全量放行,叠加项不得把它收窄成「只放行这一条」
237
- let reg = CapabilityRegistry()
238
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\"}"))
239
- @Expect(reg.scopeAllowsValue("http", "anything.at.all"), true)
240
- @Expect(reg.addRuntimeScopeAllow("http", "api.b.com"), true)
241
- @Expect(reg.scopeForPlugin("http").isNone(), true)
242
- @Expect(reg.scopeAllowsValue("http", "anything.at.all"), true)
243
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import std.collection.*
5
+ import stdx.encoding.json.*
6
+ import std.unittest.*
7
+ import std.unittest.testmacro.*
8
+
9
+ /*
10
+ * 能力安全模型单元测试:默认最小权限是框架的核心承诺,
11
+ * 这里锁住「未声明一律拒绝」「声明了才放行」「畸形清单不炸」三条行为。
12
+ */
13
+
14
+ @Test
15
+ func testFromJsonParsesAllFields(): Unit {
16
+ let raw = "{\"identifier\":\"default\",\"windows\":[\"main\"],\"commands\":[\"greet\",\"system:ping\"],\"events\":[\"tick\"]}"
17
+ let cap = Capability.fromJson(JsonValue.fromStr(raw))
18
+ @Expect(cap.isSome(), true)
19
+ let c = cap.getOrThrow()
20
+ @Expect(c.identifier, "default")
21
+ @Expect(c.commands.size, 2)
22
+ @Expect(c.canInvoke("greet"), true)
23
+ @Expect(c.canInvoke("system:ping"), true)
24
+ @Expect(c.canEmit("tick"), true)
25
+ @Expect(c.windows.size, 1)
26
+ }
27
+
28
+ @Test
29
+ func testFromJsonDefaultsToDenyAll(): Unit {
30
+ // 只给 identifier:清单合法但什么都不放行(默认最小权限)
31
+ let cap = Capability.fromJson(JsonValue.fromStr("{\"identifier\":\"empty\"}"))
32
+ @Expect(cap.isSome(), true)
33
+ let c = cap.getOrThrow()
34
+ @Expect(c.canInvoke("greet"), false)
35
+ @Expect(c.canEmit("tick"), false)
36
+ @Expect(c.commands.size, 0)
37
+ @Expect(c.events.size, 0)
38
+ }
39
+
40
+ @Test
41
+ func testFromJsonRejectsInvalidShape(): Unit {
42
+ // 顶层不是对象
43
+ @Expect(Capability.fromJson(JsonValue.fromStr("[1,2]")).isNone(), true)
44
+ // 缺 identifier
45
+ @Expect(Capability.fromJson(JsonValue.fromStr("{\"commands\":[\"a\"]}")).isNone(), true)
46
+ // identifier 不是字符串
47
+ @Expect(Capability.fromJson(JsonValue.fromStr("{\"identifier\":7}")).isNone(), true)
48
+ // commands 不是数组:宽容跳过,清单本身仍合法(放行集合为空)
49
+ let lenient = Capability.fromJson(JsonValue.fromStr("{\"identifier\":\"x\",\"commands\":\"greet\"}"))
50
+ @Expect(lenient.isSome(), true)
51
+ @Expect(lenient.getOrThrow().canInvoke("greet"), false)
52
+ // 注:非 JSON 文本在 JsonValue.fromStr 处就抛 JsonException,进不到 fromJson;
53
+ // 这层防护在调用方——InvokeRequest.parse 与 capability_loader 的 try 里(前者见 ipc 测试)。
54
+ }
55
+
56
+ @Test
57
+ func testFromJsonSkipsNonStringEntries(): Unit {
58
+ // 数组里混入数字/对象:跳过而不是整体失败
59
+ let raw = "{\"identifier\":\"mixed\",\"commands\":[\"ok\",7,{\"a\":1}],\"events\":[true,\"tick\"]}"
60
+ let cap = Capability.fromJson(JsonValue.fromStr(raw))
61
+ @Expect(cap.isSome(), true)
62
+ let c = cap.getOrThrow()
63
+ @Expect(c.canInvoke("ok"), true)
64
+ @Expect(c.commands.size, 1)
65
+ @Expect(c.canEmit("tick"), true)
66
+ @Expect(c.events.size, 1)
67
+ }
68
+
69
+ @Test
70
+ func testRegistryAggregatesCapabilities(): Unit {
71
+ let reg = CapabilityRegistry()
72
+ let a = Capability("a")
73
+ a.allowCommand("greet")
74
+ let b = Capability("b")
75
+ b.allowCommand("system:ping")
76
+ b.allowEvent("tick")
77
+ reg.add(a)
78
+ reg.add(b)
79
+ @Expect(reg.size(), 2)
80
+ // 任一清单放行即通过
81
+ @Expect(reg.canInvoke("greet"), true)
82
+ @Expect(reg.canInvoke("system:ping"), true)
83
+ // 都没声明的一律拒绝
84
+ @Expect(reg.canInvoke("system:echo"), false)
85
+ @Expect(reg.canEmit("tick"), true)
86
+ @Expect(reg.canEmit("other"), false)
87
+ }
88
+
89
+ @Test
90
+ func testRegistryEmptyDeniesEverything(): Unit {
91
+ let reg = CapabilityRegistry()
92
+ @Expect(reg.canInvoke("system:version"), false)
93
+ @Expect(reg.canEmit("tick"), false)
94
+ @Expect(reg.size(), 0)
95
+ }
96
+
97
+ @Test
98
+ func testRegistryAddFromJson(): Unit {
99
+ let reg = CapabilityRegistry()
100
+ let ok = reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"x\",\"commands\":[\"greet\"]}"))
101
+ @Expect(ok, true)
102
+ @Expect(reg.canInvoke("greet"), true)
103
+ // 畸形清单:返回 false 且不影响已有能力
104
+ let bad = reg.addFromJson(JsonValue.fromStr("{\"nope\":1}"))
105
+ @Expect(bad, false)
106
+ @Expect(reg.size(), 1)
107
+ @Expect(reg.canInvoke("greet"), true)
108
+ }
109
+
110
+ @Test
111
+ func testLoaderToleratesMissingDir(): Unit {
112
+ // 目录不存在不是错误:返回 0,注册表保持为空(首次放到没有 capabilities/ 的目录里跑就是这样)
113
+ let reg = CapabilityRegistry()
114
+ @Expect(loadCapabilitiesFromDir("no/such/capabilities/dir", reg), 0)
115
+ @Expect(reg.size(), 0)
116
+ }
117
+
118
+ /*
119
+ * 命名权限集(v1.1):清单里按集名引用,成员展开后与明文同权。
120
+ * 三条底线要锁住:集名单独存在不放行、注册了没引用不放行、未知集不生效也不炸。
121
+ */
122
+
123
+ @Test
124
+ func testFromJsonParsesPermissionSets(): Unit {
125
+ let cap = Capability.fromJson(JsonValue.fromStr("{\"identifier\":\"default\",\"permissions\":[\"fs:readonly\"]}"))
126
+ @Expect(cap.isSome(), true)
127
+ let c = cap.getOrThrow()
128
+ @Expect(c.permissions.size, 1)
129
+ // 集名只是「引用」:没有插件注册同名集时,它一条也不放行
130
+ @Expect(c.canInvoke("fs:readText"), false)
131
+ }
132
+
133
+ @Test
134
+ func testPermissionSetExpandsToMembers(): Unit {
135
+ let reg = CapabilityRegistry()
136
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"fs:readonly\"]}"))
137
+ let members = ArrayList<String>()
138
+ members.add("fs:readText")
139
+ members.add("fs:exists")
140
+ reg.addPermissionSet("fs:readonly", members)
141
+ @Expect(reg.canInvoke("fs:readText"), true)
142
+ @Expect(reg.canInvoke("fs:exists"), true)
143
+ @Expect(reg.canInvoke("fs:writeText"), false) // 集外命令照旧拒绝
144
+ @Expect(reg.canInvoke("system:version"), false) // 内置命令的放行规则没变
145
+ }
146
+
147
+ @Test
148
+ func testPermissionSetCoversEventsToo(): Unit {
149
+ let reg = CapabilityRegistry()
150
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"demo:watch\"]}"))
151
+ let members = ArrayList<String>()
152
+ members.add("demo:changed")
153
+ reg.addPermissionSet("demo:watch", members)
154
+ @Expect(reg.canEmit("demo:changed"), true)
155
+ @Expect(reg.canEmit("demo:other"), false)
156
+ }
157
+
158
+ @Test
159
+ func testRegisteredPermissionSetNotReferencedStaysDenied(): Unit {
160
+ // 集注册了,但清单没引用它:一条也不放行(默认最小权限不打折)
161
+ let reg = CapabilityRegistry()
162
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\"}"))
163
+ let members = ArrayList<String>()
164
+ members.add("fs:readText")
165
+ reg.addPermissionSet("fs:readonly", members)
166
+ @Expect(reg.canInvoke("fs:readText"), false)
167
+ }
168
+
169
+ @Test
170
+ func testPermissionSetWithoutProviderIsInert(): Unit {
171
+ let reg = CapabilityRegistry()
172
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"nope:none\"],\"commands\":[\"greet\"]}"))
173
+ @Expect(reg.canInvoke("greet"), true) // 明文命令不受影响
174
+ @Expect(reg.canInvoke("nope:thing"), false) // 未知集不生效、也不炸
175
+ @Expect(reg.declaredPermissionSets().size, 1) // 但要能被列出来,供启动提示
176
+ @Expect(reg.hasPermissionSet("nope:none"), false)
177
+ }
178
+
179
+ @Test
180
+ func testPlainNameAndPermissionSetCoexist(): Unit {
181
+ // 两种写法混用:并集生效(旧清单可以逐条迁移)
182
+ let reg = CapabilityRegistry()
183
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"commands\":[\"fs:exists\"],\"permissions\":[\"fs:readonly\"]}"))
184
+ let members = ArrayList<String>()
185
+ members.add("fs:readText")
186
+ reg.addPermissionSet("fs:readonly", members)
187
+ @Expect(reg.canInvoke("fs:exists"), true)
188
+ @Expect(reg.canInvoke("fs:readText"), true)
189
+ @Expect(reg.canInvoke("fs:writeText"), false)
190
+ }
191
+
192
+ /*
193
+ * 白名单的**运行期加白**(D-021):与清单同权、先 deny 后 allow、只收具体值。
194
+ * 这三条正是它敢开「运行期」这个口子的前提——缺一条就等于把防线交给页面。
195
+ */
196
+
197
+ @Test
198
+ func testRuntimeAllowExtendsManifestScope(): Unit {
199
+ let reg = CapabilityRegistry()
200
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"scopes\":{\"http\":{\"allow\":[\"api.a.com\"]}}}"))
201
+ @Expect(reg.scopeAllowsValue("http", "api.a.com"), true)
202
+ @Expect(reg.scopeAllowsValue("http", "api.b.com"), false)
203
+ // 叠加一条后:新旧都在,没加的照旧拒
204
+ @Expect(reg.addRuntimeScopeAllow("http", "api.b.com"), true)
205
+ @Expect(reg.scopeAllowsValue("http", "api.b.com"), true)
206
+ @Expect(reg.scopeAllowsValue("http", "api.a.com"), true)
207
+ @Expect(reg.scopeAllowsValue("http", "api.evil.com"), false)
208
+ }
209
+
210
+ @Test
211
+ func testRuntimeAllowCannotBeatDeny(): Unit {
212
+ // deny 命中即拒:加白掀不掉它(运行期条目走的仍是「先 deny 后 allow」同一条判定)
213
+ let reg = CapabilityRegistry()
214
+ reg.addFromJson(JsonValue.fromStr(
215
+ "{\"identifier\":\"d\",\"scopes\":{\"http\":{\"allow\":[\"api.a.com\"],\"deny\":[\"api.a.com\"]}}}"))
216
+ @Expect(reg.scopeAllowsValue("http", "api.a.com"), false)
217
+ @Expect(reg.addRuntimeScopeAllow("http", "api.a.com"), true)
218
+ @Expect(reg.scopeAllowsValue("http", "api.a.com"), false)
219
+ }
220
+
221
+ @Test
222
+ func testRuntimeAllowRefusesGlobPattern(): Unit {
223
+ // 页面可控字符串里带一个通配就等于全放行:注册中心必须当场拒,且名单一条都不多
224
+ let reg = CapabilityRegistry()
225
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"scopes\":{\"http\":{\"allow\":[\"api.a.com\"]}}}"))
226
+ @Expect(reg.addRuntimeScopeAllow("http", "*"), false)
227
+ @Expect(reg.addRuntimeScopeAllow("http", "api.*.com"), false)
228
+ @Expect(reg.addRuntimeScopeAllow("http", "api.a.com\n"), false)
229
+ @Expect(reg.addRuntimeScopeAllow("http", ""), false)
230
+ @Expect(reg.scopeAllowsValue("http", "api.evil.com"), false)
231
+ @Expect(reg.scopeForPlugin("http").getOrThrow().allow.size, 1)
232
+ }
233
+
234
+ @Test
235
+ func testRuntimeAllowIgnoredWhenManifestHasNoScope(): Unit {
236
+ // 清单没给该插件写 scope:调用方按旧行为全量放行,叠加项不得把它收窄成「只放行这一条」
237
+ let reg = CapabilityRegistry()
238
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\"}"))
239
+ @Expect(reg.scopeAllowsValue("http", "anything.at.all"), true)
240
+ @Expect(reg.addRuntimeScopeAllow("http", "api.b.com"), true)
241
+ @Expect(reg.scopeForPlugin("http").isNone(), true)
242
+ @Expect(reg.scopeAllowsValue("http", "anything.at.all"), true)
243
+ }
@@ -0,0 +1,73 @@
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import std.unittest.*
5
+ import std.unittest.testmacro.*
6
+
7
+ /*
8
+ * 对话框「形状」判定的单元测试(共享层 `src/dialog_spec.cj`)。
9
+ *
10
+ * 这些判定由桌面原生对话框与鸿蒙应用侧 overlay 共用,所以放这里测——原先把判定写在
11
+ * `host_arkweb.cj` 里,那个文件带 `@When[env == "ohos"]` 门禁,桌面 `cjpm test` 编不到,
12
+ * 等于这段语义一直没有回归网。
13
+ *
14
+ * `@Test` 只能修饰**顶层函数**(写成 class 方法会报 `@Test modified function is not top level`)。
15
+ */
16
+
17
+ /** 形状的可读名(enum 没派生 Equatable,断言走字符串比,绕开 `@Expect` 对 enum 的推断限制) */
18
+ func dialogShapeName(shape: DialogShape): String {
19
+ match (shape) {
20
+ case DialogShape.Message => return "message"
21
+ case DialogShape.Confirm => return "confirm"
22
+ case DialogShape.Prompt => return "prompt"
23
+ }
24
+ }
25
+
26
+ @Test
27
+ func dialogKindEmptyFallsBackToInfo() {
28
+ @Expect(normalizeDialogKind("") == "info")
29
+ @Expect(normalizeDialogKind(" ") == "info")
30
+ }
31
+
32
+ @Test
33
+ func dialogKindTrimsAndLowers() {
34
+ @Expect(normalizeDialogKind(" WARNING ") == "warning")
35
+ @Expect(normalizeDialogKind("Confirm") == "confirm")
36
+ @Expect(normalizeDialogKind("ERROR") == "error")
37
+ }
38
+
39
+ @Test
40
+ func dialogKindKeepsKnownValues() {
41
+ @Expect(normalizeDialogKind("info") == "info")
42
+ @Expect(normalizeDialogKind("prompt") == "prompt")
43
+ }
44
+
45
+ /* 页面把 kind 写错(打错字、传了个不存在的词)时回落 info:只影响外观,不该让对话框不弹 */
46
+ @Test
47
+ func dialogKindUnknownFallsBackToInfo() {
48
+ @Expect(normalizeDialogKind("fatal") == "info")
49
+ @Expect(normalizeDialogKind("警告") == "info")
50
+ }
51
+
52
+ @Test
53
+ func dialogShapeOfSingleKeyKinds() {
54
+ @Expect(dialogShapeName(dialogShapeOf("info")) == "message")
55
+ @Expect(dialogShapeName(dialogShapeOf("warning")) == "message")
56
+ @Expect(dialogShapeName(dialogShapeOf("error")) == "message")
57
+ @Expect(dialogShapeName(dialogShapeOf("")) == "message")
58
+ }
59
+
60
+ @Test
61
+ func dialogShapeOfTwoKeyKinds() {
62
+ @Expect(dialogShapeName(dialogShapeOf("confirm")) == "confirm")
63
+ @Expect(dialogShapeName(dialogShapeOf("PROMPT")) == "prompt")
64
+ }
65
+
66
+ /* 两键判定的口径:Confirm 与 Prompt 都要「确定 / 取消」,Message 只有一个确定键 */
67
+ @Test
68
+ func dialogShapeTwoKeyMatchesShape() {
69
+ @Expect(dialogShapeIsTwoKey(dialogShapeOf("confirm")))
70
+ @Expect(dialogShapeIsTwoKey(dialogShapeOf("prompt")))
71
+ @Expect(!dialogShapeIsTwoKey(dialogShapeOf("info")))
72
+ @Expect(!dialogShapeIsTwoKey(dialogShapeOf("fatal")))
73
+ }