test/dashboard-account-query.test.js
Rama master · réplica de lectura del repositorio original.
// Dashboard account list query + batch routes (2026-10-22 account-UI merge).
//
// The merged 账号管理 page is server-paginated: search, tier, status and 仅异常
// are all query params, and "select all N results" is an id fetch, not a DOM
// walk. These tests pin the contract the page relies on:
// - q matches email/id/label literally (no SQL LIKE wildcards)
// - status resolves through the same precedence ladder the row badge uses
// - total is counted with the SAME predicate as the rows (D10)
// - /accounts/ids returns exactly the filtered set
// - /accounts/batch reports per-item results and never hides partial failure
import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { config } from '../src/config.js';
import {
addAccountByKey,
configureBindHost,
getAccountInternal,
markNeedsReauth,
publicBanReason,
removeAccount,
resolveAccountState,
setAccountStatus,
_resetLockoutForTests,
} from '../src/auth.js';
import { handleDashboardApi } from '../src/dashboard/api.js';
import { probeOutcomeEntry, summarizeProbeResults } from '../src/dashboard/fleet-jobs.js';
import { _resetRuntimeConfigForTests } from '../src/runtime-config.js';
const originalDashboardPassword = config.dashboardPassword;
const originalApiKey = config.apiKey;
const originalAllowNoAuth = process.env.DASHBOARD_ALLOW_NO_AUTH;
const createdAccounts = new Set();
afterEach(() => {
for (const id of createdAccounts) removeAccount(id);
createdAccounts.clear();
_resetRuntimeConfigForTests();
_resetLockoutForTests();
config.dashboardPassword = originalDashboardPassword;
config.apiKey = originalApiKey;
if (originalAllowNoAuth === undefined) delete process.env.DASHBOARD_ALLOW_NO_AUTH;
else process.env.DASHBOARD_ALLOW_NO_AUTH = originalAllowNoAuth;
configureBindHost('0.0.0.0');
});
function fakeRes() {
return {
statusCode: 0,
headers: {},
body: '',
writeHead(status, headers) { this.statusCode = status; this.headers = headers || {}; },
end(chunk) { this.body += chunk ? String(chunk) : ''; },
json() { return this.body ? JSON.parse(this.body) : null; },
};
}
function localReq(path) {
return { url: `/dashboard/api${path}`, headers: {}, socket: { remoteAddress: '127.0.0.1' } };
}
function enableOpenLocal() {
_resetRuntimeConfigForTests();
config.dashboardPassword = '';
config.apiKey = '';
configureBindHost('127.0.0.1');
process.env.DASHBOARD_ALLOW_NO_AUTH = '1';
}
async function get(path) {
const res = fakeRes();
await handleDashboardApi('GET', path.split('?')[0], {}, localReq(path), res);
return { status: res.statusCode, body: res.json() };
}
async function post(path, body) {
const res = fakeRes();
await handleDashboardApi('POST', path, body, localReq(path), res);
return { status: res.statusCode, body: res.json() };
}
function addTestAccount(label, patch = {}) {
const account = addAccountByKey(`test-key-${label}-${Date.now()}-${Math.random().toString(16).slice(2)}`, label);
Object.assign(account, patch);
createdAccounts.add(account.id);
return account;
}
describe('GET /accounts query params', () => {
it('searches email, id and label case-insensitively and treats %/_ literally', async () => {
enableOpenLocal();
const alpha = addTestAccount('alpha@example.invalid');
const beta = addTestAccount('beta@example.invalid');
Object.assign(getAccountInternal(beta.id), { label: 'Night Shift 100%_done' });
const byEmail = await get('/accounts?view=summary&q=ALPHA');
assert.equal(byEmail.status, 200);
assert.deepEqual(byEmail.body.accounts.map((a) => a.id), [alpha.id]);
const byId = await get(`/accounts?view=summary&q=${encodeURIComponent(alpha.id)}`);
assert.deepEqual(byId.body.accounts.map((a) => a.id), [alpha.id]);
const byLabel = await get(`/accounts?view=summary&q=${encodeURIComponent('100%_done')}`);
assert.deepEqual(byLabel.body.accounts.map((a) => a.id), [beta.id],
'q must be a literal substring: % and _ are not wildcards');
const noWildcard = await get(`/accounts?view=summary&q=${encodeURIComponent('100%_d')}`);
assert.equal(noWildcard.body.total, 1);
const wildcardWouldMatch = await get(`/accounts?view=summary&q=${encodeURIComponent('100Xdone')}`);
assert.equal(wildcardWouldMatch.body.total, 0);
});
it('filters by the precedence-resolved state and ignores unknown values', async () => {
enableOpenLocal();
const now = Date.now();
const active = addTestAccount('state-active');
const cooled = addTestAccount('state-cooled', { quotaResetAt: now + 3600_000 });
const limited = addTestAccount('state-limited', { rateLimitedUntil: now + 3600_000 });
const error = addTestAccount('state-error', { status: 'error' });
const disabled = addTestAccount('state-disabled', { status: 'disabled' });
const watch = addTestAccount('state-watch', { errorCount: 2 });
for (const [state, expected] of [
['active', active.id],
['quota_cooled', cooled.id],
['rate_limited', limited.id],
['error', error.id],
['disabled', disabled.id],
['watch', watch.id],
]) {
const r = await get(`/accounts?view=summary&status=${state}`);
assert.equal(r.status, 200);
assert.deepEqual(r.body.accounts.map((a) => a.id), [expected], `status=${state}`);
assert.equal(r.body.total, 1);
assert.equal(r.body.accounts[0].state, state, 'the payload carries the resolved state');
}
const ignored = await get('/accounts?view=summary&status=not-a-real-state');
assert.equal(ignored.status, 200);
assert.ok(ignored.body.total >= 6, 'unknown status is ignored, not a 400 or an empty pool');
});
it('counts total with the same predicate as the rows (D10: flagged + tier)', async () => {
enableOpenLocal();
const now = Date.now();
// Flagged via errorCount, and pro; flagged via rate limit, and free.
addTestAccount('d10-pro-flagged', { tier: 'pro', errorCount: 3 });
addTestAccount('d10-pro-clean', { tier: 'pro' });
addTestAccount('d10-free-flagged', { tier: 'free', rateLimitedUntil: now + 60_000 });
addTestAccount('d10-free-clean', { tier: 'free' });
const r = await get('/accounts?view=summary&filter=flagged&tier=pro&page=1&pageSize=1');
assert.equal(r.status, 200);
assert.equal(r.body.accounts.length, 1);
assert.equal(r.body.total, 1, 'total must honour filter AND tier, not byTier[tier] alone');
assert.equal(r.body.hasMore, false);
const both = await get('/accounts?view=summary&filter=flagged&tier=free');
assert.equal(both.body.total, 1);
assert.equal(both.body.accounts[0].tier, 'free');
});
it('sorts by the declared keys with unknown values last', async () => {
enableOpenLocal();
const low = addTestAccount('sort-low', { credits: { weeklyPercent: 5, balance: 1 } });
const high = addTestAccount('sort-high', { credits: { weeklyPercent: 90, balance: 50 } });
const unknown = addTestAccount('sort-unknown', { credits: null });
const asc = await get('/accounts?view=summary&sort=quota_weekly&order=asc');
assert.deepEqual(asc.body.accounts.map((a) => a.id), [low.id, high.id, unknown.id],
'null quota sorts last in both directions');
const desc = await get('/accounts?view=summary&sort=quota_weekly&order=desc');
assert.deepEqual(desc.body.accounts.map((a) => a.id), [high.id, low.id, unknown.id]);
const bal = await get('/accounts?view=summary&sort=balance&order=desc');
assert.deepEqual(bal.body.accounts.map((a) => a.id), [high.id, low.id, unknown.id]);
});
it('returns the same filtered ids from /accounts/ids', async () => {
enableOpenLocal();
const pro = addTestAccount('ids-pro', { tier: 'pro' });
addTestAccount('ids-free', { tier: 'free' });
const r = await get('/accounts/ids?tier=pro');
assert.equal(r.status, 200);
assert.deepEqual(r.body.ids, [pro.id]);
assert.equal(r.body.total, 1);
const all = await get('/accounts/ids');
assert.equal(all.body.ids.length, 2);
assert.equal(all.body.total, 2);
});
});
describe('resolveAccountState precedence', () => {
it('resolves disabled > banned > reauth > error > quota > rate > capacity > near-limit > watch > active', () => {
const now = 1_000_000;
assert.equal(resolveAccountState({ status: 'disabled', errorCount: 9 }, now), 'disabled');
assert.equal(resolveAccountState({ status: 'banned', errorCount: 9 }, now), 'banned',
'banned must outrank error/cooldowns: the credential itself was rejected');
assert.equal(resolveAccountState({ status: 'banned', quotaResetAt: now + 1, rateLimitedUntil: now + 1 }, now), 'banned');
assert.equal(resolveAccountState({ status: 'reauth', errorCount: 9 }, now), 'reauth',
'a retired session token outranks every self-healing state: it never recovers on its own');
assert.equal(resolveAccountState({ status: 'reauth', quotaResetAt: now + 1, rateLimitedUntil: now + 1 }, now), 'reauth');
assert.equal(resolveAccountState({ status: 'reauth' }, now), 'reauth');
assert.equal(resolveAccountState({ status: 'error', quotaResetAt: now + 1 }, now), 'error');
assert.equal(resolveAccountState({ status: 'active', quotaResetAt: now + 1, rateLimitedUntil: now + 1 }, now), 'quota_cooled');
assert.equal(resolveAccountState({ status: 'active', rateLimitedUntil: now + 1 }, now), 'rate_limited');
assert.equal(resolveAccountState({ status: 'active', _modelRateLimits: { m: now + 1 } }, now), 'capacity');
assert.equal(resolveAccountState({ status: 'active' }, now, { rpmUsed: 9, rpmLimit: 10 }), 'near_limit');
assert.equal(resolveAccountState({ status: 'active', errorCount: 1 }, now), 'watch');
assert.equal(resolveAccountState({ status: 'active' }, now), 'active');
assert.equal(resolveAccountState({ status: 'active', quotaResetAt: now - 1, rateLimitedUntil: now - 1 }, now), 'active',
'expired cooldowns do not keep an account in a warning state');
});
});
describe('reauth accounts are visible and counted separately', () => {
it('exposes state + evidence, matches status=reauth and filter=flagged, and counts once', async () => {
enableOpenLocal();
const benched = addTestAccount('reauth@example.invalid');
markNeedsReauth(benched.id, { source: 'request', code: 'UNAUTHORIZED' });
const healthy = addTestAccount('reauth-healthy@example.invalid');
const byState = await get('/accounts?view=summary&status=reauth');
assert.equal(byState.status, 200);
assert.deepEqual(byState.body.accounts.map((a) => a.id), [benched.id]);
const row = byState.body.accounts[0];
assert.equal(row.state, 'reauth');
assert.equal(row.status, 'reauth');
assert.equal(row.reauth.source, 'request');
assert.equal(row.reauth.code, 'UNAUTHORIZED');
assert.ok(row.reauth.at > 0);
const flagged = await get('/accounts?view=summary&filter=flagged');
assert.deepEqual(flagged.body.accounts.map((a) => a.id), [benched.id],
'仅异常 must include reauth accounts');
assert.equal(flagged.body.stats.reauth, 1);
assert.equal(flagged.body.stats.banned, 0);
assert.ok(!flagged.body.accounts.some((a) => a.id === healthy.id),
'a healthy account must not be pulled in by 仅异常');
});
});
describe('banned accounts are visible as banned', () => {
it('exposes state, bannedAt and a sanitized reason, and matches status=banned and filter=flagged', async () => {
enableOpenLocal();
const now = Date.now();
const banned = addTestAccount('banned@example.invalid', {
status: 'banned',
bannedAt: now - 60_000,
bannedReason: 'Your account has been suspended. Authorization: Bearer abcdefghijklmnopqrstuvwxyz0123456789 key sk-live-abcdefghijklmnop',
errorCount: 2,
});
const healthy = addTestAccount('healthy@example.invalid');
const byState = await get('/accounts?view=summary&status=banned');
assert.equal(byState.status, 200);
assert.deepEqual(byState.body.accounts.map((a) => a.id), [banned.id]);
const row = byState.body.accounts[0];
assert.equal(row.state, 'banned');
assert.equal(row.bannedAt, now - 60_000);
assert.match(row.bannedReason, /suspended/);
assert.doesNotMatch(row.bannedReason, /abcdefghijklmnopqrstuvwxyz|sk-live/,
'the ban reason must never carry a credential-shaped token');
const flagged = await get('/accounts?view=summary&filter=flagged');
assert.deepEqual(flagged.body.accounts.map((a) => a.id), [banned.id],
'仅异常 must include banned accounts');
assert.ok(flagged.body.stats.flagged >= 1);
assert.equal(flagged.body.stats.disabled, 0,
'banned is 需处理, not 已停用 — the KPI no longer double-counts it');
assert.ok(flagged.body.stats.attention >= 1);
assert.equal(flagged.body.stats.banned, 1);
assert.ok(!flagged.body.accounts.some((a) => a.id === healthy.id),
'a healthy account must not be pulled in by 仅异常');
});
it('clears the ban metadata when an operator explicitly re-enables the account', () => {
enableOpenLocal();
const account = addTestAccount('banned-reset', {
status: 'banned',
bannedAt: Date.now(),
bannedReason: 'account banned',
_banSignalAt: Date.now(),
_banSignalCount: 2,
_banSignalLastMessage: 'account banned',
});
assert.equal(setAccountStatus(account.id, 'active'), true);
const stored = getAccountInternal(account.id);
assert.equal(stored.status, 'active');
assert.equal(stored.bannedAt, 0);
assert.equal(stored.bannedReason, '');
assert.equal(stored._banSignalCount, 0);
assert.equal(resolveAccountState(stored), 'active');
});
it('caps and redacts the public ban reason', () => {
assert.equal(publicBanReason(null), '');
assert.equal(publicBanReason(' account banned '), 'account banned');
assert.match(publicBanReason(`x ${'a'.repeat(60)} y`), /\[redacted\]/);
assert.ok(publicBanReason('z'.repeat(400)).length <= 240);
});
});
describe('probe outcome accounting', () => {
it('never counts a skipped probe as success', () => {
assert.deepEqual(probeOutcomeEntry('a', { skipped: true, reason: 'ls_not_running' }), {
id: 'a', ok: false, skipped: true, reason: 'ls_not_running', tier: 'unknown',
});
assert.deepEqual(probeOutcomeEntry('b', { tier: 'pro' }), { id: 'b', ok: true, tier: 'pro' });
assert.deepEqual(probeOutcomeEntry('c', null), { id: 'c', ok: false, error: 'ERR_ACCOUNT_NOT_FOUND' });
});
it('splits attempted/succeeded/failed/skipped and excludes skips from the attempted total', () => {
const results = [
{ id: 'ok', ok: true },
{ id: 'fail', ok: false, error: 'boom' },
{ id: 'skip', ok: false, skipped: true, reason: 'ls_not_running' },
];
assert.deepEqual(summarizeProbeResults(results), { attempted: 2, okCount: 1, failCount: 1, skippedCount: 1 });
assert.deepEqual(summarizeProbeResults([]), { attempted: 0, okCount: 0, failCount: 0, skippedCount: 0 });
});
});
describe('POST /accounts/batch', () => {
it('runs each item and reports partial failure instead of a 500', async () => {
enableOpenLocal();
const a = addTestAccount('batch-a');
const b = addTestAccount('batch-b');
const disabled = await post('/accounts/batch', { ids: [a.id, b.id, 'missing-id'], op: 'disable' });
assert.equal(disabled.status, 200);
assert.equal(disabled.body.success, true);
assert.equal(disabled.body.okCount, 2);
assert.equal(disabled.body.failCount, 1);
assert.deepEqual(disabled.body.failed, [{ id: 'missing-id', ok: false, error: 'ERR_ACCOUNT_NOT_FOUND' }]);
assert.equal(getAccountInternal(a.id).status, 'disabled');
const enabled = await post('/accounts/batch', { ids: [a.id, b.id], op: 'enable' });
assert.equal(enabled.body.okCount, 2);
assert.equal(getAccountInternal(a.id).status, 'active');
Object.assign(getAccountInternal(a.id), { errorCount: 4, status: 'error' });
const reset = await post('/accounts/batch', { ids: [a.id], op: 'reset' });
assert.equal(reset.body.okCount, 1);
assert.equal(getAccountInternal(a.id).errorCount, 0);
assert.equal(getAccountInternal(a.id).status, 'active');
const deleted = await post('/accounts/batch', { ids: [b.id], op: 'delete' });
assert.equal(deleted.body.okCount, 1);
assert.equal(getAccountInternal(b.id), null);
createdAccounts.delete(b.id);
});
it('dedupes ids and rejects an empty list, an unknown op and an oversized batch', async () => {
enableOpenLocal();
const a = addTestAccount('batch-dedupe');
const dup = await post('/accounts/batch', { ids: [a.id, a.id, a.id], op: 'enable' });
assert.equal(dup.body.results.length, 1, 'one request per unique id');
const empty = await post('/accounts/batch', { ids: [], op: 'enable' });
assert.equal(empty.status, 400);
assert.equal(empty.body.error, 'ERR_BATCH_IDS_EMPTY');
const badOp = await post('/accounts/batch', { ids: [a.id], op: 'drop-database' });
assert.equal(badOp.status, 400);
assert.equal(badOp.body.error, 'ERR_BATCH_OP_INVALID');
const tooMany = await post('/accounts/batch', { ids: Array.from({ length: 1001 }, (_, i) => `id-${i}`), op: 'enable' });
assert.equal(tooMany.status, 400);
assert.equal(tooMany.body.error, 'ERR_BATCH_TOO_LARGE');
});
});
describe('fleet probe/refresh routes', () => {
it('probe-all and refresh-credits return per-account results on an empty pool', async () => {
enableOpenLocal();
// No accounts exist in this file before this test, so neither route dials
// upstream (probe-all filters to active accounts; refresh-all iterates none).
const probe = await post('/accounts/probe-all', {});
assert.equal(probe.status, 200);
assert.equal(probe.body.success, true);
assert.deepEqual(probe.body.results, []);
assert.deepEqual(
{
total: probe.body.total,
attempted: probe.body.attempted,
okCount: probe.body.okCount,
failCount: probe.body.failCount,
skippedCount: probe.body.skippedCount,
},
{ total: 0, attempted: 0, okCount: 0, failCount: 0, skippedCount: 0 },
'an empty sweep reports zeroes, not a missing summary',
);
const refresh = await post('/accounts/refresh-credits', {});
assert.equal(refresh.status, 200);
assert.equal(refresh.body.success, true);
assert.deepEqual(refresh.body.results, []);
});
it('batch probe reports a missing account as failed, not skipped or succeeded', async () => {
enableOpenLocal();
const r = await post('/accounts/batch', { ids: ['missing-probe-id'], op: 'probe' });
assert.equal(r.status, 200);
assert.equal(r.body.okCount, 0);
assert.equal(r.body.failCount, 1);
assert.equal(r.body.skippedCount, 0);
assert.equal(r.body.attempted, 1);
assert.deepEqual(r.body.failed, [{ id: 'missing-probe-id', ok: false, error: 'ERR_ACCOUNT_NOT_FOUND' }]);
assert.deepEqual(r.body.skipped, []);
});
});