protocol-check.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. #!/usr/bin/env node
  2. /* ===========================================================================
  3. * protocol-check.js —— 两侧协议模块的一致性 / 自洽性检查(PC 侧)
  4. *
  5. * 为什么需要它:`tiktok-app/src/protocol.js` 与 `tiktok-auto/src/protocol.js`
  6. * 是**两份实体文件**(两个独立产物,做不了共享模块)。任何一边被单独改动,
  7. * 就会出现"界面认为 stopped 是终态、引擎认为不是"这种零报错的悬挂。
  8. * 这里在 PC 上把它们分别加载进**两个独立沙箱**,逐项比对。
  9. *
  10. * 用法:
  11. * $NODE tools/protocol-check.js # 期望最后一行:PROTOCOL_CLEAN
  12. * =========================================================================== */
  13. 'use strict';
  14. const fs = require('fs');
  15. const path = require('path');
  16. const vm = require('vm');
  17. const ROOT = path.join(__dirname, '..', '..');
  18. const APP_FILE = path.join(ROOT, 'tiktok-app', 'src', 'protocol.js');
  19. const AUTO_FILE = path.join(ROOT, 'tiktok-auto', 'src', 'protocol.js');
  20. /* 两侧唯一允许的差异行(挂载点) */
  21. const APP_MOUNT = 'var Protocol = (function () {';
  22. const AUTO_MOUNT = 'TikTokAuto.protocol = (function () {';
  23. let passed = 0;
  24. const failures = [];
  25. function check(name, condition, detail) {
  26. if (condition) {
  27. passed += 1;
  28. return;
  29. }
  30. failures.push(name + (detail ? ' → ' + detail : ''));
  31. }
  32. function eq(name, actual, expected) {
  33. check(name, actual === expected, `期望 ${expected},实际 ${actual}`);
  34. }
  35. /* ------------------------------------------------------------ 加载两侧 */
  36. function load(source) {
  37. const sandbox = {
  38. JSON: JSON,
  39. String: String,
  40. Number: Number,
  41. Object: Object,
  42. Array: Array,
  43. Boolean: Boolean,
  44. Error: Error,
  45. TikTokAuto: {}
  46. };
  47. vm.createContext(sandbox);
  48. vm.runInContext(source, sandbox, { filename: 'protocol.js' });
  49. return sandbox;
  50. }
  51. const appSource = fs.readFileSync(APP_FILE, 'utf8');
  52. const autoSource = fs.readFileSync(AUTO_FILE, 'utf8');
  53. const appBox = load(appSource);
  54. const autoBox = load(autoSource);
  55. const A = appBox.Protocol;
  56. const B = autoBox.TikTokAuto.protocol;
  57. check('界面侧 protocol.js 挂载出了 Protocol', !!A);
  58. check('引擎侧 protocol.js 挂载出了 TikTokAuto.protocol', !!B);
  59. if (!A || !B) {
  60. console.log('');
  61. console.log(`通过 ${passed} 项`);
  62. failures.forEach((f) => console.log(' ✗ ' + f));
  63. console.log('');
  64. console.log('PROTOCOL_FAILED');
  65. process.exit(1);
  66. }
  67. /* ------------------------------------------------- 1. 两份内容必须一致 */
  68. /* 只允许「挂载点那一行」不同;其余逐字节相同。 */
  69. const appBody = appSource.split(APP_MOUNT).join('<MOUNT>');
  70. const autoBody = autoSource.split(AUTO_MOUNT).join('<MOUNT>');
  71. check('两侧文件除挂载点外逐字节相同', appBody === autoBody,
  72. firstDiff(appBody, autoBody));
  73. eq('两侧挂载点各出现一次', appSource.split(APP_MOUNT).length, 2);
  74. eq('引擎侧不含界面侧的挂载点', autoSource.indexOf(APP_MOUNT), -1);
  75. eq('界面侧不含引擎侧的挂载点', appSource.indexOf(AUTO_MOUNT), -1);
  76. eq('两侧 VERSION 相同', A.VERSION, B.VERSION);
  77. check('两侧 SPEC 序列化后相同',
  78. JSON.stringify(A.SPEC) === JSON.stringify(B.SPEC), 'SPEC 不一致');
  79. /* ------------------------------------------------ 2. 枚举与迁移表自洽 */
  80. /* STATUS 是「常量名 → 状态值」的映射,这里要的是**状态值**。 */
  81. const STATUS_VALUES = Object.keys(A.SPEC.status).map((k) => A.SPEC.status[k]);
  82. const TERMINAL = A.SPEC.terminal;
  83. const EDGES = A.SPEC.edges;
  84. eq('状态枚举是 6 档', STATUS_VALUES.length, 6);
  85. /* 迁移表里出现的每个 from(空串除外)都必须是已知状态 */
  86. const unknownFroms = Object.keys(EDGES).filter(
  87. (k) => k !== '' && STATUS_VALUES.indexOf(k) < 0
  88. );
  89. check('迁移表的 from 都在枚举内', unknownFroms.length === 0, unknownFroms.join(','));
  90. /* 迁移表里出现的每个 to 都必须是已知状态 */
  91. const badTos = [];
  92. Object.keys(EDGES).forEach((from) => {
  93. Object.keys(EDGES[from]).forEach((to) => {
  94. if (STATUS_VALUES.indexOf(to) < 0) {
  95. badTos.push(from + '→' + to);
  96. }
  97. });
  98. });
  99. check('迁移表的 to 都在枚举内', badTos.length === 0, badTos.join(','));
  100. /* 终态必须没有出边 —— 这是"终态"的定义 */
  101. const leaky = Object.keys(TERMINAL).filter((t) => Object.keys(EDGES[t] || {}).length > 0);
  102. check('终态没有出边', leaky.length === 0, leaky.join(','));
  103. /* 终态往外走必须判 regress,不能判 unknown */
  104. const regressMiss = Object.keys(TERMINAL).filter(
  105. (t) => A.legality(t, 'running') !== 'regress'
  106. );
  107. check('终态 → running 判 regress', regressMiss.length === 0, regressMiss.join(','));
  108. eq('legality(stopped, running)', A.legality('stopped', 'running'), 'regress');
  109. eq('legality(running, running) 是幂等', A.legality('running', 'running'), 'idempotent');
  110. eq('legality(running, stopping) 合法', A.legality('running', 'stopping'), 'ok');
  111. eq('legality(空, starting) 合法', A.legality('', 'starting'), 'ok');
  112. eq('legality(空, running) 非法(第一跳必须 starting)', A.legality('', 'running'), 'unknown');
  113. /* 每个非终态 ≤2 步到终态(BFS),防止新增状态后出现"永远收不了尾" */
  114. const unreachable = STATUS_VALUES.filter(
  115. (s) => !TERMINAL[s] && stepsToTerminal(EDGES, TERMINAL, s) > 2
  116. );
  117. check('每个非终态 ≤2 步到终态', unreachable.length === 0,
  118. unreachable.map((s) => s + '=' + stepsToTerminal(EDGES, TERMINAL, s)).join(','));
  119. /* ------------------------------------------------ 3. 判定函数与数据一致 */
  120. const witness = [];
  121. STATUS_VALUES.concat(['', 'weird', 'FINISHED', null, undefined]).forEach((s) => {
  122. const expectTerminal = STATUS_VALUES.indexOf(s) >= 0 && TERMINAL[s] === true;
  123. if (A.isTerminal(s) !== expectTerminal) {
  124. witness.push('isTerminal(' + String(s) + ')');
  125. }
  126. });
  127. check('isTerminal 与终态表逐项一致', witness.length === 0, witness.join(','));
  128. const activeMiss = STATUS_VALUES.filter(
  129. (s) => A.isActive(s) !== (TERMINAL[s] !== true)
  130. );
  131. check('isActive 恰好是"非终态的已知状态"', activeMiss.length === 0, activeMiss.join(','));
  132. /* 前向兼容第一条:未知状态绝不能被视为终态 */
  133. const unknown = ['weird', 'paused', 'unknown', '', 'Starting', 'RUNNING'];
  134. const badUnknown = unknown.filter((s) => A.isTerminal(s) === true);
  135. check('未知/大小写不符的 status 都不判终态', badUnknown.length === 0, badUnknown.join(','));
  136. check('未知 status 的 levelOf 是 unknown',
  137. A.levelOf('weird') === 'unknown' && A.levelOf('') === 'unknown');
  138. eq('levelOf(running)', A.levelOf('running'), 'active');
  139. eq('levelOf(failed)', A.levelOf('failed'), 'terminal');
  140. /* ------------------------------------------------ 4. 阈值关系 */
  141. const T = A.SPEC.thresholds;
  142. check('心跳间隔为正', T.beatIntervalMs > 0);
  143. check('beatFreshMs ≥ 2 个心跳', T.beatFreshMs >= T.beatIntervalMs * 2,
  144. `${T.beatFreshMs} vs ${T.beatIntervalMs}`);
  145. check('beatDeadMs > beatFreshMs', T.beatDeadMs > T.beatFreshMs,
  146. `${T.beatDeadMs} vs ${T.beatFreshMs}`);
  147. check('startAckSoftMs < startAckMaxMs', T.startAckSoftMs < T.startAckMaxMs,
  148. `${T.startAckSoftMs} vs ${T.startAckMaxMs}`);
  149. check('stopAckMs > 一个心跳(否则慢心跳永远等不到 ack)',
  150. T.stopAckMs > T.beatIntervalMs, `${T.stopAckMs} vs ${T.beatIntervalMs}`);
  151. check('stopFinishMs > stopAckMs', T.stopFinishMs > T.stopAckMs);
  152. check('v1 回退的判死比 v2 更宽松', T.legacyNoBeatDeadMs >= T.beatDeadMs,
  153. `${T.legacyNoBeatDeadMs} vs ${T.beatDeadMs}`);
  154. check('v1 回退的停止超时是改造前的 8s', T.legacyStopTimeoutMs === 8000,
  155. String(T.legacyStopTimeoutMs));
  156. check('v1 回退的子引擎宽限期是改造前的 4s', T.legacyChildGraceMs === 4000,
  157. String(T.legacyChildGraceMs));
  158. /* ------------------------------------------------ 5. 阈值键集合一致 */
  159. const appKeys = Object.keys(T).sort().join(',');
  160. const autoKeys = Object.keys(B.SPEC.thresholds).sort().join(',');
  161. eq('两侧阈值键集合相同', appKeys, autoKeys);
  162. /* ------------------------------------------------------------ 汇总 */
  163. console.log('');
  164. console.log(`通过 ${passed} 项`);
  165. if (failures.length > 0) {
  166. console.log(`失败 ${failures.length} 项:`);
  167. failures.forEach((f) => console.log(' ✗ ' + f));
  168. console.log('');
  169. console.log('PROTOCOL_FAILED');
  170. process.exit(1);
  171. }
  172. console.log('PROTOCOL_CLEAN');
  173. /* ------------------------------------------------------------ 辅助 */
  174. function stepsToTerminal(edges, terminal, start) {
  175. let frontier = [start];
  176. const seen = {};
  177. seen[start] = true;
  178. for (let step = 1; step <= 4; step += 1) {
  179. const next = [];
  180. for (let i = 0; i < frontier.length; i += 1) {
  181. const edgesOfNode = edges[frontier[i]] || {};
  182. const targets = Object.keys(edgesOfNode);
  183. for (let j = 0; j < targets.length; j += 1) {
  184. const to = targets[j];
  185. if (terminal[to] === true) {
  186. return step;
  187. }
  188. if (seen[to] !== true) {
  189. seen[to] = true;
  190. next.push(to);
  191. }
  192. }
  193. }
  194. frontier = next;
  195. }
  196. return 99;
  197. }
  198. function firstDiff(a, b) {
  199. const len = Math.min(a.length, b.length);
  200. for (let i = 0; i < len; i += 1) {
  201. if (a[i] !== b[i]) {
  202. return `首个差异在第 ${i} 字符:${JSON.stringify(a.slice(i, i + 40))} vs ${JSON.stringify(b.slice(i, i + 40))}`;
  203. }
  204. }
  205. return `长度不同:${a.length} vs ${b.length}`;
  206. }