fix(gateway): repair routing health and MCP command wiring
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
shaggy (mosaic-dev box)
2026-08-11 15:28:57 -05:00
parent 20718b5a27
commit bda308efd9
10 changed files with 230 additions and 42 deletions
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { ProviderHealthStatus } from '@mosaicstack/types';
import { RoutingEngineService } from './routing-engine.service.js';
import type { RoutingRule, TaskClassification } from './routing.types.js';
@@ -29,7 +30,7 @@ function makeClassification(overrides: Partial<TaskClassification> = {}): TaskCl
/** Build a minimal RoutingEngineService with mocked DB and ProviderService. */
function makeService(
rules: RoutingRule[] = [],
healthMap: Record<string, { status: string }> = {},
healthMap: Record<string, { status: ProviderHealthStatus }> = {},
): RoutingEngineService {
const mockDb = {
select: vi.fn().mockReturnValue({
@@ -217,7 +218,10 @@ describe('RoutingEngineService.resolve — priority ordering', () => {
}),
];
const service = makeService(rules, { anthropic: { status: 'up' }, openai: { status: 'up' } });
const service = makeService(rules, {
anthropic: { status: 'healthy' },
openai: { status: 'healthy' },
});
const decision = await service.resolve('implement a function');
expect(decision.ruleName).toBe('high priority');
@@ -241,7 +245,10 @@ describe('RoutingEngineService.resolve — priority ordering', () => {
}),
];
const service = makeService(rules, { anthropic: { status: 'up' }, openai: { status: 'up' } });
const service = makeService(rules, {
anthropic: { status: 'healthy' },
openai: { status: 'healthy' },
});
const decision = await service.resolve('implement a function');
expect(decision.ruleName).toBe('coding rule');
@@ -270,7 +277,7 @@ describe('RoutingEngineService.resolve — unhealthy provider handling', () => {
const service = makeService(rules, {
anthropic: { status: 'down' }, // primary is unhealthy
openai: { status: 'up' },
openai: { status: 'healthy' },
});
const decision = await service.resolve('implement a function');
@@ -290,7 +297,7 @@ describe('RoutingEngineService.resolve — unhealthy provider handling', () => {
];
const service2 = makeService(unhealthyRules, {
anthropic: { status: 'up' },
anthropic: { status: 'healthy' },
openai: { status: 'down' },
});
@@ -306,7 +313,7 @@ describe('RoutingEngineService.resolve — unhealthy provider handling', () => {
const service = makeService(rules, {
anthropic: { status: 'down' }, // Sonnet is on anthropic — down
ollama: { status: 'up' }, // Haiku is also on anthropic — use Ollama as next
ollama: { status: 'healthy' }, // Haiku is also on anthropic — use Ollama as next
});
const decision = await service.resolve('hello there');
@@ -345,7 +352,7 @@ describe('RoutingEngineService.resolve — empty conditions (fallback rule)', ()
}),
];
const service = makeService(rules, { anthropic: { status: 'up' } });
const service = makeService(rules, { anthropic: { status: 'healthy' } });
const decision = await service.resolve('completely unrelated message xyz');
expect(decision.ruleName).toBe('catch-all');
@@ -369,7 +376,7 @@ describe('RoutingEngineService.resolve — empty conditions (fallback rule)', ()
}),
];
const service = makeService(rules, { anthropic: { status: 'up' } });
const service = makeService(rules, { anthropic: { status: 'healthy' } });
const codingDecision = await service.resolve('implement a function');
expect(codingDecision.ruleName).toBe('specific coding rule');
@@ -401,7 +408,7 @@ describe('RoutingEngineService.resolve — disabled rules', () => {
}),
];
const service = makeService(rules, { anthropic: { status: 'up' } });
const service = makeService(rules, { anthropic: { status: 'healthy' } });
const decision = await service.resolve('implement a function');
expect(decision.ruleName).toBe('enabled fallback');
@@ -452,9 +459,45 @@ describe('RoutingEngineService.resolve — availableProviders override', () => {
ps: unknown,
) => RoutingEngineService)(mockDb, mockProviderService);
const preSupplied = { anthropic: { status: 'up' } };
const preSupplied: Record<string, { status: ProviderHealthStatus }> = {
anthropic: { status: 'healthy' },
};
await service.resolve('implement a function', undefined, preSupplied);
expect(mockHealthCheckAll).not.toHaveBeenCalled();
});
});
// ─── resolve — canonical ProviderHealthStatus values ──────────────────────────
describe('RoutingEngineService.resolve — canonical health status routing', () => {
it('routes healthy and degraded providers by rule, and falls through to fallback when down', async () => {
const codingRule = makeRule({
name: 'coding rule',
priority: 1,
conditions: [{ field: 'taskType', operator: 'eq', value: 'coding' }],
action: { provider: 'openai', model: 'gpt-4o' },
});
// healthy → selected by its own rule, not the fallback chain
const healthyService = makeService([codingRule], { openai: { status: 'healthy' } });
const healthyDecision = await healthyService.resolve('implement a function');
expect(healthyDecision.ruleName).toBe('coding rule');
expect(healthyDecision.provider).toBe('openai');
// down → rule is skipped as unroutable, falls through to the fallback chain
const downService = makeService([codingRule], {
openai: { status: 'down' },
anthropic: { status: 'healthy' },
});
const downDecision = await downService.resolve('implement a function');
expect(downDecision.ruleName).toBe('fallback');
expect(downDecision.provider).toBe('anthropic');
// degraded → still routable, selected by its own rule, not the fallback chain
const degradedService = makeService([codingRule], { openai: { status: 'degraded' } });
const degradedDecision = await degradedService.resolve('implement a function');
expect(degradedDecision.ruleName).toBe('coding rule');
expect(degradedDecision.provider).toBe('openai');
});
});