background-step6-retry-limit.test.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. test('step 6 runs cookie cleanup and completes from background', async () => {
  5. const source = fs.readFileSync('background/steps/clear-login-cookies.js', 'utf8');
  6. const globalScope = {};
  7. const api = new Function('self', `${source}; return self.MultiPageBackgroundStep6;`)(globalScope);
  8. const events = {
  9. cleanupCalls: 0,
  10. completedSteps: [],
  11. };
  12. const executor = api.createStep6Executor({
  13. completeStepFromBackground: async (step) => {
  14. events.completedSteps.push(step);
  15. },
  16. runPreStep6CookieCleanup: async () => {
  17. events.cleanupCalls += 1;
  18. },
  19. });
  20. await executor.executeStep6();
  21. assert.equal(events.cleanupCalls, 1);
  22. assert.deepStrictEqual(events.completedSteps, [6]);
  23. });
  24. test('step 7 retries up to configured limit and then fails', async () => {
  25. const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
  26. const globalScope = {};
  27. const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
  28. const events = {
  29. refreshCalls: 0,
  30. sendCalls: 0,
  31. completed: 0,
  32. };
  33. const executor = api.createStep7Executor({
  34. addLog: async () => {},
  35. completeStepFromBackground: async () => {
  36. events.completed += 1;
  37. },
  38. getErrorMessage: (error) => error?.message || String(error || ''),
  39. getLoginAuthStateLabel: (state) => state || 'unknown',
  40. getState: async () => ({ email: 'user@example.com', password: 'secret' }),
  41. isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
  42. isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
  43. refreshOAuthUrlBeforeStep6: async () => {
  44. events.refreshCalls += 1;
  45. return `https://oauth.example/${events.refreshCalls}`;
  46. },
  47. reuseOrCreateTab: async () => {},
  48. sendToContentScriptResilient: async () => {
  49. events.sendCalls += 1;
  50. return {
  51. step6Outcome: 'recoverable',
  52. state: 'email_page',
  53. message: '当前仍停留在邮箱页。',
  54. };
  55. },
  56. STEP6_MAX_ATTEMPTS: 3,
  57. throwIfStopped: () => {},
  58. });
  59. await assert.rejects(
  60. () => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
  61. /已重试 2 次,仍未成功/
  62. );
  63. assert.equal(events.refreshCalls, 3);
  64. assert.equal(events.sendCalls, 3);
  65. assert.equal(events.completed, 0);
  66. });
  67. test('step 7 exits internal retry loop immediately when add-phone is detected', async () => {
  68. const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
  69. const globalScope = {};
  70. const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
  71. const events = {
  72. refreshCalls: 0,
  73. sendCalls: 0,
  74. completed: 0,
  75. logs: [],
  76. };
  77. const executor = api.createStep7Executor({
  78. addLog: async (message, level = 'info') => {
  79. events.logs.push({ message, level });
  80. },
  81. completeStepFromBackground: async () => {
  82. events.completed += 1;
  83. },
  84. getErrorMessage: (error) => error?.message || String(error || ''),
  85. getLoginAuthStateLabel: (state) => state || 'unknown',
  86. getState: async () => ({ email: 'user@example.com', password: 'secret' }),
  87. isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
  88. isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
  89. refreshOAuthUrlBeforeStep6: async () => {
  90. events.refreshCalls += 1;
  91. return `https://oauth.example/${events.refreshCalls}`;
  92. },
  93. reuseOrCreateTab: async () => {},
  94. sendToContentScriptResilient: async () => {
  95. events.sendCalls += 1;
  96. throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
  97. },
  98. STEP6_MAX_ATTEMPTS: 3,
  99. throwIfStopped: () => {},
  100. });
  101. await assert.rejects(
  102. () => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
  103. /add-phone/
  104. );
  105. assert.equal(events.refreshCalls, 1, 'add-phone should stop further OAuth refresh attempts');
  106. assert.equal(events.sendCalls, 1, 'add-phone should stop after the first failed login attempt');
  107. assert.equal(events.completed, 0);
  108. assert.ok(
  109. !events.logs.some(({ message }) => /准备重试/.test(message)),
  110. 'add-phone failure should not be logged as an internal retryable attempt'
  111. );
  112. });
  113. test('step 7 starts a new oauth timeout window for each refreshed oauth url', async () => {
  114. const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
  115. const globalScope = {};
  116. const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
  117. const events = {
  118. startedWindows: [],
  119. timeoutRequests: [],
  120. };
  121. const executor = api.createStep7Executor({
  122. addLog: async () => {},
  123. completeStepFromBackground: async () => {},
  124. getErrorMessage: (error) => error?.message || String(error || ''),
  125. getLoginAuthStateLabel: (state) => state || 'unknown',
  126. getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, options) => {
  127. events.timeoutRequests.push({ defaultTimeoutMs, options });
  128. return 5000;
  129. },
  130. getState: async () => ({ email: 'user@example.com', password: 'secret' }),
  131. isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
  132. isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
  133. refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
  134. reuseOrCreateTab: async () => {},
  135. sendToContentScriptResilient: async (_source, _message, options) => ({
  136. step6Outcome: 'success',
  137. usedTimeoutMs: options.timeoutMs,
  138. }),
  139. startOAuthFlowTimeoutWindow: async (payload) => {
  140. events.startedWindows.push(payload);
  141. },
  142. STEP6_MAX_ATTEMPTS: 3,
  143. throwIfStopped: () => {},
  144. });
  145. await executor.executeStep7({ email: 'user@example.com', password: 'secret' });
  146. assert.deepStrictEqual(events.startedWindows, [
  147. { step: 7, oauthUrl: 'https://oauth.example/latest' },
  148. ]);
  149. assert.deepStrictEqual(events.timeoutRequests, [
  150. {
  151. defaultTimeoutMs: 180000,
  152. options: {
  153. step: 7,
  154. actionLabel: 'OAuth 登录并进入验证码页',
  155. },
  156. },
  157. ]);
  158. });