account-switch-smoke-test.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const assert = require('assert/strict');
  4. const fs = require('fs');
  5. const os = require('os');
  6. const path = require('path');
  7. function envValue(filePath, key) {
  8. const content = fs.readFileSync(filePath, 'utf8');
  9. const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
  10. return match ? match[1].trim() : undefined;
  11. }
  12. async function main() {
  13. const packageRoot = path.resolve(__dirname, '..');
  14. const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-account-switch-'));
  15. const previousCwd = process.cwd();
  16. const previousEnv = { ...process.env };
  17. const originalFetch = global.fetch;
  18. const requests = [];
  19. let switchActiveAccount;
  20. let removeActiveAccount;
  21. let getAgentStatus;
  22. let closeAgentWorkbenches = () => {};
  23. try {
  24. process.env.USERPROFILE = tempRoot;
  25. process.env.HOME = tempRoot;
  26. process.env.QIWEI_WORKSPACE_ROOT = tempRoot;
  27. process.env.QIWEI_PACKAGE_ROOT = tempRoot;
  28. process.env.QIWEI_OUTPUTS_DIR = path.join(tempRoot, 'outputs');
  29. process.env.CLAUDE_CODE_WORKDIR = tempRoot;
  30. process.env.QIWEI_AUTH_TOKEN = 'test-account-switch-token';
  31. delete process.env.QIWEI_UID;
  32. delete process.env.QIWE_UID;
  33. delete process.env.QIWEI_GUID;
  34. delete process.env.QIWE_GUID;
  35. process.chdir(tempRoot);
  36. const credentials = require(path.join(packageRoot, 'mcp', 'src', 'core', 'credentials'));
  37. const login = require(path.join(packageRoot, 'mcp', 'src', 'tools', 'qiwei-login-run'));
  38. assert.equal(path.dirname(credentials.CREDENTIALS_FILE), path.join(tempRoot, '.claude'));
  39. const envFile = path.join(tempRoot, '.env.local');
  40. credentials.saveQiweiClientConfig({
  41. uid: 'uid-account-a',
  42. guid: 'guid-account-a',
  43. apiBase: 'https://gateway.example/api/qiwei',
  44. envRoot: tempRoot,
  45. });
  46. assert.equal(envValue(envFile, 'QIWEI_UID'), 'uid-account-a');
  47. assert.equal(envValue(envFile, 'QIWEI_GUID'), 'guid-account-a');
  48. assert.equal(credentials.readQiweiGuid({ uid: 'uid-account-b' }), '');
  49. let checkLoggedIn = false;
  50. global.fetch = async (rawUrl, options = {}) => {
  51. const url = new URL(String(rawUrl));
  52. const body = options.body ? JSON.parse(options.body) : {};
  53. requests.push({ path: url.pathname, body });
  54. let data = {};
  55. if (url.pathname.endsWith('/login/start')) data = { uid: body.uid };
  56. else if (url.pathname.endsWith('/login/check')) {
  57. data = checkLoggedIn
  58. ? { uid: body.uid, status: 2, detail: { userId: 'user-b', nickname: '账号 B', guid: 'guid-account-b' } }
  59. : { uid: body.uid, status: 10, detail: {} };
  60. } else if (url.pathname.endsWith('/login/verify')) data = { accepted: true };
  61. else if (url.pathname.endsWith('/login/status')) data = { configured: true, online: true, statusCode: 2, detail: { userId: 'user-b', nickname: '账号 B' } };
  62. return new Response(JSON.stringify({ code: 0, data }), {
  63. status: 200,
  64. headers: { 'Content-Type': 'application/json' },
  65. });
  66. };
  67. const common = {
  68. uid: 'uid-account-b',
  69. authToken: 'test-account-switch-token',
  70. apiBase: 'https://gateway.example/api/qiwei',
  71. flowUi: false,
  72. openBrowser: false,
  73. persistConfig: false,
  74. skipSubscriptionCheck: true,
  75. };
  76. await login.qiweiLoginStart(common);
  77. const pending = await login.qiweiLoginCheck(common);
  78. assert.equal(pending.status, 'needs_verify_code');
  79. await login.qiweiLoginVerify({ ...common, code: '123456' });
  80. for (const request of requests.slice(0, 3)) {
  81. assert.equal(request.body.uid, 'uid-account-b');
  82. assert.equal(Object.prototype.hasOwnProperty.call(request.body, 'guid'), false);
  83. }
  84. checkLoggedIn = true;
  85. const loggedIn = await login.qiweiLoginCheck(common);
  86. assert.equal(loggedIn.summary.loggedIn, true);
  87. assert.equal(loggedIn.data.guid, 'guid-account-b');
  88. assert.equal(Object.prototype.hasOwnProperty.call(requests.at(-1).body, 'guid'), false);
  89. ({ switchActiveAccount, removeActiveAccount, getAgentStatus, closeAgentWorkbenches } = require(path.join(packageRoot, 'mcp', 'src', 'dashboard', 'agent-service')));
  90. await switchActiveAccount({
  91. uid: 'uid-account-b',
  92. guid: loggedIn.data.guid,
  93. apiBase: common.apiBase,
  94. userId: 'user-b',
  95. nickname: '账号 B',
  96. });
  97. assert.equal(envValue(envFile, 'QIWEI_UID'), 'uid-account-b');
  98. assert.equal(envValue(envFile, 'QIWEI_GUID'), 'guid-account-b');
  99. assert.equal(credentials.readQiweiGuid({ uid: 'uid-account-b' }), 'guid-account-b');
  100. await switchActiveAccount({
  101. uid: 'uid-account-c',
  102. guid: '',
  103. apiBase: common.apiBase,
  104. userId: 'user-c',
  105. nickname: '账号 C',
  106. });
  107. assert.equal(envValue(envFile, 'QIWEI_UID'), 'uid-account-c');
  108. assert.equal(envValue(envFile, 'QIWEI_GUID'), '');
  109. assert.equal(credentials.readQiweiGuid({ uid: 'uid-account-c' }), '');
  110. const removedAccount = await removeActiveAccount({ uid: 'uid-account-c', guid: '' });
  111. assert.equal(removedAccount.summary.credentialsCleared, true);
  112. const unboundStatus = await getAgentStatus();
  113. assert.equal(unboundStatus.data.account.uid, '');
  114. assert.equal(unboundStatus.data.account.guid, '');
  115. const appSource = fs.readFileSync(path.join(packageRoot, 'mcp', 'src', 'dashboard', 'app.js'), 'utf8');
  116. assert.match(appSource, /const ACCOUNTS_KEY = 'qiwei-accounts'/);
  117. assert.match(appSource, /\/api\/accounts\/switch/);
  118. assert.match(appSource, /\/api\/accounts\/remove/);
  119. assert.match(appSource, /const removingCurrent = state\.currentAccountId === id/);
  120. assert.match(appSource, /state\.loginUid = null/);
  121. assert.match(appSource, /async function hydrateCurrentAccountBinding/);
  122. assert.match(appSource, /async function forceSwitchLogin/);
  123. assert.match(appSource, /statusCode === 4[\s\S]*登录已取消,请重新生成二维码/);
  124. assert.equal(envValue(envFile, 'QIWEI_UID'), '');
  125. assert.equal(envValue(envFile, 'QIWEI_GUID'), '');
  126. process.stdout.write(`${JSON.stringify({
  127. status: 'ok',
  128. checks: 24,
  129. coverage: [
  130. 'new_uid_does_not_reuse_old_guid',
  131. 'verify_and_check_keep_uid_isolation',
  132. 'successful_switch_persists_uid_guid',
  133. 'guidless_switch_clears_stale_guid',
  134. 'workbench_account_switch_and_remove_apis',
  135. 'cancelled_login_asks_for_new_qr',
  136. 'remove_account_clears_local_credentials',
  137. 'remove_current_account_clears_live_uid_guid',
  138. 'remove_current_account_discards_login_uid',
  139. ],
  140. }, null, 2)}\n`);
  141. } finally {
  142. closeAgentWorkbenches();
  143. global.fetch = originalFetch;
  144. process.chdir(previousCwd);
  145. for (const key of Object.keys(process.env)) {
  146. if (!(key in previousEnv)) delete process.env[key];
  147. }
  148. for (const [key, value] of Object.entries(previousEnv)) process.env[key] = value;
  149. fs.rmSync(tempRoot, { recursive: true, force: true });
  150. }
  151. }
  152. main().catch(error => {
  153. process.stderr.write(`${error.stack || error.message}\n`);
  154. process.exitCode = 1;
  155. });