tt-help-cli-ycl 1.4.183 → 1.4.185
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/package.json +1 -1
- package/scripts/test-candidate-retry.mjs +496 -0
- package/scripts/test-crash-circuit-breaker.mjs +250 -0
- package/scripts/test-error-auto-reset.mjs +103 -0
- package/scripts/test-error-report.mjs +215 -0
- package/scripts/test-login-pool-acquire-order.mjs +92 -26
- package/scripts/test-login-pool-faceban-e2e.mjs +131 -0
- package/scripts/test-login-pool-faceban.mjs +235 -0
- package/scripts/test-login-pool-ui.mjs +2 -1
- package/scripts/test-login-pool.mjs +97 -54
- package/scripts/test-process-manager-label.mjs +145 -0
- package/src/cli/explore.js +171 -25
- package/src/cli/search.js +171 -18
- package/src/lib/browser/health-checker.js +19 -3
- package/src/lib/browser/page.js +23 -0
- package/src/lib/error-report.js +119 -0
- package/src/lib/login-pool-client.js +123 -0
- package/src/watch/data-store.js +25 -0
- package/src/watch/login-pool.js +118 -81
- package/src/watch/public/app.js +7 -2
- package/src/watch/public/index.html +2 -1
- package/src/watch/server.js +53 -8
- package/src/watchdog/process-manager.js +71 -6
- package/src/watchdog/reporter.js +2 -0
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 登录池取号排序 + 换因计数 + 连败计数 + 清空日志 单测
|
|
3
|
+
* (2026-11-04 对齐 7be35e8 账号中心简化 + 1cda80f 连败计数改算法)
|
|
3
4
|
*
|
|
4
5
|
* 覆盖:
|
|
5
6
|
* B2 取号排序五条: 非同类优先生效 / 全同类仍发号 / 全冷却退全池 / 从未用过最前 / lastAppName 空值中性
|
|
6
7
|
* B1/B3 lastAppName 落账号本体(release 后保留)+ acquired history detail 带 appName
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* prev prevAppName 前一任程序记录
|
|
9
|
+
* C2 换号原因: lastSwitchReason/switchStreak(同因连击 +1 / 换因重置 1 / 未知 code 原样存 / 不带 reason 不动)
|
|
10
|
+
* E 连败计数 errStreak: 挂在被换掉的旧号(banned/captcha/network +1;scheduled 清零;中性/无因不动)
|
|
11
|
+
* C1 清空日志: history 清成一条留痕 + useCount/switchStats/换因计数/errStreak 清零 + 不动 lastUsedAt/lastAppName
|
|
9
12
|
*
|
|
10
|
-
* 跑法:
|
|
13
|
+
* 跑法: node scripts/test-login-pool-acquire-order.mjs
|
|
14
|
+
* (自建临时 HOME 并在 import 前覆盖环境变量——绝不读写真实 ~/.tt-help-watchdog-logins.json)
|
|
11
15
|
*/
|
|
12
16
|
import assert from "assert";
|
|
17
|
+
import { mkdtempSync } from "fs";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { join } from "path";
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
// ── 测试基建:临时 HOME 隔离 ──
|
|
22
|
+
// login-pool.js 的 POOL_FILE 在模块加载期由 os.homedir() 计算(POSIX 下 homedir() 实时读
|
|
23
|
+
// process.env.HOME),故必须在动态 import 之前覆盖 HOME/USERPROFILE;生产代码零改动即可隔离。
|
|
24
|
+
process.env.HOME = mkdtempSync(join(tmpdir(), "loginpool-order-test-"));
|
|
25
|
+
process.env.USERPROFILE = process.env.HOME; // Windows 语义兜底
|
|
19
26
|
|
|
20
27
|
const {
|
|
21
28
|
getLoginPoolView,
|
|
@@ -117,7 +124,7 @@ console.log("\n[B2-4] 全冷却退全池兜底");
|
|
|
117
124
|
acquireAccount({ userId: "O4b", appName: "explore" });
|
|
118
125
|
releaseAccount({ id: findAcc("ordE1").id });
|
|
119
126
|
releaseAccount({ id: findAcc("ordF1").id });
|
|
120
|
-
// 全部压进 30min
|
|
127
|
+
// 全部压进 30min 休息窗内
|
|
121
128
|
backdate("ordE1", 5 * 60 * 1000);
|
|
122
129
|
backdate("ordF1", 8 * 60 * 1000);
|
|
123
130
|
const got = acquireAccount({ userId: "O4c", appName: "explore" });
|
|
@@ -129,13 +136,16 @@ console.log("\n[B2-4] 全冷却退全池兜底");
|
|
|
129
136
|
// ── B2-5: lastAppName 空值中性 ──
|
|
130
137
|
console.log("\n[B2-5] lastAppName 空值按非同类 / 请求无 appName 时键中性化");
|
|
131
138
|
{
|
|
132
|
-
//
|
|
139
|
+
// 两个号都先 acquire+release(都盖 lastUsedAt → K1 打平),再清空其一的 lastAppName,
|
|
140
|
+
// 隔离出纯粹的「lastAppName 空 = 按非同类」K2 分支
|
|
133
141
|
add("ordExploreApp");
|
|
134
142
|
add("ordEmptyApp");
|
|
135
|
-
acquireAccount({ userId: "O5", appName: "explore" });
|
|
143
|
+
acquireAccount({ userId: "O5", appName: "explore" }); // ordExploreApp(创建序在前)
|
|
144
|
+
acquireAccount({ userId: "O5a", appName: "search" }); // ordEmptyApp(唯一剩余 pool)
|
|
136
145
|
releaseAccount({ id: findAcc("ordExploreApp").id });
|
|
146
|
+
releaseAccount({ id: findAcc("ordEmptyApp").id });
|
|
137
147
|
for (const a of _accountsInternal()) {
|
|
138
|
-
if (a.username === "ordEmptyApp") a.lastAppName = null; //
|
|
148
|
+
if (a.username === "ordEmptyApp") a.lastAppName = null; // 模拟存量账号(无 lastAppName)
|
|
139
149
|
}
|
|
140
150
|
backdate("ordEmptyApp", 32 * 60 * 1000); // eligible、lastAppName 空
|
|
141
151
|
backdate("ordExploreApp", 31 * 60 * 1000); // eligible、同类 explore
|
|
@@ -180,7 +190,6 @@ console.log("\n[prev] 前一任程序记录(页面「上次使用」列数据
|
|
|
180
190
|
const got = acquireAccount({ userId: "P2", appName: "search" });
|
|
181
191
|
check("第二次取号后 lastAppName=当前程序", got.lastAppName === "search", `got ${got.lastAppName}`);
|
|
182
192
|
check("prevAppName=前一任(explore→search 切换可追溯)", got.prevAppName === "explore", `got ${got.prevAppName}`);
|
|
183
|
-
const first = getLoginPoolView().accounts.find((x) => x.username === "ordBatchA");
|
|
184
193
|
// 从未换过程序的账号 prevAppName 应为 null(首次取号无前一任)
|
|
185
194
|
add("ordPrevFirst");
|
|
186
195
|
const g2 = acquireAccount({ userId: "P3", appName: "explore" });
|
|
@@ -188,45 +197,99 @@ console.log("\n[prev] 前一任程序记录(页面「上次使用」列数据
|
|
|
188
197
|
retireAll();
|
|
189
198
|
}
|
|
190
199
|
|
|
191
|
-
// ── C2:
|
|
192
|
-
console.log("\n[C2]
|
|
200
|
+
// ── C2: 换号原因(lastSwitchReason/switchStreak,2026-11-03 替代 switchStats 终身累加) ──
|
|
201
|
+
console.log("\n[C2] 换因计数:最近原因+连续次数(同因连击+1 / 换因重置1 / 未知code原样存 / 不带不动)");
|
|
193
202
|
{
|
|
194
203
|
add("ordReason1");
|
|
195
204
|
let got = acquireAccount({ userId: "O7", appName: "explore", reason: "banned" });
|
|
196
|
-
check("显式上报 banned
|
|
205
|
+
check("显式上报 banned → lastSwitchReason=banned ×1",
|
|
206
|
+
got.lastSwitchReason === "banned" && got.switchStreak === 1,
|
|
207
|
+
JSON.stringify({ r: got.lastSwitchReason, s: got.switchStreak }));
|
|
197
208
|
const hist = got.history.find((h) => h.event === "acquired");
|
|
198
209
|
check("acquired history detail 带原因", hist && hist.detail.includes("原因 banned"), JSON.stringify(hist && hist.detail));
|
|
199
210
|
|
|
200
211
|
releaseAccount({ id: got.id });
|
|
201
212
|
backdate("ordReason1", 31 * 60 * 1000);
|
|
202
213
|
got = acquireAccount({ userId: "O7b", appName: "explore", reason: "banned" });
|
|
203
|
-
check("
|
|
214
|
+
check("同因连击 +1(banned ×2)", got.lastSwitchReason === "banned" && got.switchStreak === 2,
|
|
215
|
+
JSON.stringify({ r: got.lastSwitchReason, s: got.switchStreak }));
|
|
204
216
|
|
|
205
217
|
releaseAccount({ id: got.id });
|
|
206
218
|
backdate("ordReason1", 31 * 60 * 1000);
|
|
207
219
|
got = acquireAccount({ userId: "O7c", appName: "explore", reason: "weird_reason" });
|
|
208
|
-
check("
|
|
220
|
+
check("换因重置为 1,未知 code 原样存不拒绝",
|
|
221
|
+
got.lastSwitchReason === "weird_reason" && got.switchStreak === 1,
|
|
222
|
+
JSON.stringify({ r: got.lastSwitchReason, s: got.switchStreak }));
|
|
223
|
+
|
|
224
|
+
releaseAccount({ id: got.id });
|
|
225
|
+
backdate("ordReason1", 31 * 60 * 1000);
|
|
226
|
+
got = acquireAccount({ userId: "O7d", appName: "explore" }); // 不带 reason
|
|
227
|
+
check("不带 reason 不计(保留上次值,无「未上报」桶)",
|
|
228
|
+
got.lastSwitchReason === "weird_reason" && got.switchStreak === 1,
|
|
229
|
+
JSON.stringify({ r: got.lastSwitchReason, s: got.switchStreak }));
|
|
230
|
+
retireAll();
|
|
231
|
+
}
|
|
209
232
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
// ── E: 连败计数 errStreak(1cda80f:挂在被换掉的旧号上,按换号原因码分类) ──
|
|
234
|
+
console.log("\n[E] errStreak 连败计数:错误类+1 / scheduled 清零 / 中性与无因不动");
|
|
235
|
+
{
|
|
236
|
+
add("ordErrP");
|
|
237
|
+
let g = acquireAccount({ userId: "E1", appName: "explore" });
|
|
238
|
+
check("前置:E1 持 ordErrP", g.username === "ordErrP", `取到 ${g.username}`);
|
|
239
|
+
add("ordErrA");
|
|
240
|
+
g = acquireAccount({ userId: "E1", appName: "explore", reason: "banned" }); // 唯一 pool=ordErrA
|
|
241
|
+
check("换号取到新号 ordErrA", g.username === "ordErrA", `取到 ${g.username}`);
|
|
242
|
+
check("banned 换号:旧号 ordErrP errStreak=+1", findAcc("ordErrP").errStreak === 1, `got ${findAcc("ordErrP").errStreak}`);
|
|
243
|
+
check("新号不背旧账(ordErrA errStreak=0)", (findAcc("ordErrA").errStreak || 0) === 0);
|
|
244
|
+
check("旧号释放历史附原因码", (findAcc("ordErrP").history || []).some((h) => h.event === "released" && h.detail.includes("banned")));
|
|
245
|
+
|
|
246
|
+
// 同号连败:ordErrP 回到 E1 手上再被换掉一次 → 1→2(连续非累计基数=每次释放事件各计)
|
|
247
|
+
backdate("ordErrP", 31 * 60 * 1000);
|
|
248
|
+
g = acquireAccount({ userId: "E1", appName: "explore" }); // 唯一 eligible pool=ordErrP;ordErrA 无因中性释放
|
|
249
|
+
check("E1 换回 ordErrP", g.username === "ordErrP", `取到 ${g.username}`);
|
|
250
|
+
check("无 reason 释放 ordErrA:errStreak 中性不动", (findAcc("ordErrA").errStreak || 0) === 0);
|
|
251
|
+
add("ordErrB");
|
|
252
|
+
g = acquireAccount({ userId: "E1", appName: "explore", reason: "banned" }); // 旧号=ordErrP
|
|
253
|
+
check("ordErrP 连败 +1 → 2(同因连击累加)", findAcc("ordErrP").errStreak === 2, `got ${findAcc("ordErrP").errStreak}`);
|
|
254
|
+
|
|
255
|
+
// scheduled=干满周期健康证明 → 清零
|
|
256
|
+
backdate("ordErrP", 31 * 60 * 1000);
|
|
257
|
+
g = acquireAccount({ userId: "E1", appName: "explore" }); // 再持 P;ordErrB 无因中性释放
|
|
258
|
+
add("ordErrC");
|
|
259
|
+
g = acquireAccount({ userId: "E1", appName: "explore", reason: "scheduled" }); // 旧号=ordErrP
|
|
260
|
+
check("scheduled 换号:ordErrP errStreak 清零", findAcc("ordErrP").errStreak === 0, `got ${findAcc("ordErrP").errStreak}`);
|
|
261
|
+
|
|
262
|
+
// 中性原因(inject_fail)不动
|
|
263
|
+
backdate("ordErrP", 31 * 60 * 1000);
|
|
264
|
+
g = acquireAccount({ userId: "E1", appName: "explore" }); // 再持 P;ordErrC 无因中性释放
|
|
265
|
+
add("ordErrD");
|
|
266
|
+
g = acquireAccount({ userId: "E1", appName: "explore", reason: "inject_fail" }); // 旧号=ordErrP
|
|
267
|
+
check("inject_fail 中性:errStreak 保持 0", findAcc("ordErrP").errStreak === 0, `got ${findAcc("ordErrP").errStreak}`);
|
|
214
268
|
retireAll();
|
|
215
269
|
}
|
|
216
270
|
|
|
217
271
|
// ── C1: 清空日志 ──
|
|
218
|
-
console.log("\n[C1] 清空日志:留痕一条 + useCount/switchStats 清零 + 不动休息窗/最近程序");
|
|
272
|
+
console.log("\n[C1] 清空日志:留痕一条 + useCount/switchStats/换因计数/errStreak 清零 + 不动休息窗/最近程序");
|
|
219
273
|
{
|
|
220
274
|
add("ordClear");
|
|
221
275
|
let got = acquireAccount({ userId: "O8", appName: "explore", reason: "banned" });
|
|
222
276
|
assert(got.useCount === 1);
|
|
223
|
-
|
|
277
|
+
// 注入连败基数与遗留 switchStats,验证清空归零
|
|
278
|
+
for (const a of _accountsInternal()) {
|
|
279
|
+
if (a.username === "ordClear") { a.errStreak = 3; a.switchStats = { legacy: 9 }; }
|
|
280
|
+
}
|
|
224
281
|
releaseAccount({ id: got.id });
|
|
282
|
+
// 基线在 release 之后读取(release 会重盖 lastUsedAt,拿 acquire 时刻的值比较会跨毫秒必挂——
|
|
283
|
+
// 本断言语义是「清日志不改 lastUsedAt」,比较对象应是 pre-clear vs post-clear)
|
|
284
|
+
const lastUsedBefore = findAcc("ordClear").lastUsedAt;
|
|
225
285
|
const cleared = clearAccountHistory({ id: got.id, by: "page" });
|
|
226
286
|
check("history 清成一条留痕", cleared.history.length === 1, `len=${cleared.history.length}`);
|
|
227
287
|
check("留痕含来源 page", cleared.history[0].detail.includes("page"), JSON.stringify(cleared.history[0]));
|
|
228
288
|
check("useCount 归零", cleared.useCount === 0);
|
|
229
|
-
check("switchStats
|
|
289
|
+
check("switchStats 清空(遗留字段一并清)", !cleared.switchStats || Object.keys(cleared.switchStats).length === 0);
|
|
290
|
+
check("lastSwitchReason 清空 + switchStreak 归零", cleared.lastSwitchReason === null && cleared.switchStreak === 0,
|
|
291
|
+
JSON.stringify({ r: cleared.lastSwitchReason, s: cleared.switchStreak }));
|
|
292
|
+
check("errStreak 归零", (cleared.errStreak || 0) === 0, `got ${cleared.errStreak}`);
|
|
230
293
|
check("不动 lastUsedAt(休息窗保留)", cleared.lastUsedAt === lastUsedBefore,
|
|
231
294
|
`before=${lastUsedBefore} after=${cleared.lastUsedAt}`);
|
|
232
295
|
check("不动 lastAppName", cleared.lastAppName === "explore");
|
|
@@ -248,6 +311,7 @@ console.log("\n[批量清空] clearAllAccountHistory 一键全清");
|
|
|
248
311
|
const snap = {};
|
|
249
312
|
for (const a of _accountsInternal()) {
|
|
250
313
|
if (a.username === "ordBatchA") snap.A = { lastUsedAt: a.lastUsedAt, lastAppName: a.lastAppName };
|
|
314
|
+
if (a.username === "ordBatchB") a.errStreak = 2; // 注入连败基数,验证批量清零
|
|
251
315
|
}
|
|
252
316
|
const r = clearAllAccountHistory({ by: "page" });
|
|
253
317
|
check("返回清空数量(含历史遗留账号)", r.cleared >= 2, `cleared=${r.cleared}`);
|
|
@@ -256,6 +320,8 @@ console.log("\n[批量清空] clearAllAccountHistory 一键全清");
|
|
|
256
320
|
check(`${u}: history 清成一条批量留痕`, a.history.length === 1 && a.history[0].detail.includes("批量"),
|
|
257
321
|
JSON.stringify(a.history));
|
|
258
322
|
check(`${u}: useCount/switchStats 清零`, a.useCount === 0 && Object.keys(a.switchStats || {}).length === 0);
|
|
323
|
+
check(`${u}: 换因计数/errStreak 清零`, a.lastSwitchReason === null && a.switchStreak === 0 && (a.errStreak || 0) === 0,
|
|
324
|
+
JSON.stringify({ r: a.lastSwitchReason, s: a.switchStreak, e: a.errStreak }));
|
|
259
325
|
}
|
|
260
326
|
const a = findAcc("ordBatchA");
|
|
261
327
|
check("批量清空不动 lastUsedAt/lastAppName", a.lastUsedAt === snap.A.lastUsedAt && a.lastAppName === snap.A.lastAppName);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* faceBans HTTP E2E — 真实 watch 服务(不起浏览器;页面用 HTML 字符串断言 + 内嵌 JS 语法编译)
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* 1. POST /api/login-pool/face 标挂/恢复(probe/manual source、by 落库)→ GET /api/login-pool 透出
|
|
6
|
+
* 2. 参数校验走 HTTP 400(非法 face / source / 缺 ban / 未知账号)
|
|
7
|
+
* 3. acquire HTTP 全链路:explore 取号跳过 ban 号、search 面照发、池空文案带面维度
|
|
8
|
+
* 4. /login-pool 页面(登录门禁后)含面标注列/标挂恢复按钮/faceDlg 函数;内嵌 JS 可编译
|
|
9
|
+
* 5. 服务端 FACE 日志行格式(仿 UPDATE 行风格)
|
|
10
|
+
*
|
|
11
|
+
* 跑法: node scripts/test-login-pool-faceban-e2e.mjs(自行起停服务,临时 HOME 零污染)
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
import { mkdtempSync } from "fs";
|
|
15
|
+
import { tmpdir } from "os";
|
|
16
|
+
import { join, dirname } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const HOME = mkdtempSync(join(tmpdir(), "faceban-e2e-"));
|
|
21
|
+
const PORT = 3913;
|
|
22
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
23
|
+
|
|
24
|
+
let pass = 0, fail = 0;
|
|
25
|
+
const check = (name, cond, extra = "") => {
|
|
26
|
+
cond ? (pass++, console.log(` ✓ ${name}`)) : (fail++, console.log(` ✗ ${name} ${extra}`));
|
|
27
|
+
};
|
|
28
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
29
|
+
const post = async (p, body, cookie) => {
|
|
30
|
+
const r = await fetch(BASE + p, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json", ...(cookie ? { Cookie: cookie } : {}) },
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
});
|
|
35
|
+
return { status: r.status, json: await r.json().catch(() => ({})), setCookie: r.headers.get("set-cookie") };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ── 起 watch ──
|
|
39
|
+
const srvErr = [];
|
|
40
|
+
const srv = spawn("node", [join(root, "src", "main.js"), "watch", "-o", join(HOME, "t.db"), "-p", String(PORT)], {
|
|
41
|
+
cwd: root, env: { ...process.env, HOME }, stdio: ["ignore", "ignore", "pipe"],
|
|
42
|
+
});
|
|
43
|
+
srv.stderr.on("data", (d) => srvErr.push(String(d)));
|
|
44
|
+
let ready = false;
|
|
45
|
+
for (let i = 0; i < 40 && !ready; i++) {
|
|
46
|
+
try { await fetch(BASE + "/api/stats"); ready = true; } catch { await sleep(500); }
|
|
47
|
+
}
|
|
48
|
+
check("watch 就绪(3913)", ready);
|
|
49
|
+
try {
|
|
50
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
51
|
+
const d = new Date();
|
|
52
|
+
const pwd = `tt@${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
|
53
|
+
const login = await post("/api/login", { password: pwd });
|
|
54
|
+
const cookie = (login.setCookie || "").split(";")[0];
|
|
55
|
+
check("页面门禁登录", login.status === 200 && cookie.startsWith("watch_token="), JSON.stringify(login.json).slice(0, 100));
|
|
56
|
+
|
|
57
|
+
// ── 入池三号 ──
|
|
58
|
+
const mk = (sid, uid) => [
|
|
59
|
+
{ name: "sessionid", value: sid, domain: ".tiktok.com", expires: Date.now() / 1000 + 86400 },
|
|
60
|
+
{ name: "unique_id", value: uid, domain: ".tiktok.com" },
|
|
61
|
+
];
|
|
62
|
+
const a1 = await post("/api/login-pool/add", { username: "e2e-good", cookies: mk("1", "e2e-good") });
|
|
63
|
+
const a2 = await post("/api/login-pool/add", { username: "e2e-expban", cookies: mk("2", "e2e-expban") });
|
|
64
|
+
const a3 = await post("/api/login-pool/add", { username: "e2e-seaban", cookies: mk("3", "e2e-seaban") });
|
|
65
|
+
check("入池 3 号", a1.json.ok && a2.json.ok && a3.json.ok);
|
|
66
|
+
|
|
67
|
+
// ── face 标注 API ──
|
|
68
|
+
const f1 = await post("/api/login-pool/face", { username: "e2e-expban", face: "explore", ban: true, source: "probe", by: "2026-11-02排查" }, cookie);
|
|
69
|
+
check("face 标挂(probe)", f1.status === 200 && f1.json.account.faceBans.explore.ban === true && f1.json.account.faceBans.explore.source === "probe");
|
|
70
|
+
const f2 = await post("/api/login-pool/face", { id: a3.json.account.id, face: "search", ban: true, source: "manual", by: "page" }, cookie);
|
|
71
|
+
check("face 标挂(manual, id 定位)", f2.status === 200 && f2.json.account.faceBans.search.ban === true);
|
|
72
|
+
const view = await (await fetch(BASE + "/api/login-pool")).json();
|
|
73
|
+
const vu = view.accounts.find((x) => x.username === "e2e-expban");
|
|
74
|
+
check("GET /api/login-pool 透出 faceBans", vu && vu.faceBans && vu.faceBans.explore && vu.faceBans.explore.ban === true && vu.faceBans.search === null);
|
|
75
|
+
check("history 落痕 faceBan", (vu.history || []).some((h) => h.event === "faceBan" && /explore=禁用/.test(h.detail)));
|
|
76
|
+
|
|
77
|
+
// ── 校验走 HTTP 400 ──
|
|
78
|
+
const bad1 = await post("/api/login-pool/face", { username: "e2e-good", face: "video", ban: true }, cookie);
|
|
79
|
+
const bad2 = await post("/api/login-pool/face", { username: "e2e-good", face: "explore", ban: true, source: "auto" }, cookie);
|
|
80
|
+
const bad3 = await post("/api/login-pool/face", { username: "nobody", face: "explore", ban: true }, cookie);
|
|
81
|
+
check("非法 face → 400", bad1.status === 400 && /非法 face/.test(bad1.json.error || ""));
|
|
82
|
+
check("非法 source → 400", bad2.status === 400 && /非法 source/.test(bad2.json.error || ""));
|
|
83
|
+
check("未知账号 → 400", bad3.status === 400 && /账号不存在/.test(bad3.json.error || ""));
|
|
84
|
+
|
|
85
|
+
// ── acquire 全链路 ──
|
|
86
|
+
const acq = async (appName) => {
|
|
87
|
+
const r = await post("/api/login-pool/acquire", { userId: "E2E-" + Math.random().toString(36).slice(2, 6), appName });
|
|
88
|
+
return r;
|
|
89
|
+
};
|
|
90
|
+
const w1 = await acq("explore");
|
|
91
|
+
check("explore 取号跳过 ban 号", w1.json.account && w1.json.account.username === "e2e-good", JSON.stringify(w1.json).slice(0, 120));
|
|
92
|
+
const rel = await post("/api/login-pool/release", { id: w1.json.account.id }, cookie);
|
|
93
|
+
check("release 还号", rel.json.ok);
|
|
94
|
+
const w2 = await acq("search");
|
|
95
|
+
check("search 取号照发 explore-ban 号(面独立)", w2.json.account && w2.json.account.username === "e2e-expban", JSON.stringify(w2.json).slice(0, 120));
|
|
96
|
+
await post("/api/login-pool/release", { id: w2.json.account.id }, cookie);
|
|
97
|
+
// 池空文案:search-ban 的 e2e-seaban 仍在池;explore-ban 的也回池了;只禁 search 面后 explore 仍可发
|
|
98
|
+
// 构造 search 面池空:把 good 与 expban 都禁 search
|
|
99
|
+
await post("/api/login-pool/face", { username: "e2e-good", face: "search", ban: true, source: "manual" }, cookie);
|
|
100
|
+
await post("/api/login-pool/face", { username: "e2e-expban", face: "search", ban: true, source: "manual" }, cookie);
|
|
101
|
+
const w3 = await acq("search");
|
|
102
|
+
check("search 池空文案带面维度", w3.status === 400 && /池中暂无 search 面可用账号(共3 个,面禁用 3 个,租约中 0 个)/.test(w3.json.error || ""), w3.json.error);
|
|
103
|
+
const w4 = await acq("explore");
|
|
104
|
+
check("explore 同池不空(面独立,K1 发从未用过的 seaban)",
|
|
105
|
+
w4.json.account && w4.json.account.username === "e2e-seaban" && !(w4.json.account.faceBans.explore && w4.json.account.faceBans.explore.ban),
|
|
106
|
+
JSON.stringify(w4.json).slice(0, 120));
|
|
107
|
+
await post("/api/login-pool/release", { id: w4.json.account.id }, cookie);
|
|
108
|
+
|
|
109
|
+
// ── 页面 ──
|
|
110
|
+
const page = await (await fetch(BASE + "/login-pool", { headers: { Cookie: cookie } })).text();
|
|
111
|
+
check("页面含 explore/search 列头", page.includes(">explore</th>") && page.includes(">search</th>"));
|
|
112
|
+
check("页面含标挂/恢复按钮 + faceDlg", page.includes(">标挂<") === false && page.includes(">标挂</button>") && page.includes(">恢复</button>") && page.includes("async function faceDlg("));
|
|
113
|
+
check("页面调 face API", page.includes("'/api/login-pool/face'"));
|
|
114
|
+
check("页面含面标注图例", page.includes("面标注图例"));
|
|
115
|
+
// 内嵌 JS 语法可编译(提取 <script> 块 new Function 编译,不执行)
|
|
116
|
+
const m = page.match(/<script>([\s\S]*?)<\/script>/);
|
|
117
|
+
let compiled = false;
|
|
118
|
+
try { new Function(m[1]); compiled = true; } catch (e) { console.log(" 编译错误:", e.message); }
|
|
119
|
+
check("页面内嵌 JS 语法编译通过", compiled);
|
|
120
|
+
check("页面无 faceBans 明文泄漏风险(无 cookies 字段渲染)", !page.includes("acc.cookies"));
|
|
121
|
+
|
|
122
|
+
// ── FACE 日志行 ──
|
|
123
|
+
await sleep(300);
|
|
124
|
+
const logs = srvErr.join("");
|
|
125
|
+
check("服务端 FACE 日志行(仿 UPDATE 风格)", /\[登录池 .+\] FACE: e2e-expban explore=禁用 \(probe · 2026-11-02排查\)/.test(logs), logs.split("\n").filter((l) => l.includes("FACE")).join(" | ").slice(0, 200));
|
|
126
|
+
} finally {
|
|
127
|
+
srv.kill("SIGTERM");
|
|
128
|
+
await sleep(500);
|
|
129
|
+
}
|
|
130
|
+
console.log(`\n═══ faceBans E2E: ${pass} passed, ${fail} failed ═══`);
|
|
131
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* faceBans(账号面标注 + 派号过滤)单元测试 — 模块级直驱(不起 HTTP)
|
|
3
|
+
*
|
|
4
|
+
* 覆盖(2026-11-02 任务卡验证要求):
|
|
5
|
+
* 1. faceBans 写读:setAccountFace 写入 → publicView 透出 → 池文件持久化(saveNow 立即落盘)
|
|
6
|
+
* 2. 参数校验:face/source 枚举、ban 必填 boolean、id/username 定位二选一
|
|
7
|
+
* 3. acquire 按面过滤:explore 禁用号 explore 取号跳过;另一面(search)不受影响照发
|
|
8
|
+
* 4. 缺失/null = 未验证 = 默认可用;ban=false 恢复后重新可发
|
|
9
|
+
* 5. 面禁用导致池空:文案带面维度(共X 个,面禁用 Y 个,租约中 Z 个)
|
|
10
|
+
* 6. 排序回归:K1 从未用过最前 / K2 非同类优先 / K3 lastUsedAt 升序在过滤后不变;
|
|
11
|
+
* 面禁用号即使排序键最优也不得中签
|
|
12
|
+
* 7. 与 status 正交:leased 号可标注(租约不动,faceBans 落本体)
|
|
13
|
+
*
|
|
14
|
+
* 跑法: HOME=$(mktemp -d) node scripts/test-login-pool-faceban.mjs
|
|
15
|
+
*/
|
|
16
|
+
import assert from "assert";
|
|
17
|
+
import { readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
|
|
20
|
+
const HOME = process.env.HOME;
|
|
21
|
+
if (!HOME || HOME === "/") {
|
|
22
|
+
console.error("请用隔离 HOME 运行: HOME=$(mktemp -d) node scripts/test-login-pool-faceban.mjs");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
getLoginPoolView,
|
|
28
|
+
addAccount,
|
|
29
|
+
releaseAccount,
|
|
30
|
+
acquireAccount,
|
|
31
|
+
setAccountFace,
|
|
32
|
+
isFaceBanned,
|
|
33
|
+
faceOfAppName,
|
|
34
|
+
_accountsInternal,
|
|
35
|
+
} = await import("../src/watch/login-pool.js");
|
|
36
|
+
|
|
37
|
+
let pass = 0;
|
|
38
|
+
let fail = 0;
|
|
39
|
+
const check = (name, cond, extra = "") => {
|
|
40
|
+
if (cond) {
|
|
41
|
+
pass++;
|
|
42
|
+
console.log(` ✓ ${name}`);
|
|
43
|
+
} else {
|
|
44
|
+
fail++;
|
|
45
|
+
console.log(` ✗ ${name} ${extra}`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const POOL_FILE = join(HOME, ".tt-help-watchdog-logins.json");
|
|
49
|
+
const mkCookies = (sid, uid) => [
|
|
50
|
+
{ name: "sessionid", value: sid, domain: ".tiktok.com", expires: Date.now() / 1000 + 86400 },
|
|
51
|
+
{ name: "unique_id", value: uid, domain: ".tiktok.com" },
|
|
52
|
+
];
|
|
53
|
+
const findAcc = (username) => getLoginPoolView().accounts.find((x) => x.username === username);
|
|
54
|
+
// 节间隔离:全部下架(retired 不参与取号/计数为已停用),每节干净开局
|
|
55
|
+
const retireAll = () => {
|
|
56
|
+
for (const a of _accountsInternal()) {
|
|
57
|
+
a.status = "retired";
|
|
58
|
+
a.lease = null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const backdate = (username, msAgo) => {
|
|
62
|
+
for (const a of _accountsInternal()) {
|
|
63
|
+
if (a.username === username) a.lastUsedAt = Date.now() - msAgo;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ── 1. faceOfAppName 语义 ──
|
|
68
|
+
console.log("\n[1] faceOfAppName:未知 appName 归 explore");
|
|
69
|
+
{
|
|
70
|
+
check("search → search", faceOfAppName("search") === "search");
|
|
71
|
+
check("explore → explore", faceOfAppName("explore") === "explore");
|
|
72
|
+
check("undefined → explore", faceOfAppName(undefined) === "explore");
|
|
73
|
+
check("未知字符串 → explore", faceOfAppName("comments") === "explore");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── 2. 写读 + 持久化 + 校验 ──
|
|
77
|
+
console.log("\n[2] setAccountFace 写读 + saveNow 持久化");
|
|
78
|
+
{
|
|
79
|
+
retireAll();
|
|
80
|
+
const a = addAccount({ cookies: mkCookies("s1", "faceA") });
|
|
81
|
+
const b = addAccount({ cookies: mkCookies("s2", "faceB") });
|
|
82
|
+
|
|
83
|
+
const before = findAcc("faceA");
|
|
84
|
+
check("初始 faceBans 双面均 null(未验证=可用)",
|
|
85
|
+
before.faceBans && before.faceBans.explore === null && before.faceBans.search === null);
|
|
86
|
+
check("isFaceBanned 懒判空:无标注=false", isFaceBanned(_accountsInternal()[0], "explore") === false);
|
|
87
|
+
|
|
88
|
+
const r1 = setAccountFace({ username: "faceA", face: "explore", ban: true, source: "probe", by: "2026-11-02排查" });
|
|
89
|
+
check("ban=true 写入(username 定位)", r1.faceBans.explore && r1.faceBans.explore.ban === true);
|
|
90
|
+
check("source/at/by 落库", r1.faceBans.explore.source === "probe" && typeof r1.faceBans.explore.at === "number" && r1.faceBans.explore.by === "2026-11-02排查");
|
|
91
|
+
check("另一面仍 null", r1.faceBans.search === null);
|
|
92
|
+
check("isFaceBanned=true", isFaceBanned(_accountsInternal().find((x) => x.username === "faceA"), "explore") === true);
|
|
93
|
+
check("search 面不受影响", isFaceBanned(_accountsInternal().find((x) => x.username === "faceA"), "search") === false);
|
|
94
|
+
check("history 落痕 faceBan", (r1.history || []).some((h) => h.event === "faceBan" && /explore=禁用/.test(h.detail)));
|
|
95
|
+
|
|
96
|
+
const r2 = setAccountFace({ id: b.id, face: "search", ban: true, source: "manual", by: "人工" });
|
|
97
|
+
check("ban=true(id 定位)", r2.faceBans.search && r2.faceBans.search.ban === true && r2.faceBans.search.source === "manual");
|
|
98
|
+
|
|
99
|
+
// 恢复(ban=false → ✅ 验证通过记录,不再过滤)
|
|
100
|
+
const r3 = setAccountFace({ username: "faceA", face: "explore", ban: false, source: "manual", by: "page" });
|
|
101
|
+
check("ban=false 恢复(isFaceBanned=false)",
|
|
102
|
+
r3.faceBans.explore && r3.faceBans.explore.ban === false &&
|
|
103
|
+
isFaceBanned(_accountsInternal().find((x) => x.username === "faceA"), "explore") === false);
|
|
104
|
+
check("history 落痕 faceRestore", (r3.history || []).some((h) => h.event === "faceRestore" && /explore=恢复/.test(h.detail)));
|
|
105
|
+
|
|
106
|
+
// saveNow 立即落盘(不走 3s 防抖):连续标注(同进程第二次 setAccountFace)也必须落盘
|
|
107
|
+
const disk = JSON.parse(readFileSync(POOL_FILE, "utf-8"));
|
|
108
|
+
const diskA = disk.accounts.find((x) => x.username === "faceA");
|
|
109
|
+
check("池文件持久化 faceBans(连续两次标注均落盘)", diskA && diskA.faceBans && diskA.faceBans.explore && diskA.faceBans.explore.ban === false && diskA.faceBans.search == null,
|
|
110
|
+
JSON.stringify(diskA && diskA.faceBans));
|
|
111
|
+
|
|
112
|
+
// 参数校验
|
|
113
|
+
const throws = (fn, kw) => {
|
|
114
|
+
try { fn(); return false; } catch (e) { return e.message.includes(kw); }
|
|
115
|
+
};
|
|
116
|
+
check("非法 face 拒绝", throws(() => setAccountFace({ id: "x", face: "video", ban: true }), "非法 face"));
|
|
117
|
+
check("非法 source 拒绝", throws(() => setAccountFace({ id: "x", face: "explore", ban: true, source: "auto" }), "非法 source"));
|
|
118
|
+
check("ban 缺失/非 bool 拒绝", throws(() => setAccountFace({ id: "x", face: "explore" }), "ban 必填"));
|
|
119
|
+
check("账号不存在报错", throws(() => setAccountFace({ username: "nobody", face: "explore", ban: true }), "账号不存在"));
|
|
120
|
+
check("id/username 均缺拒绝", throws(() => setAccountFace({ face: "explore", ban: true }), "账号不存在"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 3. acquire 按面过滤(核心) ──
|
|
124
|
+
console.log("\n[3] acquire 面过滤:跳过 ban 号,另一面不受影响");
|
|
125
|
+
{
|
|
126
|
+
retireAll();
|
|
127
|
+
const good = addAccount({ cookies: mkCookies("g1", "good1"), username: "good1" });
|
|
128
|
+
const expBan = addAccount({ cookies: mkCookies("e1", "expBan"), username: "expBan" });
|
|
129
|
+
const seaBan = addAccount({ cookies: mkCookies("s3", "seaBan"), username: "seaBan" });
|
|
130
|
+
setAccountFace({ id: expBan.id, face: "explore", ban: true, source: "probe" });
|
|
131
|
+
setAccountFace({ id: seaBan.id, face: "search", ban: true, source: "probe" });
|
|
132
|
+
|
|
133
|
+
// explore 取号:good1 与 expBan 都是从未用过(K1 并列),K3 createdAt 升序 → 无过滤时 expBan(先入池?)
|
|
134
|
+
// 入池顺序 good→expBan→seaBan;K1 并列时按 lastUsedAt??createdAt 升序,good1 最先 → 中签。
|
|
135
|
+
// 为证明过滤生效(而非巧合排序),把 expBan 的 createdAt 提前,使无过滤时它必中签:
|
|
136
|
+
for (const a of _accountsInternal()) if (a.username === "expBan") a.createdAt = Date.now() - 60000;
|
|
137
|
+
|
|
138
|
+
const w1 = acquireAccount({ userId: "T-1", appName: "explore" });
|
|
139
|
+
check("explore 取号跳过 explore 禁用号", w1.username === "good1", `got ${w1.username}`);
|
|
140
|
+
releaseAccount({ id: w1.id });
|
|
141
|
+
_accountsInternal().find((x) => x.id === w1.id).lastUsedAt = null; // 还原从未用过
|
|
142
|
+
|
|
143
|
+
// 另一面不受影响:search 取号可拿到 explore 禁用号(expBan createdAt 最老 → K3 最前)
|
|
144
|
+
const w2 = acquireAccount({ userId: "T-2", appName: "search" });
|
|
145
|
+
check("search 取号照发 explore 禁用号(面独立)", w2.username === "expBan", `got ${w2.username}`);
|
|
146
|
+
releaseAccount({ id: w2.id });
|
|
147
|
+
_accountsInternal().find((x) => x.id === w2.id).lastUsedAt = null;
|
|
148
|
+
|
|
149
|
+
// search 取号跳过 search 禁用号
|
|
150
|
+
const w3 = acquireAccount({ userId: "T-3", appName: "search" });
|
|
151
|
+
check("search 取号跳过 search 禁用号", w3.username === "good1", `got ${w3.username}`);
|
|
152
|
+
releaseAccount({ id: w3.id });
|
|
153
|
+
_accountsInternal().find((x) => x.id === w3.id).lastUsedAt = null;
|
|
154
|
+
|
|
155
|
+
// 未知 appName 归 explore 面
|
|
156
|
+
const w4 = acquireAccount({ userId: "T-4" });
|
|
157
|
+
check("缺省 appName 归 explore 面(跳过 explore 禁用号)", w4.username === "good1", `got ${w4.username}`);
|
|
158
|
+
releaseAccount({ id: w4.id });
|
|
159
|
+
_accountsInternal().find((x) => x.id === w4.id).lastUsedAt = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── 4. 面禁用导致池空:文案带面维度 ──
|
|
163
|
+
console.log("\n[4] 池空文案带面维度");
|
|
164
|
+
{
|
|
165
|
+
retireAll();
|
|
166
|
+
const p1 = addAccount({ cookies: mkCookies("p1", "onlyPool"), username: "onlyPool" });
|
|
167
|
+
const leased = addAccount({ cookies: mkCookies("p2", "busyOne"), username: "busyOne" });
|
|
168
|
+
setAccountFace({ id: p1.id, face: "explore", ban: true, source: "manual" });
|
|
169
|
+
const l = acquireAccount({ userId: "T-9", appName: "explore" });
|
|
170
|
+
check("租约占住 busyOne", l.username === "busyOne");
|
|
171
|
+
let err = null;
|
|
172
|
+
try { acquireAccount({ userId: "T-10", appName: "explore" }); } catch (e) { err = e.message; }
|
|
173
|
+
const total = getLoginPoolView().total;
|
|
174
|
+
check("文案含面与三类计数", err === `池中暂无 explore 面可用账号(共${total} 个,面禁用 1 个,租约中 1 个)`, `got ${err}`);
|
|
175
|
+
// search 面同池不空(onlyPool 的 search 面未禁用)——但它 lastUsedAt 刚被 acquire 盖过?
|
|
176
|
+
// 上一步 search 取号会走 30min 休息窗 eligible 空集 → 全池兑底,仍能发号(语义=有得用总比没用好)
|
|
177
|
+
const w = acquireAccount({ userId: "T-11", appName: "search" });
|
|
178
|
+
check("另一面池不空照发(兑底流水线不受面文案影响)", !!w && w.faceBans.search === null);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 5. 排序回归(K1/K2/K3 在过滤后不变) ──
|
|
182
|
+
console.log("\n[5] 排序回归:K1/K2/K3 结果不变");
|
|
183
|
+
{
|
|
184
|
+
retireAll();
|
|
185
|
+
const k1a = addAccount({ cookies: mkCookies("k1", "k1never"), username: "k1never" }); // 从未用过
|
|
186
|
+
const k1b = addAccount({ cookies: mkCookies("k2", "k1used"), username: "k1used" });
|
|
187
|
+
backdate("k1used", 61 * 60 * 1000); // 用过但已出休息窗(eligible);61min 与后续 60min 严格分离,免同毫秒 tie
|
|
188
|
+
for (const a of _accountsInternal()) if (a.username === "k1used") a.lastAppName = "search";
|
|
189
|
+
// K1: 从未用过最前
|
|
190
|
+
const w1 = acquireAccount({ userId: "T-K1", appName: "explore" });
|
|
191
|
+
check("K1 从未用过最前", w1.username === "k1never", `got ${w1.username}`);
|
|
192
|
+
releaseAccount({ id: w1.id });
|
|
193
|
+
|
|
194
|
+
// K2: 非同类优先:k1never 刚被 explore 租过(lastAppName=explore,非同类),
|
|
195
|
+
// k1used 是 search(同类)→ search 请求优先 k1never;K2 在 K3 之前分出胜负,确定性不受时间戳精度影响
|
|
196
|
+
backdate("k1never", 60 * 60 * 1000); // 出休息窗,两者都 eligible(K1 并列)
|
|
197
|
+
const w2 = acquireAccount({ userId: "T-K2", appName: "search" });
|
|
198
|
+
check("K2 非同类优先", w2.username === "k1never", `got ${w2.username}`);
|
|
199
|
+
releaseAccount({ id: w2.id });
|
|
200
|
+
|
|
201
|
+
// K3: 同类并列 → lastUsedAt 升序(两者都 lastAppName=search;k1used 严格更老 61min vs 60min)
|
|
202
|
+
for (const a of _accountsInternal()) if (a.username === "k1never") a.lastAppName = "search";
|
|
203
|
+
backdate("k1never", 60 * 60 * 1000);
|
|
204
|
+
backdate("k1used", 61 * 60 * 1000);
|
|
205
|
+
const w3 = acquireAccount({ userId: "T-K3", appName: "search" });
|
|
206
|
+
check("K3 lastUsedAt 升序", w3.username === "k1used", `got ${w3.username}`);
|
|
207
|
+
releaseAccount({ id: w3.id });
|
|
208
|
+
|
|
209
|
+
// 面禁用即使排序键最优也不中签:把 k1never 还原成从未用过(K1 最优)再 explore 标挂 → 只剩 k1used
|
|
210
|
+
for (const a of _accountsInternal()) if (a.username === "k1never") a.lastUsedAt = null;
|
|
211
|
+
setAccountFace({ id: k1a.id, face: "explore", ban: true, source: "probe" });
|
|
212
|
+
const w4 = acquireAccount({ userId: "T-K4", appName: "explore" });
|
|
213
|
+
check("面禁用号排序键最优(从未用过)也不中签", w4.username === "k1used", `got ${w4.username}`);
|
|
214
|
+
releaseAccount({ id: w4.id });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── 6. 与 status 正交:leased 号可标注 ──
|
|
218
|
+
console.log("\n[6] leased 号可标注(不动租约,下次派发生效)");
|
|
219
|
+
{
|
|
220
|
+
retireAll();
|
|
221
|
+
const a = addAccount({ cookies: mkCookies("L1", "leasedOne"), username: "leasedOne" });
|
|
222
|
+
const l = acquireAccount({ userId: "T-L", appName: "explore" });
|
|
223
|
+
check("先取到手", l.username === "leasedOne");
|
|
224
|
+
const r = setAccountFace({ id: l.id, face: "explore", ban: true, source: "manual", by: "测试" });
|
|
225
|
+
check("标注后仍 leased 且租约原样", r.status === "leased" && r.lease && r.lease.userId === "T-L");
|
|
226
|
+
check("faceBans 落本体", r.faceBans.explore && r.faceBans.explore.ban === true);
|
|
227
|
+
// release 后回池,explore 派号不再发它
|
|
228
|
+
releaseAccount({ id: a.id });
|
|
229
|
+
let got = null;
|
|
230
|
+
try { got = acquireAccount({ userId: "T-L2", appName: "explore" }); } catch (e) { got = e.message; }
|
|
231
|
+
check("释放后 explore 派发跳过该号", typeof got === "string" && got.includes("面禁用 1 个"), `got ${got}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`\n═══ faceBans 测试: ${pass} passed, ${fail} failed ═══`);
|
|
235
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
@@ -157,7 +157,8 @@ check("状态徽章=使用中", row1.includes("使用中"));
|
|
|
157
157
|
check("使用者=机器:口", row1.includes("F-11:9222"));
|
|
158
158
|
const poolSrc = await page.content();
|
|
159
159
|
check("独立页无 cookie 明文", !poolSrc.includes("SECRET-SID-XYZ"));
|
|
160
|
-
|
|
160
|
+
// d77a82c 批量改造:行内按钮改回「重登录」(与新增「检测」列解撞名);「检测」列已由上方新列断言覆盖
|
|
161
|
+
check("独立页重登录按钮已点亮(与检测列解撞名,d77a82c)", await page.$$eval("#content button:not([disabled])", (bs) => bs.some((b) => b.textContent.trim() === "重登录")));
|
|
161
162
|
check("独立页含新增账号/批量检测按钮", await page.$$eval(".bar button", (bs) => {
|
|
162
163
|
const t = bs.map((b) => b.textContent).join();
|
|
163
164
|
return t.includes("新增账号") && t.includes("批量检测");
|