@mrrisega/dsh-remote 0.7.0-beta.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/clients/dsh-remote/dsh-bridge.mjs +17 -17
- package/clients/dsh-remote/dsh-events.mjs +6 -6
- package/clients/dsh-remote/e2ee-client.mjs +7 -7
- package/clients/dsh-remote/e2ee-shim-script.js +9 -9
- package/clients/dsh-remote/mobile-adapter.mjs +21 -21
- package/clients/dsh-remote/test/bridge-gzip.test.mjs +1 -1
- package/clients/dsh-remote/test/device-registration-report.test.mjs +1 -1
- package/clients/dsh-remote/test/dsh-events.test.mjs +10 -10
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +4 -4
- package/clients/dsh-remote/test/e2ee-client.test.mjs +7 -7
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +1 -1
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +1 -1
- package/clients/dsh-remote/test/harness-cookie-authority.test.mjs +4 -4
- package/clients/dsh-remote/test/machine-fingerprint.test.mjs +2 -2
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +6 -6
- package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +1 -1
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +5 -5
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +1 -1
- package/clients/dsh-remote/test/wechat-channel.test.mjs +5 -5
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +26 -26
- package/clients/dsh-remote/test/wechat-tier-source.test.mjs +2 -2
- package/clients/dsh-remote/wechat-channel.mjs +9 -9
- package/clients/dsh-remote/wechat-runtime.mjs +38 -38
- package/docs/telemetry.md +3 -3
- package/dsh-setup.mjs +27 -27
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +56 -56
- package/packages/dsh-remote-web/lib/fix-prompt.js +1 -1
- package/packages/dsh-remote-web/lib/index.js +43 -43
- package/packages/dsh-remote-web/lib/tarball-update.js +1 -1
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +17 -17
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +6 -6
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +7 -7
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +9 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +21 -21
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +9 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +38 -38
- package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +5 -5
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +27 -27
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +2 -2
- package/packages/dsh-remote-web/test/activation-single-point.test.mjs +1 -1
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +1 -1
- package/packages/dsh-remote-web/test/connect-loop.test.mjs +1 -1
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +2 -2
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -4
- package/packages/dsh-remote-web/test/diag-report.test.mjs +1 -1
- package/packages/dsh-remote-web/test/fix-prompt.test.mjs +2 -2
- package/packages/dsh-remote-web/test/harness-restart.test.mjs +1 -1
- package/packages/dsh-remote-web/test/install-profile-targets.test.mjs +3 -3
- package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
- package/packages/dsh-remote-web/test/launchd-domain.test.mjs +2 -2
- package/packages/dsh-remote-web/test/patch-activation.test.mjs +2 -2
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +1 -1
- package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
- package/packages/dsh-remote-web/test/quota-absent.test.mjs +4 -4
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -2
- package/packages/dsh-remote-web/test/settings-entry-placement.test.mjs +6 -6
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -2
- package/packages/dsh-remote-web/test/settings-open-path.test.mjs +4 -4
- package/packages/dsh-remote-web/test/setup-output.test.mjs +11 -11
- package/packages/dsh-remote-web/test/tarball-update.test.mjs +1 -1
- package/packages/dsh-remote-web/test/telemetry.test.mjs +4 -4
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +2 -2
- package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +1 -1
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +7 -7
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +5 -5
- package/scripts/test-net-guard.cjs +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 📣 公告(插件端)回归:徽标 / 面板入口 / 电脑端弹窗 / 已读上报 / 一条铁律(**不轮询**)。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 契约:服务端公告契约文档(冻结 v1)(冻结 v1)+ 926 迭代 §1(client=desktop)。
|
|
4
4
|
// GET /dsh-remote/announcements?client=desktop → { bubble_enabled, items[], unread_count, popup|null }
|
|
5
5
|
// POST /dsh-remote/announcements/click { id } → 记 clicked_at(**现契约只有 /click,没有 /read**)
|
|
6
6
|
//
|
|
@@ -506,7 +506,7 @@ test("注册来源透传:/dsh-remote/register 全量转发 body(reg_source=p
|
|
|
506
506
|
assert.equal(r.ok, true);
|
|
507
507
|
const reg = relay.seen.filter((s) => s.path === "/api/register").pop();
|
|
508
508
|
assert.ok(reg, "应转发到企业端 /api/register");
|
|
509
|
-
assert.equal(reg.body.reg_source, "panel_register", "
|
|
509
|
+
assert.equal(reg.body.reg_source, "panel_register", "注册来源必须透传(增长约定)");
|
|
510
510
|
assert.equal(reg.body.invite_code, "A8K2M4XQ", "未知/扩展字段不得被丢弃");
|
|
511
511
|
assert.equal(reg.body.captcha_id, "cap-1");
|
|
512
512
|
assert.equal(reg.body.captcha_answer, "1234");
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// 已授权设备列表(全程不刷新页面);
|
|
9
9
|
// ③ error 阶段必须有可操作项:重试 / 复制诊断信息 / 日志路径(不留死胡同);
|
|
10
10
|
// ④ document.hidden 时短轮询暂停(后台标签页不空转),回前台立即补一次;online 后退避到 15s;
|
|
11
|
-
// ⑤ 面板注册带注册来源 reg_source=panel_register
|
|
11
|
+
// ⑤ 面板注册带注册来源 reg_source=panel_register(增长约定)。
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
14
|
import test from "node:test";
|
|
@@ -377,7 +377,7 @@ test("轮询策略:document.hidden 时暂停短轮询,回前台立即补一
|
|
|
377
377
|
assert.ok(plugin.counts().connCalls > onlineCalls + extra, "online 后仍应按 ~15s 节奏保活(新设备扫码后列表能自动出现)");
|
|
378
378
|
});
|
|
379
379
|
|
|
380
|
-
test("面板注册带注册来源 reg_source=panel_register
|
|
380
|
+
test("面板注册带注册来源 reg_source=panel_register(增长约定:电脑端 vs 手机端网页注册)", async () => {
|
|
381
381
|
const plugin = loadPlugin({ status: NOT_LOGGED });
|
|
382
382
|
let tree = await plugin.settle();
|
|
383
383
|
const regTab = nodeWithText(tree, "注册");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 背景:DeepSeek Harness 于 2026-09-29 发布桌面端 v0.2 预览(Electron 薄壳 + 完整 dsh Web Host,
|
|
5
5
|
* Host 以 `--no-open --port 0` 启动,端口随机、每进程令牌鉴权)。dsh-remote 的两大难点
|
|
6
|
-
* (随机端口、会话 Cookie
|
|
6
|
+
* (随机端口、会话 Cookie)此前已因真实故障适配过,因此这次新增的适配**集中在三处**:
|
|
7
7
|
*
|
|
8
8
|
* ① **Electron 宿主下 process.execPath 不是 node**:它是 Electron 应用二进制。
|
|
9
9
|
* 直接 `spawn(process.execPath, [script])` 得到的不是"跑脚本的 node",而是**又开一个桌面端应用**
|
|
@@ -92,7 +92,7 @@ test("spawnEnv:NODE_INTERPRETER 变量不得挤掉调用方的业务变量,P
|
|
|
92
92
|
// ─────────────── ② 服务定义:plist / systemd 必须带上解释器变量(跨文件同构)───────────────
|
|
93
93
|
|
|
94
94
|
test("跨文件不变量:插件与安装器的 plist / systemd 模板都必须带解释器变量占位", () => {
|
|
95
|
-
// 为什么是"两份都要查"
|
|
95
|
+
// 为什么是"两份都要查":历史上前述两份模板漂移过、并因此各造成一个真机故障
|
|
96
96
|
// (插件每次自愈都会重写 plist,谁写谁说了算 → 漂移等于把另一份正确的覆盖掉)。
|
|
97
97
|
for (const [name, src] of [["lib/index.js", INDEX_SRC], ["dsh-setup.mjs", SETUP_SRC]]) {
|
|
98
98
|
assert.match(src, /const SERVICE_NODE_ENV\b/, `${name}: 必须有 SERVICE_NODE_ENV`);
|
|
@@ -214,7 +214,7 @@ test("安装器:setup() 必须把 --profile 透传给插件挂载步骤", () =
|
|
|
214
214
|
const { profileArgvOf } = makeResolveProfileDir("/Users/tester");
|
|
215
215
|
assert.deepEqual(profileArgvOf(["--profile", "desktop"]), ["--profile", "desktop"]);
|
|
216
216
|
assert.deepEqual(profileArgvOf([]), [], "未指定时不得凭空塞参数");
|
|
217
|
-
//
|
|
217
|
+
// 故障面:setup() 原本调 pluginCmd([]) → --profile desktop 被静默忽略、插件装进 web profile
|
|
218
218
|
assert.match(SETUP_SRC, /pluginCmd\(profileArgvOf\(argv\)\)/, "setup 必须透传 profile 选择");
|
|
219
219
|
assert.ok(!/pluginCmd\(\[\]\)/.test(SETUP_SRC), "不得再有硬编码空参数(会把 --profile 吞掉)");
|
|
220
220
|
});
|
|
@@ -227,7 +227,7 @@ test("安装器:desktop profile 未初始化时给桌面端专属引导(不
|
|
|
227
227
|
assert.match(fn, /isDesktopProfileDir\(profileDir\)/, "必须区分桌面端 profile");
|
|
228
228
|
assert.match(fn, /启动一次 DeepSeek Harness 应用/, "必须给桌面端专属引导:profile 由应用自己创建");
|
|
229
229
|
assert.match(fn, /完全退出应用/, "必须提醒完全退出(应用独占 profile,运行中安装不会被装载)");
|
|
230
|
-
//
|
|
230
|
+
// 故障面:桌面端 profile 由 Electron 应用初始化,npm 版 dsh 明确拒绝管理它
|
|
231
231
|
//(apps/cli 的 rejectElectronProfile)—— 照抄 Web 版那句会让用户白折腾。
|
|
232
232
|
assert.ok(!/未找到 dsh web profile/.test(fn), "不得再把桌面端统一报成 'dsh web profile'");
|
|
233
233
|
});
|
|
@@ -219,7 +219,7 @@ test("修复提示词:带当前故障上下文(目录/上游/版本),便
|
|
|
219
219
|
const b = await getJson(`${base}/dsh-remote/diag/fix-prompt`);
|
|
220
220
|
assert.equal(b.ok, true);
|
|
221
221
|
assert.match(b.prompt, new RegExp(env.relayDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), "提示词要带上本机配置目录");
|
|
222
|
-
assert.match(b.prompt, /127\.0\.0\.1:3080/, "
|
|
222
|
+
assert.match(b.prompt, /127\.0\.0\.1:3080/, "提示词要带上上游地址(端口错配是本次故障的最大嫌疑)");
|
|
223
223
|
assert.match(b.prompt, /\.dsh-bridge\.log/);
|
|
224
224
|
assert.match(b.prompt, /\.dsh-setup-install\.log/);
|
|
225
225
|
assert.match(b.reportTitle, /dsh-remote 诊断结论/, "报告格式要有固定首行,便于后台机器解析");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 「连接卡住 → 一键修复提示词 + 结论报告」契约测试(node:test,零框架)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 背景(真实故障,2026-09-23,Windows 用户):
|
|
5
5
|
* 插件面板卡在「连接阶段: starting(正在启动 Bridge…)」,手机端看不到任何设备,反复修都没用。
|
|
6
6
|
* 事后定位到三种机制(半装运行环境 / 守护活着没起作用 / bridge 起来就崩),它们在面板上长得一模一样,
|
|
7
7
|
* 面板侧诊断分不开;只能由用户本机的 agent 在自己电脑上取证。
|
|
@@ -120,7 +120,7 @@ test("报告格式:REPORT_KEYS 六个字段齐全,REPORT_FORMAT 首行即固
|
|
|
120
120
|
for (const key of ["症状", "机制判定", "关键证据", "已做的修复", "修复结果", "仍需人工处理"]) {
|
|
121
121
|
assert.ok(REPORT_KEYS.includes(key), `REPORT_KEYS 应覆盖「${key}」`);
|
|
122
122
|
}
|
|
123
|
-
assert.equal(REPORT_KEYS.length, 6, "REPORT_KEYS
|
|
123
|
+
assert.equal(REPORT_KEYS.length, 6, "REPORT_KEYS 就是这六项(多了会让解析约定漂移)");
|
|
124
124
|
assert.equal(REPORT_FORMAT.split("\n")[0], REPORT_TITLE, "报告模板首行必须是固定标题");
|
|
125
125
|
assert.equal(REPORT_TITLE, "=== dsh-remote 诊断结论 ===");
|
|
126
126
|
// 六项在模板里都要有对应行,且顺序与 REPORT_KEYS 一致
|
|
@@ -141,7 +141,7 @@ test("首次安装(运行环境在本进程内补齐)→ **不再**提示重
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
test("补装把**同版本**插件重写一遍 → 不得提示(用户实测的误报)", async () => {
|
|
144
|
-
//
|
|
144
|
+
// 故障回顾:安装器/自愈补装会把 profile 副本整目录重写,即使是**同一个版本**。
|
|
145
145
|
// 旧实现只看"插件文件 mtime 晚于进程启动"→ 误判成"有新插件要装载"→ 弹出需要重启/刷新的横幅,
|
|
146
146
|
// 而用户刷新后横幅还在(因为第二次补装又写了一遍)、最后靠重启才消失。
|
|
147
147
|
// 现在判的是「磁盘版本 != 运行版本」,版本相同就什么都不提示。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 「默认安装装给谁」契约测试(2026-10-11
|
|
2
|
+
* 「默认安装装给谁」契约测试(2026-10-11 用户约定)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 故障/反馈:用户执行官方那条 `npx --yes @mrrisega/dsh-remote@beta`(不带参数)后,
|
|
5
5
|
* 插件**全进了 Web 版 profile**,而他面对的是**桌面应用** —— 面板永远不出现,
|
|
6
6
|
* 且面板/文档里都看不出来"装错了地方"。根因:`resolveProfileDir()` 在没有 `--profile`
|
|
7
7
|
* 与 `DSH_PROFILE_DIR` 时**写死 `profileDirOfName("web")`**。
|
|
@@ -60,7 +60,7 @@ test("默认(不带 --profile):桌面端与 Web 版**都装**,实验 pro
|
|
|
60
60
|
const headless = makeProfile(home, "headless");
|
|
61
61
|
const r = runPlugin(home);
|
|
62
62
|
assert.equal(r.code, 0, `安装应成功:${r.err.slice(0, 300)}`);
|
|
63
|
-
// ★
|
|
63
|
+
// ★ 用户的核心诉求:默认也要装给 desktop
|
|
64
64
|
assert.ok(hasPlugin(desktop), "★ 默认安装必须装进 desktop profile(否则桌面端用户看不到面板)");
|
|
65
65
|
assert.ok(patched(desktop), "desktop 的 patch 必须写入激活行");
|
|
66
66
|
assert.ok(hasPlugin(web), "Web 版 profile 同样要装");
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ② 参数拼错(不认识的 flag)→ 明确报错、退出码 1、**也不碰磁盘**。
|
|
12
12
|
*
|
|
13
13
|
* ⚠️ 隔离:所有用例都伪造 HOME + DSH_PROFILE_DIR,并设 DSH_RELAY_SKIP_SERVICE=1,
|
|
14
|
-
* 否则会污染真实的自启动服务与 ~/.dsh-remote(见 setup-output.test.mjs
|
|
14
|
+
* 否则会污染真实的自启动服务与 ~/.dsh-remote(见 setup-output.test.mjs 的故障回顾)。
|
|
15
15
|
*/
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
17
|
import { execFile } from "node:child_process";
|
|
@@ -114,7 +114,7 @@ test("★ 两份 plist 模板的 PATH 必须一致(插件那份曾漏掉 /usr/
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
test("★ 被监管者(launchd)拉起的实例不得因『已有游离守护』而秒退", () => {
|
|
117
|
-
//
|
|
117
|
+
// 真机故障链:KeepAlive 重拉 → 本进程发现 pid 文件里已有游离守护 → 秒退
|
|
118
118
|
// → 监管者判为崩溃并节流(`launchctl print` = `state = spawn scheduled`)
|
|
119
119
|
// → 插件自愈认定"服务坏了" → bootout 摘作业(入口判定成立时连 plist 一起删)
|
|
120
120
|
// → **自启动彻底消失,`launchctl kickstart` 报"域里找不到该服务"**。
|
|
@@ -184,7 +184,7 @@ test("★ watcher 退出前必须确保子进程真的死了(只『请』不
|
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
test("★ 守护惊群:启动锁必须排在去重读取**之前**,且先写 pid 再释放锁", () => {
|
|
187
|
-
// 2026-09-26
|
|
187
|
+
// 2026-09-26 真机故障:一次重启窗口里同时活下来 6 个 `dsh-setup.mjs run` + 6 个 bridge,
|
|
188
188
|
// 全部用同一 deviceId 抢 router 注册(日志刷满 `replaced by new connection`),隧道无限抖动。
|
|
189
189
|
// 成因:去重是"先读 pid 文件、后写 pid 文件",N 个实例同时启动时每个都读到"没有别人"。
|
|
190
190
|
const rb = src.slice(src.indexOf("async function runBridge()"), src.indexOf("// ---------- setup"));
|
|
@@ -172,7 +172,7 @@ test("安全护栏:原子写回 —— 不留 .tmp 残留,且末尾换行规
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
test("回归护栏:plugin 子命令必须把激活结果 return 给 setup(否则热挂载验证被跳过)", () => {
|
|
175
|
-
//
|
|
175
|
+
// 故障回顾:convergePluginActivation 已经返回 {hotPatch},但 pluginCmd 没把它 return 出去,
|
|
176
176
|
// 于是 setup() 拿不到激活形态 → 跳过热挂载验证 → 明明热挂载成功却仍报「需要重启」。
|
|
177
177
|
const src = readFileSync(SETUP, "utf8");
|
|
178
178
|
const call = src.match(/return convergePluginActivation\(profileDir[^\n]*/);
|
|
@@ -230,7 +230,7 @@ test("硬约束:写入 patch 行后 bundles 必须被清空(单一激活点
|
|
|
230
230
|
});
|
|
231
231
|
|
|
232
232
|
test("回归护栏:热挂载探测必须比对**版本**,不能只看接口 200", () => {
|
|
233
|
-
//
|
|
233
|
+
// 故障回顾:profile 已更新到 0.6.4,运行中的插件仍是 beta.10,而旧探测只看
|
|
234
234
|
// `/dsh-remote/status` 是否 200(旧版本本来就有这个接口)→ 误报"热加载成功",
|
|
235
235
|
// 用户以为已生效、实际还在跑旧代码。
|
|
236
236
|
const src = readFileSync(SETUP, "utf8");
|
|
@@ -190,7 +190,7 @@ test("诊断面板必须显示目录选择器状态(这类'按钮没反应'才
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
test("接线:apply 挂的是**延后**接线(schedulePickerPin),绝不在启动期直接切 Loader", () => {
|
|
193
|
-
// 🔴 2026-09-26
|
|
193
|
+
// 🔴 2026-09-26 回归锁:旧实现是 apply() 里直接 `pinBrowseDirectoryPicker(ctx)`,
|
|
194
194
|
// 于是 dsh 还在装配插件图时就去 loader.remove/create → dsh-client-modules 反复 compose
|
|
195
195
|
// 整张前端图(buildCombo/newlineCount/identitySectionMap 热点)→ dsh web 3 分钟不监听、
|
|
196
196
|
// 表现为假死。DSH_REMOTE_NATIVE_PICKER=1 能立刻恢复,正是这条链的证据。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 插件清单与「一键更新静默失败」契约测试(node:test,零框架)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 故障背景(2026-09-15 生产反馈):
|
|
5
5
|
* A. 用户扫码后报「fail to load plugin / DeepSeek client collection」。
|
|
6
6
|
* 排查发现 package.json 的 dsh.client.inject 声明了 `@deepseek-ai/dsh-client-runtime`,
|
|
7
7
|
* 而该包在 harness 安装树里**根本不存在**(harness 只有 dsh-client-ui-* 等真包)。
|
|
@@ -84,7 +84,7 @@ test("一键更新:前端在进程结束但没成功时必须报错,而不
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
test("运行时同步:客户端脚本必须按**内容**补齐(否则手机端修复永远到不了用户)", () => {
|
|
87
|
-
//
|
|
87
|
+
// 故障背景(2026-09-15 实测):手机端遮罩 bug 的修复落在 clients/dsh-remote/mobile-adapter.mjs,
|
|
88
88
|
// 而 bridge 是把**配置目录里那份**脚本注入官方页面后发给手机的。装置器原先只在"版本号变化"时
|
|
89
89
|
// 才覆盖运行时副本,于是出现过:改了、发了版、用户升级了,但手机端拿到的仍是旧脚本 —— 修复不生效。
|
|
90
90
|
// 现在按内容比对,只要与装置器手里的不一致就覆盖。
|
|
@@ -84,11 +84,11 @@ test("面板文案:额度数据缺失时不得断言任何限制", () => {
|
|
|
84
84
|
// 【0.6.11 改断言】旧断言是
|
|
85
85
|
// assert.match(CLIENT_SRC, /quota\.max_mbps \? "带宽 ≈" \+ quota\.max_mbps/, "带宽值服务端有才写")
|
|
86
86
|
// 它编码的是**旧行为**(账号卡「更多」档位明细里的「当前:带宽 ≈N Mbps」那一行),不是不变量。
|
|
87
|
-
//
|
|
87
|
+
// 用户约定:「插件面板里面不要展示『更多』里的 PRO 版本流量带宽,我看你把数字都展示出来了。
|
|
88
88
|
// 在本地的设置面板里面,把这个功能删掉。」→ 整块(renderPlanDetail + planSpecText/freeMbpsOf)
|
|
89
89
|
// 已删,那个字符串在代码里不再存在。保留下来的**不变量**(比原来更强):
|
|
90
|
-
// 客户端不得渲染任何带宽/流量/价格**数值** ——
|
|
91
|
-
assert.doesNotMatch(CLIENT_SRC, /" Mbps"/, "
|
|
90
|
+
// 客户端不得渲染任何带宽/流量/价格**数值** —— 服务端没下发时不许编(旧约束),下发了也不摆(新约定)。
|
|
91
|
+
assert.doesNotMatch(CLIENT_SRC, /" Mbps"/, "面板不再渲染带宽数值(用户约定:本地面板不展示规格数字)");
|
|
92
92
|
assert.doesNotMatch(CLIENT_SRC, /" GB\/月,¥"/, "面板不再渲染流量/价格数值(档位明细已删)");
|
|
93
93
|
// 但「已限速 / 不限速」这条**定性**提示仍在,且仍由服务端字段驱动(只是不再带数值)——
|
|
94
94
|
// 这是上面第 ① 条不变量「服务端没说的限制一个字都不许编」的正面表达,也是必须保留的行为。
|
|
@@ -102,7 +102,7 @@ test("面板文案:额度数据缺失时不得断言任何限制", () => {
|
|
|
102
102
|
assert.doesNotMatch(CLIENT_SRC, /:\s*" · 本月流量限额 1GB"/, "硬编码限额回退已移除");
|
|
103
103
|
// 中继报告「不限量」时也不该说限额。
|
|
104
104
|
// 【0.6.10 改断言】旧断言是 /免费用户 · 当前不限速、不限流量/ —— 那是**旧产品文案**(首屏挂档位名、
|
|
105
|
-
//
|
|
105
|
+
// 用「流量」这个词),不是不变量;用户约定改为「首页面不展示会员相关信息」「流量一律叫额度」,
|
|
106
106
|
// 所以文案变成「当前不限速、不限额度」。这里保留的**不变量**是:中继明确报告不限量时必须如实说明
|
|
107
107
|
// (下面这条分支仍在、仍由服务端字段驱动),不是那句具体措辞。
|
|
108
108
|
assert.match(CLIENT_SRC, /当前不限速、不限额度/, "不限量应如实说明(首屏不再带档位名)");
|
|
@@ -428,7 +428,7 @@ test("「更多」只收纳营销入口:档位规格数字(Mbps / GB / 价
|
|
|
428
428
|
|
|
429
429
|
// 【0.6.11】档位明细整块删除:标题「套餐与额度」、PRO / Pro Max 规格行、价格与结论句都不再渲染
|
|
430
430
|
assert.ok(!textHas(body, "套餐与额度"), "「套餐与额度」标题应删除");
|
|
431
|
-
assert.ok(!textHas(body, "Mbps"), "
|
|
431
|
+
assert.ok(!textHas(body, "Mbps"), "不得再渲染带宽数值(用户约定:本地面板不展示 PRO 版本流量带宽)");
|
|
432
432
|
assert.ok(!textHas(body, "GB"), "不得再渲染流量数值");
|
|
433
433
|
assert.ok(!textHas(body, "¥"), "不得再渲染价格");
|
|
434
434
|
assert.ok(!textHas(body, "以套餐页为准"), "随档位明细一起删掉的结论句不得残留");
|
|
@@ -436,7 +436,7 @@ test("「更多」只收纳营销入口:档位规格数字(Mbps / GB / 价
|
|
|
436
436
|
assert.ok(!textHas(tree, "Mbps"), "账号卡里不得出现 Mbps");
|
|
437
437
|
assert.ok(!textHas(tree, "¥"), "账号卡里不得出现价格");
|
|
438
438
|
|
|
439
|
-
//
|
|
439
|
+
// 但两条**折叠的营销入口**仍在(用户只要数字走,入口留着)
|
|
440
440
|
const upsell = find(body, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("升级 PRO")));
|
|
441
441
|
const invite = find(body, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("带新用户换会员")));
|
|
442
442
|
assert.ok(upsell, "「更多」里应保留「🚀 升级 PRO」入口");
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// dsh-remote-web 侧栏快捷入口「落点」回归(2026-10-09
|
|
1
|
+
// dsh-remote-web 侧栏快捷入口「落点」回归(2026-10-09 桌面端故障)。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 【故障】桌面端(DSH 0.2)设置面板本身就是 `role="dialog" aria-modal="true"` 的弹层,
|
|
4
4
|
// 它左侧栏目列表的第一项叫 **「通用设置」**。旧代码用「文本包含『设置』」(`indexOf !== -1`)
|
|
5
5
|
// 去找侧栏那颗齿轮按钮 → **命中的是弹层里的「通用设置」**,于是我们的「📱 远程访问」
|
|
6
|
-
//
|
|
6
|
+
// 快捷按钮被插进了设置面板栏目列表的最上面。用户截图反馈:「植入错了地方」。
|
|
7
7
|
//
|
|
8
8
|
// 本用例把 client.js 里的导航注入辅助函数**原样切出来**,喂一个最小 DOM,逐条锁住:
|
|
9
9
|
// ① 官方「设置」触发器只按**语义锚点**(aria-haspopup="dialog")识别,且必须在弹层之外;
|
|
@@ -129,13 +129,13 @@ function desktopFixture() {
|
|
|
129
129
|
|
|
130
130
|
// ───────────────────────── 用例 ─────────────────────────
|
|
131
131
|
|
|
132
|
-
test("★
|
|
132
|
+
test("★ 回归锁:官方「设置」触发器只认语义锚点,且必须落在**设置弹层之外**", () => {
|
|
133
133
|
const { doc, trigger } = desktopFixture();
|
|
134
134
|
const { pickSettingsTrigger } = loadNavHelpers(doc);
|
|
135
135
|
assert.equal(pickSettingsTrigger(), trigger, "必须命中侧栏那颗 aria-haspopup=dialog 的齿轮按钮");
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
test("★ 2026-10-11
|
|
138
|
+
test("★ 2026-10-11 用户实测:点入口弹出的是 token 用量弹窗 —— 官方设置槽位必须优先于底部槽邻居", () => {
|
|
139
139
|
// 现场:入口挂在 sidebar.footer.action,而同一底部区域的「token 本轮用量」按钮
|
|
140
140
|
// 也带 aria-haspopup="dialog"(甚至带快捷键)→ 旧实现"谁先扫到谁"就点中了它。
|
|
141
141
|
// 现在:① 官方设置槽位里的按钮排第一;② 底部槽(我们所在那块)里的候选直接排除。
|
|
@@ -159,7 +159,7 @@ test("★ 2026-10-11 业主实测:点入口弹出的是 token 用量弹窗 —
|
|
|
159
159
|
assert.equal(pickSettingsTrigger(), trigger, "★ 必须选中官方设置槽位里的齿轮,而不是 token 用量按钮");
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
test("★
|
|
162
|
+
test("★ 回归锁:弹层里的「通用设置」永远不能成为落点", () => {
|
|
163
163
|
const { doc, general } = desktopFixture();
|
|
164
164
|
const { pickSettingsTrigger, pickNavByToken } = loadNavHelpers(doc);
|
|
165
165
|
assert.notEqual(pickSettingsTrigger(), general, "「通用设置」在弹层内,绝不能被当成侧栏设置按钮");
|
|
@@ -165,7 +165,7 @@ function loadPlugin(opts = {}) {
|
|
|
165
165
|
test("入口:settings.section 栏目(id/order/label)+ 侧栏底部官方槽 sidebar.footer.action", () => {
|
|
166
166
|
const plugin = loadPlugin();
|
|
167
167
|
|
|
168
|
-
// 2026-10-10
|
|
168
|
+
// 2026-10-10 三修(用户约定「就要放侧栏底部那个位置」):入口改注册进**官方列表槽**
|
|
169
169
|
// `sidebar.footer.action`(宿主为侧栏底部区域提供的扩展点;0.2 实测该槽当前为空)。
|
|
170
170
|
// 不再自己注入 DOM —— 前两次都是"自己找落点"找错的。
|
|
171
171
|
assert.ok(plugin.injects.has("sidebar.footer.action"), "应注入官方侧栏底部槽");
|
|
@@ -359,7 +359,7 @@ test("源码约束:侧栏注入「远程访问」按钮与官方「设置」
|
|
|
359
359
|
});
|
|
360
360
|
|
|
361
361
|
test("源码约束:侧栏入口走官方槽位/无浮动面板/无切换账号;命名统一为「远程访问」", () => {
|
|
362
|
-
// 2026-10-10
|
|
362
|
+
// 2026-10-10 三修:侧栏入口**改回**官方槽位(用户约定「就要放侧栏底部那个位置」),
|
|
363
363
|
// 所以这条从"不得出现 footer.action"翻转为"必须走官方槽位、不得自己注入 DOM"。
|
|
364
364
|
assert.match(SOURCE, /slots\.inject\("sidebar\.footer\.action"/);
|
|
365
365
|
assert.doesNotMatch(SOURCE, /insertBefore\(btn, host\)/, "不得再自己插 DOM 找落点");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 「点入口怎么打开设置」契约测试(2026-10-11
|
|
2
|
+
* 「点入口怎么打开设置」契约测试(2026-10-11 用户反馈后的最终形态)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 背景(用户原话):
|
|
5
5
|
* ① 「账号菜单里面的那个删掉。只保留官方槽位的那个。」
|
|
6
6
|
* ② 「设置界面出现了两个远程访问菜单。」—— 因为账号菜单注入的判据里加了"以设置结尾",
|
|
7
7
|
* 而「通用设置」正好以"设置"结尾 → 注入行被插进了**设置面板**里,多出一个"远程访问"。
|
|
@@ -63,9 +63,9 @@ test("★ 设置面板打开后自动点中「远程访问」栏目", () => {
|
|
|
63
63
|
assert.deepEqual(clicked, ["远程访问"], "面板开了要顺手切到我们的栏目");
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
test("★
|
|
66
|
+
test("★ 不再往账号菜单里插任何东西(用户明确要求删掉)", () => {
|
|
67
67
|
for (const banned of ["dru-menu-row", "accountMenuInjectTick", "MENU_ENTRY_SIG"]) {
|
|
68
|
-
assert.ok(!SOURCE.includes(banned), `不得再往账号菜单里插条目:${banned}
|
|
68
|
+
assert.ok(!SOURCE.includes(banned), `不得再往账号菜单里插条目:${banned}(已按用户要求删除)`);
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -26,7 +26,7 @@ const SETUP = join(HERE, "..", "..", "..", "dsh-setup.mjs");
|
|
|
26
26
|
const src = readFileSync(SETUP, "utf8");
|
|
27
27
|
|
|
28
28
|
test("测试隔离:unset 系统操作开关的用例必须同时伪造 HOME(否则污染真实自启动)", () => {
|
|
29
|
-
//
|
|
29
|
+
// 故障回顾:harness-restart 用例 unset DSH_RELAY_SKIP_SERVICE 去走真实分支,
|
|
30
30
|
// 但没伪造 HOME → 插件写 plist 的路径是 join(homedir(), "Library/LaunchAgents/…"),
|
|
31
31
|
// 于是把**指向测试临时目录**的 plist 覆盖到开发者真实的 ~/Library/LaunchAgents,
|
|
32
32
|
// 临时目录随即被删 → 真实 bridge 自启动彻底失效(本机实测踩到,macOS 26 用户反馈排查时发现)。
|
|
@@ -105,13 +105,13 @@ test("安装输出:跳过/不支持自启动时不得自相矛盾地显示「运
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
test("安装输出:服务状态字符串仍是插件面板可解析的稳定契约", () => {
|
|
108
|
-
// 插件半按 running / 未运行
|
|
109
|
-
assert.ok(/\? `✅ 运行中/.test(src), "running
|
|
110
|
-
assert.ok(/`未运行 \(\$\{st\.detail \|\| st\.status\}\)`/.test(src), "
|
|
108
|
+
// 插件半按 running / 未运行 解析,不能因为排版调整改约定
|
|
109
|
+
assert.ok(/\? `✅ 运行中/.test(src), "running 约定保留(带 pid)");
|
|
110
|
+
assert.ok(/`未运行 \(\$\{st\.detail \|\| st\.status\}\)`/.test(src), "未运行约定保留(带原因)");
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
test("版本比较:预设版 < 同号正式版;beta.10 < beta.11(更新提示不能误判)", () => {
|
|
114
|
-
//
|
|
114
|
+
// 故障背景:面板原本用 `latest !== current` 判断"有没有新版本",于是
|
|
115
115
|
// ① 0.6.4-beta.11 与 0.6.4-beta.10 无法比大小;② 正式版 0.6.4 发布后,
|
|
116
116
|
// 装了 0.6.4-beta.11 的人反而被当成"落后"(其实预设版 semver 低于正式版)。
|
|
117
117
|
const src = readFileSync(join(HERE, "..", "lib", "index.js"), "utf8");
|
|
@@ -135,8 +135,8 @@ test("版本比较:预设版 < 同号正式版;beta.10 < beta.11(更新提
|
|
|
135
135
|
assert.match(src, /cmp === null \? Boolean\(latest && latest !== current\) : cmp > 0/);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
test("★ 插件覆盖必须原子:staging → 必需文件自检 → rename 切换(2026-09-26「dsh
|
|
139
|
-
//
|
|
138
|
+
test("★ 插件覆盖必须原子:staging → 必需文件自检 → rename 切换(2026-09-26「dsh 起不来」故障)", () => {
|
|
139
|
+
// 故障:旧实现 `rmSync(dest)` → `cpSync(pluginDir, dest)`,于是拷贝期间 dest 是"半个插件":
|
|
140
140
|
// dsh 恰在此刻启动 → package.json 已声明 bundle,但 cordis.patch.yml 还没拷到 →
|
|
141
141
|
// `failed to read overlay … cordis.patch.yml: ENOENT` → **整个 GUI 起不来**。
|
|
142
142
|
// 时间证据:dsh 报错 23:46:03,cordis.patch.yml 写完 23:46:05(差 2 秒)。
|
|
@@ -153,12 +153,12 @@ test("★ 插件覆盖必须原子:staging → 必需文件自检 → rename
|
|
|
153
153
|
assert.match(fn, /renameSync\(backup, dest\)/, "切换失败必须把 backup 改回来(旧版仍可用)");
|
|
154
154
|
// 反向锁:绝不允许回到"先 rm dest、再 cpSync 逐文件"的旧写法
|
|
155
155
|
assert.ok(!/fs\.rmSync\(dest,\s*\{\s*recursive:\s*true,\s*force:\s*true\s*\}\);\s*\n\s*fs\.cpSync\(/.test(fn),
|
|
156
|
-
"🔴 不得再出现「先删 dest
|
|
156
|
+
"🔴 不得再出现「先删 dest 再原地逐文件拷贝」——那正是这次故障的成因");
|
|
157
157
|
assert.match(fn, /\.staging-\$\{process\.pid\}|\.staging-/, "被强杀留下的 staging 要能清理");
|
|
158
158
|
});
|
|
159
159
|
|
|
160
160
|
test("★ 插件拷贝必须保留**自带的 runtime/node_modules**(ws 是桥唯一的裸依赖)", () => {
|
|
161
|
-
// 2026-10-10
|
|
161
|
+
// 2026-10-10 故障形态:过滤规则写的是"任何一段叫 node_modules 就剔除",
|
|
162
162
|
// 本意只想去掉插件自己的依赖目录(那些由 profile 的包管理器管),
|
|
163
163
|
// 却把 `runtime/node_modules/ws` 一起剔了 —— 而这份运行时正是"只有插件半"的机器
|
|
164
164
|
// **自愈补装时的唯一来源**。剔掉它 → 自愈出来的运行环境缺 ws
|
|
@@ -228,8 +228,8 @@ test("★ 自启动配置里可以钉住上游(多 profile 时代,各宿主
|
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
test("★ 桌面应用在运行时,绝不改写它的 profile(否则应用会自己退出/重启)", () => {
|
|
231
|
-
// 2026-10-11
|
|
232
|
-
//
|
|
231
|
+
// 2026-10-11 故障:在应用运行中改写 ~/.dsh/profiles/desktop/ 的插件文件 →
|
|
232
|
+
// 应用随即自己退出/重启(用户:"怎么又触发了 dsh 的关闭")。安装器文案早就写了"先完全退出应用",
|
|
233
233
|
// 但必须由程序强制,不能靠人记得。
|
|
234
234
|
const src = readFileSync(SETUP, "utf8");
|
|
235
235
|
assert.match(src, /function dshDesktopAppRunning\(/, "要有「桌面应用是否在运行」的探测");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 零依赖 tarball 更新通道的回归测试(node:test,零框架)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 背景(真实故障,见 lib/tarball-update.js 顶部注释):
|
|
5
5
|
* Windows 上「一键更新」走的 `npx` 在 Node ≥20.12 起同步 EINVAL,0.6.6 及更早的升级通道
|
|
6
6
|
* 本身是坏的 —— 79 台点过一键更新的 Windows 机器全部 update_failed,且产品内无出路。
|
|
7
7
|
* 本文件就是给替代通道(HTTPS 取 tarball → 内置 zlib + 自解析 tar 落 staging)上锁:
|
|
@@ -268,7 +268,7 @@ test("触发点:plugin_loaded → install_started → runtime_ready → bridge
|
|
|
268
268
|
|
|
269
269
|
const { host, base } = await serve(routes);
|
|
270
270
|
try {
|
|
271
|
-
// ③ 运行环境补齐(dsh-setup.mjs 落地)→
|
|
271
|
+
// ③ 运行环境补齐(dsh-setup.mjs 落地)→ 面板轮询触达探测 → runtime_ready
|
|
272
272
|
await writeFile(path.join(env.relayDir, "dsh-setup.mjs"), "// runtime stub");
|
|
273
273
|
let r = await bridgeStatus(base);
|
|
274
274
|
assert.equal(r.ok, true);
|
|
@@ -351,7 +351,7 @@ test("隐私审计:对抗性 extra 字段(手机号/hostname/machine_fp/路
|
|
|
351
351
|
// 直接往唯一的构造点灌脏数据:字段白名单 + 事件名校验必须把它们全部丢掉
|
|
352
352
|
TELEMETRY.record(env.relayDir, "install_started", { device_id: "dev-telemetry", machine_fp: "fp-abc" });
|
|
353
353
|
TELEMETRY.record(env.relayDir, "plugin_loaded", { hostname: "mac.local", username: "mac" });
|
|
354
|
-
TELEMETRY.record(env.relayDir, "panel_opened", { path: "/
|
|
354
|
+
TELEMETRY.record(env.relayDir, "panel_opened", { path: "/home/user/relay", phone: "13800000000" });
|
|
355
355
|
TELEMETRY.record(env.relayDir, "update_started", { email: "a@b.c", token: "jwt", password: "pw" });
|
|
356
356
|
TELEMETRY.record(env.relayDir, "harness_restart", { ip: "1.2.3.4" });
|
|
357
357
|
// 白名单外的事件名:一条都不入队
|
|
@@ -554,7 +554,7 @@ test("冻结契约:既有事件名与既有字段一字不改(本次只新
|
|
|
554
554
|
assert.match(String(ev.node), /^\d+$/);
|
|
555
555
|
assert.ok(!("fail_code" in ev), "fail_code 仍只出现在 *_failed 事件上");
|
|
556
556
|
assert.equal(TELEMETRY.eventOf("user_login"), null, "白名单外事件名照旧不发");
|
|
557
|
-
// 通道与"一键更新"同源:面板看到的通道 =
|
|
557
|
+
// 通道与"一键更新"同源:面板看到的通道 = 上报的通道(两处约定必须是一处)
|
|
558
558
|
assert.equal(ev.channel, TELEMETRY.updateTag);
|
|
559
559
|
});
|
|
560
560
|
|
|
@@ -845,7 +845,7 @@ test("面板可见性:关于卡片写明「匿名统计 + 关闭方式」,
|
|
|
845
845
|
"客户端上报的事件,不是服务端的实时存量",
|
|
846
846
|
"bot_id",
|
|
847
847
|
]) {
|
|
848
|
-
assert.ok(doc.includes(line), "docs/telemetry.md
|
|
848
|
+
assert.ok(doc.includes(line), "docs/telemetry.md 必须写明微信通道事件的约定边界:" + line);
|
|
849
849
|
}
|
|
850
850
|
for (const code of TELEMETRY.failCodes) assert.ok(doc.includes(code), "docs/telemetry.md 必须列出失败码:" + code);
|
|
851
851
|
});
|
|
@@ -89,7 +89,7 @@ async function plantProfile(home, { extraPatch = "", unmarked = false } = {}) {
|
|
|
89
89
|
fs.mkdirSync(path.join(home, "relay"), { recursive: true });
|
|
90
90
|
// ⚠️ api_url 必须指向本地死端口:这些是**假账号**,一旦按默认地址请求生产,
|
|
91
91
|
// /api/device-login 会带着假密码失败,而服务端登录限流是按出口 IP 计的(5 次/15 分钟)——
|
|
92
|
-
// 实测结果是把**本机真实的 bridge** 连坐进限流(2026-09-19
|
|
92
|
+
// 实测结果是把**本机真实的 bridge** 连坐进限流(2026-09-19 故障)。
|
|
93
93
|
fs.writeFileSync(path.join(home, "relay", ".dsh-config.json"),
|
|
94
94
|
JSON.stringify({ phone: "13800000000", password: "pw", api_url: "http://127.0.0.1:1" }));
|
|
95
95
|
return { profile, patchFile: path.join(profile, "cordis.patch.yml") };
|
|
@@ -418,7 +418,7 @@ test("安全:三处机密写入都要显式收紧 ACL(Windows 上 mode 0o600
|
|
|
418
418
|
});
|
|
419
419
|
|
|
420
420
|
test("安全:Cookie 的每个写入点都必须紧跟着 hardenFile(按 authority 分文件后不得漏收)", () => {
|
|
421
|
-
// 0.7.0-beta.2 把 Cookie 从「一份共享文件」改成「本宿主专属 + 旧版镜像」两份(见 P0-2
|
|
421
|
+
// 0.7.0-beta.2 把 Cookie 从「一份共享文件」改成「本宿主专属 + 旧版镜像」两份(见 P0-2 故障根治)。
|
|
422
422
|
// 写点从 1 个变 2 个 —— 逐个按变量名断言,漏收的那一份在 Windows 上就是 ACL 敞开状态。
|
|
423
423
|
const mint = INDEX_SRC.slice(INDEX_SRC.indexOf("async function mintHarnessCookie"), INDEX_SRC.indexOf("function readHarnessCookie"));
|
|
424
424
|
assert.ok(mint.length > 0, "应能定位到 mintHarnessCookie(找不到说明函数被改名,请同步本用例)");
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// ⑦ 控制面缺失(bridge 没跑/版本旧)要说清楚怎么办,**绝不**留一个永久 spinner;
|
|
17
17
|
// ⑧ 无障碍:真 <button>、aria-live 播报、不靠颜色单独表意。
|
|
18
18
|
//
|
|
19
|
-
// 【0.6.12
|
|
19
|
+
// 【0.6.12 新增】产品引导(用户约定:「动效引导」+ 微信通道要求已注册并登录):
|
|
20
20
|
// ⑨ 首屏导览:未绑定时内容体最上面三行讲清用途(推送 / 回数字拍板 / 微信里交代任务),
|
|
21
21
|
// DOM 顺序在「连接微信机器人」之前,且是小字紧凑排版(不把按钮挤出首屏);
|
|
22
22
|
// ⑩ 登录门:未登录**不渲染**微信 tab,登录后当场出现(不刷新页面);会话在 tab 开着时失效
|
|
@@ -355,7 +355,7 @@ test("入口:settings.section 里**只有**「📱 远程访问」一个栏目
|
|
|
355
355
|
const list = plugin.sectionList();
|
|
356
356
|
const ids = list.map((m) => m.id);
|
|
357
357
|
// 【0.6.11 改断言】旧断言 deepEqual(ids, ["dsh-remote", "dsh-remote-wechat"]) + order 31 + label 含
|
|
358
|
-
// 「🤖
|
|
358
|
+
// 「🤖 微信机器人」编码的是**旧行为**(微信面板作为第二个设置栏目)。用户约定:「不要给它单独弄
|
|
359
359
|
// 一个菜单,直接放到『远程控制』面板里面」→ 第二个栏目已删,这里改为断言「只有一个栏目」。
|
|
360
360
|
assert.deepEqual(ids, [ROOT_SECTION_ID], `设置页只应有一个栏目,实际:${ids.join(",")}`);
|
|
361
361
|
assert.equal(list.length, 1, "settings.section 注册次数必须恰好是 1(不多不少)");
|
|
@@ -426,7 +426,7 @@ test("tab 条(home 与 wechat 共用):第三个 tab 是「微信机器人
|
|
|
426
426
|
});
|
|
427
427
|
|
|
428
428
|
test("绿泡泡与零外部资源(源码级):.dru-wx-ico 是微信品牌绿、不新增任何外部资源/依赖", () => {
|
|
429
|
-
// 图标颜色:微信品牌绿 #07c160
|
|
429
|
+
// 图标颜色:微信品牌绿 #07c160(用户约定:「增加上微信的绿泡泡小图标」)
|
|
430
430
|
assert.match(SOURCE, /\.dru-wx-ico\{[^}]*color:#07c160/, "绿泡泡必须是微信品牌绿 #07c160");
|
|
431
431
|
// 装饰性:节点上 aria-hidden(可读名字来自同 tab 的文字标签,见上一条用例)
|
|
432
432
|
assert.match(SOURCE, /className: "dru-wx-ico", "aria-hidden": "true"/, "图标节点必须 aria-hidden");
|
|
@@ -499,10 +499,10 @@ test("首屏导览:未绑定时就在最上面(DOM 顺序在「连接微信
|
|
|
499
499
|
assert.ok(textHas(intro, "继续已有任务"), "③ 要明说免费也能继续已有任务(回复即续接)");
|
|
500
500
|
assert.ok(textHas(intro, "开新任务"), "③ 要明说「在微信里开新任务」是会员能力");
|
|
501
501
|
assert.ok(textHas(intro, "切换会话"), "③ 要明说「切换会话」是会员能力");
|
|
502
|
-
// ★
|
|
502
|
+
// ★ 分层必须如实写在面板上:免费用户要能看出**付费多什么**(用户:"增加一个对比展示")。
|
|
503
503
|
// 面板里含糊其辞 → 用户绑完去微信发句话拿到付费提示 → 只会觉得产品骗人。
|
|
504
504
|
assert.ok(textHas(intro, "会员"), "必须标出哪些是会员能力(不能含糊)");
|
|
505
|
-
assert.ok(textHas(intro, "额度"), "
|
|
505
|
+
assert.ok(textHas(intro, "额度"), "免费档要写明每月消息额度(用户:免费给每月 N 条)");
|
|
506
506
|
// 未绑定/未读到状态时不显示锁,免得闪一下
|
|
507
507
|
assert.ok(!textHas(intro, "🔒") || textHas(intro, "会员"), "锁必须配「会员」字样,不能只给一个符号");
|
|
508
508
|
|
|
@@ -513,7 +513,7 @@ test("首屏导览:未绑定时就在最上面(DOM 顺序在「连接微信
|
|
|
513
513
|
assert.ok(introAt < btnAt, `导览必须排在连接按钮之前(导览 #${introAt},按钮 #${btnAt})`);
|
|
514
514
|
// 而且紧凑块排在内容体最前面:不会把连接按钮推到需要滚动才看得见的地方
|
|
515
515
|
// (harness 渲染时会对普通节点做浅拷贝,所以拿树里的同一个节点比,不比组件里的原对象)
|
|
516
|
-
// 【0.6.18】内容体第一个孩子变成了「两步走」操作引导块(.dru-wx-guide
|
|
516
|
+
// 【0.6.18】内容体第一个孩子变成了「两步走」操作引导块(.dru-wx-guide):用户约定是
|
|
517
517
|
// 先在微信通道首屏讲清"这是干啥用的 + 两步怎么接",再给"你现在能用哪些能力"的清单 ——
|
|
518
518
|
// 所以导览降为**第二个**孩子,连接按钮仍在首屏(两块都是紧凑小字卡片)。
|
|
519
519
|
assert.equal(embed.children[0], find(embed, (n) => n.props?.className === "dru-wx-guide"),
|
|
@@ -923,7 +923,7 @@ test("★★ 面板能力镜像必须与运行时判定表一致(否则面板
|
|
|
923
923
|
//
|
|
924
924
|
// 上面那条镜像一致性用例保证的是「冷启动兜底等于运行时内置表」;
|
|
925
925
|
// 下面这三条保证的是**服务端一旦下发了权益包,面板就以它为准** ——
|
|
926
|
-
//
|
|
926
|
+
// 这是用户「后台改权限无需发版」与「话术与权限必须一致、不多承诺」的交汇点:
|
|
927
927
|
// 后台把免费额度从 20 改成 5,面板必须当场说 5;说 20 就是让用户按 20 去发、
|
|
928
928
|
// 第 6 条被拦,然后认为产品坏了。
|
|
929
929
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Windows 兼容回归(0.6.7 新增)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 背景(真实故障,用户诊断报告 + 本地逐条复现):
|
|
5
5
|
* dsh-remote-web 0.6.6 及更早的整套「服务状态 / 启停 / 重启」按 macOS/Linux 写死,
|
|
6
6
|
* 在 Windows 上**运行期必死**——安装与网络都正常,坏的全是运行期:
|
|
7
7
|
* ① launchTarget() 无条件调 process.getuid():Windows 没有这个 API(typeof !== "function")
|
|
@@ -267,7 +267,7 @@ test("win32:重启 harness 生成 node 助手(不再 /bin/sh),且生成
|
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
test("契约:插件里不存在「裸 spawn」——每个子进程都必须有 'error' 监听(否则打挂宿主)", () => {
|
|
270
|
-
//
|
|
270
|
+
// 故障原文:spawn("/bin/sh") 在 Windows ENOENT,而 ChildProcess 没有 'error' 监听
|
|
271
271
|
// → 未捕获异常把 dsh web 打挂,且代码已经先把「已调度重启」写进日志、返回成功。
|
|
272
272
|
// 生成的 Windows 助手源码是模板字符串(它自己挂了 error 监听),先从「真实代码」里剔除
|
|
273
273
|
const helperStart = INDEX_SRC.indexOf("function buildRestartHelperSource(");
|
|
@@ -425,7 +425,7 @@ test("win32:dsh-setup 注册任务计划程序登录任务(不再只说「
|
|
|
425
425
|
test("win32:彻底卸载会删掉任务计划条目(否则下次登录又被拉起来)", () => {
|
|
426
426
|
const uninstall = INDEX_SRC.slice(INDEX_SRC.indexOf("function uninstallRuntime("), INDEX_SRC.indexOf("// ---------- DeepSeek harness 重启"));
|
|
427
427
|
assert.ok(/schtasks", \["\/Delete", "\/TN", WIN_TASK_NAME, "\/F"\]/.test(uninstall), "卸载应删除 Windows 登录任务");
|
|
428
|
-
assert.ok(/servicePlatform: isDarwin\(\) \? "launchd"/.test(uninstall), "servicePlatform 应包含 Windows
|
|
428
|
+
assert.ok(/servicePlatform: isDarwin\(\) \? "launchd"/.test(uninstall), "servicePlatform 应包含 Windows 约定");
|
|
429
429
|
});
|
|
430
430
|
|
|
431
431
|
test("win32:dsh-setup 生成的「重启 dsh web」助手也是合法可执行的 JS", async () => {
|
|
@@ -465,7 +465,7 @@ test("前端:bridge-status 轮询失败不再静默(旧版让面板永久停
|
|
|
465
465
|
});
|
|
466
466
|
|
|
467
467
|
test("测试隔离:套件不得访问任何外部地址(生产限流会被测试污染)", () => {
|
|
468
|
-
//
|
|
468
|
+
// 故障(2026-09-19):有用例在配置里写了假账号却没覆盖 api_url → 按默认地址请求**生产中继**,
|
|
469
469
|
// 其中 /api/device-login 带假密码必然失败,而服务端登录限流按**出口 IP** 计(5 次/15 分钟)——
|
|
470
470
|
// 结果不仅污染生产审计,还让**本机自己的 bridge 被连坐**(拿不到 JWT),
|
|
471
471
|
// 真机表现成"切换账号后设备一直登记不上去",排查方向被彻底带偏。
|
|
@@ -564,7 +564,7 @@ test("win32:登录任务经 wscript+VBS 隐藏启动(旧版会弹黑色空
|
|
|
564
564
|
test("win32:桌面端(Electron 解释器)自启动必须先设 ELECTRON_RUN_AS_NODE=1", async () => {
|
|
565
565
|
// 现场价值:DeepSeek Harness 桌面端宿主里 process.execPath 是 Electron 应用二进制。
|
|
566
566
|
// 不设这个变量,自启动拉起的不是 bridge 守护,而是**又开一个桌面端应用** ——
|
|
567
|
-
// 表现为「服务登记成功、进程秒退、bridge 永远起不来」,与 0.6.15
|
|
567
|
+
// 表现为「服务登记成功、进程秒退、bridge 永远起不来」,与 0.6.15 端口故障一样难排查。
|
|
568
568
|
const dir = await mkdtemp(path.join(os.tmpdir(), "dsh-win-electron-"));
|
|
569
569
|
try {
|
|
570
570
|
const src = SETUP_SRC.slice(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 测试期网络护栏(NODE_OPTIONS=--require 注入,见 package.json 的 test 脚本)。
|
|
3
3
|
*
|
|
4
|
-
* 为什么必须有(2026-09-19
|
|
4
|
+
* 为什么必须有(2026-09-19 实测故障):
|
|
5
5
|
* 有测试用例在 profile 配置里写了**假账号**(phone/password)却没覆盖 `api_url`,
|
|
6
6
|
* 于是插件按默认地址去请求**生产中继** `https://n.risegao.cn:…`。其中
|
|
7
7
|
* `/api/device-login` 带着假密码必然失败,而服务端的登录限流是**按出口 IP**
|