cj-tauri 1.1.2 → 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 (162) hide show
  1. package/README.md +137 -77
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +13 -2
  4. package/cli/cjpm.toml +22 -13
  5. package/cli/src/doctor.cj +308 -304
  6. package/cli/src/main.cj +129 -121
  7. package/cli/src/manifest.cj +632 -0
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -754
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +46 -38
  12. package/cli/src/sync.cj +526 -0
  13. package/cli/src/util.cj +9 -0
  14. package/cli/templates/app/README.md +30 -59
  15. package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
  16. package/cli/templates/app/cj-tauri.toml +32 -0
  17. package/cli/templates/app/cjpm.toml +21 -4
  18. package/cli/templates/app/src/main.cj +29 -16
  19. package/cli/templates/app-react/README.md +34 -45
  20. package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
  21. package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
  22. package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
  23. package/cli/templates/app-react/cj-tauri.toml +33 -0
  24. package/cli/templates/app-react/cjpm.toml +21 -4
  25. package/cli/templates/app-react/src/main.cj +18 -29
  26. package/cli/templates/app-vue/README.md +34 -38
  27. package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
  29. package/cli/templates/app-vue/cj-tauri.toml +33 -0
  30. package/cli/templates/app-vue/cjpm.toml +21 -4
  31. package/cli/templates/app-vue/src/main.cj +18 -29
  32. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
  35. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  36. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
  38. package/native/build_win.bat +62 -62
  39. package/native/core/bridge_core.c +1681 -1626
  40. package/native/core/bridge_core.h +675 -618
  41. package/native/core/bridge_js.h +108 -108
  42. package/native/platforms/harmonyos/README.md +115 -6
  43. package/native/platforms/linux/bridge_linux.c +2787 -2485
  44. package/native/platforms/windows/bridge_win.c +3740 -3248
  45. package/native/tests/gtk_threading_probe.c +143 -143
  46. package/native/tests/test_bridge_core.c +1339 -1228
  47. package/package.json +53 -51
  48. package/prebuilt/linux-x64/cj-tauri +0 -0
  49. package/src/api_system.cj +67 -67
  50. package/src/app.cj +325 -940
  51. package/src/app_events.cj +166 -0
  52. package/src/app_host_ops.cj +287 -0
  53. package/src/app_run.cj +283 -0
  54. package/src/app_spec.cj +76 -0
  55. package/src/arkweb_menu.cj +216 -0
  56. package/src/arkweb_script.cj +0 -12
  57. package/src/capability.cj +409 -409
  58. package/src/clipboard_bridge.cj +178 -0
  59. package/src/dialog_spec.cj +79 -0
  60. package/src/dragdrop.cj +26 -26
  61. package/src/embedded_assets.cj +98 -98
  62. package/src/host.cj +752 -604
  63. package/src/host_arkweb.cj +201 -17
  64. package/src/host_webkit.cj +663 -548
  65. package/src/host_webview2.cj +663 -548
  66. package/src/hotkey.cj +66 -66
  67. package/src/ipc_hub.cj +337 -337
  68. package/src/ipc_message.cj +286 -286
  69. package/src/launch.cj +62 -0
  70. package/src/logging.cj +127 -127
  71. package/src/menu.cj +197 -197
  72. package/src/platform/platform_app_context.cj +77 -0
  73. package/src/platform/platform_assets.cj +33 -0
  74. package/src/platform/platform_discover.cj +176 -0
  75. package/src/platform/platform_env.cj +29 -0
  76. package/src/platform/platform_feature.cj +31 -0
  77. package/src/platform/platform_page_source.cj +87 -0
  78. package/src/platform/platform_target_info.cj +32 -0
  79. package/src/platform_host_provider.cj +87 -0
  80. package/src/plugin.cj +333 -299
  81. package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
  82. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  83. package/src/plugins/fs/plugin_fs.cj +105 -1
  84. package/src/plugins/http/http_backend.cj +131 -0
  85. package/src/plugins/http/http_desktop.cj +195 -0
  86. package/src/plugins/http/http_ohos.cj +204 -0
  87. package/src/plugins/http/plugin_http.cj +310 -459
  88. package/src/plugins/log/plugin_log.cj +105 -105
  89. package/src/plugins/menu/plugin_menu.cj +8 -0
  90. package/src/plugins/notification/plugin_notification.cj +154 -146
  91. package/src/plugins/serial/plugin_serial.cj +17 -1
  92. package/src/plugins/serial/serial_bridge.cj +9 -1
  93. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  94. package/src/plugins/serial/serial_ohos.cj +529 -0
  95. package/src/plugins/shell/plugin_shell.cj +29 -1
  96. package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
  97. package/src/plugins/ws/plugin_ws.cj +488 -470
  98. package/src/process_hidden.cj +174 -174
  99. package/src/scope.cj +391 -391
  100. package/src/shell_event.cj +69 -69
  101. package/src/tests/app_spec_test.cj +126 -0
  102. package/src/tests/capability_test.cj +243 -243
  103. package/src/tests/dialog_spec_test.cj +73 -0
  104. package/src/tests/fake_host.cj +201 -186
  105. package/src/tests/ipc_hub_test.cj +487 -487
  106. package/src/tests/logging_test.cj +62 -62
  107. package/src/tests/platform_contract_test.cj +251 -0
  108. package/src/tests/plugin_clipboard_test.cj +222 -222
  109. package/src/tests/plugin_dialog_test.cj +237 -237
  110. package/src/tests/plugin_fs_test.cj +207 -0
  111. package/src/tests/plugin_http_test.cj +124 -124
  112. package/src/tests/plugin_notification_test.cj +205 -205
  113. package/src/tests/plugin_serial_test.cj +48 -0
  114. package/src/tests/plugin_test.cj +66 -3
  115. package/src/tests/plugin_ws_test.cj +40 -40
  116. package/src/tests/process_hidden_test.cj +129 -129
  117. package/src/tests/scope_test.cj +350 -350
  118. package/src/tests/system_host_test.cj +99 -81
  119. package/src/tests/urlutil_test.cj +53 -53
  120. package/src/tests/version_test.cj +75 -75
  121. package/src/tests/window_registry_test.cj +123 -123
  122. package/src/tray.cj +90 -90
  123. package/src/urlutil.cj +159 -159
  124. package/src/version.cj +44 -44
  125. package/src/window.cj +51 -51
  126. package/src/window_registry.cj +116 -116
  127. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
  128. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  129. package/src/host_factory.cj +0 -48
  130. /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
  131. /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
  132. /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
  133. /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
  134. /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
  135. /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
  136. /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
  137. /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
  138. /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
  139. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
  140. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
  141. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  142. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  143. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
  144. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
  145. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
  146. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
  147. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
  148. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
  149. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
  150. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
  151. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
  152. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
  153. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  154. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  155. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
  156. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
  157. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
  158. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
  159. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
  160. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
  161. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
  162. /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
@@ -0,0 +1,207 @@
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.fs.*
5
+ import std.collection.*
6
+ import std.fs.*
7
+ import stdx.encoding.json.*
8
+ import std.unittest.*
9
+ import std.unittest.testmacro.*
10
+
11
+ /*
12
+ * fs 插件单元测试:纯仓颉插件(零 C 桥),这里测到的是「真实落盘」。
13
+ * 覆盖新四条命令(mkdir / rename / copy / stat)+ 命名权限集闸门 + 参数校验。
14
+ * 临时目录固定 /tmp/fs-unit-test(按固定名复用,不在仓库里留测试产物)。
15
+ */
16
+
17
+ const FS_T_BASE: String = "/tmp/fs-unit-test"
18
+
19
+ /** 递归删除基座目录(用例前置清场):上一轮跑完留下的树会让 mkdir / copy 撞「already exists」 */
20
+ func fsWipeBase(dir: String): Unit {
21
+ if (!exists(dir)) {
22
+ return
23
+ }
24
+ if (FileInfo(dir).isDirectory()) {
25
+ for (info in Directory.readFrom(dir)) {
26
+ fsWipeBase("${dir}/${info.name}")
27
+ }
28
+ }
29
+ // std.fs 的 remove 同时管文件与空目录(fs:remove 的注释有说明),自底向上删到这里必然为空
30
+ remove(dir)
31
+ }
32
+
33
+ /** 取一条 fs 命令处理器 */
34
+ func fsCmd(short: String): CommandHandler {
35
+ return FsPlugin().commands().get(short).getOrThrow()
36
+ }
37
+
38
+ /** 直接调命令,返回结果(抛 CommandException 就往上冒,由用例自己接) */
39
+ func fsRun(short: String, args: JsonObject): JsonValue {
40
+ return fsCmd(short).handle("fs:${short}", args, IpcContext("main", IpcHub()))
41
+ }
42
+
43
+ /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
44
+ func fsErrOf(short: String, args: JsonObject): String {
45
+ try {
46
+ fsRun(short, args)
47
+ return ""
48
+ } catch (e: CommandException) {
49
+ return e.message
50
+ }
51
+ }
52
+
53
+ /** 只有 path 一个参数的入参 */
54
+ func fsArgs(path: String): JsonObject {
55
+ var o = JsonObject()
56
+ o.put("path", JsonString(path))
57
+ return o
58
+ }
59
+
60
+ /** path + newPath 两个路径的入参(rename / copy) */
61
+ func fsArgsTo(path: String, newPath: String): JsonObject {
62
+ var o = JsonObject()
63
+ o.put("path", JsonString(path))
64
+ o.put("newPath", JsonString(newPath))
65
+ return o
66
+ }
67
+
68
+ /** path + recursive=true 的入参 */
69
+ func fsArgsRecursive(path: String): JsonObject {
70
+ var o = JsonObject()
71
+ o.put("path", JsonString(path))
72
+ o.put("recursive", JsonBool(true))
73
+ return o
74
+ }
75
+
76
+ /** path + newPath + overwrite=true 的入参(撞既有目标时放行) */
77
+ func fsArgsToOverwrite(path: String, newPath: String): JsonObject {
78
+ var o = JsonObject()
79
+ o.put("path", JsonString(path))
80
+ o.put("newPath", JsonString(newPath))
81
+ o.put("overwrite", JsonBool(true))
82
+ return o
83
+ }
84
+
85
+ @Test
86
+ func testFsShimHasNewEntries(): Unit {
87
+ let js = FsPlugin().jsShim()
88
+ @Expect(js.contains("'fs:mkdir'"), true)
89
+ @Expect(js.contains("'fs:rename'"), true)
90
+ @Expect(js.contains("'fs:copy'"), true)
91
+ @Expect(js.contains("'fs:stat'"), true)
92
+ }
93
+
94
+ @Test
95
+ func testFsPermissionSetsGateWrites(): Unit {
96
+ let sets = pluginPermissionSets(FsPlugin())
97
+ @Expect(sets.size, 2)
98
+ let ro = sets.get("fs:readonly").getOrThrow()
99
+ @Expect(ro.size, 4)
100
+ // readonly 只含读:stat 进得来,任何写命令都不许被顺带打开
101
+ var hasWrite = false
102
+ for (n in ro) {
103
+ if (n == "fs:writeText" || n == "fs:remove" || n == "fs:mkdir" || n == "fs:rename" || n == "fs:copy") {
104
+ hasWrite = true
105
+ }
106
+ }
107
+ @Expect(hasWrite, false)
108
+ // default 是全集
109
+ let all = sets.get("fs:default").getOrThrow()
110
+ @Expect(all.size, 9)
111
+ }
112
+
113
+ @Test
114
+ func testFsMkdirCreatesAndRejectsDuplicate(): Unit {
115
+ // 基座目录先递归建出来(后面的用例都挂在这棵树上);上一轮的残留先清掉,
116
+ // 否则第二次跑 test.sh 时 mkdir / copy 会撞「already exists」(固定目录复用的代价)
117
+ fsWipeBase(FS_T_BASE)
118
+ @Expect(fsRun("mkdir", fsArgsRecursive(FS_T_BASE)).asBool().getValue(), true)
119
+ let dir = "${FS_T_BASE}/mkdir-a"
120
+ @Expect(fsRun("mkdir", fsArgs(dir)).asBool().getValue(), true)
121
+ @Expect(fsRun("exists", fsArgs(dir)).asBool().getValue(), true)
122
+ // 已存在是错误不是成功:调用方要幂等就先 exists
123
+ @Expect(fsErrOf("mkdir", fsArgs(dir)).contains("fs:mkdir failed"), true)
124
+ // recursive=true 逐级建
125
+ @Expect(fsRun("mkdir", fsArgsRecursive("${FS_T_BASE}/mkdir-b/c/d")).asBool().getValue(), true)
126
+ // 缺省非递归:中间层不存在 → 报错
127
+ @Expect(fsErrOf("mkdir", fsArgs("${FS_T_BASE}/x1/x2")).contains("fs:mkdir failed"), true)
128
+ }
129
+
130
+ @Test
131
+ func testFsRenameMovesAndHonorsOverwrite(): Unit {
132
+ let src = "${FS_T_BASE}/r-src.txt"
133
+ let dst = "${FS_T_BASE}/r-dst.txt"
134
+ File.writeTo(src, "hello".toArray())
135
+ File.writeTo(dst, "old".toArray())
136
+ // 目标已存在且没声明 overwrite:报错,两边都原样
137
+ @Expect(fsErrOf("rename", fsArgsTo(src, dst)).contains("fs:rename failed"), true)
138
+ @Expect(String.fromUtf8(File.readFrom(dst)), "old")
139
+ // overwrite=true:覆盖目标,源头消失
140
+ @Expect(fsRun("rename", fsArgsToOverwrite(src, dst)).asBool().getValue(), true)
141
+ @Expect(fsRun("exists", fsArgs(src)).asBool().getValue(), false)
142
+ @Expect(String.fromUtf8(File.readFrom(dst)), "hello")
143
+ }
144
+
145
+ @Test
146
+ func testFsCopyKeepsSourceAndCopiesTrees(): Unit {
147
+ let src = "${FS_T_BASE}/c-src.txt"
148
+ let dst = "${FS_T_BASE}/c-dst.txt"
149
+ File.writeTo(src, "copy me".toArray())
150
+ @Expect(fsRun("copy", fsArgsTo(src, dst)).asBool().getValue(), true)
151
+ // 源还在、内容一致
152
+ @Expect(fsRun("exists", fsArgs(src)).asBool().getValue(), true)
153
+ @Expect(String.fromUtf8(File.readFrom(dst)), "copy me")
154
+ // 再拷一次撞既有目标:缺省报错;overwrite=true 才放行
155
+ @Expect(fsErrOf("copy", fsArgsTo(src, dst)).contains("fs:copy failed"), true)
156
+ @Expect(fsRun("copy", fsArgsToOverwrite(src, dst)).asBool().getValue(), true)
157
+ // 目录整树拷贝:copy 的目录语义是递归
158
+ let tree = "${FS_T_BASE}/c-tree"
159
+ fsRun("mkdir", fsArgsRecursive("${tree}/sub"))
160
+ File.writeTo("${tree}/sub/inner.txt", "deep".toArray())
161
+ @Expect(fsRun("copy", fsArgsTo(tree, "${FS_T_BASE}/c-tree2")).asBool().getValue(), true)
162
+ @Expect(String.fromUtf8(File.readFrom("${FS_T_BASE}/c-tree2/sub/inner.txt")), "deep")
163
+ }
164
+
165
+ /** JsonKind 转可比较的名字(JsonKind 没有 ==,比较要走 match 分支;分支体用表达式避免块解析歧义) */
166
+ func fsKindName(v: JsonValue): String {
167
+ match (v.kind()) {
168
+ case JsonKind.JsObject => "object"
169
+ case JsonKind.JsArray => "array"
170
+ case JsonKind.JsString => "string"
171
+ case JsonKind.JsBool => "bool"
172
+ case JsonKind.JsNull => "null"
173
+ case _ => "other"
174
+ }
175
+ }
176
+
177
+ @Test
178
+ func testFsStatReportsMetadata(): Unit {
179
+ let file = "${FS_T_BASE}/s.txt"
180
+ File.writeTo(file, "12345".toArray())
181
+ let r = fsRun("stat", fsArgs(file))
182
+ @Expect(fsKindName(r), "object")
183
+ let obj = r.asObject()
184
+ @Expect(obj.get("name").getOrThrow().asString().getValue(), "s.txt")
185
+ @Expect(obj.get("dir").getOrThrow().asBool().getValue(), false)
186
+ @Expect(obj.get("size").getOrThrow().asInt().getValue(), 5)
187
+ @Expect(obj.get("lastModified").getOrThrow().asString().getValue().size > 0, true)
188
+ // 目录也能 stat:dir=true
189
+ let dirInfo = fsRun("stat", fsArgs(FS_T_BASE))
190
+ @Expect(fsKindName(dirInfo), "object")
191
+ @Expect(dirInfo.asObject().get("dir").getOrThrow().asBool().getValue(), true)
192
+ }
193
+
194
+ @Test
195
+ func testFsNewCommandsValidateArguments(): Unit {
196
+ // 缺 newPath
197
+ @Expect(fsErrOf("rename", fsArgs("${FS_T_BASE}/r-src.txt")).contains("missing argument"), true)
198
+ @Expect(fsErrOf("copy", fsArgs("${FS_T_BASE}/r-src.txt")).contains("missing argument"), true)
199
+ // 缺 path
200
+ @Expect(fsErrOf("mkdir", JsonObject()).contains("missing argument"), true)
201
+ // 可选布尔给了非布尔值:当场拒,不静默当 false
202
+ var bad = fsArgs("${FS_T_BASE}/mkdir-a")
203
+ bad.put("recursive", JsonInt(1))
204
+ @Expect(fsErrOf("mkdir", bad).contains("must be a boolean"), true)
205
+ // stat 不存在的路径:报错而不是返回空对象
206
+ @Expect(fsErrOf("stat", fsArgs("${FS_T_BASE}/no-such-file")).contains("fs:stat failed"), true)
207
+ }
@@ -1,124 +1,124 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.plugins.http.*
5
- import std.collection.*
6
- import std.unittest.*
7
- import std.unittest.testmacro.*
8
-
9
- /*
10
- * http 插件的策略层用例(RFC-003 HTTP 白名单收口)。
11
- *
12
- * 只测**纯函数**(URL → host 提取、上限常量、插件声明面):真的发请求那一段归
13
- * 实机 E2E——单测进程里起真 HTTP 服务会引入对环境的依赖,不符合「渐进目标」的口径。
14
- */
15
-
16
- @Test
17
- func testHttpHostOfExtractsHostLowercased(): Unit {
18
- @Expect(httpHostOf("https://api.deepseek.com/v1/chat/completions"), "api.deepseek.com")
19
- // host 大小写不敏感:统一小写,匹配口径才收得住
20
- @Expect(httpHostOf("https://API.DeepSeek.COM/x"), "api.deepseek.com")
21
- }
22
-
23
- @Test
24
- func testHttpHostOfStripsPortAndUserinfo(): Unit {
25
- // 端口不参与匹配(白名单声明「信这个主机」)
26
- @Expect(httpHostOf("http://localhost:2375/docker"), "localhost")
27
- // userinfo(user:pass@host)不许用来伪造白名单匹配
28
- @Expect(httpHostOf("https://user:pass@api.example.com/"), "api.example.com")
29
- }
30
-
31
- @Test
32
- func testHttpHostOfRejectsNonHttpSchemes(): Unit {
33
- // file: 不给出网语义;空 host(ftp://)也当解析不出
34
- let f = match (httpHostOf("file:///etc/passwd")) {
35
- case Some(_) => true
36
- case None => false
37
- }
38
- @Expect(f, false)
39
- let n = match (httpHostOf("notaurl")) {
40
- case Some(_) => true
41
- case None => false
42
- }
43
- @Expect(n, false)
44
- }
45
-
46
- @Test
47
- func testHttpHostOfIpv6Literal(): Unit {
48
- // IPv6 字面量整体当 host(不拆端口):去端口逻辑只在非 '[' 开头时走
49
- @Expect(httpHostOf("http://[::1]:8080/x"), "[::1]")
50
- }
51
-
52
- @Test
53
- func testHttpPluginDeclarations(): Unit {
54
- let p = HttpPlugin()
55
- @Expect(p.name(), "http")
56
- @Expect(p.scopeAware(), true)
57
- // 权限集 default = get + post + delete + postStream:集声明 != 放行,清单引用才生效
58
- @Expect(p.permissions().contains("default"), true)
59
- let cmds = p.commands()
60
- @Expect(cmds.contains("get"), true)
61
- @Expect(cmds.contains("post"), true)
62
- // delete 是给「带 JSON 体的 DELETE」用的(cjh 的工作区删除靠体传 path):
63
- // 少了它前端只能退化成无体 DELETE,对端拿不到要删哪个
64
- @Expect(cmds.contains("delete"), true)
65
- @Expect(cmds.contains("postStream"), true)
66
- // 推一个事件(流式 http:chunk,短名登记)、无前端 shim
67
- @Expect(p.events().size == 1, true)
68
- @Expect(p.jsShim().size == 0, true)
69
- }
70
-
71
- @Test
72
- func testHttpHostOfRejectsWsSchemes(): Unit {
73
- // http 插件的 host 提取转发核心 urlHostOf(ws/wss 是 ws 插件的合法目标),
74
- // 但协议收紧到 http(s):ws:// 不是可抓取目标,解析不出 host 按白名单未命中拒
75
- let f = match (httpHostOf("ws://127.0.0.1:8765/ws")) {
76
- case Some(_) => true
77
- case None => false
78
- }
79
- @Expect(f, false)
80
- }
81
-
82
- @Test
83
- func testHttpLimitsAreSane(): Unit {
84
- // 上限是「防撑爆」而非「功能」:8 MiB 与 60s 与既有插件(clipboard 8MiB)同量级
85
- @Expect(CJ_HTTP_MAX_BYTES == 8 * 1024 * 1024, true)
86
- @Expect(CJ_HTTP_TIMEOUT_SECONDS > 0, true)
87
- @Expect(CJ_HTTP_MAX_HEADERS > 0, true)
88
- }
89
-
90
- /*
91
- * 流式分块的 UTF-8 边界(2026-10-16 实机 → 回归):`takeCompleteUtf8` 取走完整序列后
92
- * 必须把 pending 里的**这些字节**全移掉。曾经写成 `remove(0..(cut-1))`(Range 是右开的,
93
- * 这样就少移 1 字节),上一个序列的末字节留在 pending 里,下一轮拼出的字节序列非法,
94
- * fromUtf8 抛 IllegalArgumentException,表现是「中文回复偶发整条 postStream 请求失败」。
95
- * 判据就一条:整块都是完整序列时,取完 pending 必须**空**。
96
- */
97
-
98
- @Test
99
- func testHttpTakeCompleteUtf8ConsumesWholeCompleteChunk(): Unit {
100
- let pending = ArrayList<Byte>()
101
- for (b in "abc你".toArray()) {
102
- pending.add(b)
103
- }
104
- let cmds = HttpCommands()
105
- @Expect(cmds.takeCompleteUtf8(pending), "abc你")
106
- @Expect(pending.size, 0)
107
- }
108
-
109
- @Test
110
- func testHttpTakeCompleteUtf8KeepsPartialTail(): Unit {
111
- // 尾巴不完整(汉字只到了前 2 字节):留在 pending 里等下一块补齐,不能按原样解出来
112
- let bytes = "你".toArray()
113
- @Expect(bytes.size, 3)
114
- let pending = ArrayList<Byte>()
115
- pending.add(bytes[0])
116
- pending.add(bytes[1])
117
- let cmds = HttpCommands()
118
- @Expect(cmds.takeCompleteUtf8(pending), "")
119
- @Expect(pending.size, 2)
120
- // 第 3 字节到了:跨块的字符拼回来,pending 随之清空
121
- pending.add(bytes[2])
122
- @Expect(cmds.takeCompleteUtf8(pending), "你")
123
- @Expect(pending.size, 0)
124
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.http.*
5
+ import std.collection.*
6
+ import std.unittest.*
7
+ import std.unittest.testmacro.*
8
+
9
+ /*
10
+ * http 插件的策略层用例(RFC-003 HTTP 白名单收口)。
11
+ *
12
+ * 只测**纯函数**(URL → host 提取、上限常量、插件声明面):真的发请求那一段归
13
+ * 实机 E2E——单测进程里起真 HTTP 服务会引入对环境的依赖,不符合「渐进目标」的口径。
14
+ */
15
+
16
+ @Test
17
+ func testHttpHostOfExtractsHostLowercased(): Unit {
18
+ @Expect(httpHostOf("https://api.deepseek.com/v1/chat/completions"), "api.deepseek.com")
19
+ // host 大小写不敏感:统一小写,匹配口径才收得住
20
+ @Expect(httpHostOf("https://API.DeepSeek.COM/x"), "api.deepseek.com")
21
+ }
22
+
23
+ @Test
24
+ func testHttpHostOfStripsPortAndUserinfo(): Unit {
25
+ // 端口不参与匹配(白名单声明「信这个主机」)
26
+ @Expect(httpHostOf("http://localhost:2375/docker"), "localhost")
27
+ // userinfo(user:pass@host)不许用来伪造白名单匹配
28
+ @Expect(httpHostOf("https://user:pass@api.example.com/"), "api.example.com")
29
+ }
30
+
31
+ @Test
32
+ func testHttpHostOfRejectsNonHttpSchemes(): Unit {
33
+ // file: 不给出网语义;空 host(ftp://)也当解析不出
34
+ let f = match (httpHostOf("file:///etc/passwd")) {
35
+ case Some(_) => true
36
+ case None => false
37
+ }
38
+ @Expect(f, false)
39
+ let n = match (httpHostOf("notaurl")) {
40
+ case Some(_) => true
41
+ case None => false
42
+ }
43
+ @Expect(n, false)
44
+ }
45
+
46
+ @Test
47
+ func testHttpHostOfIpv6Literal(): Unit {
48
+ // IPv6 字面量整体当 host(不拆端口):去端口逻辑只在非 '[' 开头时走
49
+ @Expect(httpHostOf("http://[::1]:8080/x"), "[::1]")
50
+ }
51
+
52
+ @Test
53
+ func testHttpPluginDeclarations(): Unit {
54
+ let p = HttpPlugin()
55
+ @Expect(p.name(), "http")
56
+ @Expect(p.scopeAware(), true)
57
+ // 权限集 default = get + post + delete + postStream:集声明 != 放行,清单引用才生效
58
+ @Expect(p.permissions().contains("default"), true)
59
+ let cmds = p.commands()
60
+ @Expect(cmds.contains("get"), true)
61
+ @Expect(cmds.contains("post"), true)
62
+ // delete 是给「带 JSON 体的 DELETE」用的(cjh 的工作区删除靠体传 path):
63
+ // 少了它前端只能退化成无体 DELETE,对端拿不到要删哪个
64
+ @Expect(cmds.contains("delete"), true)
65
+ @Expect(cmds.contains("postStream"), true)
66
+ // 推一个事件(流式 http:chunk,短名登记)、无前端 shim
67
+ @Expect(p.events().size == 1, true)
68
+ @Expect(p.jsShim().size == 0, true)
69
+ }
70
+
71
+ @Test
72
+ func testHttpHostOfRejectsWsSchemes(): Unit {
73
+ // http 插件的 host 提取转发核心 urlHostOf(ws/wss 是 ws 插件的合法目标),
74
+ // 但协议收紧到 http(s):ws:// 不是可抓取目标,解析不出 host 按白名单未命中拒
75
+ let f = match (httpHostOf("ws://127.0.0.1:8765/ws")) {
76
+ case Some(_) => true
77
+ case None => false
78
+ }
79
+ @Expect(f, false)
80
+ }
81
+
82
+ @Test
83
+ func testHttpLimitsAreSane(): Unit {
84
+ // 上限是「防撑爆」而非「功能」:8 MiB 与 60s 与既有插件(clipboard 8MiB)同量级
85
+ @Expect(CJ_HTTP_MAX_BYTES == 8 * 1024 * 1024, true)
86
+ @Expect(CJ_HTTP_TIMEOUT_SECONDS > 0, true)
87
+ @Expect(CJ_HTTP_MAX_HEADERS > 0, true)
88
+ }
89
+
90
+ /*
91
+ * 流式分块的 UTF-8 边界(2026-10-16 实机 → 回归):`takeCompleteUtf8` 取走完整序列后
92
+ * 必须把 pending 里的**这些字节**全移掉。曾经写成 `remove(0..(cut-1))`(Range 是右开的,
93
+ * 这样就少移 1 字节),上一个序列的末字节留在 pending 里,下一轮拼出的字节序列非法,
94
+ * fromUtf8 抛 IllegalArgumentException,表现是「中文回复偶发整条 postStream 请求失败」。
95
+ * 判据就一条:整块都是完整序列时,取完 pending 必须**空**。
96
+ */
97
+
98
+ @Test
99
+ func testHttpTakeCompleteUtf8ConsumesWholeCompleteChunk(): Unit {
100
+ let pending = ArrayList<Byte>()
101
+ for (b in "abc你".toArray()) {
102
+ pending.add(b)
103
+ }
104
+ let cmds = HttpCommands()
105
+ @Expect(cmds.takeCompleteUtf8(pending), "abc你")
106
+ @Expect(pending.size, 0)
107
+ }
108
+
109
+ @Test
110
+ func testHttpTakeCompleteUtf8KeepsPartialTail(): Unit {
111
+ // 尾巴不完整(汉字只到了前 2 字节):留在 pending 里等下一块补齐,不能按原样解出来
112
+ let bytes = "你".toArray()
113
+ @Expect(bytes.size, 3)
114
+ let pending = ArrayList<Byte>()
115
+ pending.add(bytes[0])
116
+ pending.add(bytes[1])
117
+ let cmds = HttpCommands()
118
+ @Expect(cmds.takeCompleteUtf8(pending), "")
119
+ @Expect(pending.size, 2)
120
+ // 第 3 字节到了:跨块的字符拼回来,pending 随之清空
121
+ pending.add(bytes[2])
122
+ @Expect(cmds.takeCompleteUtf8(pending), "你")
123
+ @Expect(pending.size, 0)
124
+ }