cj-tauri 2.0.0 → 2.1.1

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 (157) hide show
  1. package/README.md +206 -139
  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 +643 -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 +638 -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 +3745 -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/src/api_system.cj +67 -67
  77. package/src/app.cj +335 -969
  78. package/src/app_events.cj +166 -0
  79. package/src/app_host_ops.cj +287 -0
  80. package/src/app_run.cj +283 -0
  81. package/src/app_spec.cj +76 -76
  82. package/src/arkweb_menu.cj +216 -0
  83. package/src/arkweb_script.cj +142 -142
  84. package/src/capability.cj +409 -409
  85. package/src/clipboard_bridge.cj +178 -0
  86. package/src/dialog_spec.cj +79 -0
  87. package/src/dragdrop.cj +26 -26
  88. package/src/embedded_assets.cj +98 -98
  89. package/src/host.cj +752 -607
  90. package/src/host_arkweb.cj +924 -778
  91. package/src/host_webkit.cj +663 -570
  92. package/src/host_webview2.cj +663 -570
  93. package/src/hotkey.cj +66 -66
  94. package/src/ipc_hub.cj +401 -337
  95. package/src/ipc_message.cj +286 -286
  96. package/src/launch.cj +62 -62
  97. package/src/logging.cj +127 -127
  98. package/src/menu.cj +197 -197
  99. package/src/platform/platform_app_context.cj +77 -77
  100. package/src/platform/platform_assets.cj +33 -33
  101. package/src/platform/platform_discover.cj +176 -176
  102. package/src/platform/platform_env.cj +29 -29
  103. package/src/platform/platform_feature.cj +31 -31
  104. package/src/platform/platform_page_source.cj +87 -87
  105. package/src/platform/platform_target_info.cj +32 -32
  106. package/src/platform_host_provider.cj +87 -87
  107. package/src/plugin.cj +333 -333
  108. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  109. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  110. package/src/plugins/fs/plugin_fs.cj +105 -1
  111. package/src/plugins/http/http_backend.cj +131 -0
  112. package/src/plugins/http/http_desktop.cj +195 -0
  113. package/src/plugins/http/http_ohos.cj +204 -0
  114. package/src/plugins/http/plugin_http.cj +310 -459
  115. package/src/plugins/log/plugin_log.cj +105 -105
  116. package/src/plugins/menu/plugin_menu.cj +158 -158
  117. package/src/plugins/notification/plugin_notification.cj +154 -154
  118. package/src/plugins/serial/plugin_serial.cj +458 -454
  119. package/src/plugins/serial/serial_bridge.cj +194 -194
  120. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  121. package/src/plugins/serial/serial_ohos.cj +529 -0
  122. package/src/plugins/shell/plugin_shell.cj +258 -258
  123. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  124. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  125. package/src/plugins/ws/plugin_ws.cj +488 -470
  126. package/src/process_hidden.cj +174 -174
  127. package/src/scope.cj +391 -391
  128. package/src/shell_event.cj +69 -69
  129. package/src/tests/app_spec_test.cj +130 -127
  130. package/src/tests/capability_test.cj +243 -243
  131. package/src/tests/dialog_spec_test.cj +73 -0
  132. package/src/tests/fake_host.cj +201 -201
  133. package/src/tests/ipc_hub_test.cj +563 -487
  134. package/src/tests/logging_test.cj +62 -62
  135. package/src/tests/platform_contract_test.cj +264 -251
  136. package/src/tests/plugin_clipboard_test.cj +222 -222
  137. package/src/tests/plugin_dialog_test.cj +237 -237
  138. package/src/tests/plugin_fs_test.cj +207 -0
  139. package/src/tests/plugin_http_test.cj +124 -124
  140. package/src/tests/plugin_notification_test.cj +205 -205
  141. package/src/tests/plugin_serial_test.cj +48 -0
  142. package/src/tests/plugin_test.cj +605 -585
  143. package/src/tests/plugin_ws_test.cj +40 -40
  144. package/src/tests/process_hidden_test.cj +129 -129
  145. package/src/tests/scope_test.cj +350 -350
  146. package/src/tests/system_host_test.cj +99 -81
  147. package/src/tests/urlutil_test.cj +53 -53
  148. package/src/tests/version_test.cj +75 -75
  149. package/src/tests/window_registry_test.cj +123 -123
  150. package/src/tray.cj +90 -90
  151. package/src/urlutil.cj +159 -159
  152. package/src/version.cj +44 -44
  153. package/src/window.cj +51 -51
  154. package/src/window_registry.cj +116 -116
  155. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  156. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  157. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,222 +1,222 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.plugins.clipboard.*
5
- import std.collection.*
6
- import stdx.encoding.json.*
7
- import std.unittest.*
8
- import std.unittest.testmacro.*
9
-
10
- /*
11
- * clipboard 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
- * + 「宿主已装配」时的读写转交(用 FakeHost 替身记痕)。
13
- *
14
- * 真剪贴板读写落在 C 桥(GTK / Win32 的剪贴板调用必须在宿主 UI 线程),归实机验证
15
- * (examples/plugin-clipboard);这里只锁「不碰剪贴板就能判定的入口逻辑」。
16
- */
17
-
18
- /** 造 { text } 参数 */
19
- func clipTextArgs(text: String): JsonObject {
20
- var o = JsonObject()
21
- o.put("text", JsonString(text))
22
- return o
23
- }
24
-
25
- /** 取一条「已装配宿主」的命令处理器:剪贴板命令要先经 attachHost 才有宿主可用 */
26
- func clipCmdWithHost(short: String, host: FakeHost): CommandHandler {
27
- let _ = short
28
- let c = ClipboardCommands()
29
- c.attachHost(host)
30
- return c
31
- }
32
-
33
- /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
34
- func clipErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
35
- try {
36
- handler.handle(cmd, args, IpcContext("main", IpcHub()))
37
- return ""
38
- } catch (e: CommandException) {
39
- return e.message
40
- }
41
- }
42
-
43
- @Test
44
- func testClipboardPluginSurface(): Unit {
45
- let p = ClipboardPlugin()
46
- @Expect(p.name(), "clipboard")
47
- let names = pluginCommandNames(p)
48
- @Expect(names.size, 2)
49
- var hasRead = false
50
- var hasWrite = false
51
- for (n in names) {
52
- if (n == "clipboard:read") {
53
- hasRead = true
54
- }
55
- if (n == "clipboard:write") {
56
- hasWrite = true
57
- }
58
- }
59
- @Expect(hasRead, true)
60
- @Expect(hasWrite, true)
61
- }
62
-
63
- @Test
64
- func testClipboardPluginShimTargetsItsOwnNamespace(): Unit {
65
- let js = ClipboardPlugin().jsShim()
66
- @Expect(js.contains("t.clipboard"), true)
67
- @Expect(js.contains("'clipboard:write'"), true)
68
- @Expect(js.contains("'clipboard:read'"), true)
69
- }
70
-
71
- @Test
72
- func testClipboardPluginDeclaresPermissionSets(): Unit {
73
- let sets = pluginPermissionSets(ClipboardPlugin())
74
- @Expect(sets.size, 2)
75
- // readonly 只放行读:剪贴板里可能有用户敏感内容,「只写不读」是合理的最小授权面
76
- let ro = sets.get("clipboard:readonly").getOrThrow()
77
- @Expect(ro.size, 1)
78
- @Expect(ro[0], "clipboard:read")
79
- @Expect(sets.get("clipboard:default").getOrThrow().size, 2)
80
- }
81
-
82
- @Test
83
- func testClipboardPermissionSetGatesCommands(): Unit {
84
- let out = ArrayList<String>()
85
- let reg = CapabilityRegistry()
86
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"c\",\"permissions\":[\"clipboard:readonly\"]}"))
87
- let hub = hubWithRegistry(reg, out)
88
- registerPluginCommands(hub, ClipboardPlugin())
89
- registerPluginPermissionSets(reg, ClipboardPlugin())
90
-
91
- // 集内命令放行:这条会走到「宿主未装配」的业务错误,而不是「未授权」
92
- hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"clipboard:read\"}")
93
- waitSink(out, 1)
94
- @Expect(out.size, 1)
95
- @Expect(strField(out[0], "error").contains("command not allowed"), false)
96
-
97
- // 集外命令仍拒:readonly 不夹带写能力
98
- hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"clipboard:write\",\"args\":{\"text\":\"x\"}}")
99
- @Expect(out.size, 2)
100
- @Expect(strField(out[1], "error"), "command not allowed: clipboard:write")
101
- }
102
-
103
- @Test
104
- func testClipboardWriteRequiresTextArgument(): Unit {
105
- let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
106
- let ctx = IpcContext("main", IpcHub())
107
- var threw = false
108
- try {
109
- cmd.handle("clipboard:write", JsonObject(), ctx)
110
- } catch (e: CommandException) {
111
- threw = true
112
- @Expect(e.message.contains("missing argument \"text\""), true)
113
- }
114
- @Expect(threw, true)
115
- }
116
-
117
- @Test
118
- func testClipboardWriteRejectsNonString(): Unit {
119
- var args = JsonObject()
120
- args.put("text", JsonInt(7))
121
- let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
122
- let ctx = IpcContext("main", IpcHub())
123
- var threw = false
124
- try {
125
- cmd.handle("clipboard:write", args, ctx)
126
- } catch (e: CommandException) {
127
- threw = true
128
- @Expect(e.message.contains("must be a string"), true)
129
- }
130
- @Expect(threw, true)
131
- }
132
-
133
- @Test
134
- func testClipboardWithoutHostIsReadableError(): Unit {
135
- // 没经 .plugin() 装配(没有宿主)时调用:给的是可读错误,而不是空指针崩掉
136
- @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:read", JsonObject())
137
- .contains("宿主未装配"), true)
138
- @Expect(clipErrOf(ClipboardPlugin().commands().get("write").getOrThrow(), "clipboard:write",
139
- clipTextArgs("hi")).contains("宿主未装配"), true)
140
- }
141
-
142
- @Test
143
- func testClipboardRejectsUnknownCommand(): Unit {
144
- @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:nope", JsonObject())
145
- .contains("unknown clipboard command"), true)
146
- }
147
-
148
- @Test
149
- func testClipboardWriteHandsTextToHost(): Unit {
150
- let host = FakeHost("main")
151
- let cmd = clipCmdWithHost("write", host)
152
- let r = cmd.handle("clipboard:write", clipTextArgs("hello 剪贴板"), IpcContext("main", IpcHub()))
153
- // 成功回 JsonNull(写命令没有返回值,与 Tauri 的 writeText 对齐)
154
- match (r.kind()) {
155
- case JsonKind.JsNull => ()
156
- case _ =>
157
- @Expect(false, true) // 成功路径不该回别的类型
158
- }
159
- @Expect(host.clipboardWritten, "hello 剪贴板")
160
- }
161
-
162
- @Test
163
- func testClipboardReadReturnsHostValue(): Unit {
164
- let host = FakeHost("main")
165
- let cmd = clipCmdWithHost("read", host)
166
-
167
- // 空 / 非文本 / 平台失败都归 None,转成 JsonNull——这是合法结果,不是错误
168
- let empty = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
169
- match (empty.kind()) {
170
- case JsonKind.JsNull => ()
171
- case _ => @Expect(false, true)
172
- }
173
-
174
- // 有内容:原样转成 JsonString 回给前端
175
- host.clipboardReadValue = Some("pasted 文本")
176
- let got = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
177
- @Expect(got.asString().getValue(), "pasted 文本")
178
- }
179
-
180
- /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
181
- @Test
182
- func testClipboardRoutesToTheWindowThatAsked(): Unit {
183
- // 与 dialog 同一条契约:报文里的 window 决定交给哪个宿主。
184
- // 只装单槽时第二扇窗的读写会落到默认窗那个宿主上(多窗口下的串台来源)。
185
- let mainHost = FakeHost("main")
186
- let secondHost = FakeHost("second")
187
- let cmds = ClipboardCommands()
188
- cmds.attachHost(mainHost)
189
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
190
-
191
- cmds.handle("clipboard:write", clipTextArgs("second 的剪贴板"), IpcContext("second", IpcHub()))
192
- @Expect(secondHost.clipboardWritten, "second 的剪贴板")
193
- // 关键断言:另一扇窗的宿主一点都没被碰到
194
- @Expect(mainHost.clipboardWritten, "")
195
-
196
- // 读同样按窗口解析
197
- secondHost.clipboardReadValue = Some("from second")
198
- let got = cmds.handle("clipboard:read", JsonObject(), IpcContext("second", IpcHub()))
199
- @Expect(got.asString().getValue(), "from second")
200
- @Expect(mainHost.clipboardReadValue.isNone(), true)
201
- }
202
-
203
- @Test
204
- func testClipboardReportsUnknownWindowInsteadOfFallingBack(): Unit {
205
- let mainHost = FakeHost("main")
206
- let secondHost = FakeHost("second")
207
- let cmds = ClipboardCommands()
208
- cmds.attachHost(mainHost)
209
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
210
-
211
- var threw = false
212
- try {
213
- cmds.handle("clipboard:write", clipTextArgs("x"), IpcContext("ghost", IpcHub()))
214
- } catch (e: CommandException) {
215
- threw = true
216
- @Expect(e.message.contains("ghost"), true)
217
- }
218
- @Expect(threw, true)
219
- // 报错之后两个宿主都没被写过:不回落单槽,宁可不写
220
- @Expect(mainHost.clipboardWritten, "")
221
- @Expect(secondHost.clipboardWritten, "")
222
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.clipboard.*
5
+ import std.collection.*
6
+ import stdx.encoding.json.*
7
+ import std.unittest.*
8
+ import std.unittest.testmacro.*
9
+
10
+ /*
11
+ * clipboard 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
+ * + 「宿主已装配」时的读写转交(用 FakeHost 替身记痕)。
13
+ *
14
+ * 真剪贴板读写落在 C 桥(GTK / Win32 的剪贴板调用必须在宿主 UI 线程),归实机验证
15
+ * (examples/plugin-clipboard);这里只锁「不碰剪贴板就能判定的入口逻辑」。
16
+ */
17
+
18
+ /** 造 { text } 参数 */
19
+ func clipTextArgs(text: String): JsonObject {
20
+ var o = JsonObject()
21
+ o.put("text", JsonString(text))
22
+ return o
23
+ }
24
+
25
+ /** 取一条「已装配宿主」的命令处理器:剪贴板命令要先经 attachHost 才有宿主可用 */
26
+ func clipCmdWithHost(short: String, host: FakeHost): CommandHandler {
27
+ let _ = short
28
+ let c = ClipboardCommands()
29
+ c.attachHost(host)
30
+ return c
31
+ }
32
+
33
+ /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
34
+ func clipErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
35
+ try {
36
+ handler.handle(cmd, args, IpcContext("main", IpcHub()))
37
+ return ""
38
+ } catch (e: CommandException) {
39
+ return e.message
40
+ }
41
+ }
42
+
43
+ @Test
44
+ func testClipboardPluginSurface(): Unit {
45
+ let p = ClipboardPlugin()
46
+ @Expect(p.name(), "clipboard")
47
+ let names = pluginCommandNames(p)
48
+ @Expect(names.size, 2)
49
+ var hasRead = false
50
+ var hasWrite = false
51
+ for (n in names) {
52
+ if (n == "clipboard:read") {
53
+ hasRead = true
54
+ }
55
+ if (n == "clipboard:write") {
56
+ hasWrite = true
57
+ }
58
+ }
59
+ @Expect(hasRead, true)
60
+ @Expect(hasWrite, true)
61
+ }
62
+
63
+ @Test
64
+ func testClipboardPluginShimTargetsItsOwnNamespace(): Unit {
65
+ let js = ClipboardPlugin().jsShim()
66
+ @Expect(js.contains("t.clipboard"), true)
67
+ @Expect(js.contains("'clipboard:write'"), true)
68
+ @Expect(js.contains("'clipboard:read'"), true)
69
+ }
70
+
71
+ @Test
72
+ func testClipboardPluginDeclaresPermissionSets(): Unit {
73
+ let sets = pluginPermissionSets(ClipboardPlugin())
74
+ @Expect(sets.size, 2)
75
+ // readonly 只放行读:剪贴板里可能有用户敏感内容,「只写不读」是合理的最小授权面
76
+ let ro = sets.get("clipboard:readonly").getOrThrow()
77
+ @Expect(ro.size, 1)
78
+ @Expect(ro[0], "clipboard:read")
79
+ @Expect(sets.get("clipboard:default").getOrThrow().size, 2)
80
+ }
81
+
82
+ @Test
83
+ func testClipboardPermissionSetGatesCommands(): Unit {
84
+ let out = ArrayList<String>()
85
+ let reg = CapabilityRegistry()
86
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"c\",\"permissions\":[\"clipboard:readonly\"]}"))
87
+ let hub = hubWithRegistry(reg, out)
88
+ registerPluginCommands(hub, ClipboardPlugin())
89
+ registerPluginPermissionSets(reg, ClipboardPlugin())
90
+
91
+ // 集内命令放行:这条会走到「宿主未装配」的业务错误,而不是「未授权」
92
+ hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"clipboard:read\"}")
93
+ waitSink(out, 1)
94
+ @Expect(out.size, 1)
95
+ @Expect(strField(out[0], "error").contains("command not allowed"), false)
96
+
97
+ // 集外命令仍拒:readonly 不夹带写能力
98
+ hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"clipboard:write\",\"args\":{\"text\":\"x\"}}")
99
+ @Expect(out.size, 2)
100
+ @Expect(strField(out[1], "error"), "command not allowed: clipboard:write")
101
+ }
102
+
103
+ @Test
104
+ func testClipboardWriteRequiresTextArgument(): Unit {
105
+ let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
106
+ let ctx = IpcContext("main", IpcHub())
107
+ var threw = false
108
+ try {
109
+ cmd.handle("clipboard:write", JsonObject(), ctx)
110
+ } catch (e: CommandException) {
111
+ threw = true
112
+ @Expect(e.message.contains("missing argument \"text\""), true)
113
+ }
114
+ @Expect(threw, true)
115
+ }
116
+
117
+ @Test
118
+ func testClipboardWriteRejectsNonString(): Unit {
119
+ var args = JsonObject()
120
+ args.put("text", JsonInt(7))
121
+ let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
122
+ let ctx = IpcContext("main", IpcHub())
123
+ var threw = false
124
+ try {
125
+ cmd.handle("clipboard:write", args, ctx)
126
+ } catch (e: CommandException) {
127
+ threw = true
128
+ @Expect(e.message.contains("must be a string"), true)
129
+ }
130
+ @Expect(threw, true)
131
+ }
132
+
133
+ @Test
134
+ func testClipboardWithoutHostIsReadableError(): Unit {
135
+ // 没经 .plugin() 装配(没有宿主)时调用:给的是可读错误,而不是空指针崩掉
136
+ @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:read", JsonObject())
137
+ .contains("宿主未装配"), true)
138
+ @Expect(clipErrOf(ClipboardPlugin().commands().get("write").getOrThrow(), "clipboard:write",
139
+ clipTextArgs("hi")).contains("宿主未装配"), true)
140
+ }
141
+
142
+ @Test
143
+ func testClipboardRejectsUnknownCommand(): Unit {
144
+ @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:nope", JsonObject())
145
+ .contains("unknown clipboard command"), true)
146
+ }
147
+
148
+ @Test
149
+ func testClipboardWriteHandsTextToHost(): Unit {
150
+ let host = FakeHost("main")
151
+ let cmd = clipCmdWithHost("write", host)
152
+ let r = cmd.handle("clipboard:write", clipTextArgs("hello 剪贴板"), IpcContext("main", IpcHub()))
153
+ // 成功回 JsonNull(写命令没有返回值,与 Tauri 的 writeText 对齐)
154
+ match (r.kind()) {
155
+ case JsonKind.JsNull => ()
156
+ case _ =>
157
+ @Expect(false, true) // 成功路径不该回别的类型
158
+ }
159
+ @Expect(host.clipboardWritten, "hello 剪贴板")
160
+ }
161
+
162
+ @Test
163
+ func testClipboardReadReturnsHostValue(): Unit {
164
+ let host = FakeHost("main")
165
+ let cmd = clipCmdWithHost("read", host)
166
+
167
+ // 空 / 非文本 / 平台失败都归 None,转成 JsonNull——这是合法结果,不是错误
168
+ let empty = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
169
+ match (empty.kind()) {
170
+ case JsonKind.JsNull => ()
171
+ case _ => @Expect(false, true)
172
+ }
173
+
174
+ // 有内容:原样转成 JsonString 回给前端
175
+ host.clipboardReadValue = Some("pasted 文本")
176
+ let got = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
177
+ @Expect(got.asString().getValue(), "pasted 文本")
178
+ }
179
+
180
+ /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
181
+ @Test
182
+ func testClipboardRoutesToTheWindowThatAsked(): Unit {
183
+ // 与 dialog 同一条契约:报文里的 window 决定交给哪个宿主。
184
+ // 只装单槽时第二扇窗的读写会落到默认窗那个宿主上(多窗口下的串台来源)。
185
+ let mainHost = FakeHost("main")
186
+ let secondHost = FakeHost("second")
187
+ let cmds = ClipboardCommands()
188
+ cmds.attachHost(mainHost)
189
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
190
+
191
+ cmds.handle("clipboard:write", clipTextArgs("second 的剪贴板"), IpcContext("second", IpcHub()))
192
+ @Expect(secondHost.clipboardWritten, "second 的剪贴板")
193
+ // 关键断言:另一扇窗的宿主一点都没被碰到
194
+ @Expect(mainHost.clipboardWritten, "")
195
+
196
+ // 读同样按窗口解析
197
+ secondHost.clipboardReadValue = Some("from second")
198
+ let got = cmds.handle("clipboard:read", JsonObject(), IpcContext("second", IpcHub()))
199
+ @Expect(got.asString().getValue(), "from second")
200
+ @Expect(mainHost.clipboardReadValue.isNone(), true)
201
+ }
202
+
203
+ @Test
204
+ func testClipboardReportsUnknownWindowInsteadOfFallingBack(): Unit {
205
+ let mainHost = FakeHost("main")
206
+ let secondHost = FakeHost("second")
207
+ let cmds = ClipboardCommands()
208
+ cmds.attachHost(mainHost)
209
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
210
+
211
+ var threw = false
212
+ try {
213
+ cmds.handle("clipboard:write", clipTextArgs("x"), IpcContext("ghost", IpcHub()))
214
+ } catch (e: CommandException) {
215
+ threw = true
216
+ @Expect(e.message.contains("ghost"), true)
217
+ }
218
+ @Expect(threw, true)
219
+ // 报错之后两个宿主都没被写过:不回落单槽,宁可不写
220
+ @Expect(mainHost.clipboardWritten, "")
221
+ @Expect(secondHost.clipboardWritten, "")
222
+ }