test/devin-connect-account-health.test.js
Rama master · réplica de lectura del repositorio original.
// C5: per-account rolling-hour health window — PERSISTED across restart.
//
// errorCount answers "is this account disabled right now"; _rpmHistory answers
// "is it busy right now". Neither answers "how has it BEHAVED over the last
// hour". This covers recording outcomes (ok/error/throttle/capacity/dead),
// the 1h prune, the hard event cap, the per-account + pool summaries, and the
// round-trip through accounts.json (the key differentiator vs an in-memory
// counter: a restart must NOT reset the picture to all-healthy).
import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
addAccountByKey, removeAccount,
reportSuccess, reportError, markRateLimited, reportDeadToken,
getAccountHealth, getPoolHealthWindow, __resetReloginState,
__flushDirtyAccounts, __isAccountsDirty,
} from '../src/auth.js';
let acct;
const KEY = 'devin-session-token$HEALTH';
const EMAIL = 'health-test@example.com';
beforeEach(() => {
acct = addAccountByKey(KEY, EMAIL);
acct.email = EMAIL;
acct.tier = 'free';
acct._health = []; // start clean
});
afterEach(() => {
if (acct) removeAccount(acct.id);
__resetReloginState();
});
describe('account health window — recording', () => {
it('counts each outcome kind into the rolling-hour summary', () => {
reportSuccess(KEY);
reportSuccess(KEY);
reportError(KEY);
markRateLimited(KEY, 60 * 1000); // default kind = throttle
markRateLimited(KEY, 60 * 1000, 'gpt', 'c'); // capacity, model-scoped
reportDeadToken(KEY);
const h = getAccountHealth(KEY);
assert.equal(h.ok, 2);
assert.equal(h.error, 1);
assert.equal(h.throttle, 1);
assert.equal(h.capacity, 1);
assert.equal(h.dead, 1);
assert.equal(h.total, 6);
});
it('returns null for an unknown apiKey', () => {
assert.equal(getAccountHealth('devin-session-token$NOPE'), null);
});
});
describe('account health window — pruning', () => {
it('drops events older than the 1h window', () => {
const now = Date.now();
// Seed two stale (>1h) and one fresh event directly.
acct._health = [
{ t: now - 90 * 60 * 1000, k: 'e' },
{ t: now - 61 * 60 * 1000, k: 't' },
{ t: now - 5 * 60 * 1000, k: 'o' },
];
const h = getAccountHealth(KEY, now);
assert.equal(h.total, 1, 'only the in-window event survives');
assert.equal(h.ok, 1);
assert.equal(h.error, 0);
});
it('caps the window so a hot account cannot bloat the store', () => {
// Push well past the cap; the summary total must be bounded.
for (let i = 0; i < 500; i++) reportSuccess(KEY);
const h = getAccountHealth(KEY);
assert.ok(h.total <= 240, `event count ${h.total} must be capped at 240`);
assert.ok(h.total >= 240, 'recent events are retained up to the cap');
});
});
describe('account health window — pool summary', () => {
it('reports per-account health across the pool without leaking secrets', () => {
reportError(KEY);
reportDeadToken(KEY);
const pool = getPoolHealthWindow();
const mine = pool.find(p => p.email === EMAIL);
assert.ok(mine, 'account present in pool health');
assert.equal(mine.health.error, 1);
assert.equal(mine.health.dead, 1);
assert.equal(mine.tier, 'free');
assert.equal(mine.status, 'active');
// No raw apiKey/password fields in the summary.
assert.equal('apiKey' in mine, false);
assert.equal('password' in mine, false);
});
});
describe('account health window — persistence across restart (C5 differentiator)', () => {
it('survives a save → reload round-trip via accounts.json', async () => {
// Record some history, then force a save and a fresh module instance to
// simulate a process restart reading the same on-disk accounts file.
reportSuccess(KEY);
reportError(KEY);
reportDeadToken(KEY);
// The save is wired into status-flipping paths; trigger one deterministically.
const auth = await import('../src/auth.js');
auth.saveAccountsSync?.();
// Re-read straight from disk and assert the window persisted (not reset).
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
const { config } = await import('../src/config.js');
const file = join(config.sharedDataDir || config.dataDir, 'accounts.json');
let onDisk;
try { onDisk = JSON.parse(readFileSync(file, 'utf8')); } catch { onDisk = null; }
assert.ok(onDisk, 'accounts.json readable from the test data dir');
const rec = (Array.isArray(onDisk) ? onDisk : []).find(a => a.apiKey === KEY);
assert.ok(rec, 'account persisted to disk');
assert.ok(Array.isArray(rec._health), '_health array persisted');
const kinds = rec._health.map(e => e.k).sort();
assert.deepEqual(kinds, ['d', 'e', 'o'], 'all three outcome events persisted across the boundary');
});
});
describe('K7 dirty-flush — lazy health/cooldown state persists without shutdown write', () => {
it('a hot-path health event marks the pool dirty; the periodic flush persists it', async () => {
// reportSuccess records a health event but does NOT save synchronously —
// this is exactly the lazy state the old shutdown flush used to capture.
reportSuccess(KEY);
assert.equal(__isAccountsDirty(), true, 'hot-path health event should mark dirty');
// The periodic timer's work, run deterministically.
__flushDirtyAccounts();
assert.equal(__isAccountsDirty(), false, 'flush clears the dirty flag');
// And the state actually reached disk (no shutdown write involved).
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
const { config } = await import('../src/config.js');
const file = join(config.sharedDataDir || config.dataDir, 'accounts.json');
const onDisk = JSON.parse(readFileSync(file, 'utf8'));
const rec = onDisk.find(a => a.apiKey === KEY);
assert.ok(rec, 'account persisted to disk via dirty-flush');
assert.ok(rec._health.some(e => e.k === 'o'), 'the ok event landed on disk');
});
it('flush is a no-op when nothing is dirty (no redundant writes)', () => {
__flushDirtyAccounts(); // drain any pending dirt from prior tests
assert.equal(__isAccountsDirty(), false);
__flushDirtyAccounts(); // second flush: nothing to do
assert.equal(__isAccountsDirty(), false, 'clean pool stays clean');
});
});