feat: implement Tier 3 features — lane queue, credential redaction, token dashboard, xAI, Voyage AI

- Lane Queue: per-session FIFO queue in gateway replacing reject-when-busy (9 tests)
- Credential Redaction: redactConfig() expanded to cover 18+ secret fields (16 tests)
- Web UI Token Dashboard: system.tokenUsage endpoint + Usage page with summary cards
- xAI (Grok) Provider: OpenAI-compatible client with model pricing
- Voyage AI Embeddings: new embedding provider with configurable dimensions (5 tests)
- Update gap analysis: 90→95 match (70%→74%), Tier 3 section marked DONE
- Update state.json: test count 1001→1034, add tier3_completion entry

Total: 1034 tests passing across 85 files, typecheck clean
This commit is contained in:
William Valentin
2026-02-09 10:32:57 -08:00
parent 1d126cddfb
commit 9be8f76bc7
26 changed files with 1395 additions and 105 deletions
+302 -7
View File
@@ -1,9 +1,11 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createSystemHandlers } from './system.js';
import type { TokenUsageEntry } from './system.js';
import { createSessionHandlers } from './sessions.js';
import { createToolHandlers } from './tools.js';
import { createAgentHandlers } from './agent.js';
import { createConfigHandlers } from './config.js';
import { createConfigHandlers, redactConfig } from './config.js';
import { LaneQueue } from '../lane-queue.js';
import { ErrorCode } from '../protocol.js';
import type { GatewayRequest, GatewayResponse, GatewayError, GatewayEvent, OutboundMessage } from '../protocol.js';
@@ -33,6 +35,64 @@ describe('system handlers', () => {
});
});
describe('system.tokenUsage handler', () => {
it('returns empty sessions when no getTokenUsage provided', async () => {
const handlers = createSystemHandlers({
startTime: Date.now(),
version: '0.1.0',
getSessionCount: () => 0,
getToolCount: () => 0,
getConnectionCount: () => 0,
});
const req: GatewayRequest = { id: 1, method: 'system.tokenUsage' };
const result = await handlers['system.tokenUsage'](req) as GatewayResponse;
expect(result.id).toBe(1);
const r = result.result as { sessions: unknown[] };
expect(r.sessions).toEqual([]);
});
it('returns session usage data from getTokenUsage callback', async () => {
const mockUsage: TokenUsageEntry[] = [
{
sessionId: 'telegram:user1',
primary: { inputTokens: 1000, outputTokens: 500, calls: 3 },
delegation: { fast: { inputTokens: 200, outputTokens: 100, calls: 1 } },
total: { inputTokens: 1200, outputTokens: 600, calls: 4, estimatedCost: 0.0234 },
},
{
sessionId: 'ws:abc-123',
primary: { inputTokens: 50, outputTokens: 25, calls: 1 },
delegation: {},
total: { inputTokens: 50, outputTokens: 25, calls: 1, estimatedCost: 0 },
},
];
const handlers = createSystemHandlers({
startTime: Date.now(),
version: '0.1.0',
getSessionCount: () => 2,
getToolCount: () => 0,
getConnectionCount: () => 1,
getTokenUsage: () => mockUsage,
});
const req: GatewayRequest = { id: 2, method: 'system.tokenUsage' };
const result = await handlers['system.tokenUsage'](req) as GatewayResponse;
expect(result.id).toBe(2);
const r = result.result as { sessions: typeof mockUsage };
expect(r.sessions).toHaveLength(2);
expect(r.sessions[0].sessionId).toBe('telegram:user1');
expect(r.sessions[0].total.inputTokens).toBe(1200);
expect(r.sessions[0].total.estimatedCost).toBe(0.0234);
expect(r.sessions[0].delegation.fast.inputTokens).toBe(200);
expect(r.sessions[1].sessionId).toBe('ws:abc-123');
expect(r.sessions[1].total.calls).toBe(1);
});
});
describe('session handlers', () => {
const mockHistory = [
{ role: 'user' as const, content: 'hello' },
@@ -188,8 +248,11 @@ describe('agent handlers', () => {
setOnToolUse: vi.fn(),
};
const laneQueue = new LaneQueue();
const handlers = createAgentHandlers({
sessionBridge: mockBridge as any,
laneQueue,
});
beforeEach(() => {
@@ -260,13 +323,38 @@ describe('agent handlers', () => {
expect(result.error.message).toContain('message');
});
it('agent.send rejects when busy', async () => {
mockBridge.isBusy.mockReturnValue(true);
const req: GatewayRequest = { id: 3, method: 'agent.send', params: { message: 'hi', connectionId: 'conn-1' } };
const send = vi.fn();
const result = await handlers['agent.send'](req, send) as GatewayError;
it('agent.send queues concurrent requests instead of rejecting', async () => {
// Simulate the first request blocking
let resolveFirst!: () => void;
const firstBlocks = new Promise<void>((r) => { resolveFirst = r; });
let callCount = 0;
mockAgent.process.mockImplementation(async () => {
callCount++;
if (callCount === 1) {
await firstBlocks;
return 'first response';
}
return 'second response';
});
expect(result.error.code).toBe(ErrorCode.AgentBusy);
const req1: GatewayRequest = { id: 3, method: 'agent.send', params: { message: 'first', connectionId: 'conn-1' } };
const req2: GatewayRequest = { id: 4, method: 'agent.send', params: { message: 'second', connectionId: 'conn-1' } };
const sent1: OutboundMessage[] = [];
const sent2: OutboundMessage[] = [];
const p1 = handlers['agent.send'](req1, vi.fn((msg: OutboundMessage) => sent1.push(msg)));
const p2 = handlers['agent.send'](req2, vi.fn((msg: OutboundMessage) => sent2.push(msg)));
// Release the first request
resolveFirst();
await Promise.all([p1, p2]);
// Both should have completed — no AgentBusy error
expect(sent1).toHaveLength(1);
expect((sent1[0] as GatewayEvent).event).toBe('done');
expect(sent2).toHaveLength(1);
expect((sent2[0] as GatewayEvent).event).toBe('done');
expect(mockAgent.process).toHaveBeenCalledTimes(2);
});
it('agent.send handles errors gracefully', async () => {
@@ -452,3 +540,210 @@ describe('config handlers', () => {
expect(result.error.code).toBe(ErrorCode.InvalidRequest);
});
});
describe('redactConfig comprehensive credential redaction', () => {
/**
* Build a full config object with secrets in every possible location.
* Optional sections (discord, slack, etc.) are included to test redaction.
*/
function makeFullConfig() {
return {
telegram: { bot_token: 'tg-secret', allowed_chat_ids: [1], require_mention: true },
discord: { bot_token: 'dc-secret', allowed_guild_ids: ['g1'], allowed_channel_ids: [], require_mention: true },
slack: { bot_token: 'sl-bot', app_token: 'sl-app', signing_secret: 'sl-sign', allowed_channel_ids: [], require_mention: false },
server: { tailscale_only: true, localhost: true, port: 18800, token: 'bearer-secret', tailscale_identity: false, auth_http: true },
models: {
default: { provider: 'anthropic' as const, model: 'claude', api_key: 'sk-def', auth_token: 'at-def',
fallback: { provider: 'openai' as const, model: 'gpt-4', api_key: 'sk-def-fb', auth_token: 'at-def-fb' },
},
fast: { provider: 'openai' as const, model: 'gpt-4o-mini', api_key: 'sk-fast',
fallback: { provider: 'gemini' as const, model: 'gemini-flash', api_key: 'sk-fast-fb' },
},
complex: { provider: 'anthropic' as const, model: 'claude-opus', auth_token: 'at-complex' },
local: { provider: 'ollama' as const, model: 'llama3' },
fallback_chain: ['anthropic'],
local_providers: {
ollama: { provider: 'ollama' as const, model: 'llama3', api_key: 'lp-key', auth_token: 'lp-token',
fallback: { provider: 'llamacpp' as const, model: 'llama', api_key: 'lp-fb-key' },
},
},
thinking: { anthropic: { budgetTokens: 4096 }, openai: { reasoningEffort: 'medium' as const }, gemini: { budgetTokens: 4096 } },
},
web_search: { provider: 'brave' as const, api_key: 'brave-key', endpoint: 'https://api.brave.com', max_results: 5 },
audio: { transcription_endpoint: 'https://api.openai.com', transcription_api_key: 'audio-key', transcription_model: 'whisper-1' },
memory: {
enabled: true, auto_extract: true, max_context_tokens: 2000,
embedding: { enabled: true, provider: 'openai' as const, model: 'text-embedding-3-small', api_key: 'embed-key', dimensions: 1536, chunk_size: 512, chunk_overlap: 50, top_k: 5, hybrid_weight: 0.7 },
},
automation: {
cron: [],
webhooks: [
{ name: 'github', secret: 'wh-secret-1', message: '{{body}}', output: { channel: 'telegram', peer: '123' }, enabled: true },
{ name: 'gitlab', secret: 'wh-secret-2', message: '{{body}}', output: { channel: 'telegram', peer: '456' }, enabled: true },
{ name: 'no-secret', message: '{{body}}', output: { channel: 'telegram', peer: '789' }, enabled: true },
],
gmail: { enabled: true, credentials_file: '/path/to/creds.json', token_file: '/path/to/token.json', watch_labels: ['INBOX'], poll_interval: '60s', output: { channel: 'telegram', peer: '123' }, message: 'new email' },
heartbeat: { enabled: false, interval: '5m', checks: ['gateway'], failure_threshold: 2, disk_threshold_mb: 100 },
},
mcp: {
servers: [
{ name: 'my-server', command: 'node', args: ['server.js'], env: { API_KEY: 'mcp-api-key', DATABASE_URL: 'postgres://secret@host/db' } },
{ name: 'no-env', command: 'python', args: ['app.py'] },
],
},
hooks: { confirm: ['shell.exec'], log: [], silent: [] },
backends: { claude_code: { enabled: false }, opencode: { enabled: false }, native: { enabled: true } },
};
}
it('redacts telegram.bot_token', () => {
const result = redactConfig(makeFullConfig() as any);
expect((result.telegram as any).bot_token).toBe('***');
});
it('redacts discord.bot_token', () => {
const result = redactConfig(makeFullConfig() as any);
expect((result.discord as any).bot_token).toBe('***');
});
it('redacts slack.bot_token, app_token, and signing_secret', () => {
const result = redactConfig(makeFullConfig() as any);
const slack = result.slack as any;
expect(slack.bot_token).toBe('***');
expect(slack.app_token).toBe('***');
expect(slack.signing_secret).toBe('***');
});
it('redacts server.token', () => {
const result = redactConfig(makeFullConfig() as any);
expect((result.server as any).token).toBe('***');
});
it('redacts model api_key and auth_token for all tiers', () => {
const result = redactConfig(makeFullConfig() as any);
const models = result.models as any;
expect(models.default.api_key).toBe('***');
expect(models.default.auth_token).toBe('***');
expect(models.fast.api_key).toBe('***');
expect(models.complex.auth_token).toBe('***');
// local has no keys — should remain unchanged
expect(models.local.api_key).toBeUndefined();
});
it('redacts model fallback api_key and auth_token', () => {
const result = redactConfig(makeFullConfig() as any);
const models = result.models as any;
expect(models.default.fallback.api_key).toBe('***');
expect(models.default.fallback.auth_token).toBe('***');
expect(models.fast.fallback.api_key).toBe('***');
});
it('redacts local_providers api_key, auth_token, and their fallbacks', () => {
const result = redactConfig(makeFullConfig() as any);
const ollama = (result.models as any).local_providers.ollama;
expect(ollama.api_key).toBe('***');
expect(ollama.auth_token).toBe('***');
expect(ollama.fallback.api_key).toBe('***');
});
it('redacts web_search.api_key', () => {
const result = redactConfig(makeFullConfig() as any);
expect((result.web_search as any).api_key).toBe('***');
});
it('redacts audio.transcription_api_key', () => {
const result = redactConfig(makeFullConfig() as any);
expect((result.audio as any).transcription_api_key).toBe('***');
});
it('redacts memory.embedding.api_key', () => {
const result = redactConfig(makeFullConfig() as any);
expect((result.memory as any).embedding.api_key).toBe('***');
});
it('redacts automation webhook secrets', () => {
const result = redactConfig(makeFullConfig() as any);
const webhooks = (result.automation as any).webhooks;
expect(webhooks[0].secret).toBe('***');
expect(webhooks[1].secret).toBe('***');
// Webhook without a secret should remain unaffected
expect(webhooks[2].secret).toBeUndefined();
});
it('redacts automation gmail credentials_file and token_file', () => {
const result = redactConfig(makeFullConfig() as any);
const gmail = (result.automation as any).gmail;
expect(gmail.credentials_file).toBe('***');
expect(gmail.token_file).toBe('***');
});
it('redacts all MCP server env vars', () => {
const result = redactConfig(makeFullConfig() as any);
const servers = (result.mcp as any).servers;
expect(servers[0].env.API_KEY).toBe('***');
expect(servers[0].env.DATABASE_URL).toBe('***');
// Server without env should be unaffected
expect(servers[1].env).toBeUndefined();
});
it('preserves non-secret fields', () => {
const result = redactConfig(makeFullConfig() as any);
// telegram
expect((result.telegram as any).allowed_chat_ids).toEqual([1]);
expect((result.telegram as any).require_mention).toBe(true);
// discord
expect((result.discord as any).allowed_guild_ids).toEqual(['g1']);
// slack
expect((result.slack as any).allowed_channel_ids).toEqual([]);
// server
expect((result.server as any).port).toBe(18800);
expect((result.server as any).tailscale_only).toBe(true);
// models
expect((result.models as any).default.provider).toBe('anthropic');
expect((result.models as any).default.model).toBe('claude');
expect((result.models as any).fallback_chain).toEqual(['anthropic']);
// web_search
expect((result.web_search as any).provider).toBe('brave');
expect((result.web_search as any).max_results).toBe(5);
// audio
expect((result.audio as any).transcription_model).toBe('whisper-1');
// memory
expect((result.memory as any).embedding.model).toBe('text-embedding-3-small');
// hooks
expect((result.hooks as any).confirm).toEqual(['shell.exec']);
// mcp
expect((result.mcp as any).servers[0].name).toBe('my-server');
expect((result.mcp as any).servers[0].command).toBe('node');
});
it('handles missing optional sections gracefully', () => {
const minimal = {
telegram: { bot_token: 'tok', allowed_chat_ids: [1] },
models: { default: { provider: 'anthropic' as const, model: 'claude' }, fallback_chain: [] },
server: { port: 18800 },
hooks: { confirm: [], log: [], silent: [] },
};
// Should not throw even when discord, slack, automation, mcp, etc. are absent
const result = redactConfig(minimal as any);
expect((result.telegram as any).bot_token).toBe('***');
expect(result.discord).toBeUndefined();
expect(result.slack).toBeUndefined();
expect(result.automation).toBeUndefined();
});
it('does not mutate the original config object', () => {
const config = makeFullConfig();
redactConfig(config as any);
// Original secrets should still be intact
expect(config.telegram.bot_token).toBe('tg-secret');
expect(config.models.default.api_key).toBe('sk-def');
expect(config.server.token).toBe('bearer-secret');
});
});