Files
vscode/extensions/copilot/script/setup/getEnv.mts
T
Giuseppe Cianci c04d3b4d48 Fix Copilot sanity tests (#323721)
* Make Copilot sanity tests mint their own token instead of using static copilot-token

The Copilot sanity tests resolve their Copilot token via
getOrCreateTestingCopilotTokenManager, which prefers VSCODE_COPILOT_CHAT_TOKEN
(set here from the `copilot-token` Key Vault secret) over GITHUB_OAUTH_TOKEN.

`copilot-token` holds a pre-minted Copilot token that is refreshed out-of-band
by a separate scheduled pipeline. When that refresher broke (the GitHub OAuth
secret it reads was rotated/removed during a security incident), `copilot-token`
silently went stale and expired. The static token manager hands the expired
token to CAPI `/models`, which returns 401, leaving no resolved model and
surfacing the misleading "server did not mark a chat fallback model" error.

Drop the static `copilot-token` so the tests fall through to GITHUB_OAUTH_TOKEN
(`capi-oauth-pipeline-token`) and exchange it for a fresh Copilot token on
demand. This path self-refreshes and removes the dependency on the external
refresher.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Re-enable sanity tests and surface model-fetch error

- Re-enable the three Copilot sanity tests that were temporarily skipped in
  #323684 now that token resolution is fixed.
- Surface the underlying model-fetch error (e.g. an expired-token 401) from
  ModelMetadataFetcher resolve methods instead of the misleading
  "server did not mark a chat fallback model" message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Omit reasoning summary instead of sending invalid 'off' value

#323639 disabled Responses API reasoning summaries by hard-coding
`reasoning.summary = 'off'`, but the Responses API only accepts
'concise', 'detailed', or 'auto' (or omitting the field to disable).
Models such as gpt-5.3-codex reject 'off' with HTTP 400
invalid_request_body, which surfaced as failing Copilot sanity tests
once they could reach the model.

Set summary to undefined so the field is omitted (summaries stay
disabled, as intended) and update the unit test accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Decouple reasoning-preserve test from disabled summary

The "should preserve reasoning object when thinking is supported" test
relied on the reasoning summary always being present to keep
body.reasoning defined. Now that the summary is omitted, give the test
model reasoning_effort support so body.reasoning is populated via effort,
which is what the test actually means to verify.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* signing commit

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 17:48:12 +00:00

96 lines
4.0 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AzureCliCredential, ChainedTokenCredential, DeviceCodeCredential, InteractiveBrowserCredential, ManagedIdentityCredential, TokenCredential } from '@azure/identity';
import { SecretClient } from '@azure/keyvault-secrets';
import * as fs from 'fs';
import * as process from 'process';
const useColors = (process.stdout.isTTY && typeof process.stdout.hasColors === 'function' && process.stdout.hasColors());
function red(s: string) { return useColors ? `\x1b[31m${s}\x1b[0m` : s; }
async function setupSecretClient(vaultUri: string) {
const credentialOptions: TokenCredential[] = [];
// Only add managed identity credential if the client ID is provided
if (process.env.AZURE_CLIENT_ID) {
credentialOptions.push(new ManagedIdentityCredential({ clientId: process.env.AZURE_CLIENT_ID }));
}
// Always add the Azure CLI as an option
credentialOptions.push(new AzureCliCredential({ tenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47' }));
// Check if terminal is interactive, non-interactive environments can't use
// InteractiveBrowserCredential and don't necessarily have access to a keychain
// For SSH sessions into Azure VMs, keychain is not available, requires managed identity
if (process.stdin.isTTY && !process.env.AZURE_CLIENT_ID && !process.env.CODESPACES) {
credentialOptions.push(new InteractiveBrowserCredential({ tenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47' }));
}
// Use DeviceCodeCredential in Codespaces
if (process.env.CODESPACES) {
const deviceCodeCredential = new DeviceCodeCredential({
tenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47',
userPromptCallback: (info) => {
console.log('To authenticate, visit:', info.verificationUri);
console.log('Enter the code:', info.userCode);
}
});
credentialOptions.push(deviceCodeCredential);
}
const credential = new ChainedTokenCredential(...credentialOptions);
return new SecretClient(vaultUri, credential);
}
async function fetchSecret(secretClient: SecretClient, secretName: string): Promise<string | undefined> {
const secret = await secretClient.getSecret(secretName);
return secret.value;
}
async function fetchSecrets(): Promise<{ [key: string]: string | undefined }> {
const keyVaultClient = await setupSecretClient('https://copilot-automation.vault.azure.net/');
const secrets: { [key: string]: string | undefined } = {};
secrets['HMAC_SECRET'] = await fetchSecret(keyVaultClient, 'hmac-secret');
if (!process.stdin.isTTY) { // only in automation
secrets['GITHUB_OAUTH_TOKEN'] = await fetchSecret(keyVaultClient, 'capi-oauth-pipeline-token');
secrets['BLACKBIRD_EMBEDDINGS_KEY'] = await fetchSecret(keyVaultClient, 'vsc-aoai-key');
secrets['BLACKBIRD_REDIS_CACHE_KEY'] = await fetchSecret(keyVaultClient, 'blackbird-redis-cache-key');
try {
secrets['ANTHROPIC_API_KEY'] = await fetchSecret(keyVaultClient, 'anthropic-key');
secrets['DEEPSEEK_API_KEY'] = await fetchSecret(keyVaultClient, 'deepseek-key');
} catch (error) {
console.log(red(`Failed to fetch optional evaluation tokens. Skipping...`));
}
}
return secrets;
}
async function main() {
const env = Object.entries(await fetchSecrets());
const raw = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') : '';
const result = raw.split('\n')
.filter(line => !env.some(([key]) => line.startsWith(`${key}=`)))
.concat(env.map(([key, value]) => `${key}=${value}`))
.filter(line => line.trim() !== '') // Remove empty lines
.join('\n');
fs.writeFileSync('.env', result);
console.log('Wrote secrets to .env');
process.exit(0);
}
main().catch(error => {
const msg = `Error when setting up .env file:\n${error}`;
console.log(msg);
console.error(red(msg));
process.exit(1);
});