feat(session): persist model tier overrides per session
Store per-session config in SQLite and route /model and /reset through command fast-paths so channel sessions keep independent model selection across reconnects and restarts.
This commit is contained in:
@@ -4,12 +4,17 @@ import type { TokenUsageEntry } from './system.js';
|
||||
import { createSessionHandlers } from './sessions.js';
|
||||
import { createToolHandlers } from './tools.js';
|
||||
import { createAgentHandlers } from './agent.js';
|
||||
import { createIntentHandlers } from './intents.js';
|
||||
import { createRoutingHandlers } from './routing.js';
|
||||
import { createHistoryHandlers } from './history.js';
|
||||
import { createConfigHandlers, redactConfig } from './config.js';
|
||||
import { createPairingHandlers } from './pairing.js';
|
||||
import { PairingManager } from '../../channels/pairing.js';
|
||||
import { LaneQueue } from '../lane-queue.js';
|
||||
import { ErrorCode } from '../protocol.js';
|
||||
import type { GatewayRequest, GatewayResponse, GatewayError, GatewayEvent, OutboundMessage } from '../protocol.js';
|
||||
import { ComponentRegistry } from '../../intents/index.js';
|
||||
import { RoutingPolicy } from '../../routing/index.js';
|
||||
|
||||
describe('system handlers', () => {
|
||||
const deps = {
|
||||
@@ -402,6 +407,124 @@ describe('agent handlers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('intent handlers', () => {
|
||||
it('intents.list returns configured rules', async () => {
|
||||
const registry = new ComponentRegistry({ matchThreshold: 0.6 });
|
||||
registry.register({
|
||||
name: 'deploy-route',
|
||||
patterns: ['deploy *'],
|
||||
target: { type: 'agent', name: 'coder' },
|
||||
priority: 5,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const handlers = createIntentHandlers({
|
||||
intentRegistry: registry,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const req: GatewayRequest = { id: 10, method: 'intents.list' };
|
||||
const result = await handlers['intents.list'](req) as GatewayResponse;
|
||||
const payload = result.result as { enabled: boolean; rules: Array<{ name: string }> };
|
||||
|
||||
expect(payload.enabled).toBe(true);
|
||||
expect(payload.rules).toHaveLength(1);
|
||||
expect(payload.rules[0].name).toBe('deploy-route');
|
||||
});
|
||||
|
||||
it('intents.match returns best rule match', async () => {
|
||||
const registry = new ComponentRegistry({ matchThreshold: 0.5 });
|
||||
registry.register({
|
||||
name: 'deploy-route',
|
||||
patterns: ['deploy *'],
|
||||
target: { type: 'agent', name: 'coder' },
|
||||
priority: 5,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const handlers = createIntentHandlers({
|
||||
intentRegistry: registry,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const req: GatewayRequest = {
|
||||
id: 11,
|
||||
method: 'intents.match',
|
||||
params: { input: 'deploy backend service' },
|
||||
};
|
||||
const result = await handlers['intents.match'](req) as GatewayResponse;
|
||||
const payload = result.result as { match: { rule: { name: string } } };
|
||||
|
||||
expect(payload.match.rule.name).toBe('deploy-route');
|
||||
});
|
||||
});
|
||||
|
||||
describe('routing handlers', () => {
|
||||
it('routing.decide returns match and policy decision', async () => {
|
||||
const registry = new ComponentRegistry({ matchThreshold: 0.5 });
|
||||
registry.register({
|
||||
name: 'deploy-route',
|
||||
patterns: ['deploy *'],
|
||||
target: { type: 'agent', name: 'coder' },
|
||||
priority: 5,
|
||||
enabled: true,
|
||||
});
|
||||
const policy = new RoutingPolicy({
|
||||
enabled: true,
|
||||
fastPathThreshold: 0.7,
|
||||
llmThreshold: 0.3,
|
||||
defaultPath: 'llm',
|
||||
});
|
||||
const handlers = createRoutingHandlers({
|
||||
intentRegistry: registry,
|
||||
routingPolicy: policy,
|
||||
});
|
||||
|
||||
const req: GatewayRequest = {
|
||||
id: 12,
|
||||
method: 'routing.decide',
|
||||
params: { input: 'deploy service' },
|
||||
};
|
||||
const result = await handlers['routing.decide'](req) as GatewayResponse;
|
||||
const payload = result.result as {
|
||||
match: { rule: { name: string } };
|
||||
decision: { path: string };
|
||||
};
|
||||
|
||||
expect(payload.match.rule.name).toBe('deploy-route');
|
||||
expect(payload.decision.path).toBe('fast');
|
||||
});
|
||||
});
|
||||
|
||||
describe('history handlers', () => {
|
||||
it('history.search returns ranked results', async () => {
|
||||
const handlers = createHistoryHandlers({
|
||||
sessionManager: {
|
||||
searchHistory: () => [{ sessionId: 'ws:test', messageId: 1, role: 'user', content: 'deploy', score: 0.9, createdAt: 123 }],
|
||||
reindexHistory: () => 0,
|
||||
} as any,
|
||||
});
|
||||
|
||||
const req: GatewayRequest = { id: 13, method: 'history.search', params: { query: 'deploy' } };
|
||||
const result = await handlers['history.search'](req) as GatewayResponse;
|
||||
const payload = result.result as { results: Array<{ sessionId: string }> };
|
||||
expect(payload.results[0].sessionId).toBe('ws:test');
|
||||
});
|
||||
|
||||
it('history.reindex returns count', async () => {
|
||||
const handlers = createHistoryHandlers({
|
||||
sessionManager: {
|
||||
searchHistory: () => [],
|
||||
reindexHistory: () => 42,
|
||||
} as any,
|
||||
});
|
||||
|
||||
const req: GatewayRequest = { id: 14, method: 'history.reindex' };
|
||||
const result = await handlers['history.reindex'](req) as GatewayResponse;
|
||||
expect((result.result as { reindexed: number }).reindexed).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('system.restart handler', () => {
|
||||
it('returns restarting:true and calls restart callback', async () => {
|
||||
const restartFn = vi.fn(async () => {});
|
||||
|
||||
Reference in New Issue
Block a user