tt-help-cli-ycl 1.4.169 → 1.4.171
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-subscription-allocator.mjs +220 -0
- package/scripts/test-subscription-auto-alloc-ui.mjs +208 -0
- package/scripts/test-subscription-auto-alloc.mjs +259 -0
- package/scripts/test-subscription-center-e2e.mjs +134 -0
- package/scripts/test-subscription-center-smoke.mjs +154 -0
- package/scripts/test-subscription-center-ui.mjs +111 -0
- package/scripts/vnc-login.sh +68 -0
- package/src/cli/info.js +6 -1
- package/src/lib/browser/cdp.js +7 -1
- package/src/watch/public/app.js +586 -15
- package/src/watch/public/index.html +70 -3
- package/src/watch/subscription-allocator.js +255 -0
- package/src/watch/subscription-center.js +590 -0
- package/src/watch/watchdog-server.js +210 -0
- package/src/watchdog/agent.js +5 -0
- package/scripts/test-api-interceptor-429.mjs +0 -122
- package/scripts/test-api-interceptor-captcha.mjs +0 -176
- package/scripts/test-claim-fix.mjs +0 -216
- package/scripts/test-clash-discover.mjs +0 -126
- package/scripts/test-clash-hysteresis.mjs +0 -131
- package/scripts/test-clash-multichannel.mjs +0 -120
- package/scripts/test-client-error-monitor.mjs +0 -261
- package/scripts/test-country-fix.mjs +0 -341
- package/scripts/test-daemon-failover.mjs +0 -301
- package/scripts/test-daemon-switch-history.mjs +0 -141
- package/scripts/test-daemon-whitelist.mjs +0 -308
- package/scripts/test-explore-captcha-wait.mjs +0 -170
- package/scripts/test-photo-post.mjs +0 -156
- package/scripts/test-precheck-perf.mjs +0 -270
- package/scripts/test-precise-index.mjs +0 -114
- package/scripts/test-probe-live.mjs +0 -83
- package/scripts/test-probe.mjs +0 -315
- package/scripts/test-product-fill-cdp.mjs +0 -336
- package/scripts/test-product-fill-db.mjs +0 -216
- package/scripts/test-product-fill-http.mjs +0 -117
- package/scripts/test-proxy-switch-restart.mjs +0 -198
- package/scripts/test-refill-order.mjs +0 -218
- package/scripts/test-sample-all-fail.mjs +0 -142
- package/scripts/test-seller-any-country.mjs +0 -167
- package/scripts/test-stats-smoke.mjs +0 -43
- package/scripts/test-upgrade-spawn.mjs +0 -179
- package/scripts/test-webserver-error-classify.mjs +0 -409
- package/scripts/test-wmi-free-upgrade.mjs +0 -98
- package/src/cli/test-real-attach.js +0 -0
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 回归测试:getPrecheckCandidates 查询改写(2026-09-18)
|
|
4
|
-
*
|
|
5
|
-
* 背景:原 SQL 的 WHERE/ORDER BY 用 COALESCE(refresh_time, ?) 表达式,
|
|
6
|
-
* 挡住索引第 4 列 + 触发 TEMP B-TREE 全排序(76 万行 ~1s)。
|
|
7
|
-
* 改写为 (refresh_time IS NULL OR refresh_time < ?) + ORDER BY refresh_time。
|
|
8
|
-
*
|
|
9
|
-
* 验证点:
|
|
10
|
-
* 1. 语义等价:新旧 SQL 返回相同的候选集与顺序(NULL 排最前 = 视作 2016 默认值)
|
|
11
|
-
* 2. 索引生效:建 idx_jobs_precheck 后查询计划不再出现 TEMP B-TREE FOR ORDER BY
|
|
12
|
-
* 3. 走真实代码路径:createStore(db).getPrecheckCandidates() 返回结构正确
|
|
13
|
-
* (uniqueId/nickname/videoCount/refreshTime + 领取后 refresh_time 被置为 now)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import Database from "better-sqlite3";
|
|
17
|
-
import path from "path";
|
|
18
|
-
import fs from "fs";
|
|
19
|
-
import { fileURLToPath } from "url";
|
|
20
|
-
|
|
21
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const dbPath = path.join(__dirname, "../test-precheck-perf.db");
|
|
23
|
-
|
|
24
|
-
// ── 准备库 ──
|
|
25
|
-
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
26
|
-
const db = new Database(dbPath);
|
|
27
|
-
|
|
28
|
-
db.exec(`
|
|
29
|
-
CREATE TABLE jobs (
|
|
30
|
-
unique_id TEXT PRIMARY KEY,
|
|
31
|
-
nickname TEXT,
|
|
32
|
-
status TEXT DEFAULT 'pending',
|
|
33
|
-
video_count INTEGER,
|
|
34
|
-
tt_seller INTEGER DEFAULT 0,
|
|
35
|
-
verified INTEGER DEFAULT 0,
|
|
36
|
-
location_created TEXT,
|
|
37
|
-
pending_refresh INTEGER,
|
|
38
|
-
refresh_time INTEGER,
|
|
39
|
-
pinned INTEGER DEFAULT 0,
|
|
40
|
-
guessed_location TEXT,
|
|
41
|
-
sources TEXT,
|
|
42
|
-
follower_count INTEGER DEFAULT 0,
|
|
43
|
-
claimed_at INTEGER,
|
|
44
|
-
claimed_by TEXT,
|
|
45
|
-
processed_at INTEGER,
|
|
46
|
-
user_update_count INTEGER,
|
|
47
|
-
no_video INTEGER,
|
|
48
|
-
sec_uid TEXT,
|
|
49
|
-
top_video_seo_url TEXT,
|
|
50
|
-
product_name TEXT,
|
|
51
|
-
product_rating REAL,
|
|
52
|
-
product_sold_count INTEGER,
|
|
53
|
-
created_at INTEGER,
|
|
54
|
-
updated_at INTEGER
|
|
55
|
-
);
|
|
56
|
-
`);
|
|
57
|
-
|
|
58
|
-
const NOW = Date.now();
|
|
59
|
-
const DAY = 86400000;
|
|
60
|
-
const OLD = 1452000000000; // 2016-01-05,模拟历史旧值(大于代码 defaultTime)
|
|
61
|
-
// 代码内 defaultTime = new Date("2016-01-01T00:00:00Z"),NULL refresh_time 折算成它
|
|
62
|
-
const DEFAULT_TIME = new Date("2016-01-01T00:00:00Z").getTime();
|
|
63
|
-
|
|
64
|
-
// 覆盖各种边界:NULL refresh_time、极旧、刚刷新、目标国/非目标国、
|
|
65
|
-
// pending_refresh 0/1/NULL、非商家、已验证
|
|
66
|
-
const rows = [
|
|
67
|
-
["u_null_de", "DE", null, 0, 1, 0], // NULL refresh_time → 候选(最优先)
|
|
68
|
-
["u_old_pl", "PL", OLD + 1000, 0, 1, 0], // 2016 年代值 → 候选
|
|
69
|
-
["u_2d_fr", "FR", NOW - 2 * DAY, 0, 1, 0], // 2 天前 → 候选(maxAge=1d 时)
|
|
70
|
-
["u_5h_es", "ES", NOW - 5 * 3600 * 1000, 0, 1, 0], // 5h 前 → 非候选(maxAge=1d)
|
|
71
|
-
["u_pr1_it", "IT", NOW - 2 * DAY, 1, 1, 0], // pending_refresh=1 → 非候选
|
|
72
|
-
["u_prnull_be", "BE", NOW - 2 * DAY, null, 1, 0], // pending_refresh NULL → 候选
|
|
73
|
-
["u_nonseller", "DE", NOW - 2 * DAY, 0, 0, 0], // 非商家 → 非候选
|
|
74
|
-
["u_verified", "DE", NOW - 2 * DAY, 0, 1, 1], // 已验证 → 非候选
|
|
75
|
-
["u_us", "US", NOW - 3 * DAY, 0, 1, 0], // US 目标国 → 候选
|
|
76
|
-
["u_cn", "CN", NOW - 3 * DAY, 0, 1, 0], // 非目标国 → 非候选
|
|
77
|
-
];
|
|
78
|
-
const insert = db.prepare(`
|
|
79
|
-
INSERT INTO jobs (unique_id, nickname, status, video_count, tt_seller, verified,
|
|
80
|
-
location_created, pending_refresh, refresh_time, created_at, updated_at)
|
|
81
|
-
VALUES (?, ?, 'done', 10, ?, ?, ?, ?, ?, ?, ?)
|
|
82
|
-
`);
|
|
83
|
-
for (const [uid, loc, rt, pr, seller, verified] of rows) {
|
|
84
|
-
insert.run(uid, uid, seller, verified, loc, pr, rt, NOW, NOW);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const TARGETS = [
|
|
88
|
-
"PL",
|
|
89
|
-
"NL",
|
|
90
|
-
"BE",
|
|
91
|
-
"DE",
|
|
92
|
-
"FR",
|
|
93
|
-
"IT",
|
|
94
|
-
"IE",
|
|
95
|
-
"ES",
|
|
96
|
-
"CZ",
|
|
97
|
-
"GR",
|
|
98
|
-
"HU",
|
|
99
|
-
"PT",
|
|
100
|
-
"AT",
|
|
101
|
-
"TH",
|
|
102
|
-
"US",
|
|
103
|
-
"MY",
|
|
104
|
-
"VN",
|
|
105
|
-
];
|
|
106
|
-
const placeholders = TARGETS.map(() => "?").join(",");
|
|
107
|
-
|
|
108
|
-
// ── 1) 语义等价:新旧 SQL 逐行对比(含顺序) ──
|
|
109
|
-
function runOld(maxAgeSec) {
|
|
110
|
-
const threshold = NOW - maxAgeSec * 1000;
|
|
111
|
-
return db
|
|
112
|
-
.prepare(
|
|
113
|
-
`SELECT unique_id, COALESCE(refresh_time, ?) AS rt FROM jobs
|
|
114
|
-
WHERE tt_seller = 1 AND verified = 0
|
|
115
|
-
AND (pending_refresh IS NULL OR pending_refresh = 0)
|
|
116
|
-
AND location_created IN (${placeholders})
|
|
117
|
-
AND COALESCE(refresh_time, ?) < ?
|
|
118
|
-
ORDER BY COALESCE(refresh_time, ?) ASC`,
|
|
119
|
-
)
|
|
120
|
-
.all(OLD, ...TARGETS, OLD, threshold, OLD)
|
|
121
|
-
.map((r) => `${r.unique_id}@${r.rt}`);
|
|
122
|
-
}
|
|
123
|
-
function runNew(maxAgeSec) {
|
|
124
|
-
const threshold = NOW - maxAgeSec * 1000;
|
|
125
|
-
return db
|
|
126
|
-
.prepare(
|
|
127
|
-
`SELECT unique_id, COALESCE(refresh_time, ?) AS rt FROM jobs
|
|
128
|
-
WHERE tt_seller = 1 AND verified = 0
|
|
129
|
-
AND (pending_refresh IS NULL OR pending_refresh = 0)
|
|
130
|
-
AND location_created IN (${placeholders})
|
|
131
|
-
AND (refresh_time IS NULL OR refresh_time < ?)
|
|
132
|
-
ORDER BY refresh_time ASC`,
|
|
133
|
-
)
|
|
134
|
-
.all(OLD, ...TARGETS, threshold)
|
|
135
|
-
.map((r) => `${r.unique_id}@${r.rt}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let failed = 0;
|
|
139
|
-
const check = (name, cond, detail) => {
|
|
140
|
-
if (cond) {
|
|
141
|
-
console.error(`✓ ${name}`);
|
|
142
|
-
} else {
|
|
143
|
-
failed++;
|
|
144
|
-
console.error(`✗ ${name} ${detail ?? ""}`);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
for (const maxAge of [86400, 43200]) {
|
|
149
|
-
const a = runOld(maxAge);
|
|
150
|
-
const b = runNew(maxAge);
|
|
151
|
-
check(
|
|
152
|
-
`新旧 SQL 结果一致 (maxAge=${maxAge}s)`,
|
|
153
|
-
JSON.stringify(a) === JSON.stringify(b),
|
|
154
|
-
`\n old=${JSON.stringify(a)}\n new=${JSON.stringify(b)}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
// 顺带验证期望集合(maxAge=1d:NULL 和旧值排最前,非候选被排除)
|
|
158
|
-
const expect1d = [
|
|
159
|
-
"u_null_de@1452000000000",
|
|
160
|
-
"u_old_pl@1452000001000",
|
|
161
|
-
"u_2d_fr",
|
|
162
|
-
"u_prnull_be",
|
|
163
|
-
"u_us",
|
|
164
|
-
]
|
|
165
|
-
.map((s) => (s.includes("@") ? s : null))
|
|
166
|
-
.filter(Boolean);
|
|
167
|
-
const got1d = runNew(86400);
|
|
168
|
-
check(
|
|
169
|
-
"新 SQL 命中预期候选集",
|
|
170
|
-
got1d.slice(0, 2).join() === expect1d.slice(0, 2).join() &&
|
|
171
|
-
got1d.length === 5 &&
|
|
172
|
-
got1d.includes("u_prnull_be@" + (NOW - 2 * DAY)),
|
|
173
|
-
JSON.stringify(got1d),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// ── 2) 索引生效:建部分索引后无 TEMP B-TREE ──
|
|
177
|
-
db.exec(
|
|
178
|
-
`CREATE INDEX idx_jobs_precheck ON jobs(refresh_time)
|
|
179
|
-
WHERE tt_seller = 1 AND verified = 0 AND (pending_refresh IS NULL OR pending_refresh = 0)`,
|
|
180
|
-
);
|
|
181
|
-
const plan = db
|
|
182
|
-
.prepare(
|
|
183
|
-
`EXPLAIN QUERY PLAN SELECT unique_id FROM jobs
|
|
184
|
-
WHERE tt_seller = 1 AND verified = 0
|
|
185
|
-
AND (pending_refresh IS NULL OR pending_refresh = 0)
|
|
186
|
-
AND location_created IN (${placeholders})
|
|
187
|
-
AND (refresh_time IS NULL OR refresh_time < ?)
|
|
188
|
-
ORDER BY refresh_time ASC LIMIT 20`,
|
|
189
|
-
)
|
|
190
|
-
.all(...TARGETS, NOW - 86400000)
|
|
191
|
-
.map((r) => r.detail)
|
|
192
|
-
.join(" | ");
|
|
193
|
-
check(
|
|
194
|
-
"查询计划走 idx_jobs_precheck 且无 TEMP B-TREE 排序",
|
|
195
|
-
plan.includes("idx_jobs_precheck") && !plan.includes("TEMP B-TREE"),
|
|
196
|
-
plan,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
// ── 3) 走真实代码路径 createStore().getPrecheckCandidates ──
|
|
200
|
-
process.env.TT_HELP_DB_PATH = dbPath; // 防御性标记,createStore 用显式路径
|
|
201
|
-
const { createStore } = await import("../src/watch/data-store.js");
|
|
202
|
-
process.chdir(path.join(__dirname, "..")); // app-config 读 cwd 相对配置
|
|
203
|
-
const store = createStore(dbPath);
|
|
204
|
-
const candidates = store.getPrecheckCandidates({
|
|
205
|
-
limit: 3,
|
|
206
|
-
intervalSeconds: 86400,
|
|
207
|
-
locations: null,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
check("返回数量 ≤ limit", candidates.length === 3, `got ${candidates.length}`);
|
|
211
|
-
check(
|
|
212
|
-
"字段结构正确(uniqueId/nickname/videoCount/refreshTime)",
|
|
213
|
-
candidates.every(
|
|
214
|
-
(c) =>
|
|
215
|
-
typeof c.uniqueId === "string" &&
|
|
216
|
-
typeof c.videoCount === "number" &&
|
|
217
|
-
(c.refreshTime === OLD || typeof c.refreshTime === "number"),
|
|
218
|
-
),
|
|
219
|
-
JSON.stringify(candidates[0]),
|
|
220
|
-
);
|
|
221
|
-
check(
|
|
222
|
-
"顺序 = refreshTime 升序(NULL 折算默认值排最前)",
|
|
223
|
-
candidates[0].uniqueId === "u_null_de" &&
|
|
224
|
-
candidates[0].refreshTime === DEFAULT_TIME &&
|
|
225
|
-
candidates[1].uniqueId === "u_old_pl" &&
|
|
226
|
-
candidates[2].uniqueId === "u_us", // 3天前 < 2天前,升序在前
|
|
227
|
-
JSON.stringify(candidates.map((c) => c.uniqueId)),
|
|
228
|
-
);
|
|
229
|
-
// 领取即锁定:选中的行 refresh_time 应已更新为 now(防重复领取)
|
|
230
|
-
const after = db
|
|
231
|
-
.prepare(
|
|
232
|
-
`SELECT unique_id, refresh_time FROM jobs WHERE unique_id IN (${candidates.map(() => "?").join(",")})`,
|
|
233
|
-
)
|
|
234
|
-
.all(...candidates.map((c) => c.uniqueId));
|
|
235
|
-
check(
|
|
236
|
-
"领取后 refresh_time 已置为当前时间(防重复领取语义保留)",
|
|
237
|
-
after.every((r) => r.refresh_time !== null && r.refresh_time >= NOW),
|
|
238
|
-
JSON.stringify(after),
|
|
239
|
-
);
|
|
240
|
-
// 锁定后再次领取不应重复返回这 3 个
|
|
241
|
-
const second = store.getPrecheckCandidates({
|
|
242
|
-
limit: 50,
|
|
243
|
-
intervalSeconds: 86400,
|
|
244
|
-
locations: null,
|
|
245
|
-
});
|
|
246
|
-
check(
|
|
247
|
-
"已领取用户不会重复发放",
|
|
248
|
-
!second.some((c) => candidates.some((p) => p.uniqueId === c.uniqueId)),
|
|
249
|
-
JSON.stringify(second.map((c) => c.uniqueId)),
|
|
250
|
-
);
|
|
251
|
-
// markPrecheckResult 双分支(needRefresh 0/1)仍正常
|
|
252
|
-
store.markPrecheckResult("u_us", { needRefresh: true, videoCount: 12 });
|
|
253
|
-
store.markPrecheckResult("u_2d_fr", { needRefresh: false, videoCount: 8 });
|
|
254
|
-
const marked = db
|
|
255
|
-
.prepare(
|
|
256
|
-
`SELECT unique_id, pending_refresh, video_count FROM jobs WHERE unique_id IN ('u_us','u_2d_fr')`,
|
|
257
|
-
)
|
|
258
|
-
.all();
|
|
259
|
-
check(
|
|
260
|
-
"markPrecheckResult 正常写回",
|
|
261
|
-
marked.find((r) => r.unique_id === "u_us").pending_refresh === 1 &&
|
|
262
|
-
marked.find((r) => r.unique_id === "u_us").video_count === 12 &&
|
|
263
|
-
marked.find((r) => r.unique_id === "u_2d_fr").pending_refresh === 0,
|
|
264
|
-
JSON.stringify(marked),
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
db.close();
|
|
268
|
-
fs.unlinkSync(dbPath);
|
|
269
|
-
console.error(failed === 0 ? "\n全部通过 ✅" : `\n${failed} 项失败 ❌`);
|
|
270
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// 验证 idx_jobs_precise_merchants 部分覆盖索引:EXPLAIN 命中 + A/B 耗时
|
|
2
|
-
// 用完即删
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { createStore } from "../src/watch/data-store.js";
|
|
5
|
-
import { getDb } from "../src/watch/db-schema.js";
|
|
6
|
-
|
|
7
|
-
const TMP = "/tmp/tiktok-precise-idx-test";
|
|
8
|
-
fs.rmSync(TMP, { recursive: true, force: true });
|
|
9
|
-
fs.mkdirSync(TMP, { recursive: true });
|
|
10
|
-
|
|
11
|
-
const store = createStore(`${TMP}/t.db`, {});
|
|
12
|
-
const db = getDb();
|
|
13
|
-
|
|
14
|
-
const IDX_SQL =
|
|
15
|
-
"CREATE INDEX IF NOT EXISTS idx_jobs_precise_merchants ON jobs(location_created, product_sold_count DESC, unique_id) WHERE tt_seller = 1 AND verified = 0 AND top_video_seo_url IS NOT NULL AND top_video_seo_url != '' AND product_review_count = 0 AND product_sold_count > 10 AND recent_ec_videos IS NOT NULL AND recent_ec_videos != ''";
|
|
16
|
-
|
|
17
|
-
// 与 data-store.js getPreciseMerchantsFromDb 逐词一致的查询(loc 参数化)
|
|
18
|
-
const COUNT_SQL = (ph) =>
|
|
19
|
-
`SELECT COUNT(*) AS n FROM jobs WHERE tt_seller = 1 AND verified = 0 AND location_created IN (${ph}) AND top_video_seo_url IS NOT NULL AND top_video_seo_url != '' AND product_review_count = 0 AND product_sold_count > 10 AND recent_ec_videos IS NOT NULL AND recent_ec_videos != '' AND EXISTS (SELECT 1 FROM json_each(recent_ec_videos) WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000)`;
|
|
20
|
-
|
|
21
|
-
const LOCS = ["MX", "BR", "DE", "US", "TH"];
|
|
22
|
-
const ph = LOCS.map(() => "?").join(", ");
|
|
23
|
-
|
|
24
|
-
// ── 造数:30 万商家(大部分不满足便宜条件)+ 500 行满足 + 1 万非商家 ──
|
|
25
|
-
console.log("造数 31 万行...");
|
|
26
|
-
db.exec(`
|
|
27
|
-
WITH RECURSIVE seq(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM seq WHERE i < 300000)
|
|
28
|
-
INSERT INTO jobs
|
|
29
|
-
(unique_id, status, tt_seller, verified, location_created,
|
|
30
|
-
top_video_seo_url, product_review_count, product_sold_count,
|
|
31
|
-
recent_ec_videos, follower_count)
|
|
32
|
-
SELECT
|
|
33
|
-
's_' || i, 'done', 1, 0,
|
|
34
|
-
CASE (i % 5) WHEN 0 THEN 'MX' WHEN 1 THEN 'BR' WHEN 2 THEN 'DE' WHEN 3 THEN 'US' ELSE 'TH' END,
|
|
35
|
-
NULL, 5, 3, NULL, 1000
|
|
36
|
-
FROM seq
|
|
37
|
-
`);
|
|
38
|
-
db.exec(`
|
|
39
|
-
WITH RECURSIVE seq(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM seq WHERE i < 500)
|
|
40
|
-
INSERT INTO jobs
|
|
41
|
-
(unique_id, status, tt_seller, verified, location_created,
|
|
42
|
-
top_video_seo_url, product_review_count, product_sold_count,
|
|
43
|
-
recent_ec_videos, follower_count)
|
|
44
|
-
SELECT
|
|
45
|
-
'hit_' || i, 'done', 1, 0,
|
|
46
|
-
CASE (i % 5) WHEN 0 THEN 'MX' WHEN 1 THEN 'BR' WHEN 2 THEN 'DE' WHEN 3 THEN 'US' ELSE 'TH' END,
|
|
47
|
-
'/shop/xxx-' || i, 0, 20 + (i % 100),
|
|
48
|
-
'[{"playCount":' || (2000 + i) || '}]', 5000
|
|
49
|
-
FROM seq
|
|
50
|
-
`);
|
|
51
|
-
db.exec(`
|
|
52
|
-
INSERT INTO jobs
|
|
53
|
-
(unique_id, status, tt_seller, verified, location_created,
|
|
54
|
-
top_video_seo_url, product_review_count, product_sold_count, recent_ec_videos, follower_count)
|
|
55
|
-
SELECT 'ns_' || i, 'done', 0, 0, 'MX', '/x', 0, 20, '[{"playCount":3000}]', 100
|
|
56
|
-
FROM (WITH RECURSIVE seq(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM seq WHERE i < 10000) SELECT i FROM seq)
|
|
57
|
-
`);
|
|
58
|
-
|
|
59
|
-
const args = [...LOCS];
|
|
60
|
-
|
|
61
|
-
// ── A/B:无新索引 vs 有新索引 ──
|
|
62
|
-
function bench(label) {
|
|
63
|
-
const t0 = Date.now();
|
|
64
|
-
const n = db.prepare(COUNT_SQL(ph)).get(...args).n;
|
|
65
|
-
const ms = Date.now() - t0;
|
|
66
|
-
console.log(` ${label}: COUNT=${n} ${ms}ms`);
|
|
67
|
-
return { n, ms };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
console.log("\n== A/B 耗时(30.6 万行)==");
|
|
71
|
-
const before = bench("无新索引");
|
|
72
|
-
db.exec(IDX_SQL);
|
|
73
|
-
const after = bench("有新索引");
|
|
74
|
-
|
|
75
|
-
console.log("\n== EXPLAIN:COUNT 与 LIST 查询计划 ==");
|
|
76
|
-
const planCount = db.prepare(`EXPLAIN QUERY PLAN ${COUNT_SQL(ph)}`).all(...args);
|
|
77
|
-
console.log("COUNT :", planCount.map((r) => r.detail).join(" | "));
|
|
78
|
-
const LIST_SQL = `SELECT unique_id, product_sold_count FROM jobs WHERE tt_seller = 1 AND verified = 0 AND location_created IN (${ph}) AND top_video_seo_url IS NOT NULL AND top_video_seo_url != '' AND product_review_count = 0 AND product_sold_count > 10 AND recent_ec_videos IS NOT NULL AND recent_ec_videos != '' AND EXISTS (SELECT 1 FROM json_each(recent_ec_videos) WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000) ORDER BY product_sold_count DESC, unique_id ASC LIMIT 100 OFFSET 0`;
|
|
79
|
-
const planList = db.prepare(`EXPLAIN QUERY PLAN ${LIST_SQL}`).all(...args);
|
|
80
|
-
console.log("LIST :", planList.map((r) => r.detail).join(" | "));
|
|
81
|
-
|
|
82
|
-
const COUNT_IDX = `SELECT COUNT(*) AS n FROM jobs INDEXED BY idx_jobs_precise_merchants WHERE tt_seller = 1 AND verified = 0 AND location_created IN (${ph}) AND top_video_seo_url IS NOT NULL AND top_video_seo_url != '' AND product_review_count = 0 AND product_sold_count > 10 AND recent_ec_videos IS NOT NULL AND recent_ec_videos != '' AND EXISTS (SELECT 1 FROM json_each(recent_ec_videos) WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000)`;
|
|
83
|
-
const planIdx = db.prepare(`EXPLAIN QUERY PLAN ${COUNT_IDX}`).all(...args);
|
|
84
|
-
const nIdx = db.prepare(COUNT_IDX).get(...args).n;
|
|
85
|
-
|
|
86
|
-
let pass = 0, fail = 0;
|
|
87
|
-
const check = (name, cond, detail = "") => {
|
|
88
|
-
if (cond) { pass++; console.log(` ✅ ${name}${detail ? " " + detail : ""}`); }
|
|
89
|
-
else { fail++; console.log(` ❌ ${name}${detail ? " " + detail : ""}`); }
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
console.log("\n== 断言 ==");
|
|
93
|
-
check("COUNT 结果一致(500)", after.n === 500 && before.n === 500, `n=${after.n}`);
|
|
94
|
-
check("INDEXED BY 结果一致", nIdx === 500, `n=${nIdx}`);
|
|
95
|
-
check("优化器自发命中 partial index(小库可自发选中)",
|
|
96
|
-
planCount.some((r) => r.detail.includes("idx_jobs_precise_merchants")),
|
|
97
|
-
planCount.map((r) => r.detail).join(" | ").slice(0, 120));
|
|
98
|
-
check("INDEXED BY 强制命中 partial index", planIdx.some((r) => r.detail.includes("idx_jobs_precise_merchants")));
|
|
99
|
-
|
|
100
|
-
// 回退:drop 索引后 INDEXED BY 应报 no such index(业务层 catch 后走普通查询)
|
|
101
|
-
db.exec("DROP INDEX idx_jobs_precise_merchants");
|
|
102
|
-
let fellBack = false;
|
|
103
|
-
try {
|
|
104
|
-
db.prepare(COUNT_IDX).get(...args);
|
|
105
|
-
} catch (e) {
|
|
106
|
-
fellBack = /no such index/i.test(e.message);
|
|
107
|
-
}
|
|
108
|
-
check("索引缺失时抛 no such index(业务层回退依赖此错误)", fellBack);
|
|
109
|
-
const nPlainAfterDrop = db.prepare(COUNT_SQL(ph)).get(...args).n;
|
|
110
|
-
check("普通查询不受影响", nPlainAfterDrop === 500, `n=${nPlainAfterDrop}`);
|
|
111
|
-
|
|
112
|
-
console.log(`\n===== 结果: ${pass} passed, ${fail} failed =====`);
|
|
113
|
-
fs.rmSync(TMP, { recursive: true, force: true });
|
|
114
|
-
process.exit(fail > 0 ? 1 : 0);
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 探测模块集成测试(真实浏览器 + 真实 TikTok)
|
|
3
|
-
*
|
|
4
|
-
* 用法:
|
|
5
|
-
* node scripts/test-probe-live.mjs [ports] [points]
|
|
6
|
-
* 例: node scripts/test-probe-live.mjs 4222
|
|
7
|
-
* node scripts/test-probe-live.mjs 4222 video,comment
|
|
8
|
-
*
|
|
9
|
-
* 说明:
|
|
10
|
-
* - 连接本地已启动的 CDP 端口(不 kill 业务浏览器)
|
|
11
|
-
* - 会对 TikTok 真实发起访问(探测本身是访问行为)
|
|
12
|
-
* - 断言输出结构 + 至少能完成调度;业务可用性结果只打印不判成败
|
|
13
|
-
*/
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { runProbe, POINT_NAMES } from "../src/lib/probe/index.js";
|
|
16
|
-
|
|
17
|
-
const ports = (process.argv[2] || "4222")
|
|
18
|
-
.split(",")
|
|
19
|
-
.map((s) => Number(s.trim()))
|
|
20
|
-
.filter((n) => Number.isInteger(n) && n > 0);
|
|
21
|
-
const points = process.argv[3]
|
|
22
|
-
? process.argv[3].split(",").map((s) => s.trim()).filter(Boolean)
|
|
23
|
-
: POINT_NAMES;
|
|
24
|
-
|
|
25
|
-
console.error(`[live-test] ports=${ports.join(",")} points=${points.join(",")}`);
|
|
26
|
-
console.error(`[live-test] 说明:将对 TikTok 真实访问,属正常探测行为\n`);
|
|
27
|
-
|
|
28
|
-
const t0 = Date.now();
|
|
29
|
-
const result = await runProbe({ ports, points, log: console.error });
|
|
30
|
-
const totalMs = Date.now() - t0;
|
|
31
|
-
|
|
32
|
-
console.error(`\n[结果] 总耗时 ${totalMs}ms`);
|
|
33
|
-
console.log(JSON.stringify(result, null, 2));
|
|
34
|
-
|
|
35
|
-
// ── 结构断言(不判业务成败,只判调度/输出正确) ──
|
|
36
|
-
let failed = 0;
|
|
37
|
-
try {
|
|
38
|
-
assert.ok(Array.isArray(result.ports), "ports 应为数组");
|
|
39
|
-
assert.equal(result.ports.length, ports.length, "端口数应一致");
|
|
40
|
-
assert.ok(result.ports.every((p) => Number.isInteger(p.port)), "端口号应为整数");
|
|
41
|
-
assert.ok(
|
|
42
|
-
result.ports.every((p) => typeof p.loggedIn === "boolean"),
|
|
43
|
-
"loggedIn 应为布尔",
|
|
44
|
-
);
|
|
45
|
-
// 每端口包含请求的功能点键
|
|
46
|
-
for (const p of result.ports) {
|
|
47
|
-
for (const name of points) {
|
|
48
|
-
assert.ok(
|
|
49
|
-
name in p.points,
|
|
50
|
-
`端口 ${p.port} 应包含功能点 ${name}`,
|
|
51
|
-
);
|
|
52
|
-
const r = p.points[name];
|
|
53
|
-
assert.equal(typeof r.ok, "boolean", `${name}.ok 应为布尔`);
|
|
54
|
-
assert.equal(typeof r.count, "number", `${name}.count 应为数字`);
|
|
55
|
-
assert.equal(typeof r.elapsedMs, "number", `${name}.elapsedMs 应为数字`);
|
|
56
|
-
assert.ok(
|
|
57
|
-
typeof r.error === "string" || r.error === null,
|
|
58
|
-
`${name}.error 应为 string|null`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
console.error(`\n[断言] 结构校验全部通过 ✓`);
|
|
63
|
-
} catch (e) {
|
|
64
|
-
failed = 1;
|
|
65
|
-
console.error(`\n[断言] 失败: ${e.message}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 汇总业务结果
|
|
69
|
-
console.error("\n[业务汇总]");
|
|
70
|
-
for (const p of result.ports) {
|
|
71
|
-
for (const name of points) {
|
|
72
|
-
const r = p.points[name];
|
|
73
|
-
if (!r) continue;
|
|
74
|
-
const tag = r.skipped
|
|
75
|
-
? `跳过(${r.skipReason})`
|
|
76
|
-
: r.ok
|
|
77
|
-
? `✓ ${r.count}`
|
|
78
|
-
: `✗ ${r.error}`;
|
|
79
|
-
console.error(` port ${p.port} ${name.padEnd(8)} ${tag} [${r.elapsedMs}ms]`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
process.exit(failed);
|