const assert = require('node:assert/strict'); const { readFileSync } = require('node:fs'); const { resolve } = require('node:path'); const { Module } = require('node:module'); const test = require('node:test'); const ts = require('typescript'); function compile(file, imports = {}) { const loaded = new Module(file, module); loaded.require = id => imports[id] || require(id); loaded._compile(ts.transpileModule(readFileSync(file, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText, file); return loaded.exports; } const collection = compile(resolve(__dirname, '../src/utils/account-collection-status.ts')); const running = { alias: 'environment', state: 'running', status: 'ready', gateway_reachable: true }; const stopped = { ...running, state: 'runtime_missing', status: 'stopped' }; const account = { id: 'account', login_status: 'logged_in', runtime_status: 'active', environment: { alias: 'environment', runtime_id: 'old-record' } }; function setup({ current = account, snapshots = [running], result = { status: 'logged_in', account_id: 'account' }, actionError } = {}) { const calls = []; const queue = [...snapshots]; const api = { creatorGet: async path => { calls.push(['get', path]); if (path === '/phase-a/accounts/account') return current; if (path === '/browsers/environment') return queue.length > 1 ? queue.shift() : queue[0]; throw new Error('Unexpected request: ' + path); }, creatorAction: async (path, body) => { calls.push(['post', path, body]); if (actionError) throw actionError; if (path === '/creator/accounts/account/verify') return result; if (path === '/browsers/environment/start' || path === '/phase-a/accounts/account/start') return {}; throw new Error('Unexpected action: ' + path); }, }; return { calls, ...compile(resolve(__dirname, '../src/components/accounts/account-actions.ts'), { '@/services/api': api, '@/utils/account-collection-status': collection, }) }; } for (const [row, runtime, expected] of [ [{ pending_environment: true }, undefined, ['login', '登录并同步账号']], [{ ...account, runtime_status: 'paused' }, running, ['resume', '恢复运行']], [account, undefined, ['check', '检查状态']], [account, { ...running, gateway_reachable: false }, ['check', '检查状态']], [account, { ...running, state: 'recorded' }, ['check', '检查状态']], [account, { ...running, cleanup_pending: true }, ['check', '检查状态']], [account, stopped, ['start', '启动环境']], [account, running, ['sync', '同步资料']], [{ ...account, login_status: 'manual_login' }, running, ['login', '重新登录']], [{ ...account, works_collection: { status: 'blocked', last_error: 'browser is awaiting user login' } }, running, ['login', '重新登录']], ]) { test(`primary action ${expected.join('/')} uses actual browser state`, () => { const s = setup(); assert.deepEqual(s.accountPrimaryAction(row, runtime), { key: expected[0], label: expected[1] }); }); } test('latest successful verification overrides an older login-required collection result', () => { const s = setup(); assert.equal(s.accountPrimaryAction({ ...account, works_collection: { status: 'blocked', last_error: 'browser is awaiting user login' } }, running, 'logged_in').key, 'sync'); }); test('normal sync checks live state and verifies identity without restarting or requesting QR', async () => { const s = setup(); assert.equal((await s.performAccountPrimaryAction('account', 'sync')).status, 'logged_in'); assert.deepEqual(s.calls.map(call => call.slice(0, 2)), [ ['get', '/phase-a/accounts/account'], ['get', '/browsers/environment'], ['post', '/creator/accounts/account/verify'], ]); }); test('login-required result is explicit; other failures must not be disguised as QR login', async () => { const s = setup({ result: { status: 'manual_login', account_id: 'account', reason: 'awaiting_login' } }); assert.equal((await s.performAccountPrimaryAction('account', 'login')).status, 'manual_login'); assert.ok(s.calls.every(call => !call[1].includes('/qr'))); await assert.rejects(setup({ actionError: new Error('identity UID mismatch') }).performAccountPrimaryAction('account', 'sync'), /UID mismatch/); await assert.rejects(setup({ result: { status: 'unknown' } }).performAccountPrimaryAction('account', 'sync'), /未识别/); await assert.rejects(setup({ result: { status: 'manual_login', reason: 'platform_restricted', account_id: 'account' } }).performAccountPrimaryAction('account', 'login'), /未识别/); }); test('starting is not reported successful until the gateway reports a running browser', async () => { const s = setup({ snapshots: [stopped, stopped] }); await assert.rejects(s.performAccountPrimaryAction('account', 'start'), /启动尚未完成/); assert.ok(!s.calls.some(call => call[1].endsWith('/verify'))); }); test('starting a stopped browser does not restore or overwrite the account', async () => { const s = setup({ snapshots: [stopped, running] }); await s.performAccountPrimaryAction('account', 'start'); assert.deepEqual(s.calls.filter(call => call[0] === 'post').map(call => call[1]), ['/browsers/environment/start', '/creator/accounts/account/verify']); }); test('starting an already running browser only verifies it', async () => { const s = setup(); await s.performAccountPrimaryAction('account', 'start'); assert.ok(!s.calls.some(call => call[1] === '/browsers/environment/start')); }); test('restoring a paused account is explicit and uses the dedicated account start route', async () => { const s = setup({ current: { ...account, runtime_status: 'paused' } }); await s.performAccountPrimaryAction('account', 'resume'); assert.equal(s.calls[1][1], '/phase-a/accounts/account/start'); assert.ok(!s.calls.some(call => call[1] === '/browsers/environment/start')); }); test('stale login or sync buttons never implicitly restore a paused account', async () => { for (const intent of ['sync', 'login', 'start']) { const s = setup({ current: { ...account, runtime_status: 'paused' } }); await assert.rejects(s.performAccountPrimaryAction('account', intent), /先恢复运行/); assert.equal(s.calls.length, 1); } }); test('a stopped browser is checked without mutation and stale sync does not start it', async () => { const s = setup({ snapshots: [stopped] }); assert.equal((await s.performAccountPrimaryAction('account', 'check')).status, 'stopped'); assert.ok(s.calls.every(call => call[0] === 'get')); await assert.rejects(setup({ snapshots: [stopped] }).performAccountPrimaryAction('account', 'sync'), /启动环境/); }); test('gateway failure, wrong alias and unexpected browser state stop the action', async () => { for (const runtime of [{ ...running, gateway_reachable: false }, { ...running, alias: 'other' }, { ...running, state: 'recorded' }, { ...running, cleanup_pending: true }]) { const s = setup({ snapshots: [runtime] }); await assert.rejects(s.performAccountPrimaryAction('account', 'start')); assert.ok(s.calls.every(call => call[0] === 'get')); } }); test('wrong account responses, missing binding and changed verification identity fail visibly', async () => { await assert.rejects(setup({ current: { ...account, id: 'other' } }).performAccountPrimaryAction('account', 'sync'), /账号/); await assert.rejects(setup({ current: { ...account, environment: null } }).performAccountPrimaryAction('account', 'sync'), /环境/); await assert.rejects(setup({ result: { status: 'logged_in', account_id: 'other' } }).performAccountPrimaryAction('account', 'sync'), /账号/); });