Ctrl K
ComunidadGuías…

test/dashboard-stats-log.test.js

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

Archivo original ↗Preguntar a KiBot
// Request log for the stats panel: per-request rows that join the attempt
// (model/account/status/latency/credit) with its usage block (tokens in/out).
//
// The join is the risky part: recordRequest and recordTokenUsage are called at
// different depths and in either order, and the server is concurrent. These
// tests pin the async-scope join, the query filters/pagination, and the
// aggregate arithmetic (table and aggregates must share one denominator).

import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { config } from '../src/config.js';
import {
  finalizeStatsScope,
  getRequestLog,
  importStats,
  recordRequest,
  recordTokenUsage,
  resetStats,
  withStatsScope,
} from '../src/dashboard/stats.js';
import { handleDashboardApi } from '../src/dashboard/api.js';
import { accountRef } from '../src/log-safety.js';
import { _resetRuntimeConfigForTests } from '../src/runtime-config.js';

const originalDashboardPassword = config.dashboardPassword;
const originalApiKey = config.apiKey;

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 req(path, headers = {}) {
  return { url: `/dashboard/api${path}`, headers, socket: { remoteAddress: '127.0.0.1' } };
}

function authed(path, extra = {}) {
  return req(path, { 'x-dashboard-password': 'dash-secret', ...extra });
}

function seed(rows) {
  const result = importStats({ recentRequests: rows }, { mode: 'replace' });
  assert.equal(result.ok, true, 'seed snapshot must import');
}

function row(i, overrides = {}) {
  return {
    ts: 1_700_000_000_000 + i * 1000,
    model: 'm',
    success: true,
    ms: 100 + i,
    account: 'acct0001',
    credit: 0,
    ...overrides,
  };
}

beforeEach(() => {
  resetStats();
  _resetRuntimeConfigForTests();
});

afterEach(() => {
  resetStats();
  config.dashboardPassword = originalDashboardPassword;
  config.apiKey = originalApiKey;
  _resetRuntimeConfigForTests();
});

describe('request log scope join', () => {
  it('attaches usage recorded after the attempt row', () => {
    withStatsScope(() => {
      recordRequest('m-after', true, 25, 'acct-after');
      recordTokenUsage({ prompt_tokens: 10, completion_tokens: 4 }, 'm-after');
    });
    const page = getRequestLog({ model: 'm-after' });
    assert.equal(page.items.length, 1);
    assert.equal(page.items[0].tokensIn, 10);
    assert.equal(page.items[0].tokensOut, 4);
  });

  it('holds usage that arrives before the attempt row', () => {
    withStatsScope(() => {
      recordTokenUsage({ prompt_tokens: 7, completion_tokens: 3 }, 'm-before');
      recordRequest('m-before', true, 30, 'acct-before');
    });
    const page = getRequestLog({ model: 'm-before' });
    assert.equal(page.items[0].tokensIn, 7);
    assert.equal(page.items[0].tokensOut, 3);
  });

  it('never crosses concurrent scopes, even for the same model', async () => {
    await Promise.all([
      withStatsScope(async () => {
        recordRequest('same-model', true, 10, 'acct-a');
        await new Promise(resolve => setTimeout(resolve, 15));
        recordTokenUsage({ prompt_tokens: 11, completion_tokens: 1 }, 'same-model');
      }),
      withStatsScope(async () => {
        recordRequest('same-model', true, 20, 'acct-b');
        recordTokenUsage({ prompt_tokens: 22, completion_tokens: 2 }, 'same-model');
      }),
    ]);
    const page = getRequestLog({ model: 'same-model' });
    const byAccount = Object.fromEntries(page.items.map(r => [r.account, r]));
    assert.equal(byAccount[accountRef('acct-a')].tokensIn, 11);
    assert.equal(byAccount[accountRef('acct-a')].tokensOut, 1);
    assert.equal(byAccount[accountRef('acct-b')].tokensIn, 22);
    assert.equal(byAccount[accountRef('acct-b')].tokensOut, 2);
  });

  it('counts cache read/write as billable input and keeps the breakdown', () => {
    withStatsScope(() => {
      recordRequest('cache-model', true, 40, null);
      recordTokenUsage({
        prompt_tokens: 100,
        completion_tokens: 9,
        prompt_tokens_details: { cached_tokens: 40 },
        cache_creation_input_tokens: 5,
      }, 'cache-model');
    });
    const item = getRequestLog({ model: 'cache-model' }).items[0];
    assert.equal(item.cacheRead, 40);
    assert.equal(item.cacheWrite, 5);
    assert.equal(item.tokensIn, 105, 'fresh 60 + cache read 40 + cache write 5');
    assert.equal(item.tokensOut, 9);
  });

  it('stamps the final HTTP status on the last attempt only', () => {
    const scope = { rows: [], pendingUsage: null, status: 451 };
    withStatsScope((s) => {
      Object.assign(scope, s, { status: 451 });
      recordRequest('attempt-model', false, 5, null);
      recordRequest('attempt-model', true, 6, null);
    });
    finalizeStatsScope(scope);
    // withStatsScope reused the object passed in only for assertions; the rows
    // live on that same scope object.
    const page = getRequestLog({ model: 'attempt-model' });
    assert.deepEqual(page.items.map(r => r.status), [451, 500]);
  });

  it('keeps the success-derived status without a scope', () => {
    recordRequest('no-scope-ok', true, 1, null);
    recordRequest('no-scope-err', false, 1, null);
    const page = getRequestLog();
    const byModel = Object.fromEntries(page.items.map(r => [r.model, r.status]));
    assert.equal(byModel['no-scope-ok'], 200);
    assert.equal(byModel['no-scope-err'], 500);
  });
});

describe('GET /stats/log query', () => {
  beforeEach(() => {
    seed([
      row(0, { model: 'alpha', account: 'acct0001', status: 200, tokensIn: 10, tokensOut: 2, credit: 1 }),
      row(1, { model: 'beta', account: 'acct0002', status: 500, success: false, tokensIn: 0, tokensOut: 0, credit: 0 }),
      row(2, { model: 'alpha', account: 'acct0002', status: 200, tokensIn: 30, tokensOut: 6, credit: 3 }),
      row(3, { model: 'gamma', account: 'acct0001', status: 429, success: false, tokensIn: 0, tokensOut: 0, credit: 0 }),
    ]);
  });

  it('returns newest first with total and retention, not a page-sized total', () => {
    const page = getRequestLog({ limit: 2 });
    assert.deepEqual(page.items.map(r => r.model), ['gamma', 'alpha']);
    assert.equal(page.total, 4);
    assert.equal(page.hasMore, true);
    assert.equal(page.retention.stored, 4);
    assert.equal(page.retention.capacity, 500);
  });

  it('pages by offset and stops claiming more at the end', () => {
    const page = getRequestLog({ limit: 3, offset: 3 });
    assert.deepEqual(page.items.map(r => r.model), ['alpha']);
    assert.equal(page.hasMore, false);
  });

  it('filters by model, account, status and time window', () => {
    assert.deepEqual(getRequestLog({ model: 'alpha' }).items.map(r => r.model), ['alpha', 'alpha']);
    assert.deepEqual(getRequestLog({ account: accountRef('acct0002') }).items.map(r => r.account),
      [accountRef('acct0002'), accountRef('acct0002')]);
    assert.deepEqual(getRequestLog({ status: 200 }).items.map(r => r.status), [200, 200]);
    const from = 1_700_000_000_000 + 2 * 1000;
    const to = 1_700_000_000_000 + 3 * 1000;
    assert.deepEqual(getRequestLog({ from, to }).items.map(r => r.model), ['gamma', 'alpha']);
  });

  it('derives status for legacy rows that predate the status field', () => {
    seed([{ ts: 1_700_000_000_000, model: 'legacy-ok', success: true, ms: 1, account: null, credit: 0 },
      { ts: 1_700_000_001_000, model: 'legacy-err', success: false, ms: 1, account: null, credit: 0 }]);
    const page = getRequestLog();
    assert.deepEqual(page.items.map(r => [r.model, r.status]), [['legacy-err', 500], ['legacy-ok', 200]]);
    assert.equal(page.filters.statuses.includes(500), true);
  });

  it('aggregates over the whole filtered window, not the page', () => {
    const page = getRequestLog({ limit: 1 });
    assert.equal(page.summary.requests, 4);
    assert.equal(page.summary.success, 2);
    assert.equal(page.summary.errors, 2);
    assert.equal(page.summary.tokensIn, 40);
    assert.equal(page.summary.tokensOut, 8);
    assert.equal(page.summary.credits, 4);
    assert.equal(page.summary.avgMs, Math.round((100 + 101 + 102 + 103) / 4));

    const alpha = page.byModel.find(m => m.model === 'alpha');
    assert.equal(alpha.requests, 2);
    assert.equal(alpha.tokensIn, 40);
    assert.equal(alpha.tokensOut, 8);
    assert.equal(alpha.credits, 4);
    assert.equal(alpha.errors, 0);

    const acct2 = page.byAccount.find(a => a.account === accountRef('acct0002'));
    assert.equal(acct2.requests, 2);
    assert.equal(acct2.errors, 1);

    assert.equal(page.byDay.length, 1);
    assert.equal(page.byDay[0].requests, 4);
    assert.equal(page.byDay[0].errors, 2);
  });

  it('keeps filter options stable while a filter is active', () => {
    const page = getRequestLog({ model: 'alpha' });
    assert.deepEqual(page.filters.models, ['alpha', 'beta', 'gamma']);
    assert.deepEqual(page.filters.accounts, [accountRef('acct0001'), accountRef('acct0002')].sort());
    assert.deepEqual(page.filters.statuses, [200, 429, 500]);
  });
});

describe('GET /dashboard/api/stats/log', () => {
  beforeEach(() => {
    config.dashboardPassword = 'dash-secret';
    config.apiKey = '';
    seed([
      row(0, { model: 'alpha', account: 'acct0001', status: 200, tokensIn: 5, tokensOut: 1, credit: 2 }),
      row(1, { model: 'beta', account: 'acct0002', status: 502, success: false }),
    ]);
  });

  it('fails closed without dashboard auth', async () => {
    const res = fakeRes();
    await handleDashboardApi('GET', '/stats/log', {}, req('/stats/log'), res);
    assert.equal(res.statusCode, 401);
  });

  it('parses filters and pagination from the query string', async () => {
    const res = fakeRes();
    await handleDashboardApi('GET', '/stats/log', {}, authed('/stats/log?model=alpha&status=200&limit=10&offset=0'), res);
    assert.equal(res.statusCode, 200);
    const body = res.json();
    assert.equal(body.items.length, 1);
    assert.equal(body.items[0].model, 'alpha');
    assert.equal(body.items[0].tokensIn, 5);
    assert.equal(body.total, 1);
    assert.equal(body.summary.requests, 1);
  });

  it('clamps a hostile limit and keeps /stats compatible', async () => {
    const logRes = fakeRes();
    await handleDashboardApi('GET', '/stats/log', {}, authed('/stats/log?limit=99999'), logRes);
    assert.equal(logRes.statusCode, 200);
    assert.equal(logRes.json().limit, 200);

    const statsRes = fakeRes();
    await handleDashboardApi('GET', '/stats', {}, authed('/stats'), statsRes);
    assert.equal(statsRes.statusCode, 200);
    const stats = statsRes.json();
    assert.equal(typeof stats.totalRequests, 'number');
    assert.equal(typeof stats.modelCounts, 'object');
    assert.equal(typeof stats.tokenTotals, 'object');
    assert.equal(typeof stats.modelBilling, 'object');
    assert.equal(stats.accountAttribution.total, stats.totalRequests);
    assert.equal(stats.accountAttribution.attributed + stats.accountAttribution.unattributed, stats.totalRequests);
  });
});