tt-help-cli-ycl 1.4.169 → 1.4.172

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 (46) hide show
  1. package/package.json +1 -1
  2. package/scripts/test-subscription-allocator.mjs +220 -0
  3. package/scripts/test-subscription-auto-alloc-ui.mjs +208 -0
  4. package/scripts/test-subscription-auto-alloc.mjs +259 -0
  5. package/scripts/test-subscription-center-e2e.mjs +134 -0
  6. package/scripts/test-subscription-center-smoke.mjs +154 -0
  7. package/scripts/test-subscription-center-ui.mjs +111 -0
  8. package/scripts/vnc-login.sh +68 -0
  9. package/src/cli/info.js +6 -1
  10. package/src/lib/browser/cdp.js +7 -1
  11. package/src/watch/public/app.js +713 -39
  12. package/src/watch/public/index.html +74 -3
  13. package/src/watch/subscription-allocator.js +255 -0
  14. package/src/watch/subscription-center.js +590 -0
  15. package/src/watch/watchdog-server.js +210 -0
  16. package/src/watchdog/agent.js +104 -10
  17. package/scripts/test-api-interceptor-429.mjs +0 -122
  18. package/scripts/test-api-interceptor-captcha.mjs +0 -176
  19. package/scripts/test-claim-fix.mjs +0 -216
  20. package/scripts/test-clash-discover.mjs +0 -126
  21. package/scripts/test-clash-hysteresis.mjs +0 -131
  22. package/scripts/test-clash-multichannel.mjs +0 -120
  23. package/scripts/test-client-error-monitor.mjs +0 -261
  24. package/scripts/test-country-fix.mjs +0 -341
  25. package/scripts/test-daemon-failover.mjs +0 -301
  26. package/scripts/test-daemon-switch-history.mjs +0 -141
  27. package/scripts/test-daemon-whitelist.mjs +0 -308
  28. package/scripts/test-explore-captcha-wait.mjs +0 -170
  29. package/scripts/test-photo-post.mjs +0 -156
  30. package/scripts/test-precheck-perf.mjs +0 -270
  31. package/scripts/test-precise-index.mjs +0 -114
  32. package/scripts/test-probe-live.mjs +0 -83
  33. package/scripts/test-probe.mjs +0 -315
  34. package/scripts/test-product-fill-cdp.mjs +0 -336
  35. package/scripts/test-product-fill-db.mjs +0 -216
  36. package/scripts/test-product-fill-http.mjs +0 -117
  37. package/scripts/test-proxy-switch-restart.mjs +0 -198
  38. package/scripts/test-refill-order.mjs +0 -218
  39. package/scripts/test-sample-all-fail.mjs +0 -142
  40. package/scripts/test-seller-any-country.mjs +0 -167
  41. package/scripts/test-stats-smoke.mjs +0 -43
  42. package/scripts/test-upgrade-spawn.mjs +0 -179
  43. package/scripts/test-webserver-error-classify.mjs +0 -409
  44. package/scripts/test-wmi-free-upgrade.mjs +0 -98
  45. package/src/cli/test-real-attach.js +0 -0
  46. package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
@@ -1,216 +0,0 @@
1
- /**
2
- * product-fill 服务端数据层集成测试
3
- *
4
- * 用临时 SQLite 库验证:
5
- * - getPendingProductTasks:只领取"有 seoUrl 且缺商品详情"的商家,已补全/无 seoUrl 的不领
6
- * - updateProductInfo:回写 product_* 字段到 jobs 表,成功后移出候选
7
- *
8
- * 运行: TMPDIR=$(mktemp -d) node scripts/test-product-fill-db.mjs
9
- */
10
- import { createStore } from "../src/watch/data-store.js";
11
- import { getDb } from "../src/watch/db-schema.js";
12
- import { join } from "node:path";
13
-
14
- const tmp = process.env.TMPDIR || "/tmp";
15
- const dbPath = join(tmp, `tt-test-product-${Date.now()}.db`);
16
-
17
- const store = createStore(dbPath);
18
- const db = getDb();
19
- if (!db) {
20
- console.error("❌ db 未初始化");
21
- process.exit(1);
22
- }
23
-
24
- const now = Date.now();
25
- // A: 有 seoUrl、无商品 → 应被领取
26
- db.prepare(
27
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, updated_at)
28
- VALUES ('sellerA', 'done', 1, 'https://www.tiktok.com/shop/pdp/a1', NULL, ?)`,
29
- ).run(now);
30
- // B: 有 seoUrl、已有商品 → 不应被领取
31
- db.prepare(
32
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, updated_at)
33
- VALUES ('sellerB', 'done', 1, 'https://www.tiktok.com/shop/pdp/b1', '已有商品', ?)`,
34
- ).run(now);
35
- // C: 无 seoUrl → 不应被领取
36
- db.prepare(
37
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, updated_at)
38
- VALUES ('sellerC', 'done', 1, NULL, NULL, ?)`,
39
- ).run(now);
40
- // D: 有 seoUrl、name 空但 rating 有值 → 已抓到数据,不应被领取(防死循环)
41
- db.prepare(
42
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, product_rating, updated_at)
43
- VALUES ('sellerD', 'done', 1, 'https://www.tiktok.com/shop/pdp/d1', NULL, 4.5, ?)`,
44
- ).run(now);
45
- // E: 有 seoUrl、name 空但 sold_count 有值 → 已抓到数据,不应被领取(防死循环)
46
- db.prepare(
47
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, product_sold_count, updated_at)
48
- VALUES ('sellerE', 'done', 1, 'https://www.tiktok.com/shop/pdp/e1', '', 120, ?)`,
49
- ).run(now);
50
- // F: 有 seoUrl、无商品 → 与 A 一起验证 claim 防重复领取
51
- db.prepare(
52
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, updated_at)
53
- VALUES ('sellerF', 'done', 1, 'https://www.tiktok.com/shop/pdp/f1', NULL, ?)`,
54
- ).run(now + 1);
55
-
56
- let passed = 0;
57
- function check(name, fn) {
58
- try {
59
- fn();
60
- passed++;
61
- console.log(` ✅ ${name}`);
62
- } catch (e) {
63
- console.error(` ❌ ${name}\n ${e.message}`);
64
- process.exitCode = 1;
65
- }
66
- }
67
-
68
- console.log("===== product-fill 服务端数据层测试 =====");
69
-
70
- console.log("\n[1] getPendingProductTasks:只领缺商品的");
71
- const tasks = store.getPendingProductTasks(5);
72
- check("只返回缺商品的(sellerA/sellerF,不含 B/C/D/E)", () => {
73
- const ids = tasks.map((t) => t.uniqueId);
74
- assert(ids.length === 2, `实际: ${ids.join(",")}`);
75
- assert(
76
- ids.includes("sellerA") && ids.includes("sellerF"),
77
- `实际: ${ids.join(",")}`,
78
- );
79
- });
80
- check("任务带 seoUrl / productId / productTitle", () => {
81
- const a = tasks.find((t) => t.uniqueId === "sellerA");
82
- assert(a.seoUrl === "https://www.tiktok.com/shop/pdp/a1");
83
- assert("productId" in a && "productTitle" in a);
84
- });
85
- check("limit=1 只取 1 条", () => {
86
- db.prepare(
87
- "UPDATE jobs SET claimed_at = NULL, claimed_by = NULL WHERE unique_id IN ('sellerA','sellerF')",
88
- ).run();
89
- assert(store.getPendingProductTasks(1).length === 1);
90
- db.prepare(
91
- "UPDATE jobs SET claimed_at = NULL, claimed_by = NULL WHERE unique_id IN ('sellerA','sellerF')",
92
- ).run();
93
- });
94
- check("领取即锁定:写入 claimed_by/claimed_at", () => {
95
- const claimed = store.getPendingProductTasks(5, "testerA");
96
- assert(claimed.length === 2, `实际: ${claimed.length}`);
97
- const row = db
98
- .prepare(
99
- "SELECT claimed_by, claimed_at FROM jobs WHERE unique_id = 'sellerA'",
100
- )
101
- .get();
102
- assert(row.claimed_by === "testerA", JSON.stringify(row));
103
- assert(row.claimed_at !== null && row.claimed_at !== undefined);
104
- });
105
- check("锁定期间不重复领取(多客户端防重复)", () => {
106
- const again = store.getPendingProductTasks(5, "testerB");
107
- assert(
108
- again.length === 0,
109
- `实际领到: ${again.map((t) => t.uniqueId).join(",")}`,
110
- );
111
- });
112
- check("TTL 过期后自动回收", () => {
113
- db.prepare("UPDATE jobs SET claimed_at = ? WHERE unique_id = 'sellerF'").run(
114
- Date.now() - 6 * 60 * 1000,
115
- );
116
- const recycled = store.getPendingProductTasks(5, "testerC");
117
- assert(
118
- recycled.some((t) => t.uniqueId === "sellerF"),
119
- `实际: ${recycled.map((t) => t.uniqueId).join(",")}`,
120
- );
121
- });
122
- check("name 空但 rating 有值 → 不进候选", () => {
123
- assert(!tasks.some((t) => t.uniqueId === "sellerD"));
124
- });
125
- check("name 空但 soldCount 有值 → 不进候选", () => {
126
- assert(!tasks.some((t) => t.uniqueId === "sellerE"));
127
- });
128
-
129
- console.log("\n[2] updateProductInfo:回写商品详情");
130
- const ret = store.updateProductInfo("sellerA", {
131
- rating: 4.7,
132
- reviewCount: 3303,
133
- soldCount: 296,
134
- productName: "测试商品A",
135
- });
136
- check("返回 ok", () => assert(ret.ok === true, JSON.stringify(ret)));
137
- const rowA = db
138
- .prepare("SELECT * FROM jobs WHERE unique_id = ?")
139
- .get("sellerA");
140
- check("jobs 表已写入 product_* 字段", () => {
141
- assert(rowA.product_rating === 4.7);
142
- assert(rowA.product_review_count === 3303);
143
- assert(rowA.product_sold_count === 296);
144
- assert(rowA.product_name === "测试商品A");
145
- });
146
- check("未增加 user_update_count", () => {
147
- assert((rowA.user_update_count || 0) === 0);
148
- });
149
-
150
- console.log("\n[3] 补全后移出候选");
151
- const tasksAfter = store.getPendingProductTasks(5);
152
- check("sellerA 不再出现在候选", () => {
153
- assert(!tasksAfter.some((t) => t.uniqueId === "sellerA"));
154
- });
155
-
156
- console.log("\n[3b] skus 入库(product_skus JSON 列)");
157
- const skuRet = store.updateProductInfo("sellerB", {
158
- rating: 4.8,
159
- reviewCount: 174,
160
- soldCount: 962,
161
- productName: "Sku Product",
162
- skus: [
163
- {
164
- skuId: "1731140428076257811",
165
- specs: "Shade: Wanderlust",
166
- stock: 106,
167
- stockStatus: 1,
168
- warehouseId: "7676336681096611597",
169
- },
170
- {
171
- skuId: "1729888889849549331",
172
- specs: "Shade: Ballerina",
173
- stock: 2017,
174
- stockStatus: 1,
175
- warehouseId: "7676336681096611597",
176
- },
177
- ],
178
- });
179
- check("带 skus 回写返回 ok", () => assert(skuRet.ok === true));
180
- check("product_skus 列已写入 JSON(读回可解析)", () => {
181
- const rowB = db
182
- .prepare("SELECT product_skus FROM jobs WHERE unique_id = ?")
183
- .get("sellerB");
184
- const parsed = JSON.parse(rowB.product_skus);
185
- assert(parsed.length === 2);
186
- assert(parsed[0].skuId === "1731140428076257811");
187
- assert(parsed[1].stock === 2017);
188
- });
189
- check("无 skus 回写(老客户端)不报错", () => {
190
- const ret = store.updateProductInfo("sellerB", {
191
- rating: 4.0,
192
- soldCount: 10,
193
- productName: "Old",
194
- reviewCount: 5,
195
- });
196
- assert(ret.ok === true);
197
- });
198
-
199
- console.log("\n[4] 异常输入");
200
- check("缺 productDetail → error", () => {
201
- const r = store.updateProductInfo("sellerB", null);
202
- assert(r.ok !== true && r.error);
203
- });
204
- check("不存在的用户 → error", () => {
205
- const r = store.updateProductInfo("ghostUser", { rating: 1 });
206
- assert(r.ok !== true && r.error);
207
- });
208
-
209
- store.stopBackup?.();
210
- console.log(
211
- `\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
212
- );
213
-
214
- function assert(cond, msg = "断言失败") {
215
- if (!cond) throw new Error(msg);
216
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * product-fill HTTP 端到端测试
3
- *
4
- * 起真实 watch server(临时库),验证两个端点:
5
- * - GET /api/product-tasks?limit= → 领取缺商品任务
6
- * - POST /api/product-fill/:uniqueId → 回写商品详情
7
- *
8
- * 运行: TMPDIR=$(mktemp -d) node scripts/test-product-fill-http.mjs
9
- */
10
- import { startWatchServer } from "../src/watch/server.js";
11
- import { createStore } from "../src/watch/data-store.js";
12
- import { getDb } from "../src/watch/db-schema.js";
13
- import { join } from "node:path";
14
-
15
- const tmp = process.env.TMPDIR || "/tmp";
16
- const dbPath = join(tmp, `tt-test-http-${Date.now()}.db`);
17
-
18
- const store = createStore(dbPath);
19
- const db = getDb();
20
- const now = Date.now();
21
- db.prepare(
22
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, updated_at)
23
- VALUES ('sellerA', 'done', 1, 'https://www.tiktok.com/shop/pdp/a1', NULL, ?)`,
24
- ).run(now);
25
- db.prepare(
26
- `INSERT INTO jobs (unique_id, status, tt_seller, top_video_seo_url, product_name, updated_at)
27
- VALUES ('sellerB', 'done', 1, 'https://www.tiktok.com/shop/pdp/b1', '已有', ?)`,
28
- ).run(now);
29
-
30
- let passed = 0;
31
- function check(name, fn) {
32
- try {
33
- fn();
34
- passed++;
35
- console.log(` ✅ ${name}`);
36
- } catch (e) {
37
- console.error(` ❌ ${name}\n ${e.message}`);
38
- process.exitCode = 1;
39
- }
40
- }
41
- function assert(cond, msg = "断言失败") {
42
- if (!cond) throw new Error(msg);
43
- }
44
-
45
- const { server } = await startWatchServer(dbPath, 0, store, {
46
- llmDiscover: false,
47
- });
48
- const port = server.address().port;
49
- const base = `http://127.0.0.1:${port}`;
50
-
51
- console.log("===== product-fill HTTP 端到端测试 =====");
52
- try {
53
- console.log("\n[1] GET /api/product-tasks");
54
- const res = await fetch(`${base}/api/product-tasks?limit=5`);
55
- const data = await res.json();
56
- check("返回 200 + total/tasks", () => {
57
- assert(
58
- res.ok && typeof data.total === "number" && Array.isArray(data.tasks),
59
- );
60
- });
61
- check("只返回缺商品的 sellerA", () => {
62
- const ids = data.tasks.map((t) => t.uniqueId);
63
- assert(ids.length === 1 && ids[0] === "sellerA", `实际: ${ids.join(",")}`);
64
- assert(data.tasks[0].seoUrl === "https://www.tiktok.com/shop/pdp/a1");
65
- });
66
-
67
- console.log("\n[2] POST /api/product-fill/:uniqueId");
68
- const fill = await fetch(`${base}/api/product-fill/sellerA`, {
69
- method: "POST",
70
- headers: { "Content-Type": "application/json" },
71
- body: JSON.stringify({
72
- productDetail: {
73
- rating: 4.7,
74
- reviewCount: 3303,
75
- soldCount: 296,
76
- productName: "HTTP测试商品",
77
- },
78
- }),
79
- });
80
- const fillData = await fill.json();
81
- check("回传 ok", () =>
82
- assert(fillData.ok === true, JSON.stringify(fillData)),
83
- );
84
-
85
- console.log("\n[3] 补全后任务消失");
86
- const res2 = await fetch(`${base}/api/product-tasks?limit=5`);
87
- const data2 = await res2.json();
88
- check("sellerA 不再出现", () =>
89
- assert(!data2.tasks.some((t) => t.uniqueId === "sellerA")),
90
- );
91
-
92
- console.log("\n[4] 无效回传(缺 body)");
93
- const bad = await fetch(`${base}/api/product-fill/sellerB`, {
94
- method: "POST",
95
- headers: { "Content-Type": "application/json" },
96
- body: JSON.stringify({}),
97
- });
98
- const badData = await bad.json();
99
- check("返回 error", () =>
100
- assert(bad.status === 400 && badData.error, JSON.stringify(badData)),
101
- );
102
- } finally {
103
- // 强制关闭连接 + 超时兜底,避免 keep-alive/定时器拖住进程
104
- try {
105
- server.closeAllConnections?.();
106
- } catch {}
107
- await Promise.race([
108
- new Promise((r) => server.close(r)),
109
- new Promise((r) => setTimeout(r, 1000)),
110
- ]);
111
- store.stopBackup?.();
112
- }
113
-
114
- console.log(
115
- `\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
116
- );
117
- process.exit(process.exitCode || 0);
@@ -1,198 +0,0 @@
1
- /**
2
- * 代理切换 → watchdog 强制重启命令 链路测试(2026-08-28,先打通路版本)
3
- *
4
- * 验证:
5
- * T1 daemon _markSwitchTime 递增 restartSeq(首次为 1,连续递增)
6
- * T2 daemon _writeHealth 把 restartSeq 带进 health 文件
7
- * T3 daemon 未切换时 restartSeq 保持 0
8
- * T4 ProcessManager.restartAll 先停后起(真实子进程,pid 变化、数量恢复)
9
- * T5 restartAll 跳过 stopped / 无 child 的命令
10
- * T6 无运行命令时 restartAll 返回 {restarted:0}
11
- * T7 Agent._checkProxySwitch 首次读取仅建基线不触发
12
- * T8 health.restartSeq 变大 → 触发强制重启(真实子进程 pid 变化)
13
- * T9 health.restartSeq 变小(daemon 重启归零)→ 不触发
14
- * T10 通道重启:主文件 seq 不变时不触发
15
- *
16
- * 运行: node --test scripts/test-proxy-switch-restart.mjs
17
- */
18
- import { test } from "node:test";
19
- import assert from "node:assert/strict";
20
- import { writeFileSync, rmSync, existsSync, readFileSync } from "fs";
21
- import { join } from "path";
22
- import { tmpdir } from "os";
23
-
24
- const HEALTH_FILE = join(tmpdir(), "clash-health.json");
25
-
26
- /** 清理并写 health 文件(模拟 daemon 写入) */
27
- function writeHealthFile(extra) {
28
- writeFileSync(
29
- HEALTH_FILE,
30
- JSON.stringify(
31
- {
32
- node: "🇺🇸 美国01",
33
- healthy: true,
34
- lastCheck: new Date().toISOString(),
35
- ...extra,
36
- },
37
- null,
38
- 2,
39
- ),
40
- );
41
- }
42
-
43
- /** 生成一个常驻假命令(测试业务进程) */
44
- function fakeCmd(id) {
45
- return {
46
- id,
47
- name: id,
48
- command: process.execPath,
49
- args: ["-e", "setInterval(()=>{},1000)"],
50
- enabled: true,
51
- };
52
- }
53
-
54
- function cleanup() {
55
- try {
56
- rmSync(HEALTH_FILE, { force: true });
57
- } catch {}
58
- }
59
-
60
- test("T1-T3: daemon 切换信号(_markSwitchTime 递增 + _writeHealth 带出)", async () => {
61
- const { ClashDaemon } = await import("../src/clash/daemon.js");
62
- const ipc = await import("../src/clash/ipc.js");
63
- cleanup();
64
- try {
65
- const d = new ClashDaemon({ dryRun: true });
66
- const c = { proxyPort: 0 };
67
-
68
- // T3: 未切换 → restartSeq = 0
69
- d._writeHealth(c, { node: "A", healthy: true });
70
- let h = ipc.readHealth(0);
71
- assert.equal(h.restartSeq, 0, "未切换时 restartSeq 应为 0");
72
-
73
- // T1: 首次切换 → 1
74
- d._markSwitchTime(c);
75
- assert.equal(c._restartSeq, 1, "首次切换 restartSeq 应为 1");
76
- d._markSwitchTime(c);
77
- assert.equal(c._restartSeq, 2, "连续切换 restartSeq 递增");
78
-
79
- // T2: _writeHealth 带出 restartSeq
80
- d._writeHealth(c, { node: "B", healthy: true, lastSwitch: "A → B" });
81
- h = ipc.readHealth(0);
82
- assert.equal(h.restartSeq, 2, "health 文件应携带 restartSeq=2");
83
- assert.equal(h.lastSwitch, "A → B");
84
- } finally {
85
- cleanup();
86
- }
87
- });
88
-
89
- test("T4-T6: ProcessManager.restartAll 先停后起", async () => {
90
- const { ProcessManager } = await import("../src/watchdog/process-manager.js");
91
- const pm = new ProcessManager();
92
- try {
93
- // T6: 空集
94
- const empty = await pm.restartAll("测试");
95
- assert.deepEqual(empty, { restarted: 0 }, "无运行命令应返回 0");
96
-
97
- pm.startCommand(fakeCmd("proc-a"));
98
- await new Promise((r) => setTimeout(r, 500));
99
- assert.equal(pm.runningCount, 1);
100
- const oldPid = pm.processes.get("proc-a").child.pid;
101
-
102
- // T4: 先停后起,pid 变化、数量恢复
103
- const r = await pm.restartAll("代理切换(测试)");
104
- assert.equal(r.restarted, 1, "应重启 1 个命令");
105
- assert.equal(pm.runningCount, 1, "重启后数量恢复");
106
- const newPid = pm.processes.get("proc-a").child.pid;
107
- assert.notEqual(newPid, oldPid, "重启后应是新进程(pid 不同)");
108
-
109
- // T5: stopped 的命令不参与重启
110
- pm.startCommand(fakeCmd("proc-b"));
111
- await new Promise((r) => setTimeout(r, 500));
112
- pm.stopCommand("proc-b");
113
- const r2 = await pm.restartAll("二次重启");
114
- assert.equal(r2.restarted, 1, "只重启 running 的 1 个,stopped 的不算");
115
- assert.equal(pm.runningCount, 1);
116
- assert.ok(!pm.processes.has("proc-b"), "proc-b 保持停止状态");
117
- } finally {
118
- pm.stopAll();
119
- }
120
- });
121
-
122
- test("T7-T13: Agent._checkProxySwitch 感知切换(默认禁用,开启后才重启)", async () => {
123
- const { WatchdogAgent } = await import("../src/watchdog/agent.js");
124
- cleanup();
125
- const agent = new WatchdogAgent({
126
- serverUrl: "http://127.0.0.1:9",
127
- clientId: "test-switch",
128
- intervalMs: 60000,
129
- spawnDaemon: false,
130
- });
131
- try {
132
- // 准备一个运行中的假业务命令
133
- agent.processManager.startCommand(fakeCmd("proc-c"));
134
- await new Promise((r) => setTimeout(r, 500));
135
- const pid0 = agent.processManager.processes.get("proc-c").child.pid;
136
-
137
- // T7: 首次读取建基线,不触发
138
- writeHealthFile({ restartSeq: 0, lastSwitch: "初始化" });
139
- await agent._checkProxySwitch();
140
- assert.equal(
141
- agent.processManager.processes.get("proc-c").child.pid,
142
- pid0,
143
- "首次读取仅建基线,不应重启",
144
- );
145
-
146
- // T10: seq 不变不触发
147
- writeHealthFile({ restartSeq: 0, lastSwitch: "初始化" });
148
- await agent._checkProxySwitch();
149
- assert.equal(
150
- agent.processManager.processes.get("proc-c").child.pid,
151
- pid0,
152
- "seq 不变不应重启",
153
- );
154
-
155
- // T11: 默认禁用时 seq 变大 → 不触发(但基线已跟随更新)
156
- writeHealthFile({ restartSeq: 1, lastSwitch: "禁用期间切换: A → B" });
157
- await agent._checkProxySwitch();
158
- assert.equal(
159
- agent.processManager.processes.get("proc-c").child.pid,
160
- pid0,
161
- "默认禁用时检测到切换也不应重启",
162
- );
163
-
164
- // T12: 开启开关后,seq 仍为 1(无新切换)→ 不补触发历史切换
165
- agent._applySwitchRestartConfig({ enabled: true });
166
- await agent._checkProxySwitch();
167
- assert.equal(
168
- agent.processManager.processes.get("proc-c").child.pid,
169
- pid0,
170
- "开启开关后不应补触发开启前的历史切换",
171
- );
172
-
173
- // T8: 开启后 seq 1→2 → 触发强制重启
174
- writeHealthFile({ restartSeq: 2, lastSwitch: "开启后切换: B → C" });
175
- await agent._checkProxySwitch();
176
- const pid1 = agent.processManager.processes.get("proc-c").child.pid;
177
- assert.notEqual(pid1, pid0, "开启后检测到切换应强制重启(pid 变化)");
178
- assert.equal(agent.processManager.runningCount, 1, "重启后数量恢复");
179
-
180
- // T9: seq 变小(daemon 重启归零)→ 不触发
181
- writeHealthFile({ restartSeq: 0, lastSwitch: "daemon 重启" });
182
- await agent._checkProxySwitch();
183
- assert.equal(
184
- agent.processManager.processes.get("proc-c").child.pid,
185
- pid1,
186
- "seq 变小(daemon 重启归零)不应触发",
187
- );
188
-
189
- // T13: 归零后再次真实切换(0 → 1)应重新触发
190
- writeHealthFile({ restartSeq: 1, lastSwitch: "再次切换: C → D" });
191
- await agent._checkProxySwitch();
192
- const pid2 = agent.processManager.processes.get("proc-c").child.pid;
193
- assert.notEqual(pid2, pid1, "归零后再次切换应重新触发");
194
- } finally {
195
- await agent.stop();
196
- cleanup();
197
- }
198
- });