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,1228 +1,1339 @@
1
- /*
2
- * bridge_core.c 自检:不依赖仓颉 SDK,也不依赖 GTK / WebKit / WebView2。
3
- *
4
- * 被测的是「两平台逐字相同」的公共核心(回投队列与批处理、对话框单槽状态机、窗口配置存储、
5
- * 生命周期标志、菜单线路格式解析、串口路径白名单、以及全部 cj_bridge_* 导出)。做法:用一个「只记录调用」的桩平台实现
6
- * bridge_core.h 里的 25 个 cj_plat_* 原语,再把 core 与桩一起编译成单文件可执行程序——
7
- * 链接期就能证明 core 只经 cj_plat_* 触达平台、不 include 任何平台头。
8
- *
9
- * 跑法:bash scripts/test-bridge-core.sh
10
- */
11
- #define _POSIX_C_SOURCE 200809L
12
-
13
- #include "bridge_core.h"
14
-
15
- #include <pthread.h>
16
- #include <stdio.h>
17
- #include <stdlib.h>
18
- #include <string.h>
19
- #include <sys/stat.h>
20
- #include <unistd.h>
21
-
22
- /* mingw 的 <io.h> 把 mkdir 声明成单参(MSVCRT 语义,且已标 deprecated),
23
- POSIX 的两参形态在 Windows 上直接编不过(too many arguments to function call)——
24
- 用例里统一走这个包装,两边的语义都是「建一级目录」。 */
25
- #ifdef _WIN32
26
- #include <direct.h>
27
- static int cj_test_mkdir(const char *p) { return _mkdir(p); }
28
- #else
29
- static int cj_test_mkdir(const char *p) { return mkdir(p, 0700); }
30
- #endif
31
-
32
- /* ===== 断言与用例 ===== */
33
-
34
- static int g_pass = 0;
35
- static int g_fail = 0;
36
- static const char *g_case = "?";
37
-
38
- #define CHECK(cond, what) \
39
- do { \
40
- if (cond) { \
41
- g_pass++; \
42
- } else { \
43
- g_fail++; \
44
- fprintf(stderr, " [FAIL] %s:%d [%s] %s\n", __FILE__, __LINE__, \
45
- g_case, what); \
46
- } \
47
- } while (0)
48
-
49
- static void case_begin(const char *name) {
50
- g_case = name;
51
- fprintf(stderr, " - %s\n", name);
52
- }
53
-
54
- /* ===== 日志落盘用例的辅助(RFC-003 第 5 期) =====
55
- 开启日志后 fd 2 就被日志文件接管了,此时 CHECK 的失败行会**写进日志文件**、外面根本看不见。
56
- 所以接管期间改用 LOG_CHECK:结果先攒在内存里,等 stderr 还原之后再一次性打出来。 */
57
- static char g_log_fail_buf[4096];
58
-
59
- static void cj_test_log_check(int cond, int line, const char *what) {
60
- if (cond) {
61
- g_pass++;
62
- return;
63
- }
64
- g_fail++;
65
- if (strlen(g_log_fail_buf) + 240 < sizeof(g_log_fail_buf)) {
66
- char row[200];
67
- snprintf(row, sizeof(row), " [FAIL] %s:%d [log file] %s\n", __FILE__, line, what);
68
- strcat(g_log_fail_buf, row);
69
- }
70
- }
71
-
72
- #define LOG_CHECK(cond, what) cj_test_log_check((cond) ? 1 : 0, __LINE__, what)
73
-
74
- static int cj_test_file_exists(const char *path) {
75
- FILE *f = fopen(path, "rb");
76
- if (!f) return 0;
77
- fclose(f);
78
- return 1;
79
- }
80
-
81
- /* 读全文(截断到 cap-1);返回读到的字节数,-1 = 打不开 */
82
- static long cj_test_read_all(const char *path, char *buf, long cap) {
83
- FILE *f = fopen(path, "rb");
84
- long n;
85
- if (!f) {
86
- if (cap > 0) buf[0] = '\0';
87
- return -1;
88
- }
89
- n = (long)fread(buf, 1, (size_t)(cap - 1), f);
90
- buf[n > 0 ? n : 0] = '\0';
91
- fclose(f);
92
- return n;
93
- }
94
-
95
- /* ===== 桩平台:只记录调用,不做真实副作用 ===== */
96
-
97
- struct cj_sync {
98
- pthread_mutex_t m;
99
- pthread_cond_t c;
100
- };
101
-
102
- static int g_post_js_calls;
103
- static char *g_last_js; /* 最近一次 cj_plat_post_js 的脚本(拷贝) */
104
- static int g_wake_calls;
105
- static int g_quit_calls;
106
- static int g_start_calls;
107
- static int g_fini_calls;
108
- static int g_run_dialog_calls;
109
- static int g_post_dialog_calls;
110
- static int g_load_url_calls;
111
- static int g_reload_calls;
112
- static int g_devtools_calls;
113
- static int g_is_ui_thread = 1; /* 默认假装调用方就站在 UI 线程上 */
114
- static int g_dialog_ok = 1; /* cj_plat_run_dialog 的返回值 */
115
- static const char *g_dialog_path = "/tmp/picked.txt";
116
- static int g_set_menu_calls;
117
- static char *g_last_menu_text;
118
- static int g_menu_state_calls;
119
- static char g_last_menu_state_id[256];
120
- static int g_last_menu_state_enabled;
121
- static int g_last_menu_state_checked;
122
- static int g_caps_calls;
123
- static int g_caps_value = CJ_CAP_MENU; /* 桩平台自称的能力位(用例里改它,验证 core 只是如实缓存) */
124
-
125
- /* 拖放策略:桩平台只记录调用(core 的职责是 pending 存取与 drop 报文解析)*/
126
- static int g_drag_calls;
127
- static int g_drag_last_enabled;
128
- static int g_drag_last_files;
129
-
130
- /* 标题栏副标题:桩平台只记录调用(core 的职责是参数校验 + 透传)*/
131
- static int g_subtitle_calls;
132
- static char *g_last_subtitle_text;
133
-
134
- static void reset_counters(void) {
135
- g_post_js_calls = g_wake_calls = g_quit_calls = g_start_calls = 0;
136
- g_fini_calls = g_run_dialog_calls = g_post_dialog_calls = 0;
137
- g_load_url_calls = g_reload_calls = g_devtools_calls = 0;
138
- g_set_menu_calls = g_menu_state_calls = g_caps_calls = 0;
139
- g_drag_calls = g_drag_last_enabled = g_drag_last_files = 0;
140
- g_subtitle_calls = 0;
141
- free(g_last_subtitle_text);
142
- g_last_subtitle_text = NULL;
143
- free(g_last_js);
144
- g_last_js = NULL;
145
- free(g_last_menu_text);
146
- g_last_menu_text = NULL;
147
- }
148
-
149
- /* 数一段脚本里有几个换行:批处理按「每条一段 + 换行」拼接,用它反推条数 */
150
- static int count_nl(const char *s) {
151
- int n = 0;
152
- while (s && *s) {
153
- if (*s++ == '\n') n++;
154
- }
155
- return n;
156
- }
157
-
158
- cj_sync *cj_plat_sync_create(void) {
159
- cj_sync *s = (cj_sync *)calloc(1, sizeof(cj_sync));
160
- if (!s) return NULL;
161
- pthread_mutex_init(&s->m, NULL);
162
- pthread_cond_init(&s->c, NULL);
163
- return s;
164
- }
165
-
166
- void cj_plat_sync_destroy(cj_sync *s) {
167
- if (!s) return;
168
- pthread_mutex_destroy(&s->m);
169
- pthread_cond_destroy(&s->c);
170
- free(s);
171
- }
172
-
173
- void cj_plat_sync_lock(cj_sync *s) { if (s) pthread_mutex_lock(&s->m); }
174
- void cj_plat_sync_unlock(cj_sync *s) { if (s) pthread_mutex_unlock(&s->m); }
175
-
176
- void cj_plat_sync_wait(cj_sync *s, int *ready) {
177
- /* core 的约定:进入时已持锁,返回时 *ready 为真(允许虚假唤醒)*/
178
- while (s && ready && !*ready) pthread_cond_wait(&s->c, &s->m);
179
- }
180
-
181
- void cj_plat_sync_signal(cj_sync *s) { if (s) pthread_cond_signal(&s->c); }
182
-
183
- void cj_plat_init(cj_host *h) { (void)h; }
184
- int cj_plat_fini(cj_host *h) { (void)h; g_fini_calls++; return 1; }
185
- void cj_plat_start(cj_host *h) { (void)h; g_start_calls++; }
186
- void cj_plat_quit(cj_host *h) { (void)h; g_quit_calls++; }
187
- void cj_plat_wake_ui(cj_host *h) { (void)h; g_wake_calls++; }
188
-
189
- void cj_plat_post_js(cj_host *h, const char *script_utf8) {
190
- (void)h;
191
- g_post_js_calls++;
192
- free(g_last_js);
193
- g_last_js = script_utf8 ? strdup(script_utf8) : NULL;
194
- }
195
-
196
- int cj_plat_is_ui_thread(cj_host *h) { (void)h; return g_is_ui_thread; }
197
- void cj_plat_post_dialog(cj_host *h, dlg_req *req) { (void)h; (void)req; g_post_dialog_calls++; }
198
-
199
- /* ---- 剪贴板桩:记调用次数与最后一次写的文本;读返回桩文本 ---- */
200
- static int g_clip_write_calls = 0;
201
- static int g_clip_read_calls = 0;
202
- static char *g_clip_last_written = NULL;
203
- static int g_clip_write_ok = 1; /* 用例可扳成 0 模拟平台失败 */
204
-
205
- int cj_plat_post_ui(cj_host *h, cj_ui_fn fn, void *ud) {
206
- (void)h;
207
- fn(ud); /* 桩平台没有真 UI 线程:就地执行(core 的 is_ui_thread 桩可扳成 0 验投递路径)*/
208
- return 1;
209
- }
210
-
211
- int cj_plat_clipboard_write_text(cj_host *h, const char *text, char *err, int err_len) {
212
- (void)h; (void)err; (void)err_len;
213
- g_clip_write_calls++;
214
- if (!g_clip_write_ok) return -3;
215
- free(g_clip_last_written);
216
- g_clip_last_written = strdup(text ? text : "");
217
- return 0;
218
- }
219
-
220
- char *cj_plat_clipboard_read_text(cj_host *h, char *err, int err_len) {
221
- (void)h; (void)err; (void)err_len;
222
- g_clip_read_calls++;
223
- if (!g_clip_last_written) return NULL;
224
- return strdup(g_clip_last_written);
225
- }
226
-
227
- /* ---- 通知桩:记调用次数与最后一次的标题;用例可扳失败 ---- */
228
- static int g_note_calls = 0;
229
- static char *g_note_last_title = NULL;
230
- static int g_note_ok = 1;
231
-
232
- int cj_plat_notification_notify(cj_host *h, const char *title, const char *body,
233
- char *err, int err_len) {
234
- (void)h; (void)body; (void)err; (void)err_len;
235
- g_note_calls++;
236
- if (!g_note_ok) return -3;
237
- free(g_note_last_title);
238
- g_note_last_title = strdup(title ? title : "");
239
- return 0;
240
- }
241
-
242
- int cj_plat_run_dialog(cj_host *h, dlg_req *req) {
243
- (void)h;
244
- g_run_dialog_calls++;
245
- if (req && (req->kind == 0 || req->kind == 1)) { /* 文件类才有路径 */
246
- free(req->path);
247
- req->path = strdup(g_dialog_path);
248
- }
249
- return g_dialog_ok;
250
- }
251
-
252
- void cj_plat_open_devtools(cj_host *h) { (void)h; g_devtools_calls++; }
253
- void cj_plat_load_url(cj_host *h, const char *url) { (void)h; (void)url; g_load_url_calls++; }
254
- void cj_plat_reload(cj_host *h) { (void)h; g_reload_calls++; }
255
-
256
- void cj_plat_set_menu(cj_host *h, const char *wire) {
257
- (void)h;
258
- g_set_menu_calls++;
259
- free(g_last_menu_text);
260
- g_last_menu_text = wire ? strdup(wire) : NULL;
261
- }
262
-
263
- /* 桩平台:副标题一律接受(真平台未就绪返回 -3、Windows 返回 -4,那是平台自己的事)*/
264
- int cj_plat_set_titlebar_subtitle(cj_host *h, const char *text) {
265
- (void)h;
266
- g_subtitle_calls++;
267
- free(g_last_subtitle_text);
268
- g_last_subtitle_text = text ? strdup(text) : NULL;
269
- return 0;
270
- }
271
-
272
- void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
273
- (void)h;
274
- g_menu_state_calls++;
275
- snprintf(g_last_menu_state_id, sizeof(g_last_menu_state_id), "%s", id ? id : "");
276
- g_last_menu_state_enabled = enabled;
277
- g_last_menu_state_checked = checked;
278
- }
279
-
280
- /* ---- 拖放策略:声明在计数器区(reset_counters 之前),这里只留原语 ---- */
281
- void cj_plat_set_drag_drop(cj_host *h, int enabled, int files) {
282
- (void)h;
283
- g_drag_calls++;
284
- g_drag_last_enabled = enabled;
285
- g_drag_last_files = files;
286
- }
287
-
288
- /* ---- 全局快捷键:桩平台没有 X11,只记录调用(接线层自检用)---- */
289
- static int g_hotkey_calls;
290
- static char g_hotkey_last_wire[512];
291
- void cj_plat_set_global_hotkeys(cj_host *h, const char *wire) {
292
- (void)h;
293
- g_hotkey_calls++;
294
- snprintf(g_hotkey_last_wire, sizeof(g_hotkey_last_wire), "%s", wire ? wire : "");
295
- }
296
-
297
- /* ---- 托盘:桩平台没有 appindicator,只记录调用(接线层自检用)---- */
298
- static int g_tray_calls;
299
- static char g_tray_last_wire[512];
300
- void cj_plat_set_tray(cj_host *h, const char *wire) {
301
- (void)h;
302
- g_tray_calls++;
303
- snprintf(g_tray_last_wire, sizeof(g_tray_last_wire), "%s", wire ? wire : "");
304
- }
305
-
306
- /* ---- 窗口可见性 / 最小化到托盘:桩平台没有窗口,只记录调用(接线层自检用)---- */
307
- static int g_win_visible_calls;
308
- static int g_win_visible_last = -1;
309
- void cj_plat_set_window_visible(cj_host *h, int visible) {
310
- (void)h;
311
- g_win_visible_calls++;
312
- g_win_visible_last = visible;
313
- }
314
-
315
- static int g_min_tray_calls;
316
- static int g_min_tray_last = -1;
317
- void cj_plat_set_minimize_to_tray(cj_host *h, int enabled) {
318
- (void)h;
319
- g_min_tray_calls++;
320
- g_min_tray_last = enabled;
321
- }
322
-
323
- int cj_plat_host_capabilities(cj_host *h) {
324
- (void)h;
325
- g_caps_calls++;
326
- return g_caps_value;
327
- }
328
-
329
- /* ---- 串口:桩平台只记录调用,不碰真设备(core 侧的职责是白名单与参数校验)---- */
330
- static int g_serial_open_calls;
331
- static char g_serial_last_path[256];
332
- static cj_serial_cfg g_serial_last_cfg;
333
- static long long g_serial_open_ret = 42; /* 桩返回的假句柄(>0)*/
334
- static int g_serial_read_calls;
335
- static int g_serial_write_calls;
336
- static int g_serial_close_calls;
337
- static int g_serial_read_ret = 1; /* 默认「读到 1 字节」*/
338
- static int g_serial_write_ret = -1; /* 负值 = 按写满 len 处理 */
339
- static int g_serial_write_last_len;
340
- static unsigned char g_serial_write_first;
341
-
342
- long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *err, int err_len) {
343
- g_serial_open_calls++;
344
- snprintf(g_serial_last_path, sizeof(g_serial_last_path), "%s", path ? path : "");
345
- if (cfg) g_serial_last_cfg = *cfg;
346
- if (g_serial_open_ret <= 0 && err && err_len > 0) {
347
- snprintf(err, (size_t)err_len, "stub: 平台打开失败");
348
- }
349
- return g_serial_open_ret;
350
- }
351
-
352
- int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms,
353
- char *err, int err_len) {
354
- (void)h;
355
- (void)timeout_ms;
356
- (void)err;
357
- (void)err_len;
358
- g_serial_read_calls++;
359
- if (buf && len > 0 && g_serial_read_ret > 0) buf[0] = 'Z';
360
- return g_serial_read_ret;
361
- }
362
-
363
- int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
364
- char *err, int err_len) {
365
- (void)h;
366
- (void)timeout_ms;
367
- (void)err;
368
- (void)err_len;
369
- g_serial_write_calls++;
370
- g_serial_write_last_len = len;
371
- g_serial_write_first = (buf && len > 0) ? buf[0] : 0;
372
- return g_serial_write_ret >= 0 ? g_serial_write_ret : len;
373
- }
374
-
375
- int cj_plat_serial_close(long long h, char *err, int err_len) {
376
- (void)h;
377
- (void)err;
378
- (void)err_len;
379
- g_serial_close_calls++;
380
- return 0;
381
- }
382
-
383
- /* 枚举桩:回 0(没有串口不是错误),只为让 core 的 cj_bridge_serial_list 可链接可调 */
384
- int cj_plat_serial_list(char *out, int out_len) {
385
- if (out && out_len > 0) out[0] = '\0';
386
- return 0;
387
- }
388
-
389
- /* ===== 静默子进程桩:桩平台建不了真进程,只为让 core 的**参数校验与转发**可调可断言 =====
390
- 真进程那条路(Windows 的 DETACHED_PROCESS / Linux 的 posix_spawn)由平台实机验证。 */
391
- long long cj_plat_spawn_hidden(const char *exe, const char *args, char *err, int err_len) {
392
- (void)args;
393
- if (err && err_len > 0) {
394
- err[0] = '\0';
395
- }
396
- /* 只认一个哨兵:命中就给个假 pid,其余回 0 并留一句原因(两条分支 core 都要覆盖) */
397
- if (exe && strcmp(exe, "stub-ok") == 0) {
398
- return 4242;
399
- }
400
- if (err && err_len > 0) {
401
- snprintf(err, (size_t)err_len, "stub: 不能建进程");
402
- }
403
- return 0;
404
- }
405
-
406
- int cj_plat_kill_pid(long long pid) {
407
- (void)pid;
408
- return 1; /* 幂等口径:桩上一律算「已结束」 */
409
- }
410
-
411
- int cj_plat_pid_alive(long long pid) {
412
- (void)pid;
413
- return 0; /* 桩平台没有真进程 */
414
- }
415
-
416
- /* ===== 内嵌资源桩:桩平台没有 PE 资源表,如实返回 0 或留一句原因,
417
- 让 core 的三条分支(参数校验 / 无资源 / 失败)都能在无 SDK 的机器上被断言。 ===== */
418
- int cj_plat_extract_assets(const char *dir, char *err, int err_len) {
419
- if (err && err_len > 0) err[0] = '\0';
420
- if (dir && strcmp(dir, "stub-missing") == 0) {
421
- if (err && err_len > 0) snprintf(err, (size_t)err_len, "桩平台:没有内嵌资源");
422
- return -1;
423
- }
424
- return 0; /* 普通产物没有内嵌资源——如实回报 */
425
- }
426
-
427
- /* ===== 动态库预加载桩:桩平台不去真加载库,只让 core 的「空路径拒绝 / 失败出口」可断言。
428
- 真加载那条路(Windows 的 LoadLibraryW / Linux 的 dlopen)由平台实机验证。 ===== */
429
- int cj_plat_preload_library(const char *path, char *err, int err_len) {
430
- if (err && err_len > 0) err[0] = '\0';
431
- if (path && strcmp(path, "stub-broken") == 0) {
432
- if (err && err_len > 0) snprintf(err, (size_t)err_len, "桩平台:加载失败");
433
- return 0;
434
- }
435
- return 1; /* 其余一律算加载成功(core 只关心 0 / 非 0 与日志) */
436
- }
437
-
438
- /* ===== 线路解析的回调收集器(菜单是树,桩侧用一张表记录 core 走出来的每一行)===== */
439
-
440
- typedef struct row_rec {
441
- int depth;
442
- char kind;
443
- char id[64];
444
- char label[64];
445
- int flags;
446
- char accel[64];
447
- } row_rec;
448
-
449
- typedef struct row_log {
450
- row_rec rows[32];
451
- int n;
452
- } row_log;
453
-
454
- static void collect_row(void *ctx, int depth, char kind, const char *id,
455
- const char *label, int flags, const char *accel) {
456
- row_log *log = (row_log *)ctx;
457
- row_rec *r;
458
- if (!log || log->n >= 32) return;
459
- r = &log->rows[log->n++];
460
- r->depth = depth;
461
- r->kind = kind;
462
- snprintf(r->id, sizeof(r->id), "%s", id ? id : "");
463
- snprintf(r->label, sizeof(r->label), "%s", label ? label : "");
464
- r->flags = flags;
465
- snprintf(r->accel, sizeof(r->accel), "%s", accel ? accel : "");
466
- }
467
-
468
- /* ===== 仓颉侧回调(桩)===== */
469
-
470
- static int g_msg_calls;
471
- static int g_destroy_calls;
472
- static cj_host *g_destroy_cb_host; /* 销毁回调收到的句柄(多窗口下这是唯一能分辨「哪扇窗没了」的凭据)*/
473
- static int g_dialog_cb_calls;
474
- static cj_host *g_dialog_cb_host; /* 结果回调收到的句柄:必须等于**发起这次对话框**的那个宿主 */
475
- static char g_dialog_cb_path[256];
476
-
477
- static void on_message(const char *json) { (void)json; g_msg_calls++; }
478
- static void on_destroy(cj_host *host) {
479
- g_destroy_calls++;
480
- g_destroy_cb_host = host;
481
- }
482
- static void on_dialog(cj_host *host, const char *path) {
483
- g_dialog_cb_calls++;
484
- g_dialog_cb_host = host;
485
- snprintf(g_dialog_cb_path, sizeof(g_dialog_cb_path), "%s", path ? path : "");
486
- }
487
-
488
- /* shell 事件回调:**首参必须是宿主句柄**,桩侧把句柄与 JSON 载荷原样记下供断言比对 */
489
- static int g_shell_cb_calls;
490
- static cj_host *g_shell_cb_host;
491
- static char g_shell_cb_json[512];
492
-
493
- static void on_shell(cj_host *host, const char *json) {
494
- g_shell_cb_calls++;
495
- g_shell_cb_host = host;
496
- snprintf(g_shell_cb_json, sizeof(g_shell_cb_json), "%s", json ? json : "");
497
- }
498
-
499
- /* ===== 用例 ===== */
500
-
501
- int main(void) {
502
- cj_host *h;
503
- cj_host *h2;
504
- int i;
505
-
506
- fprintf(stderr, "[bridge-core] 自检开始(桩平台,无需 SDK / 图形栈)\n");
507
-
508
- /* --- 1. 建宿主:默认值与 NULL 安全 --- */
509
- case_begin("create 默认值与导出对 NULL 安全");
510
- h = cj_bridge_create();
511
- CHECK(h != NULL, "create 返回非 NULL");
512
- if (!h) return 1;
513
- CHECK(h->win_w == 900 && h->win_h == 640, "默认窗口 900x640");
514
- CHECK(h->devtools == 1, "默认开 devtools");
515
- CHECK(h->win_title == NULL, "默认标题为空(由平台兜底)");
516
- CHECK(h->ready == 0 && h->should_quit == 0 && h->window_gone == 0,
517
- "初始生命周期标志全 0");
518
- CHECK(cj_bridge_is_ready(h) == 0 && cj_bridge_should_quit(h) == 0, "访问器初值 0");
519
- CHECK(cj_bridge_is_ready(NULL) == 0 && cj_bridge_should_quit(NULL) == 0,
520
- "访问器对 NULL 返回 0");
521
- cj_bridge_destroy(NULL);
522
- cj_bridge_quit(NULL);
523
- cj_bridge_run_js(NULL, "x");
524
- cj_bridge_set_window(NULL, "t", 1, 1);
525
- cj_bridge_add_init_script(NULL, "x");
526
- cj_bridge_show_dialog(NULL, 2, "t", "m", "");
527
- CHECK(1, "全部导出对 NULL 安全(未崩)");
528
-
529
- /* --- 2. 窗口配置:标题/图标都是拷贝,非法尺寸不覆盖 --- */
530
- case_begin("set_window / set_icon / set_devtools");
531
- cj_bridge_set_window(h, "First", 800, 600);
532
- CHECK(h->win_w == 800 && h->win_h == 600, "尺寸生效");
533
-
534
- CHECK(h->win_title && strcmp(h->win_title, "First") == 0, "标题是拷贝");
535
- cj_bridge_set_window(h, "Second", 0, -1);
536
- CHECK(h->win_title && strcmp(h->win_title, "Second") == 0, "标题替换(旧值已释放)");
537
- CHECK(h->win_w == 800 && h->win_h == 600, "宽高 <=0 时保持原值");
538
- cj_bridge_set_icon(h, "");
539
- CHECK(h->win_icon == NULL, "空图标路径 = 系统默认");
540
- cj_bridge_set_icon(h, "/tmp/a.png");
541
- CHECK(h->win_icon && strcmp(h->win_icon, "/tmp/a.png") == 0, "图标路径是拷贝");
542
- cj_bridge_set_devtools(h, 0);
543
- CHECK(h->devtools == 0, "devtools 可关");
544
- cj_bridge_set_devtools(h, 1);
545
- CHECK(h->devtools == 1, "devtools 可开");
546
-
547
- /* --- 2b. 窗口可见性 / 最小化到托盘:两头都能用,但「建窗前」只记不下发 --- */
548
- case_begin("set_window_visible / set_minimize_to_tray");
549
- cj_bridge_set_window_visible(h, 0);
550
- CHECK(h->win_visible == 0, "未就绪:记下待定可见性");
551
- CHECK(g_win_visible_calls == 0, "未就绪:不下发平台(建窗时按 h->win_visible 定稿,省一次闪烁)");
552
- h->ready = 1;
553
- cj_bridge_set_window_visible(h, 1);
554
- CHECK(h->win_visible == 1 && g_win_visible_calls == 1 && g_win_visible_last == 1,
555
- "就绪后:投平台执行显示");
556
- cj_bridge_set_minimize_to_tray(h, 1);
557
- CHECK(h->minimize_to_tray == 1 && g_min_tray_calls == 1 && g_min_tray_last == 1,
558
- "最小化到托盘策略转给平台");
559
- cj_bridge_set_minimize_to_tray(h, 0);
560
- CHECK(h->minimize_to_tray == 0 && g_min_tray_last == 0, "策略可关(回到普通最小化)");
561
- h->ready = 0; /* 后面的用例还按「未启动」的前提走 */
562
-
563
- /* --- 3. 预执行脚本:忽略空串、顺序保持、容量翻倍 --- */
564
- case_begin("add_init_script 顺序与扩容");
565
- cj_bridge_add_init_script(h, "");
566
- cj_bridge_add_init_script(h, NULL);
567
- CHECK(h->init_script_count == 0, "空串 / NULL 脚本被忽略");
568
- cj_bridge_add_init_script(h, "s1");
569
- cj_bridge_add_init_script(h, "s2");
570
- cj_bridge_add_init_script(h, "s3");
571
- cj_bridge_add_init_script(h, "s4");
572
- cj_bridge_add_init_script(h, "s5"); /* 第 5 条触发扩容(4 -> 8)*/
573
- CHECK(h->init_script_count == 5, "5 条脚本入列");
574
- CHECK(h->init_script_cap >= 5, "容量已扩容");
575
- CHECK(h->init_scripts[0] && strcmp(h->init_scripts[0], "s1") == 0 &&
576
- h->init_scripts[4] && strcmp(h->init_scripts[4], "s5") == 0,
577
- "顺序保持(注册序 = 注入序)");
578
-
579
- /* --- 4. 页面来源二选一 + 运行期操作透传平台 --- */
580
- case_begin("start / load_url / reload / open_devtools");
581
- cj_bridge_start(h, "<html>hi</html>");
582
- CHECK(g_start_calls == 1, "start 调到平台");
583
- CHECK(h->pending_html && strcmp(h->pending_html, "<html>hi</html>") == 0,
584
- "内联 HTML 记下");
585
- cj_bridge_start(h, ""); /* 空串 = 页面由 URL 指定,不清掉已记的来源 */
586
- CHECK(h->pending_html != NULL, "空串 start 不改页面来源");
587
- cj_bridge_load_url(h, "http://x/");
588
- CHECK(g_load_url_calls == 1 && h->pending_url &&
589
- strcmp(h->pending_url, "http://x/") == 0, "URL 记下并投递");
590
- cj_bridge_load_url(h, "");
591
- CHECK(g_load_url_calls == 1, "空 URL 忽略");
592
- cj_bridge_reload(h);
593
- cj_bridge_open_devtools(h);
594
- CHECK(g_reload_calls == 1 && g_devtools_calls == 1, "reload / devtools 投递到平台");
595
-
596
- /* --- 5. 回投:单条快路径 --- */
597
- case_begin("run_js + flush:单条快路径");
598
- reset_counters();
599
- cj_bridge_run_js(h, "console.log(1)");
600
- CHECK(g_wake_calls == 1, "入队即唤醒 UI");
601
- CHECK(g_post_js_calls == 0, "入队本身不执行 JS");
602
- cj_core_flush_js(h);
603
- CHECK(g_post_js_calls == 1, "单条只 post 一次");
604
- CHECK(g_last_js && strcmp(g_last_js, "console.log(1)") == 0, "快路径原样透传(不加换行)");
605
- CHECK(g_wake_calls == 1, "队列已空:不再自续唤醒");
606
- cj_core_flush_js(h);
607
- CHECK(g_post_js_calls == 1, "空队列 flush 不产生回投");
608
-
609
- /* --- 6. 回投:多条拼批 --- */
610
- case_begin("run_js + flush:多条拼批(顺序 + 换行分隔)");
611
- reset_counters();
612
- cj_bridge_run_js(h, "A");
613
- cj_bridge_run_js(h, "B");
614
- cj_bridge_run_js(h, "C");
615
- CHECK(g_wake_calls == 1, "队列由空转非空:只唤醒一次");
616
- cj_core_flush_js(h);
617
- CHECK(g_post_js_calls == 1, "三条合成一段脚本:只 post 一次");
618
- CHECK(g_last_js && strcmp(g_last_js, "A\nB\nC\n") == 0,
619
- "顺序保持、换行分隔(末条也带换行)");
620
-
621
- /* --- 7. 回投:批上限 64 + 自续唤醒(长队列肯让出 UI 线程)--- */
622
- case_begin("flush 批上限 CJ_JS_BATCH_MAX 与自续唤醒");
623
- reset_counters();
624
- for (i = 0; i < 70; i++) {
625
- char buf[32];
626
- snprintf(buf, sizeof(buf), "x%d", i);
627
- cj_bridge_run_js(h, buf);
628
- }
629
- cj_core_flush_js(h);
630
- CHECK(g_post_js_calls == 1, "首批只发一段");
631
- CHECK(g_last_js && count_nl(g_last_js) == 64, "首批恰好 64 条");
632
- CHECK(g_last_js && strncmp(g_last_js, "x0\n", 3) == 0, "从队首开始");
633
- CHECK(g_wake_calls >= 1, "队列未取空:自续唤醒");
634
- cj_core_flush_js(h);
635
- CHECK(g_post_js_calls == 2 && g_last_js && count_nl(g_last_js) == 6,
636
- "第二批 6 条(70 - 64)");
637
- CHECK(count_nl(g_last_js) == 6 && strncmp(g_last_js, "x64\n", 4) == 0, "第二批接着队尾");
638
-
639
- /* --- 8. quit:置标志 + 走平台同一路径 --- */
640
- case_begin("quit 置 should_quit 并投递平台");
641
- reset_counters();
642
- CHECK(cj_bridge_should_quit(h) == 0, "quit 前 shouldQuit=0");
643
- cj_bridge_quit(h);
644
- CHECK(cj_bridge_should_quit(h) == 1, "quit 置 should_quit");
645
- CHECK(g_quit_calls == 1, "投递到平台(与用户关窗同一条销毁路径)");
646
-
647
- /* --- 9. 窗口销毁:标志 + onDestroy 恰好一次 --- */
648
- case_begin("window_destroyed 幂等(onDestroy 恰好一次)");
649
- reset_counters();
650
- cj_bridge_init(h, on_message, on_destroy);
651
- cj_core_window_destroyed(h);
652
- CHECK(h->window_gone == 1 && h->should_quit == 1, "置 window_gone + should_quit");
653
- CHECK(g_destroy_calls == 1, "onDestroy 触发一次");
654
- CHECK(g_destroy_cb_host == h, "销毁回调带回的是这个宿主的句柄");
655
- CHECK(g_msg_calls == 0, "on_message 不该被碰");
656
- cj_core_window_destroyed(h);
657
- CHECK(g_destroy_calls == 1, "重复通知不再触发 onDestroy");
658
-
659
- /* --- 9b. 句柄身份:销毁另一个宿主时,回调收到的是它自己 --- */
660
- case_begin("window_destroyed 两宿主不串(回调带回被销毁者的句柄)");
661
- {
662
- cj_host *hb = cj_bridge_create();
663
- int before;
664
- CHECK(hb != NULL, "另建一个宿主可独立创建");
665
- if (hb) {
666
- before = g_destroy_calls;
667
- cj_bridge_init(hb, on_message, on_destroy);
668
- cj_core_window_destroyed(hb);
669
- CHECK(g_destroy_calls == before + 1 && g_destroy_cb_host == hb,
670
- "回调收到 hb(先 init 的 h 不会被误报)");
671
- cj_bridge_destroy(hb);
672
- }
673
- }
674
-
675
- /* --- 10. 对话框:宿主未就绪直接拒绝 --- */
676
- case_begin("dialog:宿主未就绪返回 0");
677
- reset_counters();
678
- h2 = cj_bridge_create();
679
- CHECK(h2 != NULL, "第二个宿主可独立创建(无进程级单例)");
680
- if (!h2) return 1;
681
- cj_bridge_set_dialog_callback(h2, on_dialog);
682
- CHECK(cj_bridge_show_dialog(h2, 2, "t", "m", "") == 0, "未就绪返回 0");
683
- CHECK(g_run_dialog_calls == 0, "未就绪时平台没被调用");
684
-
685
- /* --- 11. 对话框:UI 线程快路径(不投递、不阻塞)--- */
686
- case_begin("dialog:UI 线程快路径与路径回调");
687
- reset_counters();
688
- cj_core_set_ready(h2);
689
- CHECK(cj_bridge_is_ready(h2) == 1, "set_ready 生效");
690
- CHECK(cj_bridge_show_dialog(h2, 0, "open", "pick", "*.txt") == 1, "文件类返回平台结果 1");
691
- CHECK(g_run_dialog_calls == 1, "在 UI 线程上直接弹(投递 + 阻塞会自锁)");
692
- CHECK(g_post_dialog_calls == 0, "快路径不发消息");
693
- CHECK(g_dialog_cb_calls == 1 && strcmp(g_dialog_cb_path, "/tmp/picked.txt") == 0,
694
- "选中路径回调送回");
695
- CHECK(h2->dlg_busy == 0, "槽位交回");
696
- CHECK(cj_bridge_show_dialog(h2, 2, "info", "m", "") == 1, "第二次调用仍可用");
697
- CHECK(h2->dlg_busy == 0, "第二次调用后槽位仍空");
698
-
699
- /* --- 12. 对话框作废:幂等 --- */
700
- case_begin("dialog_abort 幂等");
701
- cj_core_dialog_abort(h2);
702
- cj_core_dialog_abort(h2);
703
- CHECK(h2->dlg_cur == NULL, "无进行中对话框时 abort 是空操作");
704
-
705
- /* --- 12b. 句柄身份:结果回调必须落回**发起这次对话框**的宿主 --- */
706
- case_begin("dialog 结果回调带回发起窗口的句柄(两宿主不串)");
707
- {
708
- cj_host *hd = cj_bridge_create();
709
- int before;
710
- CHECK(hd != NULL, "另建宿主用于对照");
711
- if (hd) {
712
- cj_bridge_set_dialog_callback(hd, on_dialog);
713
- cj_core_set_ready(hd);
714
- before = g_dialog_cb_calls;
715
- CHECK(cj_bridge_show_dialog(h2, 0, "open", "p", "") == 1, "h2 弹框成功");
716
- CHECK(g_dialog_cb_calls == before + 1 && g_dialog_cb_host == h2,
717
- "h2 的结果回调收到 h2");
718
- CHECK(cj_bridge_show_dialog(hd, 0, "open", "p", "") == 1, "hd 弹框成功");
719
- CHECK(g_dialog_cb_calls == before + 2 && g_dialog_cb_host == hd,
720
- "hd 的结果回调收到 hd(不是上一条的 h2)");
721
- cj_bridge_destroy(hd);
722
- }
723
- }
724
-
725
- /* --- 13. 菜单线路解析(两平台翻译层共用这一份)--- */
726
- case_begin("menu_walk:线路格式解析(层级 / 字段 / flags)");
727
- {
728
- static const char *wire =
729
- "m\t\t视图\t1\t\n"
730
- "\tc\tview.sidebar\t侧栏\t3\t\n"
731
- "\tn\tview.zoom\t放大\t1\tCtrl+=\n"
732
- "s\t\t\t1\t\n"
733
- "n\tfile.open\t打开\t1\t\n";
734
- row_log log;
735
- log.n = 0;
736
- cj_core_menu_walk(wire, collect_row, &log);
737
- CHECK(log.n == 5, "5 行全部解析出来");
738
- CHECK(log.n == 5 && log.rows[0].depth == 0 && log.rows[0].kind == 'm' &&
739
- log.rows[0].id[0] == '\0' && strcmp(log.rows[0].label, "视图") == 0,
740
- "子菜单:层级 0、id 为空、label 保留");
741
- CHECK(log.n == 5 && log.rows[1].depth == 1 && log.rows[1].kind == 'c' &&
742
- strcmp(log.rows[1].id, "view.sidebar") == 0 && log.rows[1].flags == 3,
743
- "勾选项:行首制表符 = 层级 1、flags=3(可用 + 勾选)");
744
- CHECK(log.n == 5 && log.rows[2].kind == 'n' && strcmp(log.rows[2].label, "放大") == 0 &&
745
- strcmp(log.rows[2].accel, "Ctrl+=") == 0,
746
- "普通项:accel 原样带出(Windows 侧要并进标签右对齐)");
747
- CHECK(log.n == 5 && log.rows[3].kind == 's' && log.rows[3].id[0] == '\0' &&
748
- log.rows[3].label[0] == '\0',
749
- "分隔线:id / label 为空但仍是完整一行");
750
- CHECK(log.n == 5 && strcmp(log.rows[4].id, "file.open") == 0 && log.rows[4].flags == 1,
751
- "顶层普通项");
752
- }
753
- {
754
- /* 容错:缺字段按空串 / 0;kind 空的行整行跳过;末行不带换行也要出 */
755
- row_log log2;
756
- log2.n = 0;
757
- cj_core_menu_walk("n\tonly-id\n\t\t\nn\ttail\t尾\t1", collect_row, &log2);
758
- CHECK(log2.n == 2, "kind 为空的行被跳过(容错优于崩溃)");
759
- CHECK(log2.n == 2 && strcmp(log2.rows[0].id, "only-id") == 0 &&
760
- log2.rows[0].label[0] == '\0' && log2.rows[0].flags == 0 &&
761
- log2.rows[0].accel[0] == '\0',
762
- "缺字段按空串 / 0");
763
- CHECK(log2.n == 2 && strcmp(log2.rows[1].id, "tail") == 0 &&
764
- log2.rows[1].accel[0] == '\0', "末行不带换行也解析");
765
- cj_core_menu_walk(NULL, collect_row, &log2);
766
- cj_core_menu_walk("n\tx\tX\t1\t\n", NULL, NULL);
767
- CHECK(1, "walk 对 NULL 参数安全(未崩)");
768
- }
769
-
770
- /* --- 14. set_menu:core 存下 + 立即透传平台 --- */
771
- case_begin("set_menu 存下线路文本并透传平台");
772
- reset_counters();
773
- cj_bridge_set_menu(h, "n\tfile.open\t打开\t1\t\n");
774
- CHECK(h->pending_menu_text && strcmp(h->pending_menu_text, "n\tfile.open\t打开\t1\t\n") == 0,
775
- "线路文本由 core 存下(平台建窗时读它)");
776
- CHECK(g_set_menu_calls == 1 && g_last_menu_text &&
777
- strcmp(g_last_menu_text, h->pending_menu_text) == 0, "同时透传平台(未就绪时平台只记下)");
778
- cj_bridge_set_menu(h, "n\tfile.save\t保存\t0\t\n");
779
- CHECK(h->pending_menu_text && strcmp(h->pending_menu_text, "n\tfile.save\t保存\t0\t\n") == 0,
780
- "再次 set:替换并释放旧文本");
781
- cj_bridge_set_menu(h, "");
782
- CHECK(h->pending_menu_text && h->pending_menu_text[0] == '\0', "空串 = 清空菜单栏(仍然记下并透传)");
783
- cj_bridge_set_menu(h, NULL);
784
- CHECK(h->pending_menu_text && h->pending_menu_text[0] == '\0', "NULL 忽略(不误清)");
785
- cj_bridge_set_menu(NULL, "x");
786
- CHECK(1, "set_menu 对 NULL 宿主安全");
787
-
788
- /* --- 15. titlebar 副标题:core 校验参数、原样透传平台 --- */
789
- case_begin("titlebar 副标题:校验 + 透传平台");
790
- reset_counters();
791
- CHECK(cj_bridge_set_titlebar_subtitle(h, "n.md · 已保存 · 54 字") == 0, "正常文本返回 0");
792
- CHECK(g_subtitle_calls == 1 && g_last_subtitle_text &&
793
- strcmp(g_last_subtitle_text, "n.md · 已保存 · 54 字") == 0,
794
- "原文透传平台(UTF-8 多字节不截断)");
795
- cj_bridge_set_titlebar_subtitle(h, "");
796
- CHECK(g_last_subtitle_text && g_last_subtitle_text[0] == '\0', "空串 = 清空副标题(仍透传)");
797
- {
798
- /* 超 1024 字节:core 当场拒绝,不透传——平台在 UI 线程里不该再做第二份校验 */
799
- char big[1100];
800
- memset(big, 'a', sizeof(big) - 1);
801
- big[sizeof(big) - 1] = '\0';
802
- reset_counters();
803
- CHECK(cj_bridge_set_titlebar_subtitle(h, big) == -1, "超过 1024 字节返回 -1");
804
- CHECK(g_subtitle_calls == 0, "被拒的请求不透传到平台");
805
- }
806
- CHECK(cj_bridge_set_titlebar_subtitle(h, NULL) == -1, "NULL 文本返回 -1");
807
- CHECK(cj_bridge_set_titlebar_subtitle(NULL, "x") == -1, "NULL 宿主返回 -1");
808
-
809
- /* --- 16. 菜单点击:shell 回调带回宿主身份 + JSON 载荷 --- */
810
- case_begin("menu 点击回调带宿主句柄 + JSON 载荷(多窗口不串台)");
811
- reset_counters();
812
- g_shell_cb_calls = 0;
813
- cj_bridge_set_shell_callback(h, on_shell);
814
- cj_core_menu_clicked(h, "view.sidebar", 1, 1);
815
- CHECK(g_shell_cb_calls == 1, "点击回调触发一次");
816
- CHECK(g_shell_cb_host == h, "首参就是该宿主句柄");
817
- CHECK(strstr(g_shell_cb_json, "\"kind\":\"menu\"") != NULL &&
818
- strstr(g_shell_cb_json, "\"id\":\"view.sidebar\"") != NULL &&
819
- strstr(g_shell_cb_json, "\"enabled\":true") != NULL &&
820
- strstr(g_shell_cb_json, "\"checked\":true") != NULL,
821
- "载荷是 §5.5 的 JSON(kind / id / enabled / checked)");
822
- cj_core_menu_clicked(h, "quote\"and\\back", 0, 0);
823
- CHECK(g_shell_cb_calls == 2 &&
824
- strstr(g_shell_cb_json, "\"id\":\"quote\\\"and\\\\back\"") != NULL,
825
- "id 里的 \\ 与 \" 被转义(JSON 不被弄坏)");
826
- cj_core_menu_clicked(h, "", 1, 0);
827
- CHECK(g_shell_cb_calls == 2, "空 id 不回调(分隔线 / 子菜单走不到这里)");
828
- cj_bridge_set_shell_callback(h2, on_shell);
829
- cj_core_menu_clicked(h2, "panel.reload", 0, 0);
830
- CHECK(g_shell_cb_calls == 3 && g_shell_cb_host == h2,
831
- "第二个宿主的点击带回的是它自己的句柄");
832
- cj_core_menu_clicked(NULL, "x", 1, 1);
833
- CHECK(1, "clicked 对 NULL 宿主安全");
834
-
835
- /* --- 16. menu_item_state:透传平台 --- */
836
- case_begin("menu_item_state 透传平台");
837
- reset_counters();
838
- cj_bridge_set_menu_item_state(h, "view.sidebar", 1, 0);
839
- CHECK(g_menu_state_calls == 1 && strcmp(g_last_menu_state_id, "view.sidebar") == 0 &&
840
- g_last_menu_state_enabled == 1 && g_last_menu_state_checked == 0,
841
- "id 与状态透传到平台");
842
- cj_bridge_set_menu_item_state(h, "", 0, 0);
843
- CHECK(g_menu_state_calls == 1, "空 id 忽略");
844
- cj_bridge_set_menu_item_state(NULL, "x", 1, 1);
845
- CHECK(1, "对 NULL 宿主安全");
846
-
847
- /* --- 16b. 拖放:策略透传 + drop 报文解析(RFC-002 §5 / §7.B)--- */
848
- case_begin("set_drag_drop 透传与 pending");
849
- reset_counters();
850
- cj_bridge_set_drag_drop(h, 1, 1);
851
- CHECK(g_drag_calls == 1 && g_drag_last_enabled == 1 && g_drag_last_files == 1,
852
- "策略透传到平台原语");
853
- CHECK(h->pending_drop_enabled == 1 && h->pending_drop_files == 1,
854
- "策略同时记进 pending(建 view 时读)");
855
- cj_bridge_set_drag_drop(h, 0, 0);
856
- CHECK(g_drag_calls == 2 && h->pending_drop_enabled == 0 && h->pending_drop_files == 0,
857
- "关掉也透传(平台摘 drag dest)");
858
- cj_bridge_set_drag_drop(NULL, 1, 1);
859
- CHECK(1, "对 NULL 宿主安全");
860
-
861
- case_begin("cj_core_drop_files 解析与报文");
862
- reset_counters();
863
- g_shell_cb_calls = 0; /* shell 回调计数不在 reset_counters 里(menu 用例也是手动清零) */
864
- cj_core_drop_files(h, "file:///tmp/a.png\r\nfile:///tmp/b%20c.txt\r\n");
865
- CHECK(g_shell_cb_calls == 1 && g_shell_cb_host == h, "drop 回调一次、带回该宿主句柄");
866
- CHECK(strstr(g_shell_cb_json, "\"kind\":\"drop\"") != NULL &&
867
- strstr(g_shell_cb_json, "\"paths\":[\"/tmp/a.png\",\"/tmp/b c.txt\"]") != NULL,
868
- "file:// 解码成路径(\\r\\n 分隔、%20 还原空格),报文是 {kind,paths} JSON");
869
- cj_core_drop_files(h, "# comment line\n\nfile:///tmp/only.txt\nhttps://example.com/x");
870
- CHECK(g_shell_cb_calls == 2 &&
871
- strstr(g_shell_cb_json, "\"/tmp/only.txt\"") != NULL &&
872
- strstr(g_shell_cb_json, "\"https://example.com/x\"") != NULL &&
873
- strstr(g_shell_cb_json, "comment") == NULL,
874
- "注释/空行跳过,非 file:// URI 原样保留");
875
- cj_core_drop_files(h, "");
876
- CHECK(g_shell_cb_calls == 2, "空载荷不回调");
877
- cj_core_drop_files(h, NULL);
878
- CHECK(g_shell_cb_calls == 2, "NULL 载荷安全");
879
- cj_bridge_set_shell_callback(h2, on_shell);
880
- cj_core_drop_files(h2, "file:///tmp/second.txt");
881
- CHECK(g_shell_cb_calls == 3 && g_shell_cb_host == h2,
882
- "第二个宿主的 drop 带回它自己的句柄(句柄路由不串台)");
883
- cj_core_drop_files(NULL, "file:///tmp/x");
884
- CHECK(1, "对 NULL 宿主安全");
885
-
886
- /* --- 16c. 全局快捷键:接线层(wire 记 pending + 透传平台;触发报文走 shell 通道)--- */
887
- case_begin("set_global_hotkeys 接线(pending + 平台透传)");
888
- reset_counters();
889
- g_hotkey_calls = 0;
890
- g_hotkey_last_wire[0] = '\0';
891
- cj_bridge_set_global_hotkeys(h, "Ctrl+Alt+C:show-panel\nCtrl+Shift+P:pause");
892
- CHECK(g_hotkey_calls == 1 && strcmp(g_hotkey_last_wire,
893
- "Ctrl+Alt+C:show-panel\nCtrl+Shift+P:pause") == 0,
894
- "wire 透传到平台原语(解析与 grab 在平台层)");
895
- CHECK(h->pending_hotkeys != NULL &&
896
- strcmp(h->pending_hotkeys, "Ctrl+Alt+C:show-panel\nCtrl+Shift+P:pause") == 0,
897
- "wire 同时记进 pending(grab 线程启动时读)");
898
- cj_bridge_set_global_hotkeys(h, "");
899
- CHECK(g_hotkey_calls == 2 && h->pending_hotkeys != NULL &&
900
- strcmp(h->pending_hotkeys, "") == 0,
901
- "空 wire = 全部注销(整表替换语义)");
902
- cj_bridge_set_global_hotkeys(NULL, "Ctrl+Alt+C:x");
903
- CHECK(1, "对 NULL 宿主安全");
904
-
905
- case_begin("cj_core_hotkey_pressed 触发报文");
906
- g_shell_cb_calls = 0;
907
- cj_bridge_set_shell_callback(h, on_shell);
908
- cj_core_hotkey_pressed(h, "show-panel");
909
- CHECK(g_shell_cb_calls == 1 && g_shell_cb_host == h,
910
- "触发回调一次、带回该宿主句柄");
911
- CHECK(strstr(g_shell_cb_json, "\"kind\":\"hotkey\"") != NULL &&
912
- strstr(g_shell_cb_json, "\"id\":\"show-panel\"") != NULL,
913
- "载荷是 {kind,id} JSON(与菜单点击同族,走 on_shell 通道)");
914
- cj_core_hotkey_pressed(h, "");
915
- CHECK(g_shell_cb_calls == 1, "空 id 不回调(无法路由,直接丢弃)");
916
- cj_bridge_set_shell_callback(h2, on_shell);
917
- cj_core_hotkey_pressed(h2, "second");
918
- CHECK(g_shell_cb_calls == 2 && g_shell_cb_host == h2,
919
- "第二个宿主的触发带回它自己的句柄(句柄路由不串台)");
920
- cj_core_hotkey_pressed(NULL, "x");
921
- CHECK(1, "对 NULL 宿主安全");
922
-
923
- /* --- 16d. 托盘:接线层(wire 记 pending + 透传平台;点击报文走 shell 通道)--- */
924
- case_begin("set_tray 接线(pending + 平台透传)");
925
- reset_counters();
926
- g_tray_calls = 0;
927
- g_tray_last_wire[0] = '\0';
928
- cj_bridge_set_tray(h, "icon\t/tmp/tray.png\ntooltip\tcj-tauri\nn\ttoggle\t显示/隐藏\t1\t\nn\tquit\t退出\t1\t\n");
929
- CHECK(g_tray_calls == 1 && strstr(g_tray_last_wire, "icon\t/tmp/tray.png") != NULL &&
930
- strstr(g_tray_last_wire, "n\ttoggle") != NULL,
931
- "wire 透传到平台原语(解析与 appindicator 在平台层)");
932
- CHECK(h->pending_tray != NULL && strstr(h->pending_tray, "tooltip\tcj-tauri") != NULL,
933
- "wire 同时记进 pending(宿主线程装配时读)");
934
- cj_bridge_set_tray(h, "");
935
- CHECK(g_tray_calls == 2 && h->pending_tray != NULL && strcmp(h->pending_tray, "") == 0,
936
- "空 wire = 移除托盘(pending 同步清空)");
937
- cj_bridge_set_tray(NULL, "icon\tx");
938
- CHECK(1, "对 NULL 宿主安全");
939
-
940
- case_begin("cj_core_tray_clicked 触发报文");
941
- g_shell_cb_calls = 0;
942
- cj_bridge_set_shell_callback(h, on_shell);
943
- cj_core_tray_clicked(h, "toggle");
944
- CHECK(g_shell_cb_calls == 1 && g_shell_cb_host == h,
945
- "点击回调一次、带回该宿主句柄");
946
- CHECK(strstr(g_shell_cb_json, "\"kind\":\"tray\"") != NULL &&
947
- strstr(g_shell_cb_json, "\"id\":\"toggle\"") != NULL,
948
- "载荷是 {kind,id} JSON(RFC-002 §5.5 的 tray 信封,走 on_shell 通道)");
949
- cj_core_tray_clicked(h, "");
950
- CHECK(g_shell_cb_calls == 1, "空 id 不回调(无法路由,直接丢弃)");
951
- cj_bridge_set_shell_callback(h2, on_shell);
952
- cj_core_tray_clicked(h2, "quit");
953
- CHECK(g_shell_cb_calls == 2 && g_shell_cb_host == h2,
954
- "第二个宿主的点击带回它自己的句柄(句柄路由不串台)");
955
- cj_core_tray_clicked(NULL, "x");
956
- CHECK(1, "对 NULL 宿主安全");
957
-
958
- /* --- 17. 能力位:cj_plat_init 时问一次,此后只读 --- */
959
- case_begin("host capabilities 透传(system:host 的依据)");
960
- {
961
- cj_host *h3 = cj_bridge_create();
962
- reset_counters();
963
- g_caps_value = CJ_CAP_MENU | CJ_CAP_DRAG_DROP;
964
- cj_bridge_init(h3, on_message, on_destroy);
965
- CHECK(g_caps_calls == 1, "cj_plat_init 时问平台一次");
966
- CHECK(h3 && cj_bridge_host_capabilities(h3) == (CJ_CAP_MENU | CJ_CAP_DRAG_DROP) &&
967
- h3->capabilities == (CJ_CAP_MENU | CJ_CAP_DRAG_DROP),
968
- "能力位由平台给出并被 core 缓存(托盘没编进来 → 不置位)");
969
- CHECK(cj_bridge_host_capabilities(NULL) == 0, "NULL 宿主返回 0");
970
- CHECK(h->capabilities == CJ_CAP_MENU, "先建的宿主保留自己那次 init 的结果");
971
- g_caps_value = CJ_CAP_MENU;
972
- cj_bridge_destroy(h3);
973
- }
974
-
975
- /* --- 18. 销毁:先走平台 fini,再回收句柄 --- */
976
- case_begin("destroy 先走 cj_plat_fini 再回收");
977
- reset_counters();
978
- cj_bridge_destroy(h2);
979
- cj_bridge_destroy(h);
980
- CHECK(g_fini_calls == 2, "两个宿主各自走一次平台 fini");
981
-
982
- /* --- 19. 串口:白名单是安全边界,导出只做校验 + 转发 --- */
983
- case_begin("serial 白名单与导出转发");
984
- CHECK(cj_serial_path_allowed("/dev/ttyUSB0"), "放行 /dev/ttyUSB0");
985
- CHECK(cj_serial_path_allowed("/dev/ttyS3"), "放行 /dev/ttyS3");
986
- CHECK(cj_serial_path_allowed("/dev/pts/7"), "放行伪终端 /dev/pts/7(socat 造的虚拟串口)");
987
- CHECK(cj_serial_path_allowed("/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0"),
988
- "放行 udev 稳定链接");
989
- CHECK(cj_serial_path_allowed("/dev/cu.usbserial-1420"), "放行 macOS /dev/cu.*");
990
- CHECK(cj_serial_path_allowed("COM3"), "放行 Windows COM3");
991
- CHECK(cj_serial_path_allowed("\\\\.\\COM12"), "放行 Windows 长名形式");
992
- CHECK(!cj_serial_path_allowed("/etc/shadow"), "拒绝 /etc/shadow");
993
- CHECK(!cj_serial_path_allowed("/dev/tty"), "拒绝裸 /dev/tty(那是控制终端,不是串口)");
994
- CHECK(!cj_serial_path_allowed("/dev/ttyUSB0/../../etc/shadow"), "拒绝借白名单前缀做 .. 穿越");
995
- CHECK(!cj_serial_path_allowed("/dev/serial/by-id/../../etc/passwd"), "拒绝符号链接段的 .. 穿越");
996
- CHECK(!cj_serial_path_allowed("COMfoo"), "拒绝 COMfoo(COM 前缀后必须是数字)");
997
- CHECK(!cj_serial_path_allowed("COM1234"), "拒绝 COM1234(超过 3 位)");
998
- CHECK(!cj_serial_path_allowed(""), "拒绝空路径");
999
- CHECK(!cj_serial_path_allowed(NULL), "拒绝 NULL 路径");
1000
-
1001
- g_serial_open_calls = 0;
1002
- CHECK(cj_bridge_serial_open("/etc/shadow", 115200, 8, 0, 1) == -1,
1003
- "白名单外的路径被 core 拒绝(-1)");
1004
- CHECK(g_serial_open_calls == 0, "被拒路径不触达平台");
1005
- CHECK(cj_bridge_serial_last_error()[0] != '\0', "拒绝时 last_error 有可读原因");
1006
- CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 9, 0, 1) == -1, "dataBits=9 被拒");
1007
- CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 0, 8, 0, 1) == -1, "baud=0 被拒");
1008
- CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 3, 1) == -1, "parity=3 被拒");
1009
- CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 0, 3) == -1, "stopBits=3 被拒");
1010
- CHECK(g_serial_open_calls == 0, "参数非法同样不触达平台");
1011
-
1012
- CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 2, 1) == 42,
1013
- "合法路径转发平台并原样带回句柄");
1014
- CHECK(g_serial_open_calls == 1 && strcmp(g_serial_last_path, "/dev/ttyUSB0") == 0,
1015
- "路径原样传给平台");
1016
- CHECK(g_serial_last_cfg.baud == 115200 && g_serial_last_cfg.data_bits == 8 &&
1017
- g_serial_last_cfg.parity == 2 && g_serial_last_cfg.stop_bits == 1,
1018
- "四项配置(含偶校验)原样传给平台");
1019
-
1020
- g_serial_open_ret = -3;
1021
- CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 0, 1) == -3, "平台失败码原样返回");
1022
- CHECK(strstr(cj_bridge_serial_last_error(), "stub") != NULL, "平台给的原因进 last_error");
1023
- g_serial_open_ret = 42;
1024
-
1025
- {
1026
- unsigned char rd[4];
1027
- unsigned char wr[3];
1028
- int n;
1029
-
1030
- rd[0] = 0;
1031
- wr[0] = 'A';
1032
- wr[1] = 'B';
1033
- wr[2] = 'C';
1034
- n = cj_bridge_serial_read(42, rd, (int)sizeof(rd), 0);
1035
- CHECK(n == 1 && rd[0] == 'Z' && g_serial_read_calls == 1, "read 转发并带回字节");
1036
- n = cj_bridge_serial_write(42, wr, (int)sizeof(wr), 0);
1037
- CHECK(n == 3 && g_serial_write_calls == 1 && g_serial_write_last_len == 3 &&
1038
- g_serial_write_first == 'A', "write 转发(平台拿到原缓冲)");
1039
- CHECK(cj_bridge_serial_close(42) == 0 && g_serial_close_calls == 1, "close 转发");
1040
-
1041
- CHECK(cj_bridge_serial_read(0, rd, 4, 0) == -1, "handle<=0 的 read 被拒");
1042
- CHECK(cj_bridge_serial_read(42, NULL, 4, 0) == -1, "空缓冲的 read 被拒");
1043
- CHECK(cj_bridge_serial_write(42, wr, 0, 0) == -1, "len=0 的 write 被拒");
1044
- CHECK(cj_bridge_serial_close(0) == -1, "handle<=0 的 close 被拒");
1045
- CHECK(g_serial_read_calls == 1 && g_serial_write_calls == 1 && g_serial_close_calls == 1,
1046
- "参数非法的调用没有触达平台");
1047
- CHECK(1, "串口导出不需要宿主(本用例跑在两个宿主都已销毁之后)");
1048
- }
1049
-
1050
- /* --- 20. 剪贴板:参数校验在 core,平台失败码原样返回 --- */
1051
- case_begin("clipboard 校验与转发");
1052
- CHECK(cj_bridge_clipboard_write_text(NULL, "x") == -1, "NULL 文本被拒(-1)");
1053
- {
1054
- char *big = (char *)malloc((8u << 20) + 1);
1055
- cj_host *hc = cj_bridge_create(); /* 剪贴板是宿主能力:要一个活句柄(不 start,window_gone=0)*/
1056
- if (big && hc) {
1057
- memset(big, 'a', (8u << 20));
1058
- big[8u << 20] = '\0';
1059
- CHECK(cj_bridge_clipboard_write_text(hc, big) == -1, "超 8 MiB 被拒");
1060
- free(big);
1061
- }
1062
- if (hc) {
1063
- g_clip_write_calls = 0;
1064
- g_clip_read_calls = 0;
1065
- free(g_clip_last_written);
1066
- g_clip_last_written = NULL;
1067
- CHECK(cj_bridge_clipboard_write_text(hc, "hello clip") == 0, "写文本成功");
1068
- CHECK(g_clip_write_calls == 1 && g_clip_last_written &&
1069
- strcmp(g_clip_last_written, "hello clip") == 0, "文本原样到达平台");
1070
- {
1071
- char *got = cj_bridge_clipboard_read_text(hc);
1072
- CHECK(got && strcmp(got, "hello clip") == 0, "读回刚写的文本");
1073
- cj_bridge_clipboard_free(got);
1074
- CHECK(g_clip_read_calls == 1, "读触达平台一次");
1075
- }
1076
- cj_bridge_clipboard_free(NULL);
1077
- CHECK(1, "free(NULL) 幂等安全");
1078
- g_clip_write_ok = 0;
1079
- CHECK(cj_bridge_clipboard_write_text(hc, "x") == -3, "平台失败码返回 -3");
1080
- g_clip_write_ok = 1;
1081
- /* 通知:参数校验 + 发后即忘(投递即返回 0,平台成败看 stderr)*/
1082
- g_note_calls = 0;
1083
- hc->capabilities |= CJ_CAP_NOTIFICATION; /* 通知是宿主能力:声明了才走到平台 */
1084
- CHECK(cj_bridge_notification_notify(hc, "", "b") == -1, "空标题被拒");
1085
- CHECK(cj_bridge_notification_notify(hc, NULL, "b") == -1, "NULL 标题被拒");
1086
- {
1087
- char long_title[258];
1088
- memset(long_title, 't', sizeof(long_title));
1089
- long_title[sizeof(long_title) - 1] = '\0';
1090
- CHECK(cj_bridge_notification_notify(hc, long_title, "b") == -1, "超长标题被拒");
1091
- }
1092
- CHECK(g_note_calls == 0, "被拒的通知不触达平台");
1093
- CHECK(cj_bridge_notification_notify(hc, "hello", "body") == 0, "合法通知提交成功(发后即忘)");
1094
- CHECK(g_note_calls == 1 && g_note_last_title && strcmp(g_note_last_title, "hello") == 0,
1095
- "标题原样到达平台");
1096
- g_note_ok = 0;
1097
- CHECK(cj_bridge_notification_notify(hc, "x", NULL) == -3, "UI 直行路径平台失败如实回 -3");
1098
- g_note_ok = 1;
1099
- /* 未声明通知能力的平台(如 Windows 桩)要当场拒绝,不能走「发后即忘」被读成成功 */
1100
- hc->capabilities &= ~CJ_CAP_NOTIFICATION;
1101
- g_note_calls = 0;
1102
- CHECK(cj_bridge_notification_notify(hc, "x", "b") == -4, "未声明能力位如实回 -4");
1103
- CHECK(g_note_calls == 0, "未声明能力位不触达平台");
1104
- cj_bridge_destroy(hc);
1105
- }
1106
- }
1107
-
1108
- /* --- 日志落盘(RFC-003 第 5 期):接管 stderr、按大小轮转、失败如实回 0 --- */
1109
- case_begin("log file:接管 stderr / 按大小轮转 / 失败如实回 0");
1110
- {
1111
- char dir[256];
1112
- char path[300];
1113
- char archive1[320];
1114
- char archive2[320];
1115
- char archive3[320];
1116
- char blocker[320];
1117
- char unwritable[340];
1118
- char long_path[1200];
1119
- char buf[65536];
1120
- FILE *bf;
1121
- int saved_fd;
1122
- int i;
1123
-
1124
- snprintf(dir, sizeof(dir), "/tmp/cj-bridge-logtest-%ld", (long)getpid());
1125
- cj_test_mkdir(dir);
1126
- /* 路径故意多两级不存在的目录:默认日志目录(~/.local/state/cj-tauri)常常还不存在,
1127
- 走到 ③ 顺带证明「父目录按需建出来」 */
1128
- snprintf(path, sizeof(path), "%s/nested/deep/app.log", dir);
1129
- snprintf(archive1, sizeof(archive1), "%s.1", path);
1130
- snprintf(archive2, sizeof(archive2), "%s.2", path);
1131
- snprintf(archive3, sizeof(archive3), "%s.3", path);
1132
-
1133
- /* ① 未开启(对照组):回读空串、不创建文件,c j_logf 仍写 stderr */
1134
- CHECK(strcmp(cj_bridge_log_file(), "") == 0, "未开启时回读空串");
1135
- cj_logf("[bridge-core] probe before enable\n");
1136
- CHECK(cj_test_file_exists(path) == 0, "未开启时不创建日志文件");
1137
-
1138
- /* ② 参数与失败路径都回 0(stderr 完好,原因打在 stderr 上) */
1139
- CHECK(cj_bridge_set_log_file(NULL, 0, 0) == 0, "NULL 路径被拒");
1140
- CHECK(cj_bridge_set_log_file("", 0, 0) == 0, "空路径被拒");
1141
- memset(long_path, 'a', sizeof(long_path) - 1);
1142
- long_path[sizeof(long_path) - 1] = '\0';
1143
- CHECK(cj_bridge_set_log_file(long_path, 0, 0) == 0, "超长路径被拒");
1144
- /* 「不可写」不能拿一个不存在的目录测:父目录缺失会被如实建出来(默认目录要的就是这行为),
1145
- 所以拿一个**普通文件**当路径的中间段——父级不是目录,建也建不了、开也开不了 */
1146
- snprintf(blocker, sizeof(blocker), "%s/blocker", dir);
1147
- bf = fopen(blocker, "wb");
1148
- if (bf) fclose(bf);
1149
- snprintf(unwritable, sizeof(unwritable), "%s/app.log", blocker);
1150
- CHECK(cj_bridge_set_log_file(unwritable, 0, 0) == 0, "不可写路径被拒(父级是普通文件)");
1151
- CHECK(cj_test_file_exists(path) == 0, "被拒的开启不产生文件");
1152
- CHECK(strcmp(cj_bridge_log_file(), "") == 0, "被拒之后仍未激活");
1153
-
1154
- /* ③ 留底原 stderr fd(必须在接管之前取),然后开启:max 1KB、保留 2 份 */
1155
- saved_fd = dup(2);
1156
- LOG_CHECK(saved_fd >= 0, "留底原 stderr fd 成功");
1157
- LOG_CHECK(cj_bridge_set_log_file(path, 1, 2) == 1, "开启成功回 1(父目录缺失也按需建出来)");
1158
- LOG_CHECK(strcmp(cj_bridge_log_file(), path) == 0, "回读路径与传入一致");
1159
- LOG_CHECK(cj_bridge_set_log_file(path, 1, 2) == 0, "重复开启被拒(换了目标只能重启)");
1160
-
1161
- /* ④ 开启后的诊断确实落在文件里 */
1162
- cj_logf("[bridge-core] after enable marker=alpha\n");
1163
- CHECK(1, "接管期间断言走 LOG_CHECK(失败行不会丢进日志文件)");
1164
- LOG_CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0, "日志文件有内容");
1165
- LOG_CHECK(strstr(buf, "[cj-bridge] log file: ") != NULL, "首行记录了日志路径与预算");
1166
- LOG_CHECK(strstr(buf, "marker=alpha") != NULL, "开启后的 cj_logf 落到文件");
1167
- LOG_CHECK(strstr(buf, "[2") != NULL, "落盘行带时间戳前缀([YYYY-…)");
1168
- LOG_CHECK(strstr(buf, "probe before enable") == NULL, "开启之前的输出不会被追认进文件");
1169
-
1170
- /* ⑤ 前端/插件通道:写一行 + NULL 拒绝 */
1171
- LOG_CHECK(cj_bridge_log_write(NULL) == -1, "log_write(NULL) 被拒");
1172
- LOG_CHECK(cj_bridge_log_write("[frontend] marker=beta") == 0, "log_write 回 0");
1173
- LOG_CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0 && strstr(buf, "marker=beta") != NULL,
1174
- "log_write 的行也落在同一份文件");
1175
-
1176
- /* ⑥ 轮转:1KB 预算下连写,档案要出现、keep 上限要守住 */
1177
- for (i = 0; i < 60; i++) cj_logf("[bridge-core] filler line %02d padding ................\n", i);
1178
- LOG_CHECK(cj_test_file_exists(archive1) != 0, "超预算后出现 log.1");
1179
- LOG_CHECK(cj_test_read_all(archive1, buf, sizeof(buf)) > 0, "log.1 有内容");
1180
- LOG_CHECK(cj_test_file_exists(archive2) != 0, "继续写出现 log.2");
1181
- LOG_CHECK(cj_test_file_exists(archive3) == 0, "keep=2 时不出现 log.3(上限守住)");
1182
- LOG_CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0 &&
1183
- strstr(buf, "log rotated") != NULL, "轮转后当前文件仍继续写(有 rotated 行)");
1184
-
1185
- /* ⑦ 还原 stderr:后续断言与收尾输出照常可见(顺带证明接管可逆) */
1186
- fflush(stderr);
1187
- dup2(saved_fd, 2);
1188
- close(saved_fd);
1189
- fprintf(stderr, " - [bridge-core] stderr 已还原(日志用例期间的失败行如下,无输出即全过)\n");
1190
- if (g_log_fail_buf[0]) fprintf(stderr, "%s", g_log_fail_buf);
1191
- CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0, "还原后仍能读日志文件");
1192
- }
1193
-
1194
- /* ===== 静默子进程:core 侧的校验与转发(真进程由平台原语负责,实机验证) ===== */
1195
- {
1196
- case_begin("spawn hidden");
1197
- CHECK(cj_bridge_spawn_hidden(NULL, "") == 0, "spawn_hidden(NULL) 被拒");
1198
- CHECK(cj_bridge_spawn_hidden("", "") == 0, "spawn_hidden 空路径被拒");
1199
- CHECK(cj_bridge_spawn_hidden("stub-ok", "a\x1f" "b") == 4242,
1200
- "参数串按 0x1F 分隔转发给平台,pid 原样回给上层");
1201
- CHECK(cj_bridge_spawn_hidden("stub-no", "") == 0, "平台失败时 core 回 0(绝不谎报 pid)");
1202
- CHECK(cj_bridge_kill_pid(0) == 0, "kill_pid(0) 被拒");
1203
- CHECK(cj_bridge_kill_pid(-5) == 0, "kill_pid(负数) 被拒");
1204
- CHECK(cj_bridge_pid_alive(0) == 0, "pid_alive(0) 恒为 0(不落到平台原语)");
1205
- }
1206
-
1207
- /* ===== 内嵌资源释放:单文件包随包资源(core 负责校验与错误出口) ===== */
1208
- {
1209
- case_begin("extract assets");
1210
- CHECK(cj_bridge_extract_assets(NULL) == -1, "extract_assets(NULL) 被拒");
1211
- CHECK(cj_bridge_extract_assets("") == -1, "extract_assets 空目录被拒");
1212
- CHECK(cj_bridge_extract_assets("some/runtime/dir") == 0,
1213
- "没有内嵌资源的产物如实回 0(普通构建就是这样,不是错误)");
1214
- CHECK(cj_bridge_extract_assets("stub-missing") == -1,
1215
- "平台报错时 core 回 -1(绝不把失败说成 0 个文件)");
1216
- }
1217
-
1218
- fprintf(stderr, "[bridge-core] 断言 %d 项:通过 %d,失败 %d\n", g_pass + g_fail,
1219
- g_pass, g_fail);
1220
- free(g_last_js);
1221
- free(g_last_menu_text);
1222
- if (g_fail) {
1223
- fprintf(stderr, "[FAIL] bridge_core 自检未通过\n");
1224
- return 1;
1225
- }
1226
- fprintf(stderr, "[OK] bridge_core 自检全过\n");
1227
- return 0;
1228
- }
1
+ /*
2
+ * bridge_core.c 自检:不依赖仓颉 SDK,也不依赖 GTK / WebKit / WebView2。
3
+ *
4
+ * 被测的是「两平台逐字相同」的公共核心(回投队列与批处理、对话框单槽状态机、窗口配置存储、
5
+ * 生命周期标志、菜单线路格式解析、串口路径白名单、以及全部 cj_bridge_* 导出)。做法:用一个「只记录调用」的桩平台实现
6
+ * bridge_core.h 里的 25 个 cj_plat_* 原语,再把 core 与桩一起编译成单文件可执行程序——
7
+ * 链接期就能证明 core 只经 cj_plat_* 触达平台、不 include 任何平台头。
8
+ *
9
+ * 跑法:bash scripts/test-bridge-core.sh
10
+ */
11
+ #define _POSIX_C_SOURCE 200809L
12
+
13
+ #include "bridge_core.h"
14
+
15
+ #include <pthread.h>
16
+ #include <stdio.h>
17
+ #include <stdlib.h>
18
+ #include <string.h>
19
+ #include <sys/stat.h>
20
+ #include <unistd.h>
21
+
22
+ /* mingw 的 <io.h> 把 mkdir 声明成单参(MSVCRT 语义,且已标 deprecated),
23
+ POSIX 的两参形态在 Windows 上直接编不过(too many arguments to function call)——
24
+ 用例里统一走这个包装,两边的语义都是「建一级目录」。 */
25
+ #ifdef _WIN32
26
+ #include <direct.h>
27
+ static int cj_test_mkdir(const char *p) { return _mkdir(p); }
28
+ #else
29
+ static int cj_test_mkdir(const char *p) { return mkdir(p, 0700); }
30
+ #endif
31
+
32
+ /* ===== 断言与用例 ===== */
33
+
34
+ static int g_pass = 0;
35
+ static int g_fail = 0;
36
+ static const char *g_case = "?";
37
+
38
+ #define CHECK(cond, what) \
39
+ do { \
40
+ if (cond) { \
41
+ g_pass++; \
42
+ } else { \
43
+ g_fail++; \
44
+ fprintf(stderr, " [FAIL] %s:%d [%s] %s\n", __FILE__, __LINE__, \
45
+ g_case, what); \
46
+ } \
47
+ } while (0)
48
+
49
+ static void case_begin(const char *name) {
50
+ g_case = name;
51
+ fprintf(stderr, " - %s\n", name);
52
+ }
53
+
54
+ /* ===== 日志落盘用例的辅助(RFC-003 第 5 期) =====
55
+ 开启日志后 fd 2 就被日志文件接管了,此时 CHECK 的失败行会**写进日志文件**、外面根本看不见。
56
+ 所以接管期间改用 LOG_CHECK:结果先攒在内存里,等 stderr 还原之后再一次性打出来。 */
57
+ static char g_log_fail_buf[4096];
58
+
59
+ static void cj_test_log_check(int cond, int line, const char *what) {
60
+ if (cond) {
61
+ g_pass++;
62
+ return;
63
+ }
64
+ g_fail++;
65
+ if (strlen(g_log_fail_buf) + 240 < sizeof(g_log_fail_buf)) {
66
+ char row[200];
67
+ snprintf(row, sizeof(row), " [FAIL] %s:%d [log file] %s\n", __FILE__, line, what);
68
+ strcat(g_log_fail_buf, row);
69
+ }
70
+ }
71
+
72
+ #define LOG_CHECK(cond, what) cj_test_log_check((cond) ? 1 : 0, __LINE__, what)
73
+
74
+ static int cj_test_file_exists(const char *path) {
75
+ FILE *f = fopen(path, "rb");
76
+ if (!f) return 0;
77
+ fclose(f);
78
+ return 1;
79
+ }
80
+
81
+ /* 读全文(截断到 cap-1);返回读到的字节数,-1 = 打不开 */
82
+ static long cj_test_read_all(const char *path, char *buf, long cap) {
83
+ FILE *f = fopen(path, "rb");
84
+ long n;
85
+ if (!f) {
86
+ if (cap > 0) buf[0] = '\0';
87
+ return -1;
88
+ }
89
+ n = (long)fread(buf, 1, (size_t)(cap - 1), f);
90
+ buf[n > 0 ? n : 0] = '\0';
91
+ fclose(f);
92
+ return n;
93
+ }
94
+
95
+ /* ===== 桩平台:只记录调用,不做真实副作用 ===== */
96
+
97
+ struct cj_sync {
98
+ pthread_mutex_t m;
99
+ pthread_cond_t c;
100
+ };
101
+
102
+ static int g_post_js_calls;
103
+ static char *g_last_js; /* 最近一次 cj_plat_post_js 的脚本(拷贝) */
104
+ static int g_wake_calls;
105
+ static int g_quit_calls;
106
+ static int g_start_calls;
107
+ static int g_fini_calls;
108
+ static int g_run_dialog_calls;
109
+ static int g_post_dialog_calls;
110
+ static int g_load_url_calls;
111
+ static int g_reload_calls;
112
+ static int g_devtools_calls;
113
+ static int g_is_ui_thread = 1; /* 默认假装调用方就站在 UI 线程上 */
114
+ static int g_dialog_ok = 1; /* cj_plat_run_dialog 的返回值 */
115
+ static const char *g_dialog_path = "/tmp/picked.txt";
116
+ static int g_set_menu_calls;
117
+ static char *g_last_menu_text;
118
+ static int g_menu_state_calls;
119
+ static char g_last_menu_state_id[256];
120
+ static int g_last_menu_state_enabled;
121
+ static int g_last_menu_state_checked;
122
+ static int g_caps_calls;
123
+ static int g_caps_value = CJ_CAP_MENU; /* 桩平台自称的能力位(用例里改它,验证 core 只是如实缓存) */
124
+
125
+ /* 拖放策略:桩平台只记录调用(core 的职责是 pending 存取与 drop 报文解析)*/
126
+ static int g_drag_calls;
127
+ static int g_drag_last_enabled;
128
+ static int g_drag_last_files;
129
+
130
+ /* 标题栏副标题:桩平台只记录调用(core 的职责是参数校验 + 透传)*/
131
+ static int g_subtitle_calls;
132
+ static char *g_last_subtitle_text;
133
+
134
+ static void reset_counters(void) {
135
+ g_post_js_calls = g_wake_calls = g_quit_calls = g_start_calls = 0;
136
+ g_fini_calls = g_run_dialog_calls = g_post_dialog_calls = 0;
137
+ g_load_url_calls = g_reload_calls = g_devtools_calls = 0;
138
+ g_set_menu_calls = g_menu_state_calls = g_caps_calls = 0;
139
+ g_drag_calls = g_drag_last_enabled = g_drag_last_files = 0;
140
+ g_subtitle_calls = 0;
141
+ free(g_last_subtitle_text);
142
+ g_last_subtitle_text = NULL;
143
+ free(g_last_js);
144
+ g_last_js = NULL;
145
+ free(g_last_menu_text);
146
+ g_last_menu_text = NULL;
147
+ }
148
+
149
+ /* 数一段脚本里有几个换行:批处理按「每条一段 + 换行」拼接,用它反推条数 */
150
+ static int count_nl(const char *s) {
151
+ int n = 0;
152
+ while (s && *s) {
153
+ if (*s++ == '\n') n++;
154
+ }
155
+ return n;
156
+ }
157
+
158
+ cj_sync *cj_plat_sync_create(void) {
159
+ cj_sync *s = (cj_sync *)calloc(1, sizeof(cj_sync));
160
+ if (!s) return NULL;
161
+ pthread_mutex_init(&s->m, NULL);
162
+ pthread_cond_init(&s->c, NULL);
163
+ return s;
164
+ }
165
+
166
+ void cj_plat_sync_destroy(cj_sync *s) {
167
+ if (!s) return;
168
+ pthread_mutex_destroy(&s->m);
169
+ pthread_cond_destroy(&s->c);
170
+ free(s);
171
+ }
172
+
173
+ void cj_plat_sync_lock(cj_sync *s) { if (s) pthread_mutex_lock(&s->m); }
174
+ void cj_plat_sync_unlock(cj_sync *s) { if (s) pthread_mutex_unlock(&s->m); }
175
+
176
+ void cj_plat_sync_wait(cj_sync *s, int *ready) {
177
+ /* core 的约定:进入时已持锁,返回时 *ready 为真(允许虚假唤醒)*/
178
+ while (s && ready && !*ready) pthread_cond_wait(&s->c, &s->m);
179
+ }
180
+
181
+ void cj_plat_sync_signal(cj_sync *s) { if (s) pthread_cond_signal(&s->c); }
182
+
183
+ void cj_plat_init(cj_host *h) { (void)h; }
184
+ int cj_plat_fini(cj_host *h) { (void)h; g_fini_calls++; return 1; }
185
+ void cj_plat_start(cj_host *h) { (void)h; g_start_calls++; }
186
+ void cj_plat_quit(cj_host *h) { (void)h; g_quit_calls++; }
187
+ void cj_plat_wake_ui(cj_host *h) { (void)h; g_wake_calls++; }
188
+
189
+ void cj_plat_post_js(cj_host *h, const char *script_utf8) {
190
+ (void)h;
191
+ g_post_js_calls++;
192
+ free(g_last_js);
193
+ g_last_js = script_utf8 ? strdup(script_utf8) : NULL;
194
+ }
195
+
196
+ int cj_plat_is_ui_thread(cj_host *h) { (void)h; return g_is_ui_thread; }
197
+ void cj_plat_post_dialog(cj_host *h, dlg_req *req) { (void)h; (void)req; g_post_dialog_calls++; }
198
+
199
+ /* ---- 剪贴板桩:记调用次数与最后一次写的文本;读返回桩文本 ---- */
200
+ static int g_clip_write_calls = 0;
201
+ static int g_clip_read_calls = 0;
202
+ static char *g_clip_last_written = NULL;
203
+ static int g_clip_write_ok = 1; /* 用例可扳成 0 模拟平台失败 */
204
+
205
+ int cj_plat_post_ui(cj_host *h, cj_ui_fn fn, void *ud) {
206
+ (void)h;
207
+ fn(ud); /* 桩平台没有真 UI 线程:就地执行(core 的 is_ui_thread 桩可扳成 0 验投递路径)*/
208
+ return 1;
209
+ }
210
+
211
+ int cj_plat_clipboard_write_text(cj_host *h, const char *text, char *err, int err_len) {
212
+ (void)h; (void)err; (void)err_len;
213
+ g_clip_write_calls++;
214
+ if (!g_clip_write_ok) return -3;
215
+ free(g_clip_last_written);
216
+ g_clip_last_written = strdup(text ? text : "");
217
+ return 0;
218
+ }
219
+
220
+ char *cj_plat_clipboard_read_text(cj_host *h, char *err, int err_len) {
221
+ (void)h; (void)err; (void)err_len;
222
+ g_clip_read_calls++;
223
+ if (!g_clip_last_written) return NULL;
224
+ return strdup(g_clip_last_written);
225
+ }
226
+
227
+ /* ---- 通知桩:记调用次数与最后一次的标题;用例可扳失败 ---- */
228
+ static int g_note_calls = 0;
229
+ static char *g_note_last_title = NULL;
230
+ static int g_note_ok = 1;
231
+
232
+ int cj_plat_notification_notify(cj_host *h, const char *title, const char *body,
233
+ char *err, int err_len) {
234
+ (void)h; (void)body; (void)err; (void)err_len;
235
+ g_note_calls++;
236
+ if (!g_note_ok) return -3;
237
+ free(g_note_last_title);
238
+ g_note_last_title = strdup(title ? title : "");
239
+ return 0;
240
+ }
241
+
242
+ int cj_plat_run_dialog(cj_host *h, dlg_req *req) {
243
+ (void)h;
244
+ g_run_dialog_calls++;
245
+ if (req && (req->kind == 0 || req->kind == 1)) { /* 文件类才有路径 */
246
+ free(req->path);
247
+ req->path = strdup(g_dialog_path);
248
+ }
249
+ return g_dialog_ok;
250
+ }
251
+
252
+ void cj_plat_open_devtools(cj_host *h) { (void)h; g_devtools_calls++; }
253
+ void cj_plat_load_url(cj_host *h, const char *url) { (void)h; (void)url; g_load_url_calls++; }
254
+ void cj_plat_reload(cj_host *h) { (void)h; g_reload_calls++; }
255
+
256
+ void cj_plat_set_menu(cj_host *h, const char *wire) {
257
+ (void)h;
258
+ g_set_menu_calls++;
259
+ free(g_last_menu_text);
260
+ g_last_menu_text = wire ? strdup(wire) : NULL;
261
+ }
262
+
263
+ /* 桩平台:副标题一律接受(真平台未就绪返回 -3、Windows 返回 -4,那是平台自己的事)*/
264
+ int cj_plat_set_titlebar_subtitle(cj_host *h, const char *text) {
265
+ (void)h;
266
+ g_subtitle_calls++;
267
+ free(g_last_subtitle_text);
268
+ g_last_subtitle_text = text ? strdup(text) : NULL;
269
+ return 0;
270
+ }
271
+
272
+ void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
273
+ (void)h;
274
+ g_menu_state_calls++;
275
+ snprintf(g_last_menu_state_id, sizeof(g_last_menu_state_id), "%s", id ? id : "");
276
+ g_last_menu_state_enabled = enabled;
277
+ g_last_menu_state_checked = checked;
278
+ }
279
+
280
+ /* ---- 拖放策略:声明在计数器区(reset_counters 之前),这里只留原语 ---- */
281
+ void cj_plat_set_drag_drop(cj_host *h, int enabled, int files) {
282
+ (void)h;
283
+ g_drag_calls++;
284
+ g_drag_last_enabled = enabled;
285
+ g_drag_last_files = files;
286
+ }
287
+
288
+ /* ---- 全局快捷键:桩平台没有 X11,只记录调用(接线层自检用)---- */
289
+ static int g_hotkey_calls;
290
+ static char g_hotkey_last_wire[512];
291
+ void cj_plat_set_global_hotkeys(cj_host *h, const char *wire) {
292
+ (void)h;
293
+ g_hotkey_calls++;
294
+ snprintf(g_hotkey_last_wire, sizeof(g_hotkey_last_wire), "%s", wire ? wire : "");
295
+ }
296
+
297
+ /* ---- 托盘:桩平台没有 appindicator,只记录调用(接线层自检用)---- */
298
+ static int g_tray_calls;
299
+ static char g_tray_last_wire[512];
300
+ void cj_plat_set_tray(cj_host *h, const char *wire) {
301
+ (void)h;
302
+ g_tray_calls++;
303
+ snprintf(g_tray_last_wire, sizeof(g_tray_last_wire), "%s", wire ? wire : "");
304
+ }
305
+
306
+ /* ---- 窗口可见性 / 最小化到托盘:桩平台没有窗口,只记录调用(接线层自检用)---- */
307
+ static int g_win_visible_calls;
308
+ static int g_win_visible_last = -1;
309
+ void cj_plat_set_window_visible(cj_host *h, int visible) {
310
+ (void)h;
311
+ g_win_visible_calls++;
312
+ g_win_visible_last = visible;
313
+ }
314
+
315
+ static int g_min_tray_calls;
316
+ static int g_min_tray_last = -1;
317
+ void cj_plat_set_minimize_to_tray(cj_host *h, int enabled) {
318
+ (void)h;
319
+ g_min_tray_calls++;
320
+ g_min_tray_last = enabled;
321
+ }
322
+
323
+ /* ---- 附庸窗:桩平台同样只记录调用(owner 记成「有没有」而不是指针值)---- */
324
+ static int g_owner_calls;
325
+ static int g_owner_has_owner = -1;
326
+ void cj_plat_set_owner_window(cj_host *h, cj_host *owner) {
327
+ (void)h;
328
+ g_owner_calls++;
329
+ g_owner_has_owner = owner ? 1 : 0;
330
+ }
331
+
332
+ /* --- 内嵌视图(PLT-006):桩只记调用与入参,语义契约由 core 的校验覆盖 --- */
333
+ static int g_view_create_calls;
334
+ static long long g_view_create_last;
335
+ static int g_view_visible_calls;
336
+ static int g_view_visible_last = -1;
337
+ static int g_view_bounds_calls;
338
+ static int g_view_bounds_last[4];
339
+ static int g_view_navigate_calls;
340
+ static int g_view_destroy_calls;
341
+ static long long g_view_seq;
342
+
343
+ long long cj_plat_view_create(cj_host *h, const char *url, int x, int y, int w, int hgt) {
344
+ (void)h;
345
+ if (!url || !url[0]) return 0;
346
+ g_view_create_calls++;
347
+ g_view_seq++;
348
+ g_view_create_last = g_view_seq;
349
+ g_view_bounds_last[0] = x;
350
+ g_view_bounds_last[1] = y;
351
+ g_view_bounds_last[2] = w;
352
+ g_view_bounds_last[3] = hgt;
353
+ return g_view_seq;
354
+ }
355
+
356
+ void cj_plat_view_set_visible(cj_host *h, long long view, int visible) {
357
+ (void)h;
358
+ (void)view;
359
+ g_view_visible_calls++;
360
+ g_view_visible_last = visible;
361
+ }
362
+
363
+ void cj_plat_view_set_bounds(cj_host *h, long long view, int x, int y, int w, int hgt) {
364
+ (void)h;
365
+ (void)view;
366
+ g_view_bounds_calls++;
367
+ g_view_bounds_last[0] = x;
368
+ g_view_bounds_last[1] = y;
369
+ g_view_bounds_last[2] = w;
370
+ g_view_bounds_last[3] = hgt;
371
+ }
372
+
373
+ void cj_plat_view_navigate(cj_host *h, long long view, const char *url) {
374
+ (void)h;
375
+ (void)view;
376
+ (void)url;
377
+ g_view_navigate_calls++;
378
+ }
379
+
380
+ void cj_plat_view_destroy(cj_host *h, long long view) {
381
+ (void)h;
382
+ (void)view;
383
+ g_view_destroy_calls++;
384
+ }
385
+
386
+ int cj_plat_host_capabilities(cj_host *h) {
387
+ (void)h;
388
+ g_caps_calls++;
389
+ return g_caps_value;
390
+ }
391
+
392
+ /* ---- 串口:桩平台只记录调用,不碰真设备(core 侧的职责是白名单与参数校验)---- */
393
+ static int g_serial_open_calls;
394
+ static char g_serial_last_path[256];
395
+ static cj_serial_cfg g_serial_last_cfg;
396
+ static long long g_serial_open_ret = 42; /* 桩返回的假句柄(>0)*/
397
+ static int g_serial_read_calls;
398
+ static int g_serial_write_calls;
399
+ static int g_serial_close_calls;
400
+ static int g_serial_read_ret = 1; /* 默认「读到 1 字节」*/
401
+ static int g_serial_write_ret = -1; /* 负值 = 按写满 len 处理 */
402
+ static int g_serial_write_last_len;
403
+ static unsigned char g_serial_write_first;
404
+
405
+ long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *err, int err_len) {
406
+ g_serial_open_calls++;
407
+ snprintf(g_serial_last_path, sizeof(g_serial_last_path), "%s", path ? path : "");
408
+ if (cfg) g_serial_last_cfg = *cfg;
409
+ if (g_serial_open_ret <= 0 && err && err_len > 0) {
410
+ snprintf(err, (size_t)err_len, "stub: 平台打开失败");
411
+ }
412
+ return g_serial_open_ret;
413
+ }
414
+
415
+ int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms,
416
+ char *err, int err_len) {
417
+ (void)h;
418
+ (void)timeout_ms;
419
+ (void)err;
420
+ (void)err_len;
421
+ g_serial_read_calls++;
422
+ if (buf && len > 0 && g_serial_read_ret > 0) buf[0] = 'Z';
423
+ return g_serial_read_ret;
424
+ }
425
+
426
+ int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
427
+ char *err, int err_len) {
428
+ (void)h;
429
+ (void)timeout_ms;
430
+ (void)err;
431
+ (void)err_len;
432
+ g_serial_write_calls++;
433
+ g_serial_write_last_len = len;
434
+ g_serial_write_first = (buf && len > 0) ? buf[0] : 0;
435
+ return g_serial_write_ret >= 0 ? g_serial_write_ret : len;
436
+ }
437
+
438
+ int cj_plat_serial_close(long long h, char *err, int err_len) {
439
+ (void)h;
440
+ (void)err;
441
+ (void)err_len;
442
+ g_serial_close_calls++;
443
+ return 0;
444
+ }
445
+
446
+ /* 枚举桩:回 0(没有串口不是错误),只为让 core 的 cj_bridge_serial_list 可链接可调 */
447
+ int cj_plat_serial_list(char *out, int out_len) {
448
+ if (out && out_len > 0) out[0] = '\0';
449
+ return 0;
450
+ }
451
+
452
+ /* ===== 静默子进程桩:桩平台建不了真进程,只为让 core 的**参数校验与转发**可调可断言 =====
453
+ 真进程那条路(Windows 的 DETACHED_PROCESS / Linux 的 posix_spawn)由平台实机验证。 */
454
+ long long cj_plat_spawn_hidden(const char *exe, const char *args, char *err, int err_len) {
455
+ (void)args;
456
+ if (err && err_len > 0) {
457
+ err[0] = '\0';
458
+ }
459
+ /* 只认一个哨兵:命中就给个假 pid,其余回 0 并留一句原因(两条分支 core 都要覆盖) */
460
+ if (exe && strcmp(exe, "stub-ok") == 0) {
461
+ return 4242;
462
+ }
463
+ if (err && err_len > 0) {
464
+ snprintf(err, (size_t)err_len, "stub: 不能建进程");
465
+ }
466
+ return 0;
467
+ }
468
+
469
+ int cj_plat_kill_pid(long long pid) {
470
+ (void)pid;
471
+ return 1; /* 幂等口径:桩上一律算「已结束」 */
472
+ }
473
+
474
+ int cj_plat_pid_alive(long long pid) {
475
+ (void)pid;
476
+ return 0; /* 桩平台没有真进程 */
477
+ }
478
+
479
+ /* ===== 内嵌资源桩:桩平台没有 PE 资源表,如实返回 0 或留一句原因,
480
+ 让 core 的三条分支(参数校验 / 无资源 / 失败)都能在无 SDK 的机器上被断言。 ===== */
481
+ int cj_plat_extract_assets(const char *dir, char *err, int err_len) {
482
+ if (err && err_len > 0) err[0] = '\0';
483
+ if (dir && strcmp(dir, "stub-missing") == 0) {
484
+ if (err && err_len > 0) snprintf(err, (size_t)err_len, "桩平台:没有内嵌资源");
485
+ return -1;
486
+ }
487
+ return 0; /* 普通产物没有内嵌资源——如实回报 */
488
+ }
489
+
490
+ /* ===== 动态库预加载桩:桩平台不去真加载库,只让 core 的「空路径拒绝 / 失败出口」可断言。
491
+ 真加载那条路(Windows 的 LoadLibraryW / Linux 的 dlopen)由平台实机验证。 ===== */
492
+ int cj_plat_preload_library(const char *path, char *err, int err_len) {
493
+ if (err && err_len > 0) err[0] = '\0';
494
+ if (path && strcmp(path, "stub-broken") == 0) {
495
+ if (err && err_len > 0) snprintf(err, (size_t)err_len, "桩平台:加载失败");
496
+ return 0;
497
+ }
498
+ return 1; /* 其余一律算加载成功(core 只关心 0 / 非 0 与日志) */
499
+ }
500
+
501
+ /* ===== 线路解析的回调收集器(菜单是树,桩侧用一张表记录 core 走出来的每一行)===== */
502
+
503
+ typedef struct row_rec {
504
+ int depth;
505
+ char kind;
506
+ char id[64];
507
+ char label[64];
508
+ int flags;
509
+ char accel[64];
510
+ } row_rec;
511
+
512
+ typedef struct row_log {
513
+ row_rec rows[32];
514
+ int n;
515
+ } row_log;
516
+
517
+ static void collect_row(void *ctx, int depth, char kind, const char *id,
518
+ const char *label, int flags, const char *accel) {
519
+ row_log *log = (row_log *)ctx;
520
+ row_rec *r;
521
+ if (!log || log->n >= 32) return;
522
+ r = &log->rows[log->n++];
523
+ r->depth = depth;
524
+ r->kind = kind;
525
+ snprintf(r->id, sizeof(r->id), "%s", id ? id : "");
526
+ snprintf(r->label, sizeof(r->label), "%s", label ? label : "");
527
+ r->flags = flags;
528
+ snprintf(r->accel, sizeof(r->accel), "%s", accel ? accel : "");
529
+ }
530
+
531
+ /* ===== 仓颉侧回调(桩)===== */
532
+
533
+ static int g_msg_calls;
534
+ static int g_destroy_calls;
535
+ static cj_host *g_destroy_cb_host; /* 销毁回调收到的句柄(多窗口下这是唯一能分辨「哪扇窗没了」的凭据)*/
536
+ static int g_dialog_cb_calls;
537
+ static cj_host *g_dialog_cb_host; /* 结果回调收到的句柄:必须等于**发起这次对话框**的那个宿主 */
538
+ static char g_dialog_cb_path[256];
539
+
540
+ static void on_message(const char *json) { (void)json; g_msg_calls++; }
541
+ static void on_destroy(cj_host *host) {
542
+ g_destroy_calls++;
543
+ g_destroy_cb_host = host;
544
+ }
545
+ static void on_dialog(cj_host *host, const char *path) {
546
+ g_dialog_cb_calls++;
547
+ g_dialog_cb_host = host;
548
+ snprintf(g_dialog_cb_path, sizeof(g_dialog_cb_path), "%s", path ? path : "");
549
+ }
550
+
551
+ /* shell 事件回调:**首参必须是宿主句柄**,桩侧把句柄与 JSON 载荷原样记下供断言比对 */
552
+ static int g_shell_cb_calls;
553
+ static cj_host *g_shell_cb_host;
554
+ static char g_shell_cb_json[512];
555
+
556
+ static void on_shell(cj_host *host, const char *json) {
557
+ g_shell_cb_calls++;
558
+ g_shell_cb_host = host;
559
+ snprintf(g_shell_cb_json, sizeof(g_shell_cb_json), "%s", json ? json : "");
560
+ }
561
+
562
+ /* ===== 用例 ===== */
563
+
564
+ int main(void) {
565
+ cj_host *h;
566
+ cj_host *h2;
567
+ int i;
568
+
569
+ fprintf(stderr, "[bridge-core] 自检开始(桩平台,无需 SDK / 图形栈)\n");
570
+
571
+ /* --- 1. 建宿主:默认值与 NULL 安全 --- */
572
+ case_begin("create 默认值与导出对 NULL 安全");
573
+ h = cj_bridge_create();
574
+ CHECK(h != NULL, "create 返回非 NULL");
575
+ if (!h) return 1;
576
+ CHECK(h->win_w == 900 && h->win_h == 640, "默认窗口 900x640");
577
+ CHECK(h->devtools == 1, "默认开 devtools");
578
+ CHECK(h->win_title == NULL, "默认标题为空(由平台兜底)");
579
+ CHECK(h->ready == 0 && h->should_quit == 0 && h->window_gone == 0,
580
+ "初始生命周期标志全 0");
581
+ CHECK(cj_bridge_is_ready(h) == 0 && cj_bridge_should_quit(h) == 0, "访问器初值 0");
582
+ CHECK(cj_bridge_is_ready(NULL) == 0 && cj_bridge_should_quit(NULL) == 0,
583
+ "访问器对 NULL 返回 0");
584
+ cj_bridge_destroy(NULL);
585
+ cj_bridge_quit(NULL);
586
+ cj_bridge_run_js(NULL, "x");
587
+ cj_bridge_set_window(NULL, "t", 1, 1);
588
+ cj_bridge_add_init_script(NULL, "x");
589
+ cj_bridge_show_dialog(NULL, 2, "t", "m", "");
590
+ CHECK(1, "全部导出对 NULL 安全(未崩)");
591
+
592
+ /* --- 2. 窗口配置:标题/图标都是拷贝,非法尺寸不覆盖 --- */
593
+ case_begin("set_window / set_icon / set_devtools");
594
+ cj_bridge_set_window(h, "First", 800, 600);
595
+ CHECK(h->win_w == 800 && h->win_h == 600, "尺寸生效");
596
+
597
+ CHECK(h->win_title && strcmp(h->win_title, "First") == 0, "标题是拷贝");
598
+ cj_bridge_set_window(h, "Second", 0, -1);
599
+ CHECK(h->win_title && strcmp(h->win_title, "Second") == 0, "标题替换(旧值已释放)");
600
+ CHECK(h->win_w == 800 && h->win_h == 600, "宽高 <=0 时保持原值");
601
+ cj_bridge_set_icon(h, "");
602
+ CHECK(h->win_icon == NULL, "空图标路径 = 系统默认");
603
+ cj_bridge_set_icon(h, "/tmp/a.png");
604
+ CHECK(h->win_icon && strcmp(h->win_icon, "/tmp/a.png") == 0, "图标路径是拷贝");
605
+ cj_bridge_set_devtools(h, 0);
606
+ CHECK(h->devtools == 0, "devtools 可关");
607
+ cj_bridge_set_devtools(h, 1);
608
+ CHECK(h->devtools == 1, "devtools 可开");
609
+
610
+ /* --- 2b. 窗口可见性 / 最小化到托盘:两头都能用,但「建窗前」只记不下发 --- */
611
+ case_begin("set_window_visible / set_minimize_to_tray");
612
+ cj_bridge_set_window_visible(h, 0);
613
+ CHECK(h->win_visible == 0, "未就绪:记下待定可见性");
614
+ CHECK(g_win_visible_calls == 0, "未就绪:不下发平台(建窗时按 h->win_visible 定稿,省一次闪烁)");
615
+ h->ready = 1;
616
+ cj_bridge_set_window_visible(h, 1);
617
+ CHECK(h->win_visible == 1 && g_win_visible_calls == 1 && g_win_visible_last == 1,
618
+ "就绪后:投平台执行显示");
619
+ cj_bridge_set_minimize_to_tray(h, 1);
620
+ CHECK(h->minimize_to_tray == 1 && g_min_tray_calls == 1 && g_min_tray_last == 1,
621
+ "最小化到托盘策略转给平台");
622
+ cj_bridge_set_minimize_to_tray(h, 0);
623
+ CHECK(h->minimize_to_tray == 0 && g_min_tray_last == 0, "策略可关(回到普通最小化)");
624
+ h->ready = 0; /* 后面的用例还按「未启动」的前提走 */
625
+
626
+ /* --- 3. 预执行脚本:忽略空串、顺序保持、容量翻倍 --- */
627
+ case_begin("add_init_script 顺序与扩容");
628
+ cj_bridge_add_init_script(h, "");
629
+ cj_bridge_add_init_script(h, NULL);
630
+ CHECK(h->init_script_count == 0, "空串 / NULL 脚本被忽略");
631
+ cj_bridge_add_init_script(h, "s1");
632
+ cj_bridge_add_init_script(h, "s2");
633
+ cj_bridge_add_init_script(h, "s3");
634
+ cj_bridge_add_init_script(h, "s4");
635
+ cj_bridge_add_init_script(h, "s5"); /* 第 5 条触发扩容(4 -> 8)*/
636
+ CHECK(h->init_script_count == 5, "5 条脚本入列");
637
+ CHECK(h->init_script_cap >= 5, "容量已扩容");
638
+ CHECK(h->init_scripts[0] && strcmp(h->init_scripts[0], "s1") == 0 &&
639
+ h->init_scripts[4] && strcmp(h->init_scripts[4], "s5") == 0,
640
+ "顺序保持(注册序 = 注入序)");
641
+
642
+ /* --- 4. 页面来源二选一 + 运行期操作透传平台 --- */
643
+ case_begin("start / load_url / reload / open_devtools");
644
+ cj_bridge_start(h, "<html>hi</html>");
645
+ CHECK(g_start_calls == 1, "start 调到平台");
646
+ CHECK(h->pending_html && strcmp(h->pending_html, "<html>hi</html>") == 0,
647
+ "内联 HTML 记下");
648
+ cj_bridge_start(h, ""); /* 空串 = 页面由 URL 指定,不清掉已记的来源 */
649
+ CHECK(h->pending_html != NULL, "空串 start 不改页面来源");
650
+ cj_bridge_load_url(h, "http://x/");
651
+ CHECK(g_load_url_calls == 1 && h->pending_url &&
652
+ strcmp(h->pending_url, "http://x/") == 0, "URL 记下并投递");
653
+ cj_bridge_load_url(h, "");
654
+ CHECK(g_load_url_calls == 1, "空 URL 忽略");
655
+ cj_bridge_reload(h);
656
+ cj_bridge_open_devtools(h);
657
+ CHECK(g_reload_calls == 1 && g_devtools_calls == 1, "reload / devtools 投递到平台");
658
+
659
+ /* --- 5. 回投:单条快路径 --- */
660
+ case_begin("run_js + flush:单条快路径");
661
+ reset_counters();
662
+ cj_bridge_run_js(h, "console.log(1)");
663
+ CHECK(g_wake_calls == 1, "入队即唤醒 UI");
664
+ CHECK(g_post_js_calls == 0, "入队本身不执行 JS");
665
+ cj_core_flush_js(h);
666
+ CHECK(g_post_js_calls == 1, "单条只 post 一次");
667
+ CHECK(g_last_js && strcmp(g_last_js, "console.log(1)") == 0, "快路径原样透传(不加换行)");
668
+ CHECK(g_wake_calls == 1, "队列已空:不再自续唤醒");
669
+ cj_core_flush_js(h);
670
+ CHECK(g_post_js_calls == 1, "空队列 flush 不产生回投");
671
+
672
+ /* --- 6. 回投:多条拼批 --- */
673
+ case_begin("run_js + flush:多条拼批(顺序 + 换行分隔)");
674
+ reset_counters();
675
+ cj_bridge_run_js(h, "A");
676
+ cj_bridge_run_js(h, "B");
677
+ cj_bridge_run_js(h, "C");
678
+ CHECK(g_wake_calls == 1, "队列由空转非空:只唤醒一次");
679
+ cj_core_flush_js(h);
680
+ CHECK(g_post_js_calls == 1, "三条合成一段脚本:只 post 一次");
681
+ CHECK(g_last_js && strcmp(g_last_js, "A\nB\nC\n") == 0,
682
+ "顺序保持、换行分隔(末条也带换行)");
683
+
684
+ /* --- 7. 回投:批上限 64 + 自续唤醒(长队列肯让出 UI 线程)--- */
685
+ case_begin("flush 批上限 CJ_JS_BATCH_MAX 与自续唤醒");
686
+ reset_counters();
687
+ for (i = 0; i < 70; i++) {
688
+ char buf[32];
689
+ snprintf(buf, sizeof(buf), "x%d", i);
690
+ cj_bridge_run_js(h, buf);
691
+ }
692
+ cj_core_flush_js(h);
693
+ CHECK(g_post_js_calls == 1, "首批只发一段");
694
+ CHECK(g_last_js && count_nl(g_last_js) == 64, "首批恰好 64 条");
695
+ CHECK(g_last_js && strncmp(g_last_js, "x0\n", 3) == 0, "从队首开始");
696
+ CHECK(g_wake_calls >= 1, "队列未取空:自续唤醒");
697
+ cj_core_flush_js(h);
698
+ CHECK(g_post_js_calls == 2 && g_last_js && count_nl(g_last_js) == 6,
699
+ "第二批 6 条(70 - 64)");
700
+ CHECK(count_nl(g_last_js) == 6 && strncmp(g_last_js, "x64\n", 4) == 0, "第二批接着队尾");
701
+
702
+ /* --- 8. quit:置标志 + 走平台同一路径 --- */
703
+ case_begin("quit 置 should_quit 并投递平台");
704
+ reset_counters();
705
+ CHECK(cj_bridge_should_quit(h) == 0, "quit 前 shouldQuit=0");
706
+ cj_bridge_quit(h);
707
+ CHECK(cj_bridge_should_quit(h) == 1, "quit 置 should_quit");
708
+ CHECK(g_quit_calls == 1, "投递到平台(与用户关窗同一条销毁路径)");
709
+
710
+ /* --- 9. 窗口销毁:标志 + onDestroy 恰好一次 --- */
711
+ case_begin("window_destroyed 幂等(onDestroy 恰好一次)");
712
+ reset_counters();
713
+ cj_bridge_init(h, on_message, on_destroy);
714
+ cj_core_window_destroyed(h);
715
+ CHECK(h->window_gone == 1 && h->should_quit == 1, "置 window_gone + should_quit");
716
+ CHECK(g_destroy_calls == 1, "onDestroy 触发一次");
717
+ CHECK(g_destroy_cb_host == h, "销毁回调带回的是这个宿主的句柄");
718
+ CHECK(g_msg_calls == 0, "on_message 不该被碰");
719
+ cj_core_window_destroyed(h);
720
+ CHECK(g_destroy_calls == 1, "重复通知不再触发 onDestroy");
721
+
722
+ /* --- 9b. 句柄身份:销毁另一个宿主时,回调收到的是它自己 --- */
723
+ case_begin("window_destroyed 两宿主不串(回调带回被销毁者的句柄)");
724
+ {
725
+ cj_host *hb = cj_bridge_create();
726
+ int before;
727
+ CHECK(hb != NULL, "另建一个宿主可独立创建");
728
+ if (hb) {
729
+ before = g_destroy_calls;
730
+ cj_bridge_init(hb, on_message, on_destroy);
731
+ cj_core_window_destroyed(hb);
732
+ CHECK(g_destroy_calls == before + 1 && g_destroy_cb_host == hb,
733
+ "回调收到 hb(先 init 的 h 不会被误报)");
734
+ cj_bridge_destroy(hb);
735
+ }
736
+ }
737
+
738
+ /* --- 10. 对话框:宿主未就绪直接拒绝 --- */
739
+ case_begin("dialog:宿主未就绪返回 0");
740
+ reset_counters();
741
+ h2 = cj_bridge_create();
742
+ CHECK(h2 != NULL, "第二个宿主可独立创建(无进程级单例)");
743
+ if (!h2) return 1;
744
+ cj_bridge_set_dialog_callback(h2, on_dialog);
745
+ CHECK(cj_bridge_show_dialog(h2, 2, "t", "m", "") == 0, "未就绪返回 0");
746
+ CHECK(g_run_dialog_calls == 0, "未就绪时平台没被调用");
747
+
748
+ /* --- 11. 对话框:UI 线程快路径(不投递、不阻塞)--- */
749
+ case_begin("dialog:UI 线程快路径与路径回调");
750
+ reset_counters();
751
+ cj_core_set_ready(h2);
752
+ CHECK(cj_bridge_is_ready(h2) == 1, "set_ready 生效");
753
+ CHECK(cj_bridge_show_dialog(h2, 0, "open", "pick", "*.txt") == 1, "文件类返回平台结果 1");
754
+ CHECK(g_run_dialog_calls == 1, "在 UI 线程上直接弹(投递 + 阻塞会自锁)");
755
+ CHECK(g_post_dialog_calls == 0, "快路径不发消息");
756
+ CHECK(g_dialog_cb_calls == 1 && strcmp(g_dialog_cb_path, "/tmp/picked.txt") == 0,
757
+ "选中路径回调送回");
758
+ CHECK(h2->dlg_busy == 0, "槽位交回");
759
+ CHECK(cj_bridge_show_dialog(h2, 2, "info", "m", "") == 1, "第二次调用仍可用");
760
+ CHECK(h2->dlg_busy == 0, "第二次调用后槽位仍空");
761
+
762
+ /* --- 12. 对话框作废:幂等 --- */
763
+ case_begin("dialog_abort 幂等");
764
+ cj_core_dialog_abort(h2);
765
+ cj_core_dialog_abort(h2);
766
+ CHECK(h2->dlg_cur == NULL, "无进行中对话框时 abort 是空操作");
767
+
768
+ /* --- 12b. 句柄身份:结果回调必须落回**发起这次对话框**的宿主 --- */
769
+ case_begin("dialog 结果回调带回发起窗口的句柄(两宿主不串)");
770
+ {
771
+ cj_host *hd = cj_bridge_create();
772
+ int before;
773
+ CHECK(hd != NULL, "另建宿主用于对照");
774
+ if (hd) {
775
+ cj_bridge_set_dialog_callback(hd, on_dialog);
776
+ cj_core_set_ready(hd);
777
+ before = g_dialog_cb_calls;
778
+ CHECK(cj_bridge_show_dialog(h2, 0, "open", "p", "") == 1, "h2 弹框成功");
779
+ CHECK(g_dialog_cb_calls == before + 1 && g_dialog_cb_host == h2,
780
+ "h2 的结果回调收到 h2");
781
+ CHECK(cj_bridge_show_dialog(hd, 0, "open", "p", "") == 1, "hd 弹框成功");
782
+ CHECK(g_dialog_cb_calls == before + 2 && g_dialog_cb_host == hd,
783
+ "hd 的结果回调收到 hd(不是上一条的 h2)");
784
+ cj_bridge_destroy(hd);
785
+ }
786
+ }
787
+
788
+ /* --- 13. 菜单线路解析(两平台翻译层共用这一份)--- */
789
+ case_begin("menu_walk:线路格式解析(层级 / 字段 / flags)");
790
+ {
791
+ static const char *wire =
792
+ "m\t\t视图\t1\t\n"
793
+ "\tc\tview.sidebar\t侧栏\t3\t\n"
794
+ "\tn\tview.zoom\t放大\t1\tCtrl+=\n"
795
+ "s\t\t\t1\t\n"
796
+ "n\tfile.open\t打开\t1\t\n";
797
+ row_log log;
798
+ log.n = 0;
799
+ cj_core_menu_walk(wire, collect_row, &log);
800
+ CHECK(log.n == 5, "5 行全部解析出来");
801
+ CHECK(log.n == 5 && log.rows[0].depth == 0 && log.rows[0].kind == 'm' &&
802
+ log.rows[0].id[0] == '\0' && strcmp(log.rows[0].label, "视图") == 0,
803
+ "子菜单:层级 0、id 为空、label 保留");
804
+ CHECK(log.n == 5 && log.rows[1].depth == 1 && log.rows[1].kind == 'c' &&
805
+ strcmp(log.rows[1].id, "view.sidebar") == 0 && log.rows[1].flags == 3,
806
+ "勾选项:行首制表符 = 层级 1、flags=3(可用 + 勾选)");
807
+ CHECK(log.n == 5 && log.rows[2].kind == 'n' && strcmp(log.rows[2].label, "放大") == 0 &&
808
+ strcmp(log.rows[2].accel, "Ctrl+=") == 0,
809
+ "普通项:accel 原样带出(Windows 侧要并进标签右对齐)");
810
+ CHECK(log.n == 5 && log.rows[3].kind == 's' && log.rows[3].id[0] == '\0' &&
811
+ log.rows[3].label[0] == '\0',
812
+ "分隔线:id / label 为空但仍是完整一行");
813
+ CHECK(log.n == 5 && strcmp(log.rows[4].id, "file.open") == 0 && log.rows[4].flags == 1,
814
+ "顶层普通项");
815
+ }
816
+ {
817
+ /* 容错:缺字段按空串 / 0;kind 空的行整行跳过;末行不带换行也要出 */
818
+ row_log log2;
819
+ log2.n = 0;
820
+ cj_core_menu_walk("n\tonly-id\n\t\t\nn\ttail\t尾\t1", collect_row, &log2);
821
+ CHECK(log2.n == 2, "kind 为空的行被跳过(容错优于崩溃)");
822
+ CHECK(log2.n == 2 && strcmp(log2.rows[0].id, "only-id") == 0 &&
823
+ log2.rows[0].label[0] == '\0' && log2.rows[0].flags == 0 &&
824
+ log2.rows[0].accel[0] == '\0',
825
+ "缺字段按空串 / 0");
826
+ CHECK(log2.n == 2 && strcmp(log2.rows[1].id, "tail") == 0 &&
827
+ log2.rows[1].accel[0] == '\0', "末行不带换行也解析");
828
+ cj_core_menu_walk(NULL, collect_row, &log2);
829
+ cj_core_menu_walk("n\tx\tX\t1\t\n", NULL, NULL);
830
+ CHECK(1, "walk 对 NULL 参数安全(未崩)");
831
+ }
832
+
833
+ /* --- 14. set_menu:core 存下 + 立即透传平台 --- */
834
+ case_begin("set_menu 存下线路文本并透传平台");
835
+ reset_counters();
836
+ cj_bridge_set_menu(h, "n\tfile.open\t打开\t1\t\n");
837
+ CHECK(h->pending_menu_text && strcmp(h->pending_menu_text, "n\tfile.open\t打开\t1\t\n") == 0,
838
+ "线路文本由 core 存下(平台建窗时读它)");
839
+ CHECK(g_set_menu_calls == 1 && g_last_menu_text &&
840
+ strcmp(g_last_menu_text, h->pending_menu_text) == 0, "同时透传平台(未就绪时平台只记下)");
841
+ cj_bridge_set_menu(h, "n\tfile.save\t保存\t0\t\n");
842
+ CHECK(h->pending_menu_text && strcmp(h->pending_menu_text, "n\tfile.save\t保存\t0\t\n") == 0,
843
+ "再次 set:替换并释放旧文本");
844
+ cj_bridge_set_menu(h, "");
845
+ CHECK(h->pending_menu_text && h->pending_menu_text[0] == '\0', "空串 = 清空菜单栏(仍然记下并透传)");
846
+ cj_bridge_set_menu(h, NULL);
847
+ CHECK(h->pending_menu_text && h->pending_menu_text[0] == '\0', "NULL 忽略(不误清)");
848
+ cj_bridge_set_menu(NULL, "x");
849
+ CHECK(1, "set_menu 对 NULL 宿主安全");
850
+
851
+ /* --- 15. titlebar 副标题:core 校验参数、原样透传平台 --- */
852
+ case_begin("titlebar 副标题:校验 + 透传平台");
853
+ reset_counters();
854
+ CHECK(cj_bridge_set_titlebar_subtitle(h, "n.md · 已保存 · 54 字") == 0, "正常文本返回 0");
855
+ CHECK(g_subtitle_calls == 1 && g_last_subtitle_text &&
856
+ strcmp(g_last_subtitle_text, "n.md · 已保存 · 54 字") == 0,
857
+ "原文透传平台(UTF-8 多字节不截断)");
858
+ cj_bridge_set_titlebar_subtitle(h, "");
859
+ CHECK(g_last_subtitle_text && g_last_subtitle_text[0] == '\0', "空串 = 清空副标题(仍透传)");
860
+ {
861
+ /* 超 1024 字节:core 当场拒绝,不透传——平台在 UI 线程里不该再做第二份校验 */
862
+ char big[1100];
863
+ memset(big, 'a', sizeof(big) - 1);
864
+ big[sizeof(big) - 1] = '\0';
865
+ reset_counters();
866
+ CHECK(cj_bridge_set_titlebar_subtitle(h, big) == -1, "超过 1024 字节返回 -1");
867
+ CHECK(g_subtitle_calls == 0, "被拒的请求不透传到平台");
868
+ }
869
+ CHECK(cj_bridge_set_titlebar_subtitle(h, NULL) == -1, "NULL 文本返回 -1");
870
+ CHECK(cj_bridge_set_titlebar_subtitle(NULL, "x") == -1, "NULL 宿主返回 -1");
871
+
872
+ /* --- 16. 菜单点击:shell 回调带回宿主身份 + JSON 载荷 --- */
873
+ case_begin("menu 点击回调带宿主句柄 + JSON 载荷(多窗口不串台)");
874
+ reset_counters();
875
+ g_shell_cb_calls = 0;
876
+ cj_bridge_set_shell_callback(h, on_shell);
877
+ cj_core_menu_clicked(h, "view.sidebar", 1, 1);
878
+ CHECK(g_shell_cb_calls == 1, "点击回调触发一次");
879
+ CHECK(g_shell_cb_host == h, "首参就是该宿主句柄");
880
+ CHECK(strstr(g_shell_cb_json, "\"kind\":\"menu\"") != NULL &&
881
+ strstr(g_shell_cb_json, "\"id\":\"view.sidebar\"") != NULL &&
882
+ strstr(g_shell_cb_json, "\"enabled\":true") != NULL &&
883
+ strstr(g_shell_cb_json, "\"checked\":true") != NULL,
884
+ "载荷是 §5.5 的 JSON(kind / id / enabled / checked)");
885
+ cj_core_menu_clicked(h, "quote\"and\\back", 0, 0);
886
+ CHECK(g_shell_cb_calls == 2 &&
887
+ strstr(g_shell_cb_json, "\"id\":\"quote\\\"and\\\\back\"") != NULL,
888
+ "id 里的 \\ 与 \" 被转义(JSON 不被弄坏)");
889
+ cj_core_menu_clicked(h, "", 1, 0);
890
+ CHECK(g_shell_cb_calls == 2, "空 id 不回调(分隔线 / 子菜单走不到这里)");
891
+ cj_bridge_set_shell_callback(h2, on_shell);
892
+ cj_core_menu_clicked(h2, "panel.reload", 0, 0);
893
+ CHECK(g_shell_cb_calls == 3 && g_shell_cb_host == h2,
894
+ "第二个宿主的点击带回的是它自己的句柄");
895
+ cj_core_menu_clicked(NULL, "x", 1, 1);
896
+ CHECK(1, "clicked 对 NULL 宿主安全");
897
+
898
+ /* --- 16. menu_item_state:透传平台 --- */
899
+ case_begin("menu_item_state 透传平台");
900
+ reset_counters();
901
+ cj_bridge_set_menu_item_state(h, "view.sidebar", 1, 0);
902
+ CHECK(g_menu_state_calls == 1 && strcmp(g_last_menu_state_id, "view.sidebar") == 0 &&
903
+ g_last_menu_state_enabled == 1 && g_last_menu_state_checked == 0,
904
+ "id 与状态透传到平台");
905
+ cj_bridge_set_menu_item_state(h, "", 0, 0);
906
+ CHECK(g_menu_state_calls == 1, "空 id 忽略");
907
+ cj_bridge_set_menu_item_state(NULL, "x", 1, 1);
908
+ CHECK(1, "对 NULL 宿主安全");
909
+
910
+ /* --- 16b. 拖放:策略透传 + drop 报文解析(RFC-002 §5 / §7.B)--- */
911
+ case_begin("set_drag_drop 透传与 pending");
912
+ reset_counters();
913
+ cj_bridge_set_drag_drop(h, 1, 1);
914
+ CHECK(g_drag_calls == 1 && g_drag_last_enabled == 1 && g_drag_last_files == 1,
915
+ "策略透传到平台原语");
916
+ CHECK(h->pending_drop_enabled == 1 && h->pending_drop_files == 1,
917
+ "策略同时记进 pending(建 view 时读)");
918
+ cj_bridge_set_drag_drop(h, 0, 0);
919
+ CHECK(g_drag_calls == 2 && h->pending_drop_enabled == 0 && h->pending_drop_files == 0,
920
+ "关掉也透传(平台摘 drag dest)");
921
+ cj_bridge_set_drag_drop(NULL, 1, 1);
922
+ CHECK(1, "对 NULL 宿主安全");
923
+
924
+ case_begin("cj_core_drop_files 解析与报文");
925
+ reset_counters();
926
+ g_shell_cb_calls = 0; /* shell 回调计数不在 reset_counters 里(menu 用例也是手动清零) */
927
+ cj_core_drop_files(h, "file:///tmp/a.png\r\nfile:///tmp/b%20c.txt\r\n");
928
+ CHECK(g_shell_cb_calls == 1 && g_shell_cb_host == h, "drop 回调一次、带回该宿主句柄");
929
+ CHECK(strstr(g_shell_cb_json, "\"kind\":\"drop\"") != NULL &&
930
+ strstr(g_shell_cb_json, "\"paths\":[\"/tmp/a.png\",\"/tmp/b c.txt\"]") != NULL,
931
+ "file:// 解码成路径(\\r\\n 分隔、%20 还原空格),报文是 {kind,paths} JSON");
932
+ cj_core_drop_files(h, "# comment line\n\nfile:///tmp/only.txt\nhttps://example.com/x");
933
+ CHECK(g_shell_cb_calls == 2 &&
934
+ strstr(g_shell_cb_json, "\"/tmp/only.txt\"") != NULL &&
935
+ strstr(g_shell_cb_json, "\"https://example.com/x\"") != NULL &&
936
+ strstr(g_shell_cb_json, "comment") == NULL,
937
+ "注释/空行跳过,非 file:// URI 原样保留");
938
+ cj_core_drop_files(h, "");
939
+ CHECK(g_shell_cb_calls == 2, "空载荷不回调");
940
+ cj_core_drop_files(h, NULL);
941
+ CHECK(g_shell_cb_calls == 2, "NULL 载荷安全");
942
+ cj_bridge_set_shell_callback(h2, on_shell);
943
+ cj_core_drop_files(h2, "file:///tmp/second.txt");
944
+ CHECK(g_shell_cb_calls == 3 && g_shell_cb_host == h2,
945
+ "第二个宿主的 drop 带回它自己的句柄(句柄路由不串台)");
946
+ cj_core_drop_files(NULL, "file:///tmp/x");
947
+ CHECK(1, "对 NULL 宿主安全");
948
+
949
+ /* --- 16c. 全局快捷键:接线层(wire 记 pending + 透传平台;触发报文走 shell 通道)--- */
950
+ case_begin("set_global_hotkeys 接线(pending + 平台透传)");
951
+ reset_counters();
952
+ g_hotkey_calls = 0;
953
+ g_hotkey_last_wire[0] = '\0';
954
+ cj_bridge_set_global_hotkeys(h, "Ctrl+Alt+C:show-panel\nCtrl+Shift+P:pause");
955
+ CHECK(g_hotkey_calls == 1 && strcmp(g_hotkey_last_wire,
956
+ "Ctrl+Alt+C:show-panel\nCtrl+Shift+P:pause") == 0,
957
+ "wire 透传到平台原语(解析与 grab 在平台层)");
958
+ CHECK(h->pending_hotkeys != NULL &&
959
+ strcmp(h->pending_hotkeys, "Ctrl+Alt+C:show-panel\nCtrl+Shift+P:pause") == 0,
960
+ "wire 同时记进 pending(grab 线程启动时读)");
961
+ cj_bridge_set_global_hotkeys(h, "");
962
+ CHECK(g_hotkey_calls == 2 && h->pending_hotkeys != NULL &&
963
+ strcmp(h->pending_hotkeys, "") == 0,
964
+ "空 wire = 全部注销(整表替换语义)");
965
+ cj_bridge_set_global_hotkeys(NULL, "Ctrl+Alt+C:x");
966
+ CHECK(1, "对 NULL 宿主安全");
967
+
968
+ case_begin("cj_core_hotkey_pressed 触发报文");
969
+ g_shell_cb_calls = 0;
970
+ cj_bridge_set_shell_callback(h, on_shell);
971
+ cj_core_hotkey_pressed(h, "show-panel");
972
+ CHECK(g_shell_cb_calls == 1 && g_shell_cb_host == h,
973
+ "触发回调一次、带回该宿主句柄");
974
+ CHECK(strstr(g_shell_cb_json, "\"kind\":\"hotkey\"") != NULL &&
975
+ strstr(g_shell_cb_json, "\"id\":\"show-panel\"") != NULL,
976
+ "载荷是 {kind,id} JSON(与菜单点击同族,走 on_shell 通道)");
977
+ cj_core_hotkey_pressed(h, "");
978
+ CHECK(g_shell_cb_calls == 1, "空 id 不回调(无法路由,直接丢弃)");
979
+ cj_bridge_set_shell_callback(h2, on_shell);
980
+ cj_core_hotkey_pressed(h2, "second");
981
+ CHECK(g_shell_cb_calls == 2 && g_shell_cb_host == h2,
982
+ "第二个宿主的触发带回它自己的句柄(句柄路由不串台)");
983
+ cj_core_hotkey_pressed(NULL, "x");
984
+ CHECK(1, "对 NULL 宿主安全");
985
+
986
+ /* --- 16d. 托盘:接线层(wire 记 pending + 透传平台;点击报文走 shell 通道)--- */
987
+ case_begin("set_tray 接线(pending + 平台透传)");
988
+ reset_counters();
989
+ g_tray_calls = 0;
990
+ g_tray_last_wire[0] = '\0';
991
+ cj_bridge_set_tray(h, "icon\t/tmp/tray.png\ntooltip\tcj-tauri\nn\ttoggle\t显示/隐藏\t1\t\nn\tquit\t退出\t1\t\n");
992
+ CHECK(g_tray_calls == 1 && strstr(g_tray_last_wire, "icon\t/tmp/tray.png") != NULL &&
993
+ strstr(g_tray_last_wire, "n\ttoggle") != NULL,
994
+ "wire 透传到平台原语(解析与 appindicator 在平台层)");
995
+ CHECK(h->pending_tray != NULL && strstr(h->pending_tray, "tooltip\tcj-tauri") != NULL,
996
+ "wire 同时记进 pending(宿主线程装配时读)");
997
+ cj_bridge_set_tray(h, "");
998
+ CHECK(g_tray_calls == 2 && h->pending_tray != NULL && strcmp(h->pending_tray, "") == 0,
999
+ "空 wire = 移除托盘(pending 同步清空)");
1000
+ cj_bridge_set_tray(NULL, "icon\tx");
1001
+ CHECK(1, "对 NULL 宿主安全");
1002
+
1003
+ case_begin("cj_core_tray_clicked 触发报文");
1004
+ g_shell_cb_calls = 0;
1005
+ cj_bridge_set_shell_callback(h, on_shell);
1006
+ cj_core_tray_clicked(h, "toggle");
1007
+ CHECK(g_shell_cb_calls == 1 && g_shell_cb_host == h,
1008
+ "点击回调一次、带回该宿主句柄");
1009
+ CHECK(strstr(g_shell_cb_json, "\"kind\":\"tray\"") != NULL &&
1010
+ strstr(g_shell_cb_json, "\"id\":\"toggle\"") != NULL,
1011
+ "载荷是 {kind,id} JSON(RFC-002 §5.5 的 tray 信封,走 on_shell 通道)");
1012
+ cj_core_tray_clicked(h, "");
1013
+ CHECK(g_shell_cb_calls == 1, "空 id 不回调(无法路由,直接丢弃)");
1014
+ cj_bridge_set_shell_callback(h2, on_shell);
1015
+ cj_core_tray_clicked(h2, "quit");
1016
+ CHECK(g_shell_cb_calls == 2 && g_shell_cb_host == h2,
1017
+ "第二个宿主的点击带回它自己的句柄(句柄路由不串台)");
1018
+ cj_core_tray_clicked(NULL, "x");
1019
+ CHECK(1, "对 NULL 宿主安全");
1020
+
1021
+ /* --- 17. 能力位:cj_plat_init 时问一次,此后只读 --- */
1022
+ case_begin("host capabilities 透传(system:host 的依据)");
1023
+ {
1024
+ cj_host *h3 = cj_bridge_create();
1025
+ reset_counters();
1026
+ g_caps_value = CJ_CAP_MENU | CJ_CAP_DRAG_DROP;
1027
+ cj_bridge_init(h3, on_message, on_destroy);
1028
+ CHECK(g_caps_calls == 1, "cj_plat_init 时问平台一次");
1029
+ CHECK(h3 && cj_bridge_host_capabilities(h3) == (CJ_CAP_MENU | CJ_CAP_DRAG_DROP) &&
1030
+ h3->capabilities == (CJ_CAP_MENU | CJ_CAP_DRAG_DROP),
1031
+ "能力位由平台给出并被 core 缓存(托盘没编进来 → 不置位)");
1032
+ CHECK(cj_bridge_host_capabilities(NULL) == 0, "NULL 宿主返回 0");
1033
+ CHECK(h->capabilities == CJ_CAP_MENU, "先建的宿主保留自己那次 init 的结果");
1034
+ g_caps_value = CJ_CAP_MENU;
1035
+ cj_bridge_destroy(h3);
1036
+ }
1037
+
1038
+ /* --- 18. 销毁:先走平台 fini,再回收句柄 --- */
1039
+ case_begin("destroy 先走 cj_plat_fini 再回收");
1040
+ reset_counters();
1041
+ cj_bridge_destroy(h2);
1042
+ cj_bridge_destroy(h);
1043
+ CHECK(g_fini_calls == 2, "两个宿主各自走一次平台 fini");
1044
+
1045
+ /* --- 19. 串口:白名单是安全边界,导出只做校验 + 转发 --- */
1046
+ case_begin("serial 白名单与导出转发");
1047
+ CHECK(cj_serial_path_allowed("/dev/ttyUSB0"), "放行 /dev/ttyUSB0");
1048
+ CHECK(cj_serial_path_allowed("/dev/ttyS3"), "放行 /dev/ttyS3");
1049
+ CHECK(cj_serial_path_allowed("/dev/pts/7"), "放行伪终端 /dev/pts/7(socat 造的虚拟串口)");
1050
+ CHECK(cj_serial_path_allowed("/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0"),
1051
+ "放行 udev 稳定链接");
1052
+ CHECK(cj_serial_path_allowed("/dev/cu.usbserial-1420"), "放行 macOS /dev/cu.*");
1053
+ CHECK(cj_serial_path_allowed("COM3"), "放行 Windows COM3");
1054
+ CHECK(cj_serial_path_allowed("\\\\.\\COM12"), "放行 Windows 长名形式");
1055
+ CHECK(!cj_serial_path_allowed("/etc/shadow"), "拒绝 /etc/shadow");
1056
+ CHECK(!cj_serial_path_allowed("/dev/tty"), "拒绝裸 /dev/tty(那是控制终端,不是串口)");
1057
+ CHECK(!cj_serial_path_allowed("/dev/ttyUSB0/../../etc/shadow"), "拒绝借白名单前缀做 .. 穿越");
1058
+ CHECK(!cj_serial_path_allowed("/dev/serial/by-id/../../etc/passwd"), "拒绝符号链接段的 .. 穿越");
1059
+ CHECK(!cj_serial_path_allowed("COMfoo"), "拒绝 COMfoo(COM 前缀后必须是数字)");
1060
+ CHECK(!cj_serial_path_allowed("COM1234"), "拒绝 COM1234(超过 3 位)");
1061
+ CHECK(!cj_serial_path_allowed(""), "拒绝空路径");
1062
+ CHECK(!cj_serial_path_allowed(NULL), "拒绝 NULL 路径");
1063
+
1064
+ g_serial_open_calls = 0;
1065
+ CHECK(cj_bridge_serial_open("/etc/shadow", 115200, 8, 0, 1) == -1,
1066
+ "白名单外的路径被 core 拒绝(-1)");
1067
+ CHECK(g_serial_open_calls == 0, "被拒路径不触达平台");
1068
+ CHECK(cj_bridge_serial_last_error()[0] != '\0', "拒绝时 last_error 有可读原因");
1069
+ CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 9, 0, 1) == -1, "dataBits=9 被拒");
1070
+ CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 0, 8, 0, 1) == -1, "baud=0 被拒");
1071
+ CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 3, 1) == -1, "parity=3 被拒");
1072
+ CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 0, 3) == -1, "stopBits=3 被拒");
1073
+ CHECK(g_serial_open_calls == 0, "参数非法同样不触达平台");
1074
+
1075
+ CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 2, 1) == 42,
1076
+ "合法路径转发平台并原样带回句柄");
1077
+ CHECK(g_serial_open_calls == 1 && strcmp(g_serial_last_path, "/dev/ttyUSB0") == 0,
1078
+ "路径原样传给平台");
1079
+ CHECK(g_serial_last_cfg.baud == 115200 && g_serial_last_cfg.data_bits == 8 &&
1080
+ g_serial_last_cfg.parity == 2 && g_serial_last_cfg.stop_bits == 1,
1081
+ "四项配置(含偶校验)原样传给平台");
1082
+
1083
+ g_serial_open_ret = -3;
1084
+ CHECK(cj_bridge_serial_open("/dev/ttyUSB0", 115200, 8, 0, 1) == -3, "平台失败码原样返回");
1085
+ CHECK(strstr(cj_bridge_serial_last_error(), "stub") != NULL, "平台给的原因进 last_error");
1086
+ g_serial_open_ret = 42;
1087
+
1088
+ {
1089
+ unsigned char rd[4];
1090
+ unsigned char wr[3];
1091
+ int n;
1092
+
1093
+ rd[0] = 0;
1094
+ wr[0] = 'A';
1095
+ wr[1] = 'B';
1096
+ wr[2] = 'C';
1097
+ n = cj_bridge_serial_read(42, rd, (int)sizeof(rd), 0);
1098
+ CHECK(n == 1 && rd[0] == 'Z' && g_serial_read_calls == 1, "read 转发并带回字节");
1099
+ n = cj_bridge_serial_write(42, wr, (int)sizeof(wr), 0);
1100
+ CHECK(n == 3 && g_serial_write_calls == 1 && g_serial_write_last_len == 3 &&
1101
+ g_serial_write_first == 'A', "write 转发(平台拿到原缓冲)");
1102
+ CHECK(cj_bridge_serial_close(42) == 0 && g_serial_close_calls == 1, "close 转发");
1103
+
1104
+ CHECK(cj_bridge_serial_read(0, rd, 4, 0) == -1, "handle<=0 的 read 被拒");
1105
+ CHECK(cj_bridge_serial_read(42, NULL, 4, 0) == -1, "空缓冲的 read 被拒");
1106
+ CHECK(cj_bridge_serial_write(42, wr, 0, 0) == -1, "len=0 的 write 被拒");
1107
+ CHECK(cj_bridge_serial_close(0) == -1, "handle<=0 的 close 被拒");
1108
+ CHECK(g_serial_read_calls == 1 && g_serial_write_calls == 1 && g_serial_close_calls == 1,
1109
+ "参数非法的调用没有触达平台");
1110
+ CHECK(1, "串口导出不需要宿主(本用例跑在两个宿主都已销毁之后)");
1111
+ }
1112
+
1113
+ /* --- 20. 剪贴板:参数校验在 core,平台失败码原样返回 --- */
1114
+ case_begin("clipboard 校验与转发");
1115
+ CHECK(cj_bridge_clipboard_write_text(NULL, "x") == -1, "NULL 文本被拒(-1)");
1116
+ {
1117
+ char *big = (char *)malloc((8u << 20) + 1);
1118
+ cj_host *hc = cj_bridge_create(); /* 剪贴板是宿主能力:要一个活句柄(不 start,window_gone=0)*/
1119
+ if (big && hc) {
1120
+ memset(big, 'a', (8u << 20));
1121
+ big[8u << 20] = '\0';
1122
+ CHECK(cj_bridge_clipboard_write_text(hc, big) == -1, "超 8 MiB 被拒");
1123
+ free(big);
1124
+ }
1125
+ if (hc) {
1126
+ g_clip_write_calls = 0;
1127
+ g_clip_read_calls = 0;
1128
+ free(g_clip_last_written);
1129
+ g_clip_last_written = NULL;
1130
+ CHECK(cj_bridge_clipboard_write_text(hc, "hello clip") == 0, "写文本成功");
1131
+ CHECK(g_clip_write_calls == 1 && g_clip_last_written &&
1132
+ strcmp(g_clip_last_written, "hello clip") == 0, "文本原样到达平台");
1133
+ {
1134
+ char *got = cj_bridge_clipboard_read_text(hc);
1135
+ CHECK(got && strcmp(got, "hello clip") == 0, "读回刚写的文本");
1136
+ cj_bridge_clipboard_free(got);
1137
+ CHECK(g_clip_read_calls == 1, "读触达平台一次");
1138
+ }
1139
+ cj_bridge_clipboard_free(NULL);
1140
+ CHECK(1, "free(NULL) 幂等安全");
1141
+ g_clip_write_ok = 0;
1142
+ CHECK(cj_bridge_clipboard_write_text(hc, "x") == -3, "平台失败码返回 -3");
1143
+ g_clip_write_ok = 1;
1144
+ /* 通知:参数校验 + 发后即忘(投递即返回 0,平台成败看 stderr)*/
1145
+ g_note_calls = 0;
1146
+ hc->capabilities |= CJ_CAP_NOTIFICATION; /* 通知是宿主能力:声明了才走到平台 */
1147
+ CHECK(cj_bridge_notification_notify(hc, "", "b") == -1, "空标题被拒");
1148
+ CHECK(cj_bridge_notification_notify(hc, NULL, "b") == -1, "NULL 标题被拒");
1149
+ {
1150
+ char long_title[258];
1151
+ memset(long_title, 't', sizeof(long_title));
1152
+ long_title[sizeof(long_title) - 1] = '\0';
1153
+ CHECK(cj_bridge_notification_notify(hc, long_title, "b") == -1, "超长标题被拒");
1154
+ }
1155
+ CHECK(g_note_calls == 0, "被拒的通知不触达平台");
1156
+ CHECK(cj_bridge_notification_notify(hc, "hello", "body") == 0, "合法通知提交成功(发后即忘)");
1157
+ CHECK(g_note_calls == 1 && g_note_last_title && strcmp(g_note_last_title, "hello") == 0,
1158
+ "标题原样到达平台");
1159
+ g_note_ok = 0;
1160
+ CHECK(cj_bridge_notification_notify(hc, "x", NULL) == -3, "UI 直行路径平台失败如实回 -3");
1161
+ g_note_ok = 1;
1162
+ /* 未声明通知能力的平台(如 Windows 桩)要当场拒绝,不能走「发后即忘」被读成成功 */
1163
+ hc->capabilities &= ~CJ_CAP_NOTIFICATION;
1164
+ g_note_calls = 0;
1165
+ CHECK(cj_bridge_notification_notify(hc, "x", "b") == -4, "未声明能力位如实回 -4");
1166
+ CHECK(g_note_calls == 0, "未声明能力位不触达平台");
1167
+ cj_bridge_destroy(hc);
1168
+ }
1169
+ }
1170
+
1171
+ /* --- 日志落盘(RFC-003 第 5 期):接管 stderr、按大小轮转、失败如实回 0 --- */
1172
+ case_begin("log file:接管 stderr / 按大小轮转 / 失败如实回 0");
1173
+ {
1174
+ char dir[256];
1175
+ char path[300];
1176
+ char archive1[320];
1177
+ char archive2[320];
1178
+ char archive3[320];
1179
+ char blocker[320];
1180
+ char unwritable[340];
1181
+ char long_path[1200];
1182
+ char buf[65536];
1183
+ FILE *bf;
1184
+ int saved_fd;
1185
+ int i;
1186
+
1187
+ snprintf(dir, sizeof(dir), "/tmp/cj-bridge-logtest-%ld", (long)getpid());
1188
+ cj_test_mkdir(dir);
1189
+ /* 路径故意多两级不存在的目录:默认日志目录(~/.local/state/cj-tauri)常常还不存在,
1190
+ 走到 ③ 顺带证明「父目录按需建出来」 */
1191
+ snprintf(path, sizeof(path), "%s/nested/deep/app.log", dir);
1192
+ snprintf(archive1, sizeof(archive1), "%s.1", path);
1193
+ snprintf(archive2, sizeof(archive2), "%s.2", path);
1194
+ snprintf(archive3, sizeof(archive3), "%s.3", path);
1195
+
1196
+ /* ① 未开启(对照组):回读空串、不创建文件,c j_logf 仍写 stderr */
1197
+ CHECK(strcmp(cj_bridge_log_file(), "") == 0, "未开启时回读空串");
1198
+ cj_logf("[bridge-core] probe before enable\n");
1199
+ CHECK(cj_test_file_exists(path) == 0, "未开启时不创建日志文件");
1200
+
1201
+ /* ② 参数与失败路径都回 0(stderr 完好,原因打在 stderr 上) */
1202
+ CHECK(cj_bridge_set_log_file(NULL, 0, 0) == 0, "NULL 路径被拒");
1203
+ CHECK(cj_bridge_set_log_file("", 0, 0) == 0, "空路径被拒");
1204
+ memset(long_path, 'a', sizeof(long_path) - 1);
1205
+ long_path[sizeof(long_path) - 1] = '\0';
1206
+ CHECK(cj_bridge_set_log_file(long_path, 0, 0) == 0, "超长路径被拒");
1207
+ /* 「不可写」不能拿一个不存在的目录测:父目录缺失会被如实建出来(默认目录要的就是这行为),
1208
+ 所以拿一个**普通文件**当路径的中间段——父级不是目录,建也建不了、开也开不了 */
1209
+ snprintf(blocker, sizeof(blocker), "%s/blocker", dir);
1210
+ bf = fopen(blocker, "wb");
1211
+ if (bf) fclose(bf);
1212
+ snprintf(unwritable, sizeof(unwritable), "%s/app.log", blocker);
1213
+ CHECK(cj_bridge_set_log_file(unwritable, 0, 0) == 0, "不可写路径被拒(父级是普通文件)");
1214
+ CHECK(cj_test_file_exists(path) == 0, "被拒的开启不产生文件");
1215
+ CHECK(strcmp(cj_bridge_log_file(), "") == 0, "被拒之后仍未激活");
1216
+
1217
+ /* ③ 留底原 stderr fd(必须在接管之前取),然后开启:max 1KB、保留 2 份 */
1218
+ saved_fd = dup(2);
1219
+ LOG_CHECK(saved_fd >= 0, "留底原 stderr fd 成功");
1220
+ LOG_CHECK(cj_bridge_set_log_file(path, 1, 2) == 1, "开启成功回 1(父目录缺失也按需建出来)");
1221
+ LOG_CHECK(strcmp(cj_bridge_log_file(), path) == 0, "回读路径与传入一致");
1222
+ LOG_CHECK(cj_bridge_set_log_file(path, 1, 2) == 0, "重复开启被拒(换了目标只能重启)");
1223
+
1224
+ /* ④ 开启后的诊断确实落在文件里 */
1225
+ cj_logf("[bridge-core] after enable marker=alpha\n");
1226
+ CHECK(1, "接管期间断言走 LOG_CHECK(失败行不会丢进日志文件)");
1227
+ LOG_CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0, "日志文件有内容");
1228
+ LOG_CHECK(strstr(buf, "[cj-bridge] log file: ") != NULL, "首行记录了日志路径与预算");
1229
+ LOG_CHECK(strstr(buf, "marker=alpha") != NULL, "开启后的 cj_logf 落到文件");
1230
+ LOG_CHECK(strstr(buf, "[2") != NULL, "落盘行带时间戳前缀([YYYY-…)");
1231
+ LOG_CHECK(strstr(buf, "probe before enable") == NULL, "开启之前的输出不会被追认进文件");
1232
+
1233
+ /* ⑤ 前端/插件通道:写一行 + NULL 拒绝 */
1234
+ LOG_CHECK(cj_bridge_log_write(NULL) == -1, "log_write(NULL) 被拒");
1235
+ LOG_CHECK(cj_bridge_log_write("[frontend] marker=beta") == 0, "log_write 回 0");
1236
+ LOG_CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0 && strstr(buf, "marker=beta") != NULL,
1237
+ "log_write 的行也落在同一份文件");
1238
+
1239
+ /* ⑥ 轮转:1KB 预算下连写,档案要出现、keep 上限要守住 */
1240
+ for (i = 0; i < 60; i++) cj_logf("[bridge-core] filler line %02d padding ................\n", i);
1241
+ LOG_CHECK(cj_test_file_exists(archive1) != 0, "超预算后出现 log.1");
1242
+ LOG_CHECK(cj_test_read_all(archive1, buf, sizeof(buf)) > 0, "log.1 有内容");
1243
+ LOG_CHECK(cj_test_file_exists(archive2) != 0, "继续写出现 log.2");
1244
+ LOG_CHECK(cj_test_file_exists(archive3) == 0, "keep=2 时不出现 log.3(上限守住)");
1245
+ LOG_CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0 &&
1246
+ strstr(buf, "log rotated") != NULL, "轮转后当前文件仍继续写(有 rotated 行)");
1247
+
1248
+ /* ⑦ 还原 stderr:后续断言与收尾输出照常可见(顺带证明接管可逆) */
1249
+ fflush(stderr);
1250
+ dup2(saved_fd, 2);
1251
+ close(saved_fd);
1252
+ fprintf(stderr, " - [bridge-core] stderr 已还原(日志用例期间的失败行如下,无输出即全过)\n");
1253
+ if (g_log_fail_buf[0]) fprintf(stderr, "%s", g_log_fail_buf);
1254
+ CHECK(cj_test_read_all(path, buf, sizeof(buf)) > 0, "还原后仍能读日志文件");
1255
+ }
1256
+
1257
+ /* ===== 静默子进程:core 侧的校验与转发(真进程由平台原语负责,实机验证) ===== */
1258
+ {
1259
+ case_begin("spawn hidden");
1260
+ CHECK(cj_bridge_spawn_hidden(NULL, "") == 0, "spawn_hidden(NULL) 被拒");
1261
+ CHECK(cj_bridge_spawn_hidden("", "") == 0, "spawn_hidden 空路径被拒");
1262
+ CHECK(cj_bridge_spawn_hidden("stub-ok", "a\x1f" "b") == 4242,
1263
+ "参数串按 0x1F 分隔转发给平台,pid 原样回给上层");
1264
+ CHECK(cj_bridge_spawn_hidden("stub-no", "") == 0, "平台失败时 core 回 0(绝不谎报 pid)");
1265
+ CHECK(cj_bridge_kill_pid(0) == 0, "kill_pid(0) 被拒");
1266
+ CHECK(cj_bridge_kill_pid(-5) == 0, "kill_pid(负数) 被拒");
1267
+ CHECK(cj_bridge_pid_alive(0) == 0, "pid_alive(0) 恒为 0(不落到平台原语)");
1268
+ }
1269
+
1270
+ /* ===== 内嵌资源释放:单文件包随包资源(core 负责校验与错误出口) ===== */
1271
+ {
1272
+ case_begin("extract assets");
1273
+ CHECK(cj_bridge_extract_assets(NULL) == -1, "extract_assets(NULL) 被拒");
1274
+ CHECK(cj_bridge_extract_assets("") == -1, "extract_assets 空目录被拒");
1275
+ CHECK(cj_bridge_extract_assets("some/runtime/dir") == 0,
1276
+ "没有内嵌资源的产物如实回 0(普通构建就是这样,不是错误)");
1277
+ CHECK(cj_bridge_extract_assets("stub-missing") == -1,
1278
+ "平台报错时 core 回 -1(绝不把失败说成 0 个文件)");
1279
+ }
1280
+
1281
+ /* ===== 同窗内嵌视图(PLT-006):core 只做校验,句柄原样透传 ===== */
1282
+ {
1283
+ long long v;
1284
+ int before = g_view_create_calls;
1285
+
1286
+ case_begin("embedded view");
1287
+ CHECK(cj_bridge_view_create(NULL, "https://a", 0, 0, 100, 100) == 0,
1288
+ "宿主为 NULL 被拒");
1289
+ CHECK(cj_bridge_view_create(h, "", 0, 0, 100, 100) == 0, "空 url 被拒");
1290
+ CHECK(cj_bridge_view_create(h, "https://a", 0, 0, 0, 100) == 0, "宽 0 被拒");
1291
+ CHECK(cj_bridge_view_create(h, "https://a", 0, 0, 100, -3) == 0, "高负数被拒");
1292
+ CHECK(g_view_create_calls == before, "被拒的请求一个都没落到平台");
1293
+
1294
+ v = cj_bridge_view_create(h, "https://example.com/x", 12, 34, 800, 560);
1295
+ CHECK(v > 0, "合法请求拿到平台句柄");
1296
+ CHECK(g_view_bounds_last[0] == 12 && g_view_bounds_last[1] == 34 &&
1297
+ g_view_bounds_last[2] == 800 && g_view_bounds_last[3] == 560,
1298
+ "矩形按客户区坐标原样转发");
1299
+
1300
+ cj_bridge_view_set_visible(h, v, 1);
1301
+ CHECK(g_view_visible_last == 1, "显示转发到平台");
1302
+ cj_bridge_view_set_visible(h, v, 0);
1303
+ CHECK(g_view_visible_last == 0, "隐藏转发到平台");
1304
+
1305
+ g_view_bounds_calls = 0;
1306
+ cj_bridge_view_set_bounds(h, v, 0, 40, 640, 480);
1307
+ CHECK(g_view_bounds_calls == 1 && g_view_bounds_last[3] == 480, "重排矩形转发");
1308
+ cj_bridge_view_set_bounds(h, v, 0, 40, 0, 480);
1309
+ CHECK(g_view_bounds_calls == 1, "非法矩形不透传(宽 <=0)");
1310
+
1311
+ cj_bridge_view_navigate(h, v, "https://b");
1312
+ CHECK(g_view_navigate_calls == 1, "导航转发");
1313
+ cj_bridge_view_navigate(h, v, "");
1314
+ CHECK(g_view_navigate_calls == 1, "空 url 导航被拒");
1315
+
1316
+ cj_bridge_view_destroy(h, v);
1317
+ CHECK(g_view_destroy_calls == 1, "销毁转发");
1318
+ cj_bridge_view_destroy(h, 0);
1319
+ cj_bridge_view_set_visible(h, -2, 1);
1320
+ cj_bridge_view_set_bounds(NULL, v, 0, 0, 640, 480);
1321
+ cj_bridge_view_set_bounds(h, 0, 0, 0, 1, 1);
1322
+ cj_bridge_view_navigate(h, -1, "x");
1323
+ cj_bridge_view_set_visible(NULL, v, 1);
1324
+ CHECK(g_view_destroy_calls == 1 && g_view_visible_calls == 2 &&
1325
+ g_view_bounds_calls == 1,
1326
+ "非正句柄与空宿主一律不落平台(未崩)");
1327
+ }
1328
+
1329
+ fprintf(stderr, "[bridge-core] 断言 %d 项:通过 %d,失败 %d\n", g_pass + g_fail,
1330
+ g_pass, g_fail);
1331
+ free(g_last_js);
1332
+ free(g_last_menu_text);
1333
+ if (g_fail) {
1334
+ fprintf(stderr, "[FAIL] bridge_core 自检未通过\n");
1335
+ return 1;
1336
+ }
1337
+ fprintf(stderr, "[OK] bridge_core 自检全过\n");
1338
+ return 0;
1339
+ }