tt-help-cli-ycl 1.4.182 → 1.4.184
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-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-faceban-e2e.mjs +131 -0
- package/scripts/test-login-pool-faceban.mjs +235 -0
- package/scripts/test-process-manager-label.mjs +145 -0
- package/src/cli/explore.js +99 -17
- package/src/cli/search.js +106 -15
- 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 +94 -4
- 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/login-apply.js +12 -6
- package/src/watchdog/process-manager.js +71 -6
- package/src/watchdog/reporter.js +2 -0
|
@@ -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);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watchdog 进程日志前缀标签单测 — deriveDisplayLabel 纯函数直驱
|
|
3
|
+
*
|
|
4
|
+
* 覆盖(2026-11-04 定稿规则表全部 5 条分支 + 引号/路径边界):
|
|
5
|
+
* 1. --base-port 单端口 → [explore:9223]
|
|
6
|
+
* 2. --port 单端口(仅浏览器类)→ [refresh-mini:5222];webserver/watch 的 --port 不吃
|
|
7
|
+
* 3. --base-port --port-count → 区间压缩 [explore:9223-9225]
|
|
8
|
+
* 4. 解析不到端口 → [comments](不猜默认端口)
|
|
9
|
+
* 5. 程序名也解析不出 → 兜底 fallbackId(即 cmdDef.name || id)
|
|
10
|
+
* 边界: 引号形态 "tt-help" "explore" / node+脚本路径取子命令 / 脚本本体即程序 /
|
|
11
|
+
* Windows 启动器(.cmd/.exe)/ 值含引号空格不影响端口提取
|
|
12
|
+
*
|
|
13
|
+
* 跑法: node scripts/test-process-manager-label.mjs
|
|
14
|
+
*/
|
|
15
|
+
import assert from "assert";
|
|
16
|
+
import { deriveDisplayLabel } from "../src/watchdog/process-manager.js";
|
|
17
|
+
|
|
18
|
+
let pass = 0;
|
|
19
|
+
let fail = 0;
|
|
20
|
+
function check(name, actual, expected) {
|
|
21
|
+
try {
|
|
22
|
+
assert.strictEqual(actual, expected);
|
|
23
|
+
pass++;
|
|
24
|
+
console.log(` ✓ ${name}`);
|
|
25
|
+
} catch {
|
|
26
|
+
fail++;
|
|
27
|
+
console.error(` ✗ ${name}\n 期望: ${expected}\n 实际: ${actual}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log("规则 1:--base-port 单端口");
|
|
32
|
+
check(
|
|
33
|
+
"explore --base-port 9223",
|
|
34
|
+
deriveDisplayLabel("tt-help explore --base-port 9223 --server http://127.0.0.1:3001", "1793630915534"),
|
|
35
|
+
"explore:9223",
|
|
36
|
+
);
|
|
37
|
+
check(
|
|
38
|
+
"refresh --base-port 8222(无 port-count 默认 1 个端口)",
|
|
39
|
+
deriveDisplayLabel("tt-help refresh --base-port 8222", "r1"),
|
|
40
|
+
"refresh:8222",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
console.log("规则 2:--port 单端口(仅浏览器类命令提取,防误吃服务端口)");
|
|
44
|
+
check(
|
|
45
|
+
"refresh-mini --port 5222",
|
|
46
|
+
deriveDisplayLabel("tt-help refresh-mini --port 5222 --headless", "m1"),
|
|
47
|
+
"refresh-mini:5222",
|
|
48
|
+
);
|
|
49
|
+
check(
|
|
50
|
+
"webserver --port 3000 不吃端口(非浏览器类命令)",
|
|
51
|
+
deriveDisplayLabel("tt-help webserver --port 3000", "w1"),
|
|
52
|
+
"webserver",
|
|
53
|
+
);
|
|
54
|
+
check(
|
|
55
|
+
"watch --port 3000 不吃端口(非浏览器类命令)",
|
|
56
|
+
deriveDisplayLabel("tt-help watch --port 3000 -o data.db", "w2"),
|
|
57
|
+
"watch",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
console.log("规则 3:--base-port + --port-count 区间压缩");
|
|
61
|
+
check(
|
|
62
|
+
"explore --base-port 9223 --port-count 3 → 9223-9225",
|
|
63
|
+
deriveDisplayLabel("tt-help explore --base-port 9223 --port-count 3", "e1"),
|
|
64
|
+
"explore:9223-9225",
|
|
65
|
+
);
|
|
66
|
+
check(
|
|
67
|
+
"attach --base-port 9222 --port-count 5 → 9222-9226",
|
|
68
|
+
deriveDisplayLabel("tt-help attach --base-port 9222 --port-count 5", "a1"),
|
|
69
|
+
"attach:9222-9226",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
console.log("规则 4:解析不到端口 → 只显示程序名(不猜默认端口)");
|
|
73
|
+
check("comments 无端口参数", deriveDisplayLabel("tt-help comments --server http://127.0.0.1:3001", "c1"), "comments");
|
|
74
|
+
check("videostats 无端口参数", deriveDisplayLabel("tt-help videostats data/result.db -p 3", "v1"), "videostats");
|
|
75
|
+
check("recheck 无端口参数", deriveDisplayLabel("tt-help recheck", "rc1"), "recheck");
|
|
76
|
+
|
|
77
|
+
console.log("规则 5:程序名也解析不出 → 兜底 fallbackId");
|
|
78
|
+
check("空命令行", deriveDisplayLabel("", "1793630915534"), "1793630915534");
|
|
79
|
+
check("纯空白", deriveDisplayLabel(" ", "fallback-id"), "fallback-id");
|
|
80
|
+
check("null 命令行", deriveDisplayLabel(null, "fb"), "fb");
|
|
81
|
+
check("裸路径分隔符解析不出程序名", deriveDisplayLabel("/ --base-port 9223", "fb2"), "fb2");
|
|
82
|
+
|
|
83
|
+
console.log("边界:引号形态");
|
|
84
|
+
check(
|
|
85
|
+
'"tt-help" "explore" 全引号',
|
|
86
|
+
deriveDisplayLabel('"tt-help" "explore" --base-port 9223', "q1"),
|
|
87
|
+
"explore:9223",
|
|
88
|
+
);
|
|
89
|
+
check(
|
|
90
|
+
"引号值含空格不影响端口提取",
|
|
91
|
+
deriveDisplayLabel('tt-help explore --name "my explore" --base-port 9223', "q2"),
|
|
92
|
+
"explore:9223",
|
|
93
|
+
);
|
|
94
|
+
check(
|
|
95
|
+
"单引号启动器",
|
|
96
|
+
deriveDisplayLabel("'tt-help' refresh-mini --port 5222", "q3"),
|
|
97
|
+
"refresh-mini:5222",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log("边界:node/路径启动形态");
|
|
101
|
+
check(
|
|
102
|
+
"node + 脚本路径 → 取子命令 token",
|
|
103
|
+
deriveDisplayLabel("node /usr/local/lib/cli.js explore --base-port 9223", "p1"),
|
|
104
|
+
"explore:9223",
|
|
105
|
+
);
|
|
106
|
+
check(
|
|
107
|
+
"node src/main.js watch → 取子命令 watch(--port 不吃,watch 非浏览器类)",
|
|
108
|
+
deriveDisplayLabel("node src/main.js watch --port 3000 -o tmp.db", "p2"),
|
|
109
|
+
"watch",
|
|
110
|
+
);
|
|
111
|
+
check(
|
|
112
|
+
"node scraper.js --port 9222 → 脚本本体即程序;scraper 不属浏览器类词表,--port 不吃",
|
|
113
|
+
deriveDisplayLabel("node scraper.js --port 9222", "p3"),
|
|
114
|
+
"scraper",
|
|
115
|
+
);
|
|
116
|
+
check(
|
|
117
|
+
"node explore.js --port 9222 → 脚本名属浏览器类词表,吃端口",
|
|
118
|
+
deriveDisplayLabel("node explore.js --port 9222", "p3b"),
|
|
119
|
+
"explore:9222",
|
|
120
|
+
);
|
|
121
|
+
check(
|
|
122
|
+
"node.exe + 相对脚本路径",
|
|
123
|
+
deriveDisplayLabel("node.exe cli.js refresh-mini --port 5222", "p4"),
|
|
124
|
+
"refresh-mini:5222",
|
|
125
|
+
);
|
|
126
|
+
check(
|
|
127
|
+
"Windows 完整路径 tt-help.cmd 启动器",
|
|
128
|
+
deriveDisplayLabel("C:\\tools\\tt-help.cmd explore --base-port 9223 --port-count 2", "p5"),
|
|
129
|
+
"explore:9223-9224",
|
|
130
|
+
);
|
|
131
|
+
check(
|
|
132
|
+
"裸命令(无启动器)直接取 token[0]",
|
|
133
|
+
deriveDisplayLabel("explore --base-port 9223", "p6"),
|
|
134
|
+
"explore:9223",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
console.log("边界:cmdDef.args 数组形态(startCommand join 后派生)");
|
|
138
|
+
check(
|
|
139
|
+
"args 数组 join 成命令行后解析",
|
|
140
|
+
deriveDisplayLabel(["tt-help", "explore", "--base-port", "9223"].join(" "), "ar1"),
|
|
141
|
+
"explore:9223",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
console.log(`\n结果: ${pass} 通过, ${fail} 失败`);
|
|
145
|
+
process.exit(fail > 0 ? 1 : 0);
|