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,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
+ }