| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- #!/usr/bin/env node
- /* ===========================================================================
- * protocol-check.js —— 两侧协议模块的一致性 / 自洽性检查(PC 侧)
- *
- * 为什么需要它:`tiktok-app/src/protocol.js` 与 `tiktok-auto/src/protocol.js`
- * 是**两份实体文件**(两个独立产物,做不了共享模块)。任何一边被单独改动,
- * 就会出现"界面认为 stopped 是终态、引擎认为不是"这种零报错的悬挂。
- * 这里在 PC 上把它们分别加载进**两个独立沙箱**,逐项比对。
- *
- * 用法:
- * $NODE tools/protocol-check.js # 期望最后一行:PROTOCOL_CLEAN
- * =========================================================================== */
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const vm = require('vm');
- const ROOT = path.join(__dirname, '..', '..');
- const APP_FILE = path.join(ROOT, 'tiktok-app', 'src', 'protocol.js');
- const AUTO_FILE = path.join(ROOT, 'tiktok-auto', 'src', 'protocol.js');
- /* 两侧唯一允许的差异行(挂载点) */
- const APP_MOUNT = 'var Protocol = (function () {';
- const AUTO_MOUNT = 'TikTokAuto.protocol = (function () {';
- let passed = 0;
- const failures = [];
- function check(name, condition, detail) {
- if (condition) {
- passed += 1;
- return;
- }
- failures.push(name + (detail ? ' → ' + detail : ''));
- }
- function eq(name, actual, expected) {
- check(name, actual === expected, `期望 ${expected},实际 ${actual}`);
- }
- /* ------------------------------------------------------------ 加载两侧 */
- function load(source) {
- const sandbox = {
- JSON: JSON,
- String: String,
- Number: Number,
- Object: Object,
- Array: Array,
- Boolean: Boolean,
- Error: Error,
- TikTokAuto: {}
- };
- vm.createContext(sandbox);
- vm.runInContext(source, sandbox, { filename: 'protocol.js' });
- return sandbox;
- }
- const appSource = fs.readFileSync(APP_FILE, 'utf8');
- const autoSource = fs.readFileSync(AUTO_FILE, 'utf8');
- const appBox = load(appSource);
- const autoBox = load(autoSource);
- const A = appBox.Protocol;
- const B = autoBox.TikTokAuto.protocol;
- check('界面侧 protocol.js 挂载出了 Protocol', !!A);
- check('引擎侧 protocol.js 挂载出了 TikTokAuto.protocol', !!B);
- if (!A || !B) {
- console.log('');
- console.log(`通过 ${passed} 项`);
- failures.forEach((f) => console.log(' ✗ ' + f));
- console.log('');
- console.log('PROTOCOL_FAILED');
- process.exit(1);
- }
- /* ------------------------------------------------- 1. 两份内容必须一致 */
- /* 只允许「挂载点那一行」不同;其余逐字节相同。 */
- const appBody = appSource.split(APP_MOUNT).join('<MOUNT>');
- const autoBody = autoSource.split(AUTO_MOUNT).join('<MOUNT>');
- check('两侧文件除挂载点外逐字节相同', appBody === autoBody,
- firstDiff(appBody, autoBody));
- eq('两侧挂载点各出现一次', appSource.split(APP_MOUNT).length, 2);
- eq('引擎侧不含界面侧的挂载点', autoSource.indexOf(APP_MOUNT), -1);
- eq('界面侧不含引擎侧的挂载点', appSource.indexOf(AUTO_MOUNT), -1);
- eq('两侧 VERSION 相同', A.VERSION, B.VERSION);
- check('两侧 SPEC 序列化后相同',
- JSON.stringify(A.SPEC) === JSON.stringify(B.SPEC), 'SPEC 不一致');
- /* ------------------------------------------------ 2. 枚举与迁移表自洽 */
- /* STATUS 是「常量名 → 状态值」的映射,这里要的是**状态值**。 */
- const STATUS_VALUES = Object.keys(A.SPEC.status).map((k) => A.SPEC.status[k]);
- const TERMINAL = A.SPEC.terminal;
- const EDGES = A.SPEC.edges;
- eq('状态枚举是 6 档', STATUS_VALUES.length, 6);
- /* 迁移表里出现的每个 from(空串除外)都必须是已知状态 */
- const unknownFroms = Object.keys(EDGES).filter(
- (k) => k !== '' && STATUS_VALUES.indexOf(k) < 0
- );
- check('迁移表的 from 都在枚举内', unknownFroms.length === 0, unknownFroms.join(','));
- /* 迁移表里出现的每个 to 都必须是已知状态 */
- const badTos = [];
- Object.keys(EDGES).forEach((from) => {
- Object.keys(EDGES[from]).forEach((to) => {
- if (STATUS_VALUES.indexOf(to) < 0) {
- badTos.push(from + '→' + to);
- }
- });
- });
- check('迁移表的 to 都在枚举内', badTos.length === 0, badTos.join(','));
- /* 终态必须没有出边 —— 这是"终态"的定义 */
- const leaky = Object.keys(TERMINAL).filter((t) => Object.keys(EDGES[t] || {}).length > 0);
- check('终态没有出边', leaky.length === 0, leaky.join(','));
- /* 终态往外走必须判 regress,不能判 unknown */
- const regressMiss = Object.keys(TERMINAL).filter(
- (t) => A.legality(t, 'running') !== 'regress'
- );
- check('终态 → running 判 regress', regressMiss.length === 0, regressMiss.join(','));
- eq('legality(stopped, running)', A.legality('stopped', 'running'), 'regress');
- eq('legality(running, running) 是幂等', A.legality('running', 'running'), 'idempotent');
- eq('legality(running, stopping) 合法', A.legality('running', 'stopping'), 'ok');
- eq('legality(空, starting) 合法', A.legality('', 'starting'), 'ok');
- eq('legality(空, running) 非法(第一跳必须 starting)', A.legality('', 'running'), 'unknown');
- /* 每个非终态 ≤2 步到终态(BFS),防止新增状态后出现"永远收不了尾" */
- const unreachable = STATUS_VALUES.filter(
- (s) => !TERMINAL[s] && stepsToTerminal(EDGES, TERMINAL, s) > 2
- );
- check('每个非终态 ≤2 步到终态', unreachable.length === 0,
- unreachable.map((s) => s + '=' + stepsToTerminal(EDGES, TERMINAL, s)).join(','));
- /* ------------------------------------------------ 3. 判定函数与数据一致 */
- const witness = [];
- STATUS_VALUES.concat(['', 'weird', 'FINISHED', null, undefined]).forEach((s) => {
- const expectTerminal = STATUS_VALUES.indexOf(s) >= 0 && TERMINAL[s] === true;
- if (A.isTerminal(s) !== expectTerminal) {
- witness.push('isTerminal(' + String(s) + ')');
- }
- });
- check('isTerminal 与终态表逐项一致', witness.length === 0, witness.join(','));
- const activeMiss = STATUS_VALUES.filter(
- (s) => A.isActive(s) !== (TERMINAL[s] !== true)
- );
- check('isActive 恰好是"非终态的已知状态"', activeMiss.length === 0, activeMiss.join(','));
- /* 前向兼容第一条:未知状态绝不能被视为终态 */
- const unknown = ['weird', 'paused', 'unknown', '', 'Starting', 'RUNNING'];
- const badUnknown = unknown.filter((s) => A.isTerminal(s) === true);
- check('未知/大小写不符的 status 都不判终态', badUnknown.length === 0, badUnknown.join(','));
- check('未知 status 的 levelOf 是 unknown',
- A.levelOf('weird') === 'unknown' && A.levelOf('') === 'unknown');
- eq('levelOf(running)', A.levelOf('running'), 'active');
- eq('levelOf(failed)', A.levelOf('failed'), 'terminal');
- /* ------------------------------------------------ 4. 阈值关系 */
- const T = A.SPEC.thresholds;
- check('心跳间隔为正', T.beatIntervalMs > 0);
- check('beatFreshMs ≥ 2 个心跳', T.beatFreshMs >= T.beatIntervalMs * 2,
- `${T.beatFreshMs} vs ${T.beatIntervalMs}`);
- check('beatDeadMs > beatFreshMs', T.beatDeadMs > T.beatFreshMs,
- `${T.beatDeadMs} vs ${T.beatFreshMs}`);
- check('startAckSoftMs < startAckMaxMs', T.startAckSoftMs < T.startAckMaxMs,
- `${T.startAckSoftMs} vs ${T.startAckMaxMs}`);
- check('stopAckMs > 一个心跳(否则慢心跳永远等不到 ack)',
- T.stopAckMs > T.beatIntervalMs, `${T.stopAckMs} vs ${T.beatIntervalMs}`);
- check('stopFinishMs > stopAckMs', T.stopFinishMs > T.stopAckMs);
- check('v1 回退的判死比 v2 更宽松', T.legacyNoBeatDeadMs >= T.beatDeadMs,
- `${T.legacyNoBeatDeadMs} vs ${T.beatDeadMs}`);
- check('v1 回退的停止超时是改造前的 8s', T.legacyStopTimeoutMs === 8000,
- String(T.legacyStopTimeoutMs));
- check('v1 回退的子引擎宽限期是改造前的 4s', T.legacyChildGraceMs === 4000,
- String(T.legacyChildGraceMs));
- /* ------------------------------------------------ 5. 阈值键集合一致 */
- const appKeys = Object.keys(T).sort().join(',');
- const autoKeys = Object.keys(B.SPEC.thresholds).sort().join(',');
- eq('两侧阈值键集合相同', appKeys, autoKeys);
- /* ------------------------------------------------------------ 汇总 */
- console.log('');
- console.log(`通过 ${passed} 项`);
- if (failures.length > 0) {
- console.log(`失败 ${failures.length} 项:`);
- failures.forEach((f) => console.log(' ✗ ' + f));
- console.log('');
- console.log('PROTOCOL_FAILED');
- process.exit(1);
- }
- console.log('PROTOCOL_CLEAN');
- /* ------------------------------------------------------------ 辅助 */
- function stepsToTerminal(edges, terminal, start) {
- let frontier = [start];
- const seen = {};
- seen[start] = true;
- for (let step = 1; step <= 4; step += 1) {
- const next = [];
- for (let i = 0; i < frontier.length; i += 1) {
- const edgesOfNode = edges[frontier[i]] || {};
- const targets = Object.keys(edgesOfNode);
- for (let j = 0; j < targets.length; j += 1) {
- const to = targets[j];
- if (terminal[to] === true) {
- return step;
- }
- if (seen[to] !== true) {
- seen[to] = true;
- next.push(to);
- }
- }
- }
- frontier = next;
- }
- return 99;
- }
- function firstDiff(a, b) {
- const len = Math.min(a.length, b.length);
- for (let i = 0; i < len; i += 1) {
- if (a[i] !== b[i]) {
- return `首个差异在第 ${i} 字符:${JSON.stringify(a.slice(i, i + 40))} vs ${JSON.stringify(b.slice(i, i + 40))}`;
- }
- }
- return `长度不同:${a.length} vs ${b.length}`;
- }
|