Skip to content

Commit 0c7d7f3

Browse files
CopilotlpcoxCopilot
authored
test(api-proxy): add end-to-end coverage for routed Copilot efforts none and max (#9325)
* Initial plan * test: add end-to-end coverage for routed efforts none and max Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix(docs-site): bump devalue to 5.9.4 to resolve high-severity advisories Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 861889e commit 0c7d7f3

2 files changed

Lines changed: 253 additions & 3 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
'use strict';
2+
3+
/**
4+
* End-to-end coverage for routed reasoning efforts across the routing boundary:
5+
* catalogue → candidate pool → router selection → published selection →
6+
* /reflect state → per-request routing observation.
7+
*
8+
* Guards the full Copilot effort set, in particular explicit `none` and `max`,
9+
* so a future change cannot silently drop or alter them before the selected
10+
* request reaches the provider.
11+
*/
12+
13+
const { EventEmitter } = require('events');
14+
const fs = require('fs');
15+
const os = require('os');
16+
const path = require('path');
17+
18+
const { createRoutingController } = require('./routing-controller');
19+
const { createProductionRoutingSession } = require('./routing-runtime');
20+
21+
const CONVERSATION = [{ role: 'user', parts: [{ text: 'add a test' }] }];
22+
23+
const DEFAULT_MODELS = [
24+
{ id: 'gpt-test', efforts: ['none', 'low', 'max'], protocols: ['responses'] },
25+
{ id: 'chat-test', efforts: [], protocols: ['chat-completions'] },
26+
];
27+
28+
function capabilities() {
29+
return {
30+
name: 'gh-aw-router',
31+
version: '1.0.0',
32+
routing_profiles: [{ goal: 'cost', mode: 'balanced' }],
33+
execution_catalogue: { models: [] },
34+
};
35+
}
36+
37+
function copyChoice(choice) {
38+
const copy = { id: choice.id, model: choice.model };
39+
if (Object.hasOwn(choice, 'effort')) copy.effort = choice.effort;
40+
return copy;
41+
}
42+
43+
class FakeResponse extends EventEmitter {
44+
constructor() {
45+
super();
46+
this.statusCode = 200;
47+
this.writableFinished = false;
48+
}
49+
50+
write() {
51+
return true;
52+
}
53+
54+
end() {
55+
this.writableFinished = true;
56+
this.emit('finish');
57+
return this;
58+
}
59+
}
60+
61+
/**
62+
* Run a routed session with a real controller and candidate pool. The router
63+
* picks whichever offered choice `pick` returns, or `pick` may return a
64+
* fabricated choice to exercise contract rejection.
65+
*/
66+
async function runRoutedSession({ models = DEFAULT_MODELS, pick }) {
67+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-routing-efforts-'));
68+
const records = [];
69+
const routed = [];
70+
const observer = { record: record => records.push(record) };
71+
const createController = () => createRoutingController({
72+
config: {
73+
provider: 'copilot',
74+
objective: { goal: 'cost', mode: 'balanced' },
75+
task: { conversationFile: '/tmp/conversation.json' },
76+
},
77+
planner: {
78+
health: async () => 204,
79+
capabilities: async () => capabilities(),
80+
classify: async request => ({
81+
system_prompt: 'classify the task',
82+
prompt: 'add a test',
83+
ranked_choices: request.models.map(copyChoice),
84+
}),
85+
route: async request => {
86+
routed.push(request.models.map(copyChoice));
87+
return { ranked_choices: [pick(request.models.map(copyChoice))] };
88+
},
89+
},
90+
catalogue: {
91+
getSnapshot: async () => ({ provider: 'copilot', configured: true, discovery: 'complete', models }),
92+
},
93+
loadConversation: async () => JSON.parse(JSON.stringify(CONVERSATION)),
94+
executor: { execute: jest.fn(), checkBeforePrimary: async () => undefined },
95+
observer,
96+
});
97+
const session = createProductionRoutingSession({ rawConfig: '{}', outputDir, createController, observer });
98+
const result = await session.start();
99+
return { session, result, records, routed, outputDir };
100+
}
101+
102+
function readResult(outputDir, name) {
103+
return JSON.parse(fs.readFileSync(path.join(outputDir, name), 'utf8'));
104+
}
105+
106+
function sendRequest(session, records, url, payload) {
107+
const req = { method: 'POST', url, headers: {} };
108+
session.observeRequest(req, new FakeResponse(), { name: 'copilot' });
109+
expect(req.awfRouting.bodyTransform(Buffer.from(JSON.stringify(payload), 'utf8'))).toBeNull();
110+
return records.filter(record => record.stage === 'request').at(-1);
111+
}
112+
113+
const byEffort = effort => choices => choices.find(choice =>
114+
effort === undefined ? !Object.hasOwn(choice, 'effort') : choice.effort === effort);
115+
116+
describe('routed reasoning efforts', () => {
117+
it('offers explicit none and max, and keeps an omitted effort distinct from none', async () => {
118+
const { routed } = await runRoutedSession({ pick: byEffort('none') });
119+
expect(routed[0]).toEqual([
120+
{ id: 'choice-0001', model: 'github-copilot/chat-test' },
121+
{ id: 'choice-0002', model: 'github-copilot/gpt-test', effort: 'low' },
122+
{ id: 'choice-0003', model: 'github-copilot/gpt-test', effort: 'max' },
123+
{ id: 'choice-0004', model: 'github-copilot/gpt-test', effort: 'none' },
124+
]);
125+
});
126+
127+
it('offers the full Copilot effort set', async () => {
128+
const efforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
129+
const { routed } = await runRoutedSession({
130+
models: [{ id: 'gpt-test', efforts, protocols: ['responses'] }],
131+
pick: byEffort('max'),
132+
});
133+
expect(routed[0].map(choice => choice.effort).sort()).toEqual([...efforts].sort());
134+
});
135+
136+
describe.each(['none', 'max'])('selected effort %s', effort => {
137+
it('is published, reflected, and accepted only on the matching model, endpoint, and effort', async () => {
138+
const { session, result, records, outputDir } = await runRoutedSession({ pick: byEffort(effort) });
139+
140+
expect(result.ok).toBe(true);
141+
const selection = {
142+
schema: 'awf-routing-selection/v1',
143+
engine: 'copilot',
144+
provider: 'copilot',
145+
choice: { id: expect.any(String), model: 'github-copilot/gpt-test', effort },
146+
wire_model: 'gpt-test',
147+
};
148+
expect(result.selection).toEqual(selection);
149+
expect(readResult(outputDir, 'selection.json')).toEqual(selection);
150+
expect(records.find(record => record.stage === 'selection')).toMatchObject({ selected_effort: effort });
151+
152+
expect(session.getReflectState()).toEqual({
153+
status: 'selected',
154+
selection: {
155+
provider: 'copilot',
156+
model: 'github-copilot/gpt-test',
157+
wire_model: 'gpt-test',
158+
effort,
159+
endpoint: '/responses',
160+
},
161+
});
162+
163+
expect(sendRequest(session, records, '/responses', { model: 'gpt-test', reasoning: { effort } }))
164+
.toMatchObject({
165+
routed: 'as_selected',
166+
deviations: [],
167+
requested_effort: effort,
168+
selected_effort: effort,
169+
selected_endpoint: '/responses',
170+
});
171+
expect(sendRequest(session, records, '/v1/responses', { model: 'copilot/gpt-test', reasoning: { effort } }))
172+
.toMatchObject({ routed: 'as_selected', deviations: [] });
173+
174+
const otherEffort = effort === 'none' ? 'max' : 'none';
175+
for (const [url, payload, deviations] of [
176+
['/responses', { model: 'gpt-test', reasoning: { effort: otherEffort } }, ['effort']],
177+
['/responses', { model: 'gpt-test', reasoning: { effort: 'low' } }, ['effort']],
178+
['/responses', { model: 'gpt-test' }, ['effort']],
179+
['/responses', { model: 'other-model', reasoning: { effort } }, ['model']],
180+
['/chat/completions', { model: 'gpt-test', reasoning_effort: effort }, ['endpoint']],
181+
['/chat/completions', { model: 'gpt-test', reasoning: { effort } }, ['effort', 'endpoint']],
182+
]) {
183+
expect(sendRequest(session, records, url, payload)).toMatchObject({
184+
routed: 'deviated',
185+
deviations,
186+
selected_effort: effort,
187+
});
188+
}
189+
});
190+
});
191+
192+
it('keeps an omitted selected effort distinct from explicit none', async () => {
193+
const { session, result, records, outputDir } = await runRoutedSession({ pick: byEffort(undefined) });
194+
195+
expect(result.ok).toBe(true);
196+
expect(Object.hasOwn(result.selection.choice, 'effort')).toBe(false);
197+
expect(Object.hasOwn(readResult(outputDir, 'selection.json').choice, 'effort')).toBe(false);
198+
expect(records.find(record => record.stage === 'selection')).toMatchObject({ selected_effort: null });
199+
expect(session.getReflectState().selection).toMatchObject({
200+
model: 'github-copilot/chat-test',
201+
effort: null,
202+
endpoint: '/chat/completions',
203+
});
204+
205+
expect(sendRequest(session, records, '/chat/completions', { model: 'chat-test' }))
206+
.toMatchObject({ routed: 'as_selected', requested_effort: null, selected_effort: null });
207+
expect(sendRequest(session, records, '/chat/completions', { model: 'chat-test', reasoning_effort: 'none' }))
208+
.toMatchObject({ routed: 'deviated', deviations: ['effort'], requested_effort: 'none' });
209+
210+
const none = await runRoutedSession({ pick: byEffort('none') });
211+
expect(sendRequest(none.session, none.records, '/responses', { model: 'gpt-test' }))
212+
.toMatchObject({ routed: 'deviated', deviations: ['effort'], requested_effort: null, selected_effort: 'none' });
213+
});
214+
215+
it.each([
216+
['an unsupported effort', { id: 'choice-0003', model: 'github-copilot/gpt-test', effort: 'ultra' }],
217+
['a mismatched effort for an offered id', { id: 'choice-0003', model: 'github-copilot/gpt-test', effort: 'none' }],
218+
['a null effort', { id: 'choice-0004', model: 'github-copilot/gpt-test', effort: null }],
219+
['an omitted effort where none was offered', { id: 'choice-0004', model: 'github-copilot/gpt-test' }],
220+
['an explicit none where the effort was omitted', { id: 'choice-0001', model: 'github-copilot/chat-test', effort: 'none' }],
221+
])('fails closed when the router returns %s', async (_name, choice) => {
222+
const { session, result, outputDir } = await runRoutedSession({ pick: () => choice });
223+
expect(result.ok).toBe(false);
224+
expect(result.failure.code).toBe('routing_contract_error');
225+
expect(fs.existsSync(path.join(outputDir, 'selection.json'))).toBe(false);
226+
expect(readResult(outputDir, 'failure.json')).toMatchObject({ code: 'routing_contract_error' });
227+
expect(session.getReflectState()).toEqual({
228+
status: 'failed',
229+
failure_code: 'routing_contract_error',
230+
selection: null,
231+
});
232+
});
233+
234+
it('excludes unsupported catalogue efforts and fails closed when none remain', async () => {
235+
const mixed = await runRoutedSession({
236+
models: [{ id: 'gpt-test', efforts: ['ultra', 'max', 'NONE'], protocols: ['responses'] }],
237+
pick: choices => choices[0],
238+
});
239+
expect(mixed.routed[0]).toEqual([{ id: 'choice-0001', model: 'github-copilot/gpt-test', effort: 'max' }]);
240+
expect(mixed.session.getReflectState().selection).toMatchObject({ effort: 'max', endpoint: '/responses' });
241+
242+
const unsupported = await runRoutedSession({
243+
models: [{ id: 'gpt-test', efforts: ['ultra'], protocols: ['responses'] }],
244+
pick: choices => choices[0],
245+
});
246+
expect(unsupported.routed).toEqual([]);
247+
expect(unsupported.result.failure.code).toBe('no_route');
248+
expect(unsupported.session.getReflectState()).toEqual({ status: 'failed', failure_code: 'no_route', selection: null });
249+
});
250+
});

‎docs-site/package-lock.json‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)