Merge branch 'main' into eli/prompt-tile-style-clean

This commit is contained in:
Elijah King
2025-09-30 14:28:55 -07:00
committed by GitHub
32 changed files with 745 additions and 129 deletions
+1
View File
@@ -133,3 +133,4 @@ function f(x: number, y: string): void { }
- Look for existing test patterns before creating new structures
- Use `describe` and `test` consistently with existing patterns
- If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task
- Do not use `any` or `unknown` as the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined.
+2
View File
@@ -402,7 +402,9 @@ function createTypeScriptBuilder(config, projectFile, cmd) {
messageText: `CYCLIC dependency: ${error}`
});
}
delete oldErrors[filename];
newErrors[filename] = cyclicDepErrors;
cyclicDepErrors.forEach(d => onError(d));
}
}).then(() => {
// store the build versions to not rebuilt the next time
+2
View File
@@ -440,7 +440,9 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
messageText: `CYCLIC dependency: ${error}`
});
}
delete oldErrors[filename];
newErrors[filename] = cyclicDepErrors;
cyclicDepErrors.forEach(d => onError(d));
}
}).then(() => {
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "marmaid-chat-features",
"name": "mermaid-chat-features",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "marmaid-chat-features",
"name": "mermaid-chat-features",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
@@ -88,6 +88,8 @@
"sideBarTitle.foreground": "#CCCCCC",
"statusBar.background": "#181818",
"statusBar.border": "#2B2B2B",
"statusBarItem.hoverBackground": "#F1F1F133",
"statusBarItem.hoverForeground": "#FFFFFF",
"statusBar.debuggingBackground": "#0078D4",
"statusBar.debuggingForeground": "#FFFFFF",
"statusBar.focusBorder": "#0078D4",
@@ -104,7 +104,8 @@
"statusBar.background": "#F8F8F8",
"statusBar.foreground": "#3B3B3B",
"statusBar.border": "#E5E5E5",
"statusBarItem.hoverBackground": "#B8B8B850",
"statusBarItem.hoverBackground": "#1F1F1F11",
"statusBarItem.hoverForeground": "#000000",
"statusBarItem.compactHoverBackground": "#CCCCCC",
"statusBar.debuggingBackground": "#FD716C",
"statusBar.debuggingForeground": "#000000",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.105.0",
"distro": "9e823daeded19d4a7c8f7e3ba36ca5d3c68d8350",
"distro": "8b551ae26e4dadc3f6ece34bbbc3feea81ce4a97",
"author": {
"name": "Microsoft Corporation"
},
@@ -239,4 +239,4 @@
"optionalDependencies": {
"windows-foreground-love": "0.5.0"
}
}
}
+117
View File
@@ -957,3 +957,120 @@ export function scopesMatch(scopes1: readonly string[], scopes2: readonly string
return sortedScopes1.every((scope, index) => scope === sortedScopes2[index]);
}
interface CommonResponse {
status: number;
statusText: string;
json(): Promise<any>;
text(): Promise<string>;
}
interface IFetcher {
(input: string, init: { method: string; headers: Record<string, string> }): Promise<CommonResponse>;
}
export interface IFetchResourceMetadataOptions {
/**
* Headers to include only when the resource metadata URL has the same origin as the target resource
*/
sameOriginHeaders?: Record<string, string>;
/**
* Optional custom fetch implementation (defaults to global fetch)
*/
fetch?: IFetcher;
}
/**
* Fetches and validates OAuth 2.0 protected resource metadata from the given URL.
*
* @param targetResource The target resource URL to compare origins with (e.g., the MCP server URL)
* @param resourceMetadataUrl Optional URL to fetch the resource metadata from. If not provided, will try well-known URIs.
* @param options Configuration options for the fetch operation
* @returns Promise that resolves to the validated resource metadata
* @throws Error if the fetch fails, returns non-200 status, or the response is invalid
*/
export async function fetchResourceMetadata(
targetResource: string,
resourceMetadataUrl: string | undefined,
options: IFetchResourceMetadataOptions = {}
): Promise<IAuthorizationProtectedResourceMetadata> {
const {
sameOriginHeaders = {},
fetch: fetchImpl = fetch
} = options;
const targetResourceUrlObj = new URL(targetResource);
// If no resourceMetadataUrl is provided, try well-known URIs as per RFC 9728
let urlsToTry: string[];
if (!resourceMetadataUrl) {
// Try in order: 1) with path appended, 2) at root
const pathComponent = targetResourceUrlObj.pathname === '/' ? undefined : targetResourceUrlObj.pathname;
const rootUrl = `${targetResourceUrlObj.origin}${AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH}`;
if (pathComponent) {
// Only try both URLs if we have a path component
urlsToTry = [
`${rootUrl}${pathComponent}`,
rootUrl
];
} else {
// If target is already at root, only try the root URL once
urlsToTry = [rootUrl];
}
} else {
urlsToTry = [resourceMetadataUrl];
}
const errors: Error[] = [];
for (const urlToTry of urlsToTry) {
try {
// Determine if we should include same-origin headers
let headers: Record<string, string> = {
'Accept': 'application/json'
};
const resourceMetadataUrlObj = new URL(urlToTry);
if (resourceMetadataUrlObj.origin === targetResourceUrlObj.origin) {
headers = {
...headers,
...sameOriginHeaders
};
}
const response = await fetchImpl(urlToTry, { method: 'GET', headers });
if (response.status !== 200) {
let errorText: string;
try {
errorText = await response.text();
} catch {
errorText = response.statusText;
}
errors.push(new Error(`Failed to fetch resource metadata from ${urlToTry}: ${response.status} ${errorText}`));
continue;
}
const body = await response.json();
if (isAuthorizationProtectedResourceMetadata(body)) {
// Use URL constructor for normalization - it handles hostname case and trailing slashes
const prmValue = new URL(body.resource).toString();
const targetValue = targetResourceUrlObj.toString();
if (prmValue !== targetValue) {
throw new Error(`Protected Resource Metadata resource property value "${prmValue}" (length: ${prmValue.length}) does not match target server url "${targetValue}" (length: ${targetValue.length}). These MUST match to follow OAuth spec https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`);
}
return body;
} else {
errors.push(new Error(`Invalid resource metadata from ${urlToTry}. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`));
continue;
}
} catch (e) {
errors.push(e instanceof Error ? e : new Error(String(e)));
continue;
}
}
// If we've tried all URLs and none worked, throw the error(s)
if (errors.length === 1) {
throw errors[0];
} else {
throw new AggregateError(errors, 'Failed to fetch resource metadata from all attempted URLs');
}
}
+475
View File
@@ -17,6 +17,7 @@ import {
isAuthorizationTokenResponse,
parseWWWAuthenticateHeader,
fetchDynamicRegistration,
fetchResourceMetadata,
scopesMatch,
IAuthorizationJWTClaims,
IAuthorizationServerMetadata,
@@ -831,4 +832,478 @@ suite('OAuth', () => {
);
});
});
suite('fetchResourceMetadata', () => {
let sandbox: sinon.SinonSandbox;
let fetchStub: sinon.SinonStub;
setup(() => {
sandbox = sinon.createSandbox();
fetchStub = sandbox.stub();
});
teardown(() => {
sandbox.restore();
});
test('should successfully fetch and validate resource metadata', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const expectedMetadata = {
resource: 'https://example.com/api',
scopes_supported: ['read', 'write']
};
fetchStub.resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
const result = await fetchResourceMetadata(
targetResource,
resourceMetadataUrl,
{ fetch: fetchStub }
);
assert.deepStrictEqual(result, expectedMetadata);
assert.strictEqual(fetchStub.callCount, 1);
assert.strictEqual(fetchStub.firstCall.args[0], resourceMetadataUrl);
assert.strictEqual(fetchStub.firstCall.args[1].method, 'GET');
assert.strictEqual(fetchStub.firstCall.args[1].headers['Accept'], 'application/json');
});
test('should include same-origin headers when origins match', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const sameOriginHeaders = {
'X-Test-Header': 'test-value',
'X-Custom-Header': 'value'
};
const expectedMetadata = {
resource: 'https://example.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
await fetchResourceMetadata(
targetResource,
resourceMetadataUrl,
{ fetch: fetchStub, sameOriginHeaders }
);
const headers = fetchStub.firstCall.args[1].headers;
assert.strictEqual(headers['Accept'], 'application/json');
assert.strictEqual(headers['X-Test-Header'], 'test-value');
assert.strictEqual(headers['X-Custom-Header'], 'value');
});
test('should not include same-origin headers when origins differ', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://other-domain.com/.well-known/oauth-protected-resource';
const sameOriginHeaders = {
'X-Test-Header': 'test-value'
};
const expectedMetadata = {
resource: 'https://example.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
await fetchResourceMetadata(
targetResource,
resourceMetadataUrl,
{ fetch: fetchStub, sameOriginHeaders }
);
const headers = fetchStub.firstCall.args[1].headers;
assert.strictEqual(headers['Accept'], 'application/json');
assert.strictEqual(headers['X-Test-Header'], undefined);
});
test('should throw error when fetch returns non-200 status', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
fetchStub.resolves({
status: 404,
text: async () => 'Not Found'
});
await assert.rejects(
async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }),
/Failed to fetch resource metadata from.*404 Not Found/
);
});
test('should handle error when response.text() throws', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
fetchStub.resolves({
status: 500,
statusText: 'Internal Server Error',
text: async () => { throw new Error('Cannot read response'); }
});
await assert.rejects(
async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }),
/Failed to fetch resource metadata from.*500 Internal Server Error/
);
});
test('should throw error when resource property does not match target resource', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const metadata = {
resource: 'https://different.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
await assert.rejects(
async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }),
/Protected Resource Metadata resource property value.*does not match target server url.*These MUST match to follow OAuth spec/
);
});
test('should normalize URLs when comparing resource values', async () => {
const targetResource = 'https://EXAMPLE.COM/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const metadata = {
resource: 'https://example.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
// URL normalization should handle hostname case differences
const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub });
assert.deepStrictEqual(result, metadata);
});
test('should normalize hostnames when comparing resource values', async () => {
const targetResource = 'https://EXAMPLE.COM/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const metadata = {
resource: 'https://example.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub });
assert.deepStrictEqual(result, metadata);
});
test('should throw error when response is not valid resource metadata', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const invalidMetadata = {
// Missing required 'resource' property
scopes_supported: ['read', 'write']
};
fetchStub.resolves({
status: 200,
json: async () => invalidMetadata,
text: async () => JSON.stringify(invalidMetadata)
});
await assert.rejects(
async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }),
/Invalid resource metadata.*Expected to follow shape of.*is scopes_supported an array\? Is resource a string\?/
);
});
test('should throw error when scopes_supported is not an array', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const invalidMetadata = {
resource: 'https://example.com/api',
scopes_supported: 'not an array'
};
fetchStub.resolves({
status: 200,
json: async () => invalidMetadata,
text: async () => JSON.stringify(invalidMetadata)
});
await assert.rejects(
async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }),
/Invalid resource metadata/
);
});
test('should handle metadata with optional fields', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const metadata = {
resource: 'https://example.com/api',
resource_name: 'Example API',
authorization_servers: ['https://auth.example.com'],
jwks_uri: 'https://example.com/jwks',
scopes_supported: ['read', 'write', 'admin'],
bearer_methods_supported: ['header', 'body'],
resource_documentation: 'https://example.com/docs'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub });
assert.deepStrictEqual(result, metadata);
});
test('should use global fetch when custom fetch is not provided', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const metadata = {
resource: 'https://example.com/api'
};
const globalFetchStub = sandbox.stub(globalThis, 'fetch').resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
} as any);
const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl);
assert.deepStrictEqual(result, metadata);
assert.strictEqual(globalFetchStub.callCount, 1);
});
test('should handle same origin with different ports', async () => {
const targetResource = 'https://example.com:8080/api';
const resourceMetadataUrl = 'https://example.com:9090/.well-known/oauth-protected-resource';
const sameOriginHeaders = {
'X-Test-Header': 'test-value'
};
const metadata = {
resource: 'https://example.com:8080/api'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
await fetchResourceMetadata(
targetResource,
resourceMetadataUrl,
{ fetch: fetchStub, sameOriginHeaders }
);
// Different ports mean different origins
const headers = fetchStub.firstCall.args[1].headers;
assert.strictEqual(headers['X-Test-Header'], undefined);
});
test('should handle same origin with different protocols', async () => {
const targetResource = 'http://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const sameOriginHeaders = {
'X-Test-Header': 'test-value'
};
const metadata = {
resource: 'http://example.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
await fetchResourceMetadata(
targetResource,
resourceMetadataUrl,
{ fetch: fetchStub, sameOriginHeaders }
);
// Different protocols mean different origins
const headers = fetchStub.firstCall.args[1].headers;
assert.strictEqual(headers['X-Test-Header'], undefined);
});
test('should include error details in message with length information', async () => {
const targetResource = 'https://example.com/api';
const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource';
const metadata = {
resource: 'https://different.com/other'
};
fetchStub.resolves({
status: 200,
json: async () => metadata,
text: async () => JSON.stringify(metadata)
});
try {
await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub });
assert.fail('Should have thrown an error');
} catch (error: any) {
assert.ok(/length:/.test(error.message), 'Error message should include length information');
assert.ok(/https:\/\/different\.com\/other/.test(error.message), 'Error message should include actual resource value');
assert.ok(/https:\/\/example\.com\/api/.test(error.message), 'Error message should include expected resource value');
}
});
test('should fallback to well-known URI with path when no resourceMetadataUrl provided', async () => {
const targetResource = 'https://example.com/api/v1';
const expectedMetadata = {
resource: 'https://example.com/api/v1',
scopes_supported: ['read', 'write']
};
fetchStub.resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
const result = await fetchResourceMetadata(
targetResource,
undefined,
{ fetch: fetchStub }
);
assert.deepStrictEqual(result, expectedMetadata);
assert.strictEqual(fetchStub.callCount, 1);
// Should try path-appended version first
assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1');
});
test('should fallback to well-known URI at root when path version fails', async () => {
const targetResource = 'https://example.com/api/v1';
const expectedMetadata = {
resource: 'https://example.com/api/v1',
scopes_supported: ['read', 'write']
};
// First call fails, second succeeds
fetchStub.onFirstCall().resolves({
status: 404,
text: async () => 'Not Found',
statusText: 'Not Found'
});
fetchStub.onSecondCall().resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
const result = await fetchResourceMetadata(
targetResource,
undefined,
{ fetch: fetchStub }
);
assert.deepStrictEqual(result, expectedMetadata);
assert.strictEqual(fetchStub.callCount, 2);
// First attempt with path
assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1');
// Second attempt at root
assert.strictEqual(fetchStub.secondCall.args[0], 'https://example.com/.well-known/oauth-protected-resource');
});
test('should throw error when all well-known URIs fail', async () => {
const targetResource = 'https://example.com/api/v1';
fetchStub.resolves({
status: 404,
text: async () => 'Not Found',
statusText: 'Not Found'
});
await assert.rejects(
async () => fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }),
(error: any) => {
assert.ok(error instanceof AggregateError, 'Should be an AggregateError');
assert.strictEqual(error.errors.length, 2, 'Should contain 2 errors');
assert.ok(/Failed to fetch resource metadata from.*\/api\/v1.*404/.test(error.errors[0].message), 'First error should mention /api/v1 and 404');
assert.ok(/Failed to fetch resource metadata from.*\.well-known.*404/.test(error.errors[1].message), 'Second error should mention .well-known and 404');
return true;
}
); assert.strictEqual(fetchStub.callCount, 2);
});
test('should not append path when target resource is root', async () => {
const targetResource = 'https://example.com/';
const expectedMetadata = {
resource: 'https://example.com/',
scopes_supported: ['read']
};
fetchStub.resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
const result = await fetchResourceMetadata(
targetResource,
undefined,
{ fetch: fetchStub }
);
assert.deepStrictEqual(result, expectedMetadata);
assert.strictEqual(fetchStub.callCount, 1);
// Both URLs should be the same when path is /
assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource');
});
test('should include same-origin headers when using well-known fallback', async () => {
const targetResource = 'https://example.com/api';
const sameOriginHeaders = {
'X-Test-Header': 'test-value',
'X-Custom-Header': 'value'
};
const expectedMetadata = {
resource: 'https://example.com/api'
};
fetchStub.resolves({
status: 200,
json: async () => expectedMetadata,
text: async () => JSON.stringify(expectedMetadata)
});
await fetchResourceMetadata(
targetResource,
undefined,
{ fetch: fetchStub, sameOriginHeaders }
);
const headers = fetchStub.firstCall.args[1].headers;
assert.strictEqual(headers['Accept'], 'application/json');
assert.strictEqual(headers['X-Test-Header'], 'test-value');
assert.strictEqual(headers['X-Custom-Header'], 'value');
});
});
});
@@ -242,6 +242,7 @@ export class FoldingController extends Disposable implements IEditorContribution
this.localToDispose.add(this.hiddenRangeModel.onDidChange(hr => this.onHiddenRangesChanges(hr)));
this.updateScheduler = new Delayer<FoldingModel>(this.updateDebounceInfo.get(model));
this.localToDispose.add(this.updateScheduler);
this.cursorChangedScheduler = new RunOnceScheduler(() => this.revealCursor(), 200);
this.localToDispose.add(this.cursorChangedScheduler);
@@ -19,7 +19,7 @@ export class McpGalleryManifestService extends Disposable implements IMcpGallery
}
constructor(
@IProductService protected readonly productService: IProductService,
@IProductService private readonly productService: IProductService,
) {
super();
}
+12 -39
View File
@@ -8,7 +8,7 @@ import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../.
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { CancellationError } from '../../../base/common/errors.js';
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { AUTH_SERVER_METADATA_DISCOVERY_PATH, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationProtectedResourceMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js';
import { AUTH_SERVER_METADATA_DISCOVERY_PATH, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js';
import { SSEParser } from '../../../base/common/sseParser.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js';
@@ -345,24 +345,26 @@ export class McpHTTPHandle extends Disposable {
}
}
}
// Second, fetch that url's well-known server metadata
// Second, fetch the resource metadata either from the challenge URL or from well-known URIs
let serverMetadataUrl: string | undefined;
let scopesSupported: string[] | undefined;
let resource: IAuthorizationProtectedResourceMetadata | undefined;
if (resourceMetadataChallenge) {
const resourceMetadata = await this._getResourceMetadata(resourceMetadataChallenge);
// Use URL constructor for normalization - it handles hostname case and trailing slashes
const prmValue = new URL(resourceMetadata.resource).toString();
const mcpValue = new URL(mcpUrl).toString();
if (prmValue !== mcpValue) {
throw new Error(`Protected Resource Metadata resource value "${prmValue}" (length: ${prmValue.length}) does not match MCP server url "${mcpValue}" (length: ${mcpValue.length}). The MCP server must follow OAuth spec https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`);
}
try {
const resourceMetadata = await fetchResourceMetadata(mcpUrl, resourceMetadataChallenge, {
sameOriginHeaders: {
...Object.fromEntries(this._launch.headers),
'MCP-Protocol-Version': MCP.LATEST_PROTOCOL_VERSION
},
fetch: (url, init) => this._fetch(url, init)
});
// TODO:@TylerLeonhardt support multiple authorization servers
// Consider using one that has an auth provider first, over the dynamic flow
serverMetadataUrl = resourceMetadata.authorization_servers?.[0];
this._log(LogLevel.Debug, `Using auth server metadata url: ${serverMetadataUrl}`);
scopesSupported = resourceMetadata.scopes_supported;
resource = resourceMetadata;
} catch (e) {
this._log(LogLevel.Debug, `Could not fetch resource metadata: ${String(e)}`);
}
const baseUrl = new URL(originalResponse.url).origin;
@@ -401,35 +403,6 @@ export class McpHTTPHandle extends Disposable {
this._log(LogLevel.Info, 'Using default auth metadata');
}
private async _getResourceMetadata(resourceMetadata: string): Promise<IAuthorizationProtectedResourceMetadata> {
// detect if the resourceMetadata, which is a URL, is in the same origin as the MCP server
const resourceMetadataUrl = new URL(resourceMetadata);
const mcpServerUrl = new URL(this._launch.uri.toString(true));
let additionalHeaders: Record<string, string> = {};
if (resourceMetadataUrl.origin === mcpServerUrl.origin) {
additionalHeaders = {
...Object.fromEntries(this._launch.headers)
};
}
const resourceMetadataResponse = await this._fetch(resourceMetadata, {
method: 'GET',
headers: {
...additionalHeaders,
'Accept': 'application/json',
'MCP-Protocol-Version': MCP.LATEST_PROTOCOL_VERSION
}
});
if (resourceMetadataResponse.status !== 200) {
throw new Error(`Failed to fetch resource metadata: ${resourceMetadataResponse.status} ${await this._getErrText(resourceMetadataResponse)}`);
}
const body = await resourceMetadataResponse.json();
if (isAuthorizationProtectedResourceMetadata(body)) {
return body;
} else {
throw new Error(`Invalid resource metadata. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`);
}
}
private async _getAuthorizationServerMetadata(authorizationServer: string, addtionalHeaders: Record<string, string>): Promise<IAuthorizationServerMetadata> {
// For the oauth server metadata discovery path, we _INSERT_
// the well known path after the origin and before the path.
@@ -177,8 +177,8 @@
outline-offset: -1px;
}
.monaco-workbench:not(.hc-light):not(.hc-black) .part.statusbar > .items-container > .statusbar-item a:hover:not(.disabled) {
background-color: var(--vscode-statusBarItem-hoverBackground);
.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:hover:not(.disabled) {
background-color: var(--vscode-statusBarItem-hoverBackground) !important;
}
/** Status bar entry item kinds */
+7 -7
View File
@@ -336,18 +336,18 @@ export const STATUS_BAR_ITEM_FOCUS_BORDER = registerColor('statusBarItem.focusBo
export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hoverBackground', {
dark: Color.white.transparent(0.12),
light: Color.white.transparent(0.12),
hcDark: Color.white.transparent(0.12),
hcLight: Color.black.transparent(0.12)
light: Color.black.transparent(0.12),
hcDark: Color.black,
hcLight: Color.white
}, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window."));
export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', STATUS_BAR_FOREGROUND, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window."));
export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', {
dark: Color.white.transparent(0.20),
light: Color.white.transparent(0.20),
hcDark: Color.white.transparent(0.20),
hcLight: Color.black.transparent(0.20)
dark: Color.white.transparent(0.12),
light: Color.black.transparent(0.12),
hcDark: Color.black,
hcLight: Color.white
}, localize('statusBarItemCompactHoverBackground', "Status bar item background color when hovering an item that contains two hovers. The status bar is shown in the bottom of the window."));
export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', STATUS_BAR_FOREGROUND, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window."));
@@ -66,14 +66,13 @@ import { IChatSessionItem, IChatSessionsService } from '../../common/chatSession
import { ChatSessionUri } from '../../common/chatUri.js';
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js';
import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, VIEWLET_ID } from '../../common/constants.js';
import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js';
import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js';
import { IChatEditorOptions } from '../chatEditor.js';
import { ChatEditorInput, shouldShowClearEditingSessionConfirmation, showClearEditingSessionConfirmation } from '../chatEditorInput.js';
import { VIEWLET_ID } from '../chatSessions/view/chatSessionsView.js';
import { ChatViewPane } from '../chatViewPane.js';
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
import { clearChatEditor } from './chatClear.js';
@@ -28,7 +28,7 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatService } from '../../common/chatService.js';
import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { ChatSessionUri } from '../../common/chatUri.js';
import { ChatConfiguration } from '../../common/constants.js';
import { ChatConfiguration, VIEWLET_ID } from '../../common/constants.js';
import { ChatViewId, IChatWidgetService } from '../chat.js';
import { IChatEditorOptions } from '../chatEditor.js';
import { ChatEditorInput } from '../chatEditorInput.js';
@@ -36,7 +36,6 @@ import { ChatSessionItemWithProvider, findExistingChatEditorByUri, isLocalChatSe
import { ChatViewPane } from '../chatViewPane.js';
import { ACTION_ID_OPEN_CHAT, CHAT_CATEGORY } from './chatActions.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { VIEWLET_ID } from '../chatSessions/view/chatSessionsView.js';
export interface IChatSessionContext {
sessionId: string;
@@ -23,11 +23,10 @@ import { IChatAgentData, IChatAgentRequest, IChatAgentService } from '../common/
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatSession, ChatSessionStatus, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService } from '../common/chatSessionsService.js';
import { ChatSessionUri } from '../common/chatUri.js';
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
import { ChatAgentLocation, ChatModeKind, VIEWLET_ID } from '../common/constants.js';
import { CHAT_CATEGORY } from './actions/chatActions.js';
import { IChatEditorOptions } from './chatEditor.js';
import { NEW_CHAT_SESSION_ACTION_ID } from './chatSessions/common.js';
import { VIEWLET_ID } from './chatSessions/view/chatSessionsView.js';
const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsExtensionPoint[]>({
extensionPoint: 'chatSessions',
@@ -25,14 +25,12 @@ import { IChatEntitlementService } from '../../../../../services/chat/common/cha
import { IExtensionService } from '../../../../../services/extensions/common/extensions.js';
import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js';
import { IChatSessionsService, IChatSessionItemProvider, IChatSessionsExtensionPoint } from '../../../common/chatSessionsService.js';
import { ChatConfiguration } from '../../../common/constants.js';
import { ChatConfiguration, VIEWLET_ID } from '../../../common/constants.js';
import { ACTION_ID_OPEN_CHAT } from '../../actions/chatActions.js';
import { ChatSessionTracker } from '../chatSessionTracker.js';
import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js';
import { SessionsViewPane } from './sessionsViewPane.js';
export const VIEWLET_ID = 'workbench.view.chat.sessions';
export class ChatSessionsView extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatSessions';
@@ -86,3 +86,5 @@ export namespace ChatAgentLocation {
export const ChatUnsupportedFileSchemes = new Set([Schemas.vscodeChatEditor, Schemas.walkThrough, Schemas.vscodeChatSession, 'ccreq']);
export const TodoListWidgetPositionSettingId = 'chat.todoListWidget.position';
export const VIEWLET_ID = 'workbench.view.chat.sessions';
@@ -230,6 +230,10 @@ export class VariablesView extends ViewPane implements IDebugViewWithVariables {
return !!e.treeItem.canEdit;
}
if (!session.capabilities?.supportsSetVariable && !session.capabilities?.supportsSetExpression) {
return false;
}
return e instanceof Variable && !e.presentationHint?.attributes?.includes('readOnly') && !e.presentationHint?.lazy;
}
@@ -37,22 +37,24 @@ export class McpResourcePickHelper {
}
public static item(resource: IMcpResource | IMcpResourceTemplate): IQuickPickItem {
const icon = resource.icons.getUrl(22);
const iconPath = icon ? { dark: icon, light: icon } : undefined;
if (isMcpResourceTemplate(resource)) {
return {
id: resource.template.template,
label: resource.title || resource.name,
description: resource.description,
detail: localize('mcp.resource.template', 'Resource template: {0}', resource.template.template),
iconPath,
};
}
const icon = resource.icons.getUrl(22);
return {
id: resource.uri.toString(),
label: resource.title || resource.name,
description: resource.description,
detail: resource.mcpUri + (resource.sizeInBytes !== undefined ? ' (' + ByteSize.formatSize(resource.sizeInBytes) + ')' : ''),
iconPath: icon ? { dark: icon, light: icon } : undefined,
iconPath,
};
}
@@ -25,6 +25,7 @@ interface IIcon {
sizes: { width: number; height: number }[];
}
export type ParsedMcpIcons = IIcon[];
export type StoredMcpIcons = Dto<IIcon>[];
@@ -68,8 +69,8 @@ function validateIcon(icon: MCP.Icon, launch: McpServerLaunch, logger: ILogger):
return;
}
export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunch, logger: ILogger): StoredMcpIcons {
const result: StoredMcpIcons = [];
export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunch, logger: ILogger): ParsedMcpIcons {
const result: ParsedMcpIcons = [];
for (const icon of icons.icons || []) {
const uri = validateIcon(icon, launch, logger);
if (!uri) {
@@ -92,9 +93,12 @@ export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunc
}
export class McpIcons implements IMcpIcons {
public static fromStored(icons: StoredMcpIcons | undefined) {
return new McpIcons(icons?.map(i => ({ src: URI.revive(i.src), sizes: i.sizes })) || []);
return McpIcons.fromParsed(icons?.map(i => ({ src: URI.revive(i.src), sizes: i.sizes })));
}
public static fromParsed(icons: ParsedMcpIcons | undefined) {
return new McpIcons(icons || []);
}
protected constructor(private readonly _icons: IIcon[]) { }
@@ -489,7 +489,7 @@ export class McpServer extends Disposable implements IMcpServer {
return new AsyncIterableProducer<IMcpResource[]>(async emitter => {
await McpServer.callOn(this, async (handler) => {
for await (const resource of handler.listResourcesIterable({}, cts.token)) {
emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromStored(this._parseIcons(r)))));
emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromParsed(this._parseIcons(r)))));
if (cts.token.isCancellationRequested) {
return;
}
@@ -501,7 +501,7 @@ export class McpServer extends Disposable implements IMcpServer {
public resourceTemplates(token?: CancellationToken): Promise<IMcpResourceTemplate[]> {
return McpServer.callOn(this, async (handler) => {
const templates = await handler.listResourceTemplates({}, token);
return templates.map(t => new McpResourceTemplate(this, t));
return templates.map(t => new McpResourceTemplate(this, t, McpIcons.fromParsed(this._parseIcons(t))));
}, token);
}
@@ -1074,6 +1074,7 @@ class McpResourceTemplate implements IMcpResourceTemplate {
constructor(
private readonly _server: McpServer,
private readonly _definition: MCP.ResourceTemplate,
public readonly icons: IMcpIcons,
) {
this.name = _definition.name;
this.description = _definition.description;
@@ -356,6 +356,7 @@ export interface IMcpResourceTemplate {
readonly description?: string;
readonly mimeType?: string;
readonly template: UriTemplate;
readonly icons: IMcpIcons;
/** Gets string completions for the given template part. */
complete(templatePart: string, prefix: string, alreadyResolved: Record<string, string | string[]>, token: CancellationToken): Promise<string[]>;
@@ -635,7 +635,7 @@ export namespace MCP {/* JSON-RPC types */
/**
* A template description for resources available on the server.
*/
export interface ResourceTemplate extends BaseMetadata {
export interface ResourceTemplate extends BaseMetadata, Icons {
/**
* A URI template (according to RFC 6570) that can be used to construct resource URIs.
*
@@ -626,8 +626,10 @@ export async function renderReleaseNotesMarkdown(
sanitizerConfig: {
allowRelativeMediaPaths: true,
allowedLinkProtocols: {
override: [Schemas.http, Schemas.https, Schemas.command]
}
override: [Schemas.http, Schemas.https, Schemas.command, Schemas.codeSetting]
},
allowedTags: { augment: ['nav', 'svg', 'path'] },
allowedAttributes: { augment: ['aria-role', 'viewBox', 'fill', 'xmlns', 'd'] }
},
markedExtensions: [{
renderer: {
@@ -10,12 +10,12 @@
<p>text</p>
</blockquote>
<div class="toc-nav-layout">
<nav id="toc-nav">
<div>In this update</div>
<ul>
<li><a href="#chat">test</a></li>
</ul>
</nav>
<div class="notes-main">
<h2 id="test">Test</h2>
@@ -0,0 +1,7 @@
<p>Here is a setting: <code tabindex="0"><a aria-role="button" title="View or change setting" class="codesetting" href="code-setting://editor.wordWrap/on"><svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" height="14" width="14"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"></path></svg>
<span class="separator"></span>
<span class="setting-name">editor.wordWrap</span>
</a></code> and another <code tabindex="0"><a aria-role="button" title="View or change setting" class="codesetting" href="code-setting://editor.wordWrap/off"><svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" height="14" width="14"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"></path></svg>
<span class="separator"></span>
<span class="setting-name">editor.wordWrap</span>
</a></code></p>
@@ -10,7 +10,10 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
import { SimpleSettingRenderer } from '../../../markdown/browser/markdownSettingRenderer.js';
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
import { renderReleaseNotesMarkdown } from '../../browser/releaseNotesEditor.js';
import { URI } from '../../../../../base/common/uri.js';
import { Emitter } from '../../../../../base/common/event.js';
suite('Release notes renderer', () => {
@@ -55,4 +58,48 @@ Navigation End -->
const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer));
await assertSnapshot(result.toString());
});
test('Should render code settings', async () => {
// Stub preferences service with a known setting so the SimpleSettingRenderer treats it as valid
const testSettingId = 'editor.wordWrap';
instantiationService.stub(IPreferencesService, <Partial<IPreferencesService>>{
_serviceBrand: undefined,
onDidDefaultSettingsContentChanged: new Emitter<URI>().event,
userSettingsResource: undefined as any,
workspaceSettingsResource: null,
getFolderSettingsResource: () => null,
createPreferencesEditorModel: async () => null,
getDefaultSettingsContent: () => undefined,
hasDefaultSettingsContent: () => false,
createSettings2EditorModel: () => { throw new Error('not needed'); },
openPreferences: async () => undefined,
openRawDefaultSettings: async () => undefined,
openSettings: async () => undefined,
openApplicationSettings: async () => undefined,
openUserSettings: async () => undefined,
openRemoteSettings: async () => undefined,
openWorkspaceSettings: async () => undefined,
openFolderSettings: async () => undefined,
openGlobalKeybindingSettings: async () => undefined,
openDefaultKeybindingsFile: async () => undefined,
openLanguageSpecificSettings: async () => undefined,
getEditableSettingsURI: async () => null,
getSetting: (id: string) => {
if (id === testSettingId) {
// Provide the minimal fields accessed by SimpleSettingRenderer
return <any>{
key: testSettingId,
value: 'off',
type: 'string'
};
}
return undefined;
},
createSplitJsonEditorInput: () => { throw new Error('not needed'); }
});
const content = `Here is a setting: \`setting(${testSettingId}:on)\` and another \`setting(${testSettingId}:off)\``;
const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer));
await assertSnapshot(result.toString());
});
});
@@ -55,10 +55,6 @@ export class WorkbenchMcpGalleryManifestService extends McpGalleryManifestServic
}
private async doGetMcpGalleryManifest(): Promise<void> {
if (this.productService.quality === 'stable') {
return;
}
await this.getAndUpdateMcpGalleryManifest();
this._register(this.configurationService.onDidChangeConfiguration(e => {
+7 -7
View File
@@ -1666,7 +1666,7 @@ declare module 'vscode' {
/**
* An {@link Event} which fires upon cancellation.
*/
onCancellationRequested: Event<any>;
readonly onCancellationRequested: Event<any>;
}
/**
@@ -8608,7 +8608,7 @@ declare module 'vscode' {
/**
* Fires when a secret is stored or deleted.
*/
onDidChange: Event<SecretStorageChangeEvent>;
readonly onDidChange: Event<SecretStorageChangeEvent>;
}
/**
@@ -13074,7 +13074,7 @@ declare module 'vscode' {
* (Examples include: an explicit call to {@link QuickInput.hide},
* the user pressing Esc, some other input UI opening, etc.)
*/
onDidHide: Event<void>;
readonly onDidHide: Event<void>;
/**
* Dispose of this input UI and any associated resources. If it is still
@@ -18225,7 +18225,7 @@ declare module 'vscode' {
* Fired when a user has changed whether this is a default profile. The
* event contains the new value of {@link isDefault}
*/
onDidChangeDefault: Event<boolean>;
readonly onDidChangeDefault: Event<boolean>;
/**
* Whether this profile supports continuous running of requests. If so,
@@ -18604,7 +18604,7 @@ declare module 'vscode' {
* An event fired when the editor is no longer interested in data
* associated with the test run.
*/
onDidDispose: Event<void>;
readonly onDidDispose: Event<void>;
}
/**
@@ -19680,7 +19680,7 @@ declare module 'vscode' {
* The passed {@link ChatResultFeedback.result result} is guaranteed to have the same properties as the result that was
* previously returned from this chat participant's handler.
*/
onDidReceiveFeedback: Event<ChatResultFeedback>;
readonly onDidReceiveFeedback: Event<ChatResultFeedback>;
/**
* Dispose this participant and free resources.
@@ -20723,7 +20723,7 @@ declare module 'vscode' {
/**
* An event that fires when access information changes.
*/
onDidChange: Event<void>;
readonly onDidChange: Event<void>;
/**
* Checks if a request can be made to a language model.
+23 -42
View File
@@ -12,7 +12,7 @@ import * as minimist from 'minimist';
import * as vscodetest from '@vscode/test-electron';
import fetch from 'node-fetch';
import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion, ApplicationOptions } from '../../automation';
import { retry, timeout } from './utils';
import { retry } from './utils';
import { setup as setupDataLossTests } from './areas/workbench/data-loss.test';
import { setup as setupPreferencesTests } from './areas/preferences/preferences.test';
@@ -102,7 +102,7 @@ function createLogger(): Logger {
}
// Prepare logs rot path
fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 });
fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 });
fs.mkdirSync(logsRootPath, { recursive: true });
// Always log to log file
@@ -117,19 +117,6 @@ try {
logger.log(`Error enabling graceful-fs: ${error}`);
}
const testDataPath = path.join(os.tmpdir(), 'vscsmoke');
if (fs.existsSync(testDataPath)) {
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
}
fs.mkdirSync(testDataPath, { recursive: true });
process.once('exit', () => {
try {
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
} catch {
// noop
}
});
function getTestTypeSuffix(): string {
if (opts.web) {
return 'browser';
@@ -140,8 +127,21 @@ function getTestTypeSuffix(): string {
}
}
const testDataPath = path.join(os.tmpdir(), `vscsmoke-${getTestTypeSuffix()}`);
if (fs.existsSync(testDataPath)) {
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 });
}
fs.mkdirSync(testDataPath, { recursive: true });
process.once('exit', () => {
try {
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 });
} catch {
// noop
}
});
const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express';
const workspacePath = path.join(testDataPath, `vscode-smoketest-express-${getTestTypeSuffix()}`);
const workspacePath = path.join(testDataPath, `vscode-smoketest-express`);
const extensionsPath = path.join(testDataPath, 'extensions-dir');
fs.mkdirSync(extensionsPath, { recursive: true });
@@ -245,7 +245,7 @@ const userDataDir = path.join(testDataPath, 'd');
async function setupRepository(): Promise<void> {
if (opts['test-repo']) {
logger.log('Copying test project repository:', opts['test-repo']);
fs.rmSync(workspacePath, { recursive: true, force: true, maxRetries: 10 });
fs.rmSync(workspacePath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 });
// not platform friendly
if (process.platform === 'win32') {
cp.execSync(`xcopy /E "${opts['test-repo']}" "${workspacePath}"\\*`);
@@ -314,15 +314,9 @@ async function ensureStableCode(): Promise<void> {
},
error: error => logger.log(`download stable code error: ${error}`)
}
}), 'download stable code', logger), 1000, 3, () => new Promise<void>((resolve, reject) => {
fs.rm(stableCodeDestination, { recursive: true, force: true, maxRetries: 10 }, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
}));
}), 'download stable code', logger), 1000, 3, async () => {
fs.rmSync(stableCodeDestination, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 });
});
if (process.platform === 'darwin') {
// Visual Studio Code.app/Contents/MacOS/Electron
@@ -388,22 +382,9 @@ before(async function () {
// After main suite (after all tests)
after(async function () {
try {
let deleted = false;
await measureAndLog(() => Promise.race([
new Promise<void>((resolve, reject) => fs.rm(testDataPath, { recursive: true, force: true, maxRetries: 10 }, error => {
if (error) {
reject(error);
} else {
deleted = true;
resolve();
}
})),
timeout(30000).then(() => {
if (!deleted) {
throw new Error('giving up after 30s');
}
})
]), 'rimraf(testDataPath)', logger);
await measureAndLog(async () => {
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 });
}, 'rimraf(testDataPath)', logger);
} catch (error) {
logger.log(`Unable to delete smoke test workspace: ${error}. This indicates some process is locking the workspace folder.`);
}