This commit is contained in:
210
packages/agent/tests/executor-router.test.ts
Normal file
210
packages/agent/tests/executor-router.test.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
routeTask,
|
||||
type ExecutorCandidate,
|
||||
type RouteTask,
|
||||
} from '../src/executor-router.js';
|
||||
|
||||
const NOW = 1_000_000;
|
||||
|
||||
function candidate(overrides: Partial<ExecutorCandidate> = {}): ExecutorCandidate {
|
||||
return {
|
||||
id: 'persona:coder',
|
||||
kind: 'persona',
|
||||
displayName: 'Coder',
|
||||
taskFit: { coding: 0.85 },
|
||||
authClass: 'api-key',
|
||||
installed: true,
|
||||
healthy: true,
|
||||
rateLimit: { state: 'available' },
|
||||
supportsHeadless: false,
|
||||
egressDestination: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const CODING: RouteTask = { category: 'coding', privacy: 'normal' };
|
||||
|
||||
describe('routeTask — hard gates', () => {
|
||||
it('rejects a not-installed candidate with stable reason', () => {
|
||||
const d = routeTask(CODING, [candidate({ installed: false })], NOW);
|
||||
expect(d.selected).toBeNull();
|
||||
expect(d.rejected).toEqual([{ id: 'persona:coder', reason: 'not installed' }]);
|
||||
expect(d.scores).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects an unhealthy candidate', () => {
|
||||
const d = routeTask(CODING, [candidate({ healthy: false })], NOW);
|
||||
expect(d.rejected).toEqual([{ id: 'persona:coder', reason: 'unhealthy' }]);
|
||||
});
|
||||
|
||||
it('rejects an external candidate that cannot run headless', () => {
|
||||
const d = routeTask(
|
||||
CODING,
|
||||
[candidate({ id: 'external:codex', kind: 'external', supportsHeadless: false, egressDestination: 'OpenAI' })],
|
||||
NOW,
|
||||
);
|
||||
expect(d.rejected).toEqual([
|
||||
{ id: 'external:codex', reason: 'does not support headless execution' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does NOT apply the headless gate to personas', () => {
|
||||
// persona with supportsHeadless:false must survive (gate is external-only)
|
||||
const d = routeTask(CODING, [candidate({ supportsHeadless: false })], NOW);
|
||||
expect(d.selected?.id).toBe('persona:coder');
|
||||
});
|
||||
|
||||
it('rejects an egressing candidate for a private task, naming the destination', () => {
|
||||
const d = routeTask(
|
||||
{ category: 'coding', privacy: 'private' },
|
||||
[candidate({ id: 'external:claude-code', kind: 'external', supportsHeadless: true, egressDestination: 'Anthropic' })],
|
||||
NOW,
|
||||
);
|
||||
expect(d.rejected).toEqual([
|
||||
{ id: 'external:claude-code', reason: 'blocked by private-task policy (egress to Anthropic)' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows a local (no-egress) candidate for a private task', () => {
|
||||
const d = routeTask(
|
||||
{ category: 'coding', privacy: 'private' },
|
||||
[candidate({ egressDestination: null })],
|
||||
NOW,
|
||||
);
|
||||
expect(d.selected?.id).toBe('persona:coder');
|
||||
});
|
||||
|
||||
it('rejects an observed-exhausted candidate with resume time in the reason', () => {
|
||||
const d = routeTask(
|
||||
CODING,
|
||||
[candidate({ rateLimit: { state: 'observed_exhausted', resumeAtMs: 2_000_000 } })],
|
||||
NOW,
|
||||
);
|
||||
expect(d.rejected).toEqual([
|
||||
{ id: 'persona:coder', reason: 'rate limit exhausted (resumes at 2000000)' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects an observed-exhausted candidate without a resume time', () => {
|
||||
const d = routeTask(CODING, [candidate({ rateLimit: { state: 'observed_exhausted' } })], NOW);
|
||||
expect(d.rejected).toEqual([{ id: 'persona:coder', reason: 'rate limit exhausted' }]);
|
||||
});
|
||||
|
||||
it('rejects a candidate still in cooldown', () => {
|
||||
const d = routeTask(CODING, [candidate({ cooldownUntilMs: NOW + 5000 })], NOW);
|
||||
expect(d.rejected).toEqual([{ id: 'persona:coder', reason: 'in cooldown until 1005000' }]);
|
||||
});
|
||||
|
||||
it('accepts a candidate whose cooldown has expired at nowMs', () => {
|
||||
const d = routeTask(CODING, [candidate({ cooldownUntilMs: NOW })], NOW);
|
||||
expect(d.selected?.id).toBe('persona:coder');
|
||||
});
|
||||
|
||||
it('applies gates in fixed order — not-installed wins over other failures', () => {
|
||||
const d = routeTask(
|
||||
CODING,
|
||||
[candidate({ installed: false, healthy: false, rateLimit: { state: 'observed_exhausted' } })],
|
||||
NOW,
|
||||
);
|
||||
expect(d.rejected[0].reason).toBe('not installed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeTask — scoring & ordering', () => {
|
||||
it('scores parts as weighted contributions summing to total', () => {
|
||||
const d = routeTask(CODING, [candidate()], NOW);
|
||||
const s = d.scores[0];
|
||||
// taskFit .85*.5=.425, preference .5*.2=.1, reliability .15, quota 1*.1=.1, latency 1*.05=.05
|
||||
expect(s.parts).toEqual({
|
||||
taskFit: 0.425,
|
||||
preference: 0.1,
|
||||
reliability: 0.15,
|
||||
quota: 0.1,
|
||||
latency: 0.05,
|
||||
});
|
||||
expect(s.total).toBeCloseTo(0.825, 10);
|
||||
});
|
||||
|
||||
it('ranks higher task fit first regardless of input order', () => {
|
||||
const weak = candidate({ id: 'persona:general-purpose', displayName: 'GP', taskFit: { coding: 0.6 } });
|
||||
const strong = candidate({ id: 'persona:coder', taskFit: { coding: 0.85 } });
|
||||
const d = routeTask(CODING, [weak, strong], NOW);
|
||||
expect(d.selected?.id).toBe('persona:coder');
|
||||
expect(d.alternatives.map((a) => a.id)).toEqual(['persona:general-purpose']);
|
||||
expect(d.scores.map((s) => s.id)).toEqual(['persona:coder', 'persona:general-purpose']);
|
||||
});
|
||||
|
||||
it('gives an explicit preference match its weighted boost', () => {
|
||||
const base = candidate({ id: 'persona:a', taskFit: { coding: 0.8 } });
|
||||
const preferred = candidate({ id: 'persona:b', taskFit: { coding: 0.8 } });
|
||||
const d = routeTask({ category: 'coding', privacy: 'normal', preferredExecutorId: 'persona:b' }, [base, preferred], NOW);
|
||||
expect(d.selected?.id).toBe('persona:b');
|
||||
});
|
||||
|
||||
it('scores unknown-quota lower than available-quota', () => {
|
||||
const avail = candidate({ id: 'persona:a', rateLimit: { state: 'available' } });
|
||||
const unknown = candidate({ id: 'persona:b', rateLimit: { state: 'unknown' } });
|
||||
const d = routeTask(CODING, [unknown, avail], NOW);
|
||||
expect(d.selected?.id).toBe('persona:a');
|
||||
});
|
||||
|
||||
it('scores a missing category fit as 0', () => {
|
||||
const d = routeTask({ category: 'ops', privacy: 'normal' }, [candidate({ taskFit: { coding: 0.85 } })], NOW);
|
||||
expect(d.scores[0].parts.taskFit).toBe(0);
|
||||
});
|
||||
|
||||
it('caps alternatives at 3', () => {
|
||||
const cands = ['a', 'b', 'c', 'd', 'e'].map((x, i) =>
|
||||
candidate({ id: `persona:${x}`, taskFit: { coding: 0.9 - i * 0.1 } }),
|
||||
);
|
||||
const d = routeTask(CODING, cands, NOW);
|
||||
expect(d.selected?.id).toBe('persona:a');
|
||||
expect(d.alternatives).toHaveLength(3);
|
||||
expect(d.alternatives.map((a) => a.id)).toEqual(['persona:b', 'persona:c', 'persona:d']);
|
||||
expect(d.scores).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeTask — deterministic tie-breaks', () => {
|
||||
it('prefers persona over external on an exact score tie', () => {
|
||||
// equal task fit; latency weight differs (persona 1.0 vs external 0.7) would
|
||||
// already separate them, so force identical latical by same kind is impossible —
|
||||
// instead give external a higher fit to offset, producing a true total tie.
|
||||
const persona = candidate({ id: 'persona:x', kind: 'persona', taskFit: { coding: 0.6 } });
|
||||
// external latency part = .05*.7 = .035 (persona .05) → deficit .015 → +0.03 fit (*.5=.015)
|
||||
const external = candidate({
|
||||
id: 'external:x',
|
||||
kind: 'external',
|
||||
supportsHeadless: true,
|
||||
egressDestination: 'OpenAI',
|
||||
taskFit: { coding: 0.63 },
|
||||
});
|
||||
const d = routeTask(CODING, [external, persona], NOW);
|
||||
expect(d.scores[0].total).toBeCloseTo(d.scores[1].total, 10);
|
||||
expect(d.selected?.id).toBe('persona:x'); // persona wins the tie
|
||||
});
|
||||
|
||||
it('breaks a persona-vs-persona tie by id ascending', () => {
|
||||
const b = candidate({ id: 'persona:bbb', taskFit: { coding: 0.8 } });
|
||||
const a = candidate({ id: 'persona:aaa', taskFit: { coding: 0.8 } });
|
||||
const d = routeTask(CODING, [b, a], NOW);
|
||||
expect(d.scores.map((s) => s.id)).toEqual(['persona:aaa', 'persona:bbb']);
|
||||
expect(d.selected?.id).toBe('persona:aaa');
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeTask — empty & all-rejected', () => {
|
||||
it('returns an all-null decision for no candidates', () => {
|
||||
const d = routeTask(CODING, [], NOW);
|
||||
expect(d).toEqual({ selected: null, alternatives: [], rejected: [], scores: [] });
|
||||
});
|
||||
|
||||
it('returns no selection when every candidate is gated out', () => {
|
||||
const d = routeTask(CODING, [candidate({ id: 'persona:a', installed: false }), candidate({ id: 'persona:b', healthy: false })], NOW);
|
||||
expect(d.selected).toBeNull();
|
||||
expect(d.alternatives).toEqual([]);
|
||||
expect(d.scores).toEqual([]);
|
||||
expect(d.rejected.map((r) => r.id)).toEqual(['persona:a', 'persona:b']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user