Files
creator-hub/web/tests/account-actions.test.cjs
T
rogee e50bed9ad8
douyin-release-gate / verify (push) Failing after 18m54s
feat: add account events and private message management
Add Douyin notification polling, event details, and manual multi-account private messaging.

Refine environment memory settings, account operations, login collection recovery, and message UI; update tests and documentation.
2026-10-07 10:23:05 +08:00

137 lines
7.6 KiB
JavaScript

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'), /账号/);
});