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.
- package/README.md +137 -77
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +13 -2
- package/cli/cjpm.toml +22 -13
- package/cli/src/doctor.cj +308 -304
- package/cli/src/main.cj +129 -121
- package/cli/src/manifest.cj +632 -0
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -754
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +46 -38
- package/cli/src/sync.cj +526 -0
- package/cli/src/util.cj +9 -0
- package/cli/templates/app/README.md +30 -59
- package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
- package/cli/templates/app/cj-tauri.toml +32 -0
- package/cli/templates/app/cjpm.toml +21 -4
- package/cli/templates/app/src/main.cj +29 -16
- package/cli/templates/app-react/README.md +34 -45
- package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
- package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
- package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
- package/cli/templates/app-react/cj-tauri.toml +33 -0
- package/cli/templates/app-react/cjpm.toml +21 -4
- package/cli/templates/app-react/src/main.cj +18 -29
- package/cli/templates/app-vue/README.md +34 -38
- package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
- package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
- package/cli/templates/app-vue/cj-tauri.toml +33 -0
- package/cli/templates/app-vue/cjpm.toml +21 -4
- package/cli/templates/app-vue/src/main.cj +18 -29
- package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -51
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -940
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -0
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +0 -12
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -604
- package/src/host_arkweb.cj +201 -17
- package/src/host_webkit.cj +663 -548
- package/src/host_webview2.cj +663 -548
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -0
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -0
- package/src/platform/platform_assets.cj +33 -0
- package/src/platform/platform_discover.cj +176 -0
- package/src/platform/platform_env.cj +29 -0
- package/src/platform/platform_feature.cj +31 -0
- package/src/platform/platform_page_source.cj +87 -0
- package/src/platform/platform_target_info.cj +32 -0
- package/src/platform_host_provider.cj +87 -0
- package/src/plugin.cj +333 -299
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +8 -0
- package/src/plugins/notification/plugin_notification.cj +154 -146
- package/src/plugins/serial/plugin_serial.cj +17 -1
- package/src/plugins/serial/serial_bridge.cj +9 -1
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +29 -1
- package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -0
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -186
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -0
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +66 -3
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/host_factory.cj +0 -48
- /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
- /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
- /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
|
+
}
|