Ctrl K
ComunidadGuías…

test/dashboard-account-query.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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, []);
  });
});