|
|
@@ -0,0 +1,151 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+'use strict';
|
|
|
+
|
|
|
+const assert = require('assert/strict');
|
|
|
+const fs = require('fs');
|
|
|
+const os = require('os');
|
|
|
+const path = require('path');
|
|
|
+
|
|
|
+function envValue(filePath, key) {
|
|
|
+ const content = fs.readFileSync(filePath, 'utf8');
|
|
|
+ const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
|
+ return match ? match[1].trim() : undefined;
|
|
|
+}
|
|
|
+
|
|
|
+async function main() {
|
|
|
+ const packageRoot = path.resolve(__dirname, '..');
|
|
|
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-account-switch-'));
|
|
|
+ const previousCwd = process.cwd();
|
|
|
+ const previousEnv = { ...process.env };
|
|
|
+ const originalFetch = global.fetch;
|
|
|
+ const requests = [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ process.env.USERPROFILE = tempRoot;
|
|
|
+ process.env.HOME = tempRoot;
|
|
|
+ process.env.QIWEI_WORKSPACE_ROOT = tempRoot;
|
|
|
+ process.env.QIWEI_OUTPUTS_DIR = path.join(tempRoot, 'outputs');
|
|
|
+ process.env.CLAUDE_CODE_WORKDIR = tempRoot;
|
|
|
+ process.env.QIWEI_AUTH_TOKEN = 'test-account-switch-token';
|
|
|
+ delete process.env.QIWEI_UID;
|
|
|
+ delete process.env.QIWE_UID;
|
|
|
+ delete process.env.QIWEI_GUID;
|
|
|
+ delete process.env.QIWE_GUID;
|
|
|
+ process.chdir(tempRoot);
|
|
|
+
|
|
|
+ const credentials = require(path.join(packageRoot, 'mcp', 'src', 'core', 'credentials'));
|
|
|
+ const login = require(path.join(packageRoot, 'mcp', 'src', 'tools', 'qiwei-login-run'));
|
|
|
+ assert.equal(path.dirname(credentials.CREDENTIALS_FILE), path.join(tempRoot, '.claude'));
|
|
|
+
|
|
|
+ const envFile = path.join(tempRoot, '.env.local');
|
|
|
+ credentials.saveQiweiClientConfig({
|
|
|
+ uid: 'uid-account-a',
|
|
|
+ guid: 'guid-account-a',
|
|
|
+ apiBase: 'https://gateway.example/api/qiwei',
|
|
|
+ envRoot: tempRoot,
|
|
|
+ });
|
|
|
+ assert.equal(envValue(envFile, 'QIWEI_UID'), 'uid-account-a');
|
|
|
+ assert.equal(envValue(envFile, 'QIWEI_GUID'), 'guid-account-a');
|
|
|
+ assert.equal(credentials.readQiweiGuid({ uid: 'uid-account-b' }), '');
|
|
|
+
|
|
|
+ let checkLoggedIn = false;
|
|
|
+ global.fetch = async (rawUrl, options = {}) => {
|
|
|
+ const url = new URL(String(rawUrl));
|
|
|
+ const body = options.body ? JSON.parse(options.body) : {};
|
|
|
+ requests.push({ path: url.pathname, body });
|
|
|
+ let data = {};
|
|
|
+ if (url.pathname.endsWith('/login/start')) data = { uid: body.uid };
|
|
|
+ else if (url.pathname.endsWith('/login/check')) {
|
|
|
+ data = checkLoggedIn
|
|
|
+ ? { uid: body.uid, status: 2, detail: { userId: 'user-b', nickname: '账号 B', guid: 'guid-account-b' } }
|
|
|
+ : { uid: body.uid, status: 10, detail: {} };
|
|
|
+ } else if (url.pathname.endsWith('/login/verify')) data = { accepted: true };
|
|
|
+ else if (url.pathname.endsWith('/login/status')) data = { configured: true, online: true, statusCode: 2, detail: { userId: 'user-b', nickname: '账号 B' } };
|
|
|
+ return new Response(JSON.stringify({ code: 0, data }), {
|
|
|
+ status: 200,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const common = {
|
|
|
+ uid: 'uid-account-b',
|
|
|
+ authToken: 'test-account-switch-token',
|
|
|
+ apiBase: 'https://gateway.example/api/qiwei',
|
|
|
+ flowUi: false,
|
|
|
+ openBrowser: false,
|
|
|
+ persistConfig: false,
|
|
|
+ skipSubscriptionCheck: true,
|
|
|
+ };
|
|
|
+ await login.qiweiLoginStart(common);
|
|
|
+ const pending = await login.qiweiLoginCheck(common);
|
|
|
+ assert.equal(pending.status, 'needs_verify_code');
|
|
|
+ await login.qiweiLoginVerify({ ...common, code: '123456' });
|
|
|
+ for (const request of requests.slice(0, 3)) {
|
|
|
+ assert.equal(request.body.uid, 'uid-account-b');
|
|
|
+ assert.equal(Object.prototype.hasOwnProperty.call(request.body, 'guid'), false);
|
|
|
+ }
|
|
|
+
|
|
|
+ checkLoggedIn = true;
|
|
|
+ const loggedIn = await login.qiweiLoginCheck(common);
|
|
|
+ assert.equal(loggedIn.summary.loggedIn, true);
|
|
|
+ assert.equal(loggedIn.data.guid, 'guid-account-b');
|
|
|
+ assert.equal(Object.prototype.hasOwnProperty.call(requests.at(-1).body, 'guid'), false);
|
|
|
+
|
|
|
+ const { switchActiveAccount } = require(path.join(packageRoot, 'mcp', 'src', 'dashboard', 'agent-service'));
|
|
|
+ await switchActiveAccount({
|
|
|
+ uid: 'uid-account-b',
|
|
|
+ guid: loggedIn.data.guid,
|
|
|
+ apiBase: common.apiBase,
|
|
|
+ userId: 'user-b',
|
|
|
+ nickname: '账号 B',
|
|
|
+ });
|
|
|
+ assert.equal(envValue(envFile, 'QIWEI_UID'), 'uid-account-b');
|
|
|
+ assert.equal(envValue(envFile, 'QIWEI_GUID'), 'guid-account-b');
|
|
|
+ assert.equal(credentials.readQiweiGuid({ uid: 'uid-account-b' }), 'guid-account-b');
|
|
|
+
|
|
|
+ await switchActiveAccount({
|
|
|
+ uid: 'uid-account-c',
|
|
|
+ guid: '',
|
|
|
+ apiBase: common.apiBase,
|
|
|
+ userId: 'user-c',
|
|
|
+ nickname: '账号 C',
|
|
|
+ });
|
|
|
+ assert.equal(envValue(envFile, 'QIWEI_UID'), 'uid-account-c');
|
|
|
+ assert.equal(envValue(envFile, 'QIWEI_GUID'), '');
|
|
|
+ assert.equal(credentials.readQiweiGuid({ uid: 'uid-account-c' }), '');
|
|
|
+ assert.equal(JSON.parse(fs.readFileSync(credentials.CREDENTIALS_FILE, 'utf8')).guid, undefined);
|
|
|
+
|
|
|
+ const appSource = fs.readFileSync(path.join(packageRoot, 'mcp', 'src', 'dashboard', 'app.js'), 'utf8');
|
|
|
+ assert.match(appSource, /qiwei-\$\{workspaceId\}-accounts/);
|
|
|
+ assert.match(appSource, /await initializeWorkspaceAccountStorage\(\);[\s\S]*if \(currentAccount\(\)\?\.uid\)/);
|
|
|
+ assert.match(appSource, /addAccount\(account, \{ activate: false \}\)[\s\S]*await switchAccount\(accountId/);
|
|
|
+ assert.match(appSource, /if \(!switched\)[\s\S]*state\.currentAccountId = previousAccountId/);
|
|
|
+ assert.match(appSource, /statusCode === 4[\s\S]*restorePreviousAccountSelection\(\)/);
|
|
|
+
|
|
|
+ process.stdout.write(`${JSON.stringify({
|
|
|
+ status: 'ok',
|
|
|
+ checks: 20,
|
|
|
+ coverage: [
|
|
|
+ 'new_uid_does_not_reuse_old_guid',
|
|
|
+ 'verify_and_check_keep_uid_isolation',
|
|
|
+ 'successful_switch_persists_uid_guid',
|
|
|
+ 'guidless_switch_clears_stale_guid',
|
|
|
+ 'workspace_scoped_browser_accounts',
|
|
|
+ 'failed_or_cancelled_login_restores_previous_account',
|
|
|
+ ],
|
|
|
+ }, null, 2)}\n`);
|
|
|
+ } finally {
|
|
|
+ global.fetch = originalFetch;
|
|
|
+ process.chdir(previousCwd);
|
|
|
+ for (const key of Object.keys(process.env)) {
|
|
|
+ if (!(key in previousEnv)) delete process.env[key];
|
|
|
+ }
|
|
|
+ for (const [key, value] of Object.entries(previousEnv)) process.env[key] = value;
|
|
|
+ fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+main().catch(error => {
|
|
|
+ process.stderr.write(`${error.stack || error.message}\n`);
|
|
|
+ process.exitCode = 1;
|
|
|
+});
|