diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index af0becdc6307d..a100345f459b1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -137,3 +137,6 @@ function f(x: number, y: string): void { } - When adding file watching, prefer correlated file watchers (via fileService.createWatcher) to shared ones. - When adding tooltips to UI elements, prefer the use of IHoverService service. - Do not duplicate code. Always look for existing utility functions, helpers, or patterns in the codebase before implementing new functionality. Reuse and extend existing code whenever possible. + +## Learnings +- Minimize the amount of assertions in tests. Prefer one snapshot-style `assert.deepStrictEqual` over multiple precise assertions, as they are much more difficult to understand and to update. diff --git a/.github/instructions/learnings.instructions.md b/.github/instructions/learnings.instructions.md index 78a9f52a06e6e..9358a943e3d94 100644 --- a/.github/instructions/learnings.instructions.md +++ b/.github/instructions/learnings.instructions.md @@ -8,14 +8,13 @@ It is a meta-instruction file. Structure of learnings: * Each instruction file has a "Learnings" section. -* Each learning has a counter that indicates how often that learning was useful (initially 1). * Each learning has a 1-4 sentences description of the learning. Example: ```markdown ## Learnings -* Prefer `const` over `let` whenever possible (1) -* Avoid `any` type (3) +* Prefer `const` over `let` whenever possible +* Avoid `any` type ``` When the user tells you "learn!", you should: @@ -23,10 +22,7 @@ When the user tells you "learn!", you should: * identify the problem that you created * identify why it was a problem * identify how you were told to fix it/how the user fixed it + * reflect over it, maybe it can be generalized? Avoid too specific learnings. * create a learning (1-4 sentences) from that * Write this out to the user and reflect over these sentences * then, add the reflected learning to the "Learnings" section of the most appropriate instruction file - - - Important: Whenever a learning was really useful, increase the counter!! - When a learning was not useful and just caused more problems, decrease the counter. diff --git a/build/azure-pipelines/common/sanity-tests.yml b/build/azure-pipelines/common/sanity-tests.yml index 4bb3b7e44a2eb..d6e806594a5ba 100644 --- a/build/azure-pipelines/common/sanity-tests.yml +++ b/build/azure-pipelines/common/sanity-tests.yml @@ -1,33 +1,37 @@ parameters: - - name: commit + - name: name type: string - - name: quality + - name: displayName type: string - name: poolName type: string - name: os type: string + - name: args + type: string + default: "" jobs: - - job: ${{ parameters.os }} - displayName: ${{ parameters.os }} Sanity Tests + - job: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} pool: name: ${{ parameters.poolName }} os: ${{ parameters.os }} timeoutInMinutes: 30 variables: SANITY_TEST_LOGS: $(Build.SourcesDirectory)/.build/sanity-test-logs + LOG_FILE: $(SANITY_TEST_LOGS)/results.xml templateContext: outputs: - output: pipelineArtifact targetPath: $(SANITY_TEST_LOGS) - artifactName: sanity-test-logs-${{ lower(parameters.os) }}-$(System.JobAttempt) - displayName: Publish Sanity Test Logs + artifactName: sanity-test-logs-${{ parameters.name }}-$(System.JobAttempt) + displayName: Sanity Tests Logs sbomEnabled: false isProduction: false condition: succeededOrFailed() steps: - - checkout: self + - template: ./checkout.yml@self - task: NodeTool@0 inputs: @@ -35,27 +39,34 @@ jobs: versionFilePath: .nvmrc displayName: Install Node.js - - ${{ if eq(parameters.os, 'windows') }}: - - script: | - mkdir "$(SANITY_TEST_LOGS)" - displayName: Create Logs Directory + - bash: | + npm config set registry "$(NPM_REGISTRY)" + echo "##vso[task.setvariable variable=NPMRC_PATH]$(npm config get userconfig)" + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Configure NPM Registry - - ${{ else }}: - - script: | - mkdir -p "$(SANITY_TEST_LOGS)" - displayName: Create Logs Directory + - task: npmAuthenticate@0 + inputs: + workingFile: $(NPMRC_PATH) + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Authenticate with NPM Registry - - script: npm install + - script: npm ci + workingDirectory: ./test/sanity displayName: Install Dependencies - workingDirectory: $(Build.SourcesDirectory)/test/sanity - - script: npm run sanity-test -- --commit ${{ parameters.commit }} --quality ${{ parameters.quality }} --verbose --test-results $(SANITY_TEST_LOGS)/sanity-test.xml + - script: npm run compile + workingDirectory: ./test/sanity + displayName: Compile Sanity Tests + + - script: npm run start -- -c $(BUILD_COMMIT) -q $(BUILD_QUALITY) -t $(LOG_FILE) -v ${{ parameters.args }} + workingDirectory: ./test/sanity displayName: Run Sanity Tests - task: PublishTestResults@2 inputs: testResultsFormat: JUnit - testResultsFiles: $(SANITY_TEST_LOGS)/sanity-test.xml - testRunTitle: ${{ parameters.os }} Sanity Tests + testResultsFiles: $(LOG_FILE) + testRunTitle: ${{ parameters.displayName }} condition: succeededOrFailed() displayName: Publish Test Results diff --git a/build/azure-pipelines/product-sanity-tests.yml b/build/azure-pipelines/product-sanity-tests.yml index 79406964f3738..f9000fcf4576b 100644 --- a/build/azure-pipelines/product-sanity-tests.yml +++ b/build/azure-pipelines/product-sanity-tests.yml @@ -3,11 +3,12 @@ pr: none trigger: none parameters: - - name: commit - displayName: Commit + - name: BUILD_COMMIT + displayName: Published Build Commit type: string - - name: quality - displayName: Quality + + - name: BUILD_QUALITY + displayName: Published Build Quality type: string default: insider values: @@ -15,13 +16,24 @@ parameters: - insider - stable + - name: NPM_REGISTRY + displayName: Custom NPM Registry URL + type: string + default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" + variables: - name: skipComponentGovernanceDetection value: true - name: Codeql.SkipTaskAutoInjection value: true + - name: BUILD_COMMIT + value: ${{ parameters.BUILD_COMMIT }} + - name: BUILD_QUALITY + value: ${{ parameters.BUILD_QUALITY }} + - name: NPM_REGISTRY + value: ${{ parameters.NPM_REGISTRY }} -name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.quality }})" +name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.BUILD_QUALITY }} ${{ parameters.BUILD_COMMIT }})" resources: repositories: @@ -47,25 +59,35 @@ extends: sourceAnalysisPool: 1es-windows-2022-x64 createAdoIssuesForJustificationsForDisablement: false stages: - - stage: SanityTests + - stage: sanity_tests + displayName: Run Sanity Tests jobs: - template: build/azure-pipelines/common/sanity-tests.yml@self parameters: - commit: ${{ parameters.commit }} - quality: ${{ parameters.quality }} + name: Windows_x64 + displayName: Windows x64 Sanity Tests + poolName: 1es-windows-2022-x64 + os: windows + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: Windows_arm64 + displayName: Windows arm64 Sanity Tests (no runtime) poolName: 1es-windows-2022-x64 os: windows + args: --no-runtime-check --grep "win32-arm64" - template: build/azure-pipelines/common/sanity-tests.yml@self parameters: - commit: ${{ parameters.commit }} - quality: ${{ parameters.quality }} - poolName: 1es-ubuntu-22.04-x64 - os: linux + name: macOS_x64 + displayName: MacOS x64 Sanity Tests (no runtime) + poolName: AcesShared + os: macOS + args: --no-runtime-check --grep "darwin-x64" - template: build/azure-pipelines/common/sanity-tests.yml@self parameters: - commit: ${{ parameters.commit }} - quality: ${{ parameters.quality }} + name: macOS_arm64 + displayName: MacOS arm64 Sanity Tests poolName: AcesShared os: macOS diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index d8ae877716651..91d4cbc16d5e6 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -7,7 +7,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange, Worktree } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange, Worktree, RepositoryKind } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -78,6 +78,7 @@ export class ApiRepository implements Repository { readonly rootUri: Uri; readonly inputBox: InputBox; + readonly kind: RepositoryKind; readonly state: RepositoryState; readonly ui: RepositoryUIState; @@ -87,6 +88,7 @@ export class ApiRepository implements Repository { constructor(repository: BaseRepository) { this.#repository = repository; + this.kind = this.#repository.kind; this.rootUri = Uri.file(this.#repository.root); this.inputBox = new ApiInputBox(this.#repository.inputBox); this.state = new ApiRepositoryState(this.#repository); diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 18b49fcb268b2..1e3009499f4cf 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -126,6 +126,8 @@ export interface DiffChange extends Change { readonly deletions: number; } +export type RepositoryKind = 'repository' | 'submodule' | 'worktree'; + export interface RepositoryState { readonly HEAD: Branch | undefined; readonly refs: Ref[]; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 8480e6d361764..b528a89cff06d 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -11,7 +11,7 @@ import picomatch from 'picomatch'; import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; -import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git'; +import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, RepositoryKind, Status } from './api/git'; import { AutoFetcher } from './autofetch'; import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, sequentialize, throttle } from './decorators'; @@ -870,7 +870,7 @@ export class Repository implements Disposable { return this.repository.dotGit; } - get kind(): 'repository' | 'submodule' | 'worktree' { + get kind(): RepositoryKind { return this.repository.kind; } diff --git a/src/vs/base/browser/domStylesheets.ts b/src/vs/base/browser/domStylesheets.ts index 1e34173680ed3..c338502d541da 100644 --- a/src/vs/base/browser/domStylesheets.ts +++ b/src/vs/base/browser/domStylesheets.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, toDisposable, IDisposable } from '../common/lifecycle.js'; +import { DisposableStore, toDisposable, IDisposable, Disposable } from '../common/lifecycle.js'; import { autorun, IObservable } from '../common/observable.js'; import { isFirefox } from './browser.js'; import { getWindows, sharedMutationObserver } from './dom.js'; @@ -15,35 +15,27 @@ export function isGlobalStylesheet(node: Node): boolean { return globalStylesheets.has(node as HTMLStyleElement); } -/** - * A version of createStyleSheet which has a unified API to initialize/set the style content. - */ -export function createStyleSheet2(): WrappedStyleElement { - return new WrappedStyleElement(); -} - -class WrappedStyleElement { +class WrappedStyleElement extends Disposable { private _currentCssStyle = ''; private _styleSheet: HTMLStyleElement | undefined = undefined; - public setStyle(cssStyle: string): void { + setStyle(cssStyle: string): void { if (cssStyle === this._currentCssStyle) { return; } this._currentCssStyle = cssStyle; if (!this._styleSheet) { - this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.textContent = cssStyle); + this._styleSheet = createStyleSheet(mainWindow.document.head, s => s.textContent = cssStyle, this._store); } else { this._styleSheet.textContent = cssStyle; } } - public dispose(): void { - if (this._styleSheet) { - this._styleSheet.remove(); - this._styleSheet = undefined; - } + override dispose(): void { + super.dispose(); + + this._styleSheet = undefined; } } @@ -121,12 +113,10 @@ function getSharedStyleSheet(): HTMLStyleElement { function getDynamicStyleSheetRules(style: HTMLStyleElement) { if (style?.sheet?.rules) { - // Chrome, IE - return style.sheet.rules; + return style.sheet.rules; // Chrome, IE } if (style?.sheet?.cssRules) { - // FF - return style.sheet.cssRules; + return style.sheet.cssRules; // FF } return []; } @@ -174,7 +164,7 @@ function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule { export function createStyleSheetFromObservable(css: IObservable): IDisposable { const store = new DisposableStore(); - const w = store.add(createStyleSheet2()); + const w = store.add(new WrappedStyleElement()); store.add(autorun(reader => { w.setStyle(css.read(reader)); })); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 95af648f724c4..25724438958d4 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -753,6 +753,18 @@ export enum InlineCompletionTriggerKind { Explicit = 1, } +/** + * Arbitrary data that the provider can pass when firing {@link InlineCompletionsProvider.onDidChangeInlineCompletions}. + * This data is passed back to the provider in {@link InlineCompletionContext.changeHint}. + */ +export interface IInlineCompletionChangeHint { + /** + * Arbitrary data that the provider can use to identify what triggered the change. + * This data must be JSON serializable. + */ + readonly data?: unknown; +} + export interface InlineCompletionContext { /** @@ -775,6 +787,12 @@ export interface InlineCompletionContext { readonly includeInlineCompletions: boolean; readonly requestIssuedDateTime: number; readonly earliestShownDateTime: number; + + /** + * The change hint that was passed to {@link InlineCompletionsProvider.onDidChangeInlineCompletions}. + * Only set if this request was triggered by such an event. + */ + readonly changeHint?: IInlineCompletionChangeHint; } export interface IInlineCompletionModelInfo { @@ -946,7 +964,12 @@ export interface InlineCompletionsProvider; + /** + * Fired when the provider wants to trigger a new completion request. + * The event can pass a {@link IInlineCompletionChangeHint} which will be + * included in the {@link InlineCompletionContext} of the subsequent request. + */ + onDidChangeInlineCompletions?: Event; /** * Only used for {@link yieldsToGroupIds}. diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 8c3e791e0897c..97392ce8d7e76 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -6,7 +6,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { asyncTransaction, transaction } from '../../../../../base/common/observable.js'; import { splitLines } from '../../../../../base/common/strings.js'; -import { vBoolean, vObj, vOptionalProp, vString, vUndefined, vUnion, vWithJsonSchemaRef } from '../../../../../base/common/validation.js'; +import { vBoolean, vObj, vOptionalProp, vString, vUnchecked, vUndefined, vUnion, vWithJsonSchemaRef } from '../../../../../base/common/validation.js'; import * as nls from '../../../../../nls.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js'; import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -80,6 +80,7 @@ const argsValidator = vUnion(vObj({ showNoResultNotification: vOptionalProp(vBoolean()), providerId: vOptionalProp(vWithJsonSchemaRef(providerIdSchemaUri, vString())), explicit: vOptionalProp(vBoolean()), + changeHintData: vOptionalProp(vUnchecked()), }), vUndefined()); export class TriggerInlineSuggestionAction extends EditorAction { @@ -118,6 +119,7 @@ export class TriggerInlineSuggestionAction extends EditorAction { await controller?.model.get()?.trigger(tx, { provider: provider, explicit: validatedArgs?.explicit ?? true, + changeHint: validatedArgs?.changeHintData ? { data: validatedArgs.changeHintData } : undefined, }); controller?.playAccessibilitySignal(tx); }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 87928882cf50b..222e76c5e513c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -25,7 +25,7 @@ import { Selection } from '../../../../common/core/selection.js'; import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js'; import { TextLength } from '../../../../common/core/text/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; -import { InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider, InlineCompletionCommand } from '../../../../common/languages.js'; +import { IInlineCompletionChangeHint, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider, InlineCompletionCommand } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; @@ -61,7 +61,7 @@ export class InlineCompletionsModel extends Disposable { private readonly _forceUpdateExplicitlySignal = observableSignal(this); private readonly _noDelaySignal = observableSignal(this); - private readonly _fetchSpecificProviderSignal = observableSignal(this); + private readonly _fetchSpecificProviderSignal = observableSignal<{ provider: InlineCompletionsProvider; changeHint?: IInlineCompletionChangeHint } | undefined>(this); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); @@ -215,7 +215,7 @@ export class InlineCompletionsModel extends Disposable { return; } - store.add(provider.onDidChangeInlineCompletions(() => { + store.add(provider.onDidChangeInlineCompletions(changeHint => { if (!this._enabled.get()) { return; } @@ -240,7 +240,7 @@ export class InlineCompletionsModel extends Disposable { } transaction(tx => { - this._fetchSpecificProviderSignal.trigger(tx, provider); + this._fetchSpecificProviderSignal.trigger(tx, { provider, changeHint: changeHint ?? undefined }); this.trigger(tx); }); @@ -334,6 +334,7 @@ export class InlineCompletionsModel extends Disposable { onlyRequestInlineEdits: false, shouldDebounce: true, provider: undefined as InlineCompletionsProvider | undefined, + changeHint: undefined as IInlineCompletionChangeHint | undefined, textChange: false, changeReason: '', }), @@ -354,7 +355,8 @@ export class InlineCompletionsModel extends Disposable { } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { changeSummary.onlyRequestInlineEdits = true; } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { - changeSummary.provider = ctx.change; + changeSummary.provider = ctx.change?.provider; + changeSummary.changeHint = ctx.change?.changeHint; } return true; }, @@ -424,6 +426,7 @@ export class InlineCompletionsModel extends Disposable { includeInlineEdits: this._inlineEditsEnabled.read(reader), requestIssuedDateTime: requestInfo.startTime, earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.read(undefined) ? 0 : this._minShowDelay.read(undefined)), + changeHint: changeSummary.changeHint, }; if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) { @@ -474,7 +477,7 @@ export class InlineCompletionsModel extends Disposable { return availableProviders; } - public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean } = {}): Promise { + public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean; changeHint?: IInlineCompletionChangeHint } = {}): Promise { subtransaction(tx, tx => { if (options.onlyFetchInlineEdits) { this._onlyRequestInlineEditsSignal.trigger(tx); @@ -489,7 +492,7 @@ export class InlineCompletionsModel extends Disposable { this._forceUpdateExplicitlySignal.trigger(tx); } if (options.provider) { - this._fetchSpecificProviderSignal.trigger(tx, options.provider); + this._fetchSpecificProviderSignal.trigger(tx, { provider: options.provider, changeHint: options.changeHint }); } }); await this._fetchInlineCompletionsPromise.get(); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts index 0f26af1bbb8e1..c9909d4534605 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts @@ -761,4 +761,80 @@ suite('Multi Cursor Support', () => { } ); }); + + test('Change hint is passed from onDidChange to provideInlineCompletions', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider, inlineSuggest: { enabled: true } }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('foo'); + provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); + model.triggerExplicitly(); + await timeout(1000); + + const firstCallHistory = provider.getAndClearCallHistory(); + assert.strictEqual(firstCallHistory.length, 1); + assert.strictEqual((firstCallHistory[0] as { changeHint?: unknown }).changeHint, undefined); + + // Change cursor position to avoid cache hit + editor.setPosition({ lineNumber: 1, column: 3 }); + + + const changeHintData = { reason: 'modelUpdated', version: 42 }; + provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 4) }); + provider.fireOnDidChange({ data: changeHintData }); + await timeout(1000); + + const secondCallHistory = provider.getAndClearCallHistory(); + + assert.deepStrictEqual( + secondCallHistory, + [{ + changeHint: { + data: { + reason: 'modelUpdated', + version: 42, + } + }, + position: '(1,3)', + text: 'foo', + triggerKind: 0 + }] + ); + } + ); + }); + + test('Change hint is undefined when onDidChange fires without hint', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider, inlineSuggest: { enabled: true } }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('foo'); + provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); + model.triggerExplicitly(); + await timeout(1000); + + provider.getAndClearCallHistory(); + + // Change cursor position to avoid cache hit + editor.setPosition({ lineNumber: 1, column: 3 }); + + provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 4) }); + provider.fireOnDidChange(); + await timeout(1000); + + const callHistory = provider.getAndClearCallHistory(); + + assert.deepStrictEqual( + callHistory, + [{ + position: '(1,3)', + text: 'foo', + triggerKind: 0 + }] + ); + } + ); + }); }); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 2d9a1a5e2f581..bbd453dcaf5aa 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -9,7 +9,7 @@ import { Disposable, DisposableStore } from '../../../../../base/common/lifecycl import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js'; import { Position } from '../../../../common/core/position.js'; import { ITextModel } from '../../../../common/model.js'; -import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js'; +import { IInlineCompletionChangeHint, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js'; import { autorun, derived } from '../../../../../base/common/observable.js'; @@ -27,7 +27,7 @@ import { PositionOffsetTransformer } from '../../../../common/core/text/position import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js'; import { IBulkEditService } from '../../../../browser/services/bulkEditService.js'; import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; -import { Event } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; @@ -36,6 +36,9 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider private callHistory = new Array(); private calledTwiceIn50Ms = false; + private readonly _onDidChangeEmitter = new Emitter(); + public readonly onDidChangeInlineCompletions: Event = this._onDidChangeEmitter.event; + constructor( public readonly enableForwardStability = false, ) { } @@ -62,6 +65,13 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider } } + /** + * Fire an onDidChange event with an optional change hint. + */ + public fireOnDidChange(changeHint?: IInlineCompletionChangeHint): void { + this._onDidChangeEmitter.fire(changeHint); + } + private lastTimeMs: number | undefined = undefined; async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise { @@ -74,7 +84,8 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider this.callHistory.push({ position: position.toString(), triggerKind: context.triggerKind, - text: model.getValue() + text: model.getValue(), + ...(context.changeHint !== undefined ? { changeHint: context.changeHint } : {}), }); const result = new Array(); for (const v of this.returnValue) { diff --git a/src/vs/editor/test/browser/editorTestServices.ts b/src/vs/editor/test/browser/editorTestServices.ts index 4567ca518379a..38594483bac3e 100644 --- a/src/vs/editor/test/browser/editorTestServices.ts +++ b/src/vs/editor/test/browser/editorTestServices.ts @@ -19,7 +19,8 @@ export class TestCodeEditorService extends AbstractCodeEditorService { } getActiveCodeEditor(): ICodeEditor | null { - return null; + const editors = this.listCodeEditors(); + return editors.length > 0 ? editors[editors.length - 1] : null; } public lastInput?: IResourceEditorInput; override openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index bdd5231f202b4..a85f63bf973e6 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7494,6 +7494,18 @@ declare namespace monaco.languages { Explicit = 1 } + /** + * Arbitrary data that the provider can pass when firing {@link InlineCompletionsProvider.onDidChangeInlineCompletions}. + * This data is passed back to the provider in {@link InlineCompletionContext.changeHint}. + */ + export interface IInlineCompletionChangeHint { + /** + * Arbitrary data that the provider can use to identify what triggered the change. + * This data must be JSON serializable. + */ + readonly data?: unknown; + } + export interface InlineCompletionContext { /** * How the completion was triggered. @@ -7504,6 +7516,11 @@ declare namespace monaco.languages { readonly includeInlineCompletions: boolean; readonly requestIssuedDateTime: number; readonly earliestShownDateTime: number; + /** + * The change hint that was passed to {@link InlineCompletionsProvider.onDidChangeInlineCompletions}. + * Only set if this request was triggered by such an event. + */ + readonly changeHint?: IInlineCompletionChangeHint; } export interface IInlineCompletionModelInfo { @@ -7648,7 +7665,12 @@ declare namespace monaco.languages { * Will be called when a completions list is no longer in use and can be garbage-collected. */ disposeInlineCompletions(completions: T, reason: InlineCompletionsDisposeReason): void; - onDidChangeInlineCompletions?: IEvent; + /** + * Fired when the provider wants to trigger a new completion request. + * The event can pass a {@link IInlineCompletionChangeHint} which will be + * included in the {@link InlineCompletionContext} of the subsequent request. + */ + onDidChangeInlineCompletions?: IEvent; /** * Only used for {@link yieldsToGroupIds}. * Multiple providers can have the same group id. diff --git a/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts b/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts index 8a31973e9d1ad..1ebcf2728160c 100644 --- a/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts +++ b/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts @@ -15,7 +15,7 @@ import { ThemeTypeSelector } from '../common/theme.js'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { coalesce } from '../../../base/common/arrays.js'; import { getAllWindowsExcludingOffscreen } from '../../windows/electron-main/windows.js'; -import { ILogService } from '../../log/common/log.js'; +import { ILogService, LogLevel } from '../../log/common/log.js'; import { IThemeMainService } from './themeMainService.js'; // These default colors match our default themes @@ -31,12 +31,20 @@ const THEME_BG_STORAGE_KEY = 'themeBackground'; const THEME_WINDOW_SPLASH_KEY = 'windowSplash'; const THEME_WINDOW_SPLASH_OVERRIDE_KEY = 'windowSplashWorkspaceOverride'; -const AUXILIARYBAR_DEFAULT_VISIBILITY = 'workbench.secondarySideBar.defaultVisibility'; +class Setting { + constructor(public readonly key: string, public readonly defaultValue: T) { + } + getValue(configurationService: IConfigurationService): T { + return configurationService.getValue(this.key) ?? this.defaultValue; + } +} -namespace ThemeSettings { - export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme'; - export const DETECT_HC = 'window.autoDetectHighContrast'; - export const SYSTEM_COLOR_THEME = 'window.systemColorTheme'; +// in the main process, defaults are not known to the configuration service, so we need to define them here +namespace Setting { + export const DETECT_COLOR_SCHEME = new Setting('window.autoDetectColorScheme', false); + export const DETECT_HC = new Setting('window.autoDetectHighContrast', true); + export const SYSTEM_COLOR_THEME = new Setting<'default' | 'auto' | 'light' | 'dark'>('window.systemColorTheme', 'default'); + export const AUXILIARYBAR_DEFAULT_VISIBILITY = new Setting<'hidden' | 'visibleInWorkspace' | 'visible' | 'maximizedInWorkspace' | 'maximized'>('workbench.secondarySideBar.defaultVisibility', 'visibleInWorkspace'); } interface IPartSplashOverrideWorkspaces { @@ -76,22 +84,38 @@ export class ThemeMainService extends Disposable implements IThemeMainService { // System Theme if (!isLinux) { this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ThemeSettings.SYSTEM_COLOR_THEME) || e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) { + if (e.affectsConfiguration(Setting.SYSTEM_COLOR_THEME.key) || e.affectsConfiguration(Setting.DETECT_COLOR_SCHEME.key)) { this.updateSystemColorTheme(); + this.logThemeSettings(); } })); } this.updateSystemColorTheme(); + this.logThemeSettings(); // Color Scheme changes - this._register(Event.fromNodeEventEmitter(electron.nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme()))); + this._register(Event.fromNodeEventEmitter(electron.nativeTheme, 'updated')(() => { + this.logThemeSettings(); + this._onDidChangeColorScheme.fire(this.getColorScheme()); + })); + } + + private logThemeSettings(): void { + if (this.logService.getLevel() >= LogLevel.Debug) { + const logSetting = (setting: Setting) => `${setting.key}=${setting.getValue(this.configurationService)}`; + this.logService.debug(`[theme main service] ${logSetting(Setting.DETECT_COLOR_SCHEME)}, ${logSetting(Setting.DETECT_HC)}, ${logSetting(Setting.SYSTEM_COLOR_THEME)}`); + + const logProperty = (property: keyof Electron.NativeTheme) => `${String(property)}=${electron.nativeTheme[property]}`; + this.logService.debug(`[theme main service] electron.nativeTheme: ${logProperty('themeSource')}, ${logProperty('shouldUseDarkColors')}, ${logProperty('shouldUseHighContrastColors')}, ${logProperty('shouldUseInvertedColorScheme')}, ${logProperty('shouldUseDarkColorsForSystemIntegratedUI')} `); + this.logService.debug(`[theme main service] New color scheme: ${JSON.stringify(this.getColorScheme())}`); + } } private updateSystemColorTheme(): void { - if (isLinux || this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { + if (isLinux || Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) { electron.nativeTheme.themeSource = 'system'; // only with `system` we can detect the system color scheme } else { - switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>(ThemeSettings.SYSTEM_COLOR_THEME)) { + switch (Setting.SYSTEM_COLOR_THEME.getValue(this.configurationService)) { case 'dark': electron.nativeTheme.themeSource = 'dark'; break; @@ -145,11 +169,11 @@ export class ThemeMainService extends Disposable implements IThemeMainService { getPreferredBaseTheme(): ThemeTypeSelector | undefined { const colorScheme = this.getColorScheme(); - if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && colorScheme.highContrast) { + if (Setting.DETECT_HC.getValue(this.configurationService) && colorScheme.highContrast) { return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT; } - if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { + if (Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) { return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS; } @@ -334,7 +358,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } // Figure out auxiliary bar width based on workspace, configuration and overrides - const auxiliaryBarDefaultVisibility = this.configurationService.getValue(AUXILIARYBAR_DEFAULT_VISIBILITY) ?? 'visibleInWorkspace'; + const auxiliaryBarDefaultVisibility = Setting.AUXILIARYBAR_DEFAULT_VISIBILITY.getValue(this.configurationService); let auxiliaryBarWidth: number; if (workspace) { const auxiliaryBarVisible = override.layoutInfo.workspaces[workspace.id]?.auxiliaryBarVisible; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 62db354cc8e6b..18430fbfffa87 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -33,7 +33,7 @@ import * as callh from '../../contrib/callHierarchy/common/callHierarchy.js'; import * as search from '../../contrib/search/common/search.js'; import * as typeh from '../../contrib/typeHierarchy/common/typeHierarchy.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, IInlineCompletionModelInfoDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, IInlineCompletionChangeHintDto, IInlineCompletionModelInfoDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol.js'; import { InlineCompletionEndOfLifeReasonKind } from '../common/extHostTypes.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { DataChannelForwardingTelemetryService, forwardToChannelIf, isCopilotLikeExtension } from '../../../platform/dataChannel/browser/forwardingTelemetryService.js'; @@ -683,10 +683,10 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, provider); } - $emitInlineCompletionsChange(handle: number): void { + $emitInlineCompletionsChange(handle: number, changeHint: IInlineCompletionChangeHintDto | undefined): void { const obj = this._registrations.get(handle); if (obj instanceof ExtensionBackedInlineCompletionsProvider) { - obj._emitDidChange(); + obj._emitDidChange(changeHint); } } @@ -1290,8 +1290,8 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. class ExtensionBackedInlineCompletionsProvider extends Disposable implements languages.InlineCompletionsProvider { public readonly setModelId: ((modelId: string) => Promise) | undefined; - public readonly _onDidChangeEmitter = new Emitter(); - public readonly onDidChangeInlineCompletions: Event | undefined; + public readonly _onDidChangeEmitter = new Emitter(); + public readonly onDidChangeInlineCompletions: Event | undefined; public readonly _onDidChangeModelInfoEmitter = new Emitter(); public readonly onDidChangeModelInfo: Event | undefined; @@ -1334,9 +1334,9 @@ class ExtensionBackedInlineCompletionsProvider extends Disposable implements lan } } - public _emitDidChange() { + public _emitDidChange(changeHint: IInlineCompletionChangeHintDto | undefined) { if (this._supportsOnDidChange) { - this._onDidChangeEmitter.fire(); + this._onDidChangeEmitter.fire(changeHint); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5c4dd634a4b2a..000764668baab 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -491,6 +491,10 @@ export interface IInlineCompletionModelInfoDto { readonly currentModelId: string; } +export interface IInlineCompletionChangeHintDto { + readonly data?: unknown; +} + export interface MainThreadLanguageFeaturesShape extends IDisposable { $unregister(handle: number): void; $registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], label: string): void; @@ -537,7 +541,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { initialModelInfo: IInlineCompletionModelInfoDto | undefined, supportsOnDidChangeModelInfo: boolean, ): void; - $emitInlineCompletionsChange(handle: number): void; + $emitInlineCompletionsChange(handle: number, changeHint: IInlineCompletionChangeHintDto | undefined): void; $emitInlineCompletionModelInfoChange(handle: number, data: IInlineCompletionModelInfoDto | undefined): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 098b7a0e5d4a8..4311937f5c284 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2619,7 +2619,7 @@ export class ExtHostLanguageFeatures extends CoreDisposable implements extHostPr const supportsOnDidChange = isProposedApiEnabled(extension, 'inlineCompletionsAdditions') && typeof provider.onDidChange === 'function'; if (supportsOnDidChange) { - const subscription = provider.onDidChange!(_ => this._proxy.$emitInlineCompletionsChange(handle)); + const subscription = provider.onDidChange!(e => this._proxy.$emitInlineCompletionsChange(handle, e ? { data: e.data } : undefined)); result = Disposable.from(result, subscription); } diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 4e81dd810edb2..8ee357c7ee539 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -105,7 +105,14 @@ export function connectProxyResolver( extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates'); promises.push(Promise.resolve(https.globalAgent.testCertificates as string[])); } - return (await Promise.all(promises)).flat(); + const result = (await Promise.all(promises)).flat(); + mainThreadTelemetry.$publicLog2('additionalCertificates', { + count: result.length, + isRemote, + loadLocalCertificates, + useNodeSystemCerts, + }); + return result; }, env: process.env, }; @@ -257,6 +264,22 @@ function recordFetchFeatureUse(mainThreadTelemetry: MainThreadTelemetryShape, fe } } +type AdditionalCertificatesClassification = { + owner: 'chrmarti'; + comment: 'Tracks the number of additional certificates loaded for TLS connections'; + count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of additional certificates loaded' }; + isRemote: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether this is a remote extension host' }; + loadLocalCertificates: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether local certificates are loaded' }; + useNodeSystemCerts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether Node.js system certificates are used' }; +}; + +type AdditionalCertificatesEvent = { + count: number; + isRemote: boolean; + loadLocalCertificates: boolean; + useNodeSystemCerts: boolean; +}; + type ProxyResolveStatsClassification = { owner: 'chrmarti'; comment: 'Performance statistics for proxy resolution'; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 841d369223535..eb83d36d84607 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1031,9 +1031,6 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary.monaco-text-button.codicon { - background-color: transparent; - border-color: transparent; - color: var(--vscode-icon-foreground); cursor: pointer; padding: 0 3px; border-radius: 2px; @@ -1056,7 +1053,6 @@ have to be updated for changes to the rules above, or to support more deeply nes background-color: var(--vscode-toolbar-hoverBackground); } -.interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary.monaco-text-button.codicon:not(.disabled):hover, .interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button:hover { background-color: var(--vscode-toolbar-hoverBackground); } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index cd53193efa5b5..3652a5fd238ea 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -751,12 +751,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { clearWidget.dispose(); await queue; - const chatModel = await this.showModel(newModelRef); - if (chatModel) { - this._widget.input.setWorkingSetCollapsed(false); - } - - return chatModel; + return this.showModel(newModelRef); }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 05f5ee69e13da..33bac6a02734b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -204,10 +204,6 @@ export class InlineChatController implements IEditorContribution { this._store.add(result); - this._store.add(result.widget.chatWidget.input.onDidChangeCurrentLanguageModel(newModel => { - InlineChatController._selectVendorDefaultLanguageModel = Boolean(newModel.metadata.isDefaultForLocation[location.location]); - })); - result.domNode.classList.add('inline-chat-2'); return result; @@ -434,7 +430,7 @@ export class InlineChatController implements IEditorContribution { this._isActiveController.set(true, undefined); const session = this._inlineChatSessionService.createSession(this._editor); - + const store = new DisposableStore(); // fallback to the default model of the selected vendor unless an explicit selection was made for the session // or unless the user has chosen to persist their model choice @@ -451,6 +447,10 @@ export class InlineChatController implements IEditorContribution { } } + store.add(this._zone.value.widget.chatWidget.input.onDidChangeCurrentLanguageModel(newModel => { + InlineChatController._selectVendorDefaultLanguageModel = Boolean(newModel.metadata.isDefaultForLocation[session.chatModel.initialLocation]); + })); + // ADD diagnostics const entries: IChatRequestVariableEntry[] = []; for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) { @@ -502,20 +502,25 @@ export class InlineChatController implements IEditorContribution { } } - if (!arg?.resolveOnResponse) { - // DEFAULT: wait for the session to be accepted or rejected - await Event.toPromise(session.editingSession.onDidDispose); - const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected; - return !rejected; + try { + if (!arg?.resolveOnResponse) { + // DEFAULT: wait for the session to be accepted or rejected + await Event.toPromise(session.editingSession.onDidDispose); + const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected; + return !rejected; - } else { - // resolveOnResponse: ONLY wait for the file to be modified - const modifiedObs = derived(r => { - const entry = session.editingSession.readEntry(uri, r); - return entry?.state.read(r) === ModifiedFileEntryState.Modified && !entry?.isCurrentlyBeingModifiedBy.read(r); - }); - await waitForState(modifiedObs, state => state === true); - return true; + } else { + // resolveOnResponse: ONLY wait for the file to be modified + const modifiedObs = derived(r => { + const entry = session.editingSession.readEntry(uri, r); + return entry?.state.read(r) === ModifiedFileEntryState.Modified && !entry?.isCurrentlyBeingModifiedBy.read(r); + }); + await waitForState(modifiedObs, state => state === true); + return true; + } + + } finally { + store.dispose(); } } diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index a0f2ebc47dad4..8b6e1a13559f2 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -437,6 +437,10 @@ export class SCMService implements ISCMService { let bestMatchLength = Number.POSITIVE_INFINITY; for (const repository of this.repositories) { + if (repository.provider.isHidden === true) { + continue; + } + const root = repository.provider.rootUri; if (!root) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index e9e1adddbefec..34ba1c2f912ca 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -176,6 +176,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { 'sed "s/foo/bar/g"', 'sed -n "1,10p" file.txt', 'sort file.txt', - 'tree directory' + 'tree directory', + + // od + 'od somefile', + 'od -A x somefile', + + // xxd + 'xxd somefile', + 'xxd -l100 somefile', + 'xxd -r somefile', + 'xxd -rp somefile', + + // docker readonly sub-commands + 'docker ps', + 'docker ps -a', + 'docker images', + 'docker info', + 'docker version', + 'docker inspect mycontainer', + 'docker logs mycontainer', + 'docker top mycontainer', + 'docker stats', + 'docker port mycontainer', + 'docker diff mycontainer', + 'docker search nginx', + 'docker events', + 'docker container ls', + 'docker container ps', + 'docker container inspect mycontainer', + 'docker image ls', + 'docker image history myimage', + 'docker image inspect myimage', + 'docker network ls', + 'docker network inspect mynetwork', + 'docker volume ls', + 'docker volume inspect myvolume', + 'docker context ls', + 'docker context inspect mycontext', + 'docker context show', + 'docker system df', + 'docker system info', + 'docker compose ps', + 'docker compose ls', + 'docker compose top', + 'docker compose logs', + 'docker compose images', + 'docker compose config', + 'docker compose version', + 'docker compose port', + 'docker compose events', ]; const confirmationRequiredTestCases = [ // Dangerous file operations @@ -325,6 +374,21 @@ suite('RunInTerminalTool', () => { 'HTTP_PROXY=proxy:8080 wget https://example.com', 'VAR1=value1 VAR2=value2 echo test', 'A=1 B=2 C=3 ./script.sh', + + // xxd with outfile or ambiguous args + 'xxd infile outfile', + 'xxd -l 100 somefile', + + // docker write/execute sub-commands + 'docker run nginx', + 'docker exec mycontainer bash', + 'docker rm mycontainer', + 'docker rmi myimage', + 'docker build .', + 'docker push myimage', + 'docker pull nginx', + 'docker compose up', + 'docker compose down', ]; suite.skip('auto approved', () => { diff --git a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts index 0e9f18f3d1051..7505318991409 100644 --- a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts +++ b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts @@ -471,7 +471,7 @@ function equalsTokenRules(a: ITextMateThemingRule[] | null, b: ITextMateThemingR const s1 = r1.settings; const s2 = r2.settings; if (s1 && s2) { - if (s1.fontStyle !== s2.fontStyle || s1.foreground !== s2.foreground || s1.background !== s2.background) { + if (s1.fontStyle !== s2.fontStyle || s1.foreground !== s2.foreground || s1.background !== s2.background || s1.lineHeight !== s2.lineHeight || s1.fontSize !== s2.fontSize || s1.fontFamily !== s2.fontFamily) { return false; } } else if (!s1 || !s2) { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 75c05cdec531a..314dac44116be 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -408,6 +408,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } this.tokenColorIndex = undefined; + this.tokenFontIndex = undefined; this.textMateThemingRules = undefined; this.customTokenScopeMatchers = undefined; } @@ -437,6 +438,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } this.tokenColorIndex = undefined; + this.tokenFontIndex = undefined; this.textMateThemingRules = undefined; this.customTokenScopeMatchers = undefined; } @@ -462,6 +464,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } this.tokenColorIndex = undefined; + this.tokenFontIndex = undefined; this.textMateThemingRules = undefined; } @@ -585,6 +588,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { public clearCaches() { this.tokenColorIndex = undefined; + this.tokenFontIndex = undefined; this.textMateThemingRules = undefined; this.themeTokenScopeMatchers = undefined; this.customTokenScopeMatchers = undefined; diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index ed9f5022b2f4e..deef5f2a74fa3 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -135,7 +135,12 @@ declare module 'vscode' { // eslint-disable-next-line local/vscode-dts-provider-naming handleListEndOfLifetime?(list: InlineCompletionList, reason: InlineCompletionsDisposeReason): void; - readonly onDidChange?: Event; + /** + * Fired when the provider wants to trigger a new completion request. + * Can optionally pass a {@link InlineCompletionChangeHint} which will be + * included in the {@link InlineCompletionContext.changeHint} of the subsequent request. + */ + readonly onDidChange?: Event; readonly modelInfo?: InlineCompletionModelInfo; readonly onDidChangeModelInfo?: Event; @@ -199,6 +204,18 @@ declare module 'vscode' { export type InlineCompletionsDisposeReason = { kind: InlineCompletionsDisposeReasonKind }; + /** + * Arbitrary data that the provider can pass when firing {@link InlineCompletionItemProvider.onDidChange}. + * This data is passed back to the provider in {@link InlineCompletionContext.changeHint}. + */ + export interface InlineCompletionChangeHint { + /** + * Arbitrary data that the provider can use to identify what triggered the change. + * This data must be JSON serializable. + */ + readonly data?: unknown; + } + export interface InlineCompletionContext { readonly userPrompt?: string; @@ -207,6 +224,12 @@ declare module 'vscode' { readonly requestIssuedDateTime: number; readonly earliestShownDateTime: number; + + /** + * The change hint that was passed to {@link InlineCompletionItemProvider.onDidChange}. + * Only set if this request was triggered by such an event. + */ + readonly changeHint?: InlineCompletionChangeHint; } export interface PartialAcceptInfo { diff --git a/test/sanity/package-lock.json b/test/sanity/package-lock.json index 1c246d774bedb..ee85c10be1ae6 100644 --- a/test/sanity/package-lock.json +++ b/test/sanity/package-lock.json @@ -10,11 +10,16 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "minimist": "^1.2.8", + "mocha": "^11.7.5", "mocha-junit-reporter": "^2.2.1", "node-fetch": "^3.3.2", - "playwright": "^1.57.0" + "playwright": "^1.57.0", + "typescript": "^6.0.0-dev.20251110" }, "devDependencies": { + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", "@types/node": "22.x" } }, @@ -23,7 +28,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -42,11 +46,24 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=14" } }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", @@ -62,7 +79,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -75,7 +91,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -90,22 +105,19 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0", - "peer": true + "license": "Python-2.0" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -114,15 +126,13 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -135,7 +145,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -152,7 +161,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -174,7 +182,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -190,7 +197,6 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -205,7 +211,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -214,15 +219,13 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -237,7 +240,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -250,7 +252,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -268,7 +269,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -280,15 +280,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -338,7 +336,6 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -351,7 +348,6 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.3.1" } @@ -360,22 +356,19 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -385,7 +378,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -421,7 +413,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -438,7 +429,6 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "license": "BSD-3-Clause", - "peer": true, "bin": { "flat": "cli.js" } @@ -448,7 +438,6 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", - "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -491,7 +480,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", - "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -501,7 +489,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -522,7 +509,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -532,7 +518,6 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "license": "MIT", - "peer": true, "bin": { "he": "bin/he" } @@ -548,7 +533,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -558,7 +542,6 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -568,7 +551,6 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -578,7 +560,6 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -590,15 +571,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -614,7 +593,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -627,7 +605,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -643,7 +620,6 @@ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -659,8 +635,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/md5": { "version": "2.3.0", @@ -678,7 +653,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -689,12 +663,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -719,7 +701,6 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "license": "MIT", - "peer": true, "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", @@ -837,7 +818,6 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -853,7 +833,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -868,15 +847,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0", - "peer": true + "license": "BlueOak-1.0.0" }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -886,7 +863,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -896,7 +872,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -912,8 +887,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/playwright": { "version": "1.57.0", @@ -950,7 +924,6 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -960,7 +933,6 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -974,7 +946,6 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -997,15 +968,13 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -1015,7 +984,6 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1028,7 +996,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1038,7 +1005,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", - "peer": true, "engines": { "node": ">=14" }, @@ -1051,7 +1017,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1070,7 +1035,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1085,7 +1049,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1094,15 +1057,13 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1115,7 +1076,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1132,7 +1092,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1145,7 +1104,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1155,7 +1113,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -1168,7 +1125,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -1179,6 +1135,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/typescript": { + "version": "6.0.0-dev.20260113", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20260113.tgz", + "integrity": "sha512-frXm5LJtstQlM511cGZLCalQjX5YUdUhvNSQAEcI4EuHoflAaqvCa2KIzPKNbyH3KmFPjA3EOs9FphTSKNc4CQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -1200,7 +1169,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -1215,15 +1183,13 @@ "version": "9.3.4", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1242,7 +1208,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1260,7 +1225,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1269,15 +1233,13 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1292,7 +1254,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1305,7 +1266,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1324,7 +1284,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", - "peer": true, "engines": { "node": ">=10" } @@ -1334,7 +1293,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", - "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -1353,7 +1311,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -1363,7 +1320,6 @@ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "license": "MIT", - "peer": true, "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -1379,7 +1335,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1388,15 +1343,13 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1411,7 +1364,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1424,7 +1376,6 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/test/sanity/package.json b/test/sanity/package.json index a1974fc790643..2080734447bf1 100644 --- a/test/sanity/package.json +++ b/test/sanity/package.json @@ -5,15 +5,20 @@ "main": "./out/index.js", "scripts": { "postinstall": "playwright install --with-deps chromium webkit", - "compile": "node ../../node_modules/typescript/bin/tsc", + "compile": "tsc", "start": "node ./out/index.js" }, "dependencies": { + "minimist": "^1.2.8", + "mocha": "^11.7.5", "mocha-junit-reporter": "^2.2.1", "node-fetch": "^3.3.2", - "playwright": "^1.57.0" + "playwright": "^1.57.0", + "typescript": "^6.0.0-dev.20251110" }, "devDependencies": { + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", "@types/node": "22.x" } } diff --git a/test/sanity/src/cli.test.ts b/test/sanity/src/cli.test.ts index 90b0125713159..13694169b3a25 100644 --- a/test/sanity/src/cli.test.ts +++ b/test/sanity/src/cli.test.ts @@ -5,128 +5,131 @@ import assert from 'assert'; import { spawn } from 'child_process'; +import { test } from 'mocha'; import { TestContext } from './context'; export function setup(context: TestContext) { - describe('CLI', () => { - if (context.platform === 'linux-arm64') { - it('cli-alpine-arm64', async () => { - const dir = await context.downloadAndUnpack('cli-alpine-arm64'); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('cli-alpine-x64', async () => { - const dir = await context.downloadAndUnpack('cli-alpine-x64'); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('cli-alpine-arm64', async () => { + const dir = await context.downloadAndUnpack('cli-alpine-arm64'); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('cli-alpine-x64', async () => { + const dir = await context.downloadAndUnpack('cli-alpine-x64'); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-arm64') { + test('cli-darwin-arm64', async () => { + const dir = await context.downloadAndUnpack('cli-darwin-arm64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-x64') { + test('cli-darwin-x64', async () => { + const dir = await context.downloadAndUnpack('cli-darwin-x64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('cli-linux-arm64', async () => { + const dir = await context.downloadAndUnpack('cli-linux-arm64'); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm') { + test('cli-linux-armhf', async () => { + const dir = await context.downloadAndUnpack('cli-linux-armhf'); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('cli-linux-x64', async () => { + const dir = await context.downloadAndUnpack('cli-linux-x64'); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-arm64') { + test('cli-win32-arm64', async () => { + const dir = await context.downloadAndUnpack('cli-win32-arm64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-x64') { + test('cli-win32-x64', async () => { + const dir = await context.downloadAndUnpack('cli-win32-x64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getEntryPoint('cli', dir); + await testCliApp(entryPoint); + }); + } + + async function testCliApp(entryPoint: string) { + if (context.skipRuntimeCheck) { + return; } - if (context.platform === 'darwin-arm64') { - it('cli-darwin-arm64', async () => { - const dir = await context.downloadAndUnpack('cli-darwin-arm64'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); + const result = context.runNoErrors(entryPoint, '--version'); + const version = result.stdout.trim(); + assert.ok(version.includes(`(commit ${context.commit})`)); + + const workspaceDir = context.createTempDir(); + process.chdir(workspaceDir); + context.log(`Changed current directory to: ${workspaceDir}`); + + const args = [ + '--cli-data-dir', context.createTempDir(), + '--user-data-dir', context.createTempDir(), + 'tunnel', + '--accept-server-license-terms', + '--server-data-dir', context.createTempDir(), + '--extensions-dir', context.createTempDir(), + ]; + + context.log(`Running CLI ${entryPoint} with args ${args.join(' ')}`); + const cli = spawn(entryPoint, args, { detached: true }); + + cli.stderr.on('data', (data) => { + context.error(`[CLI Error] ${data.toString().trim()}`); + }); + + cli.stdout.on('data', (data) => { + const text = data.toString().trim(); + text.split('\n').forEach((line: string) => { + context.log(`[CLI Output] ${line}`); }); - } - - if (context.platform === 'darwin-x64') { - it('cli-darwin-x64', async () => { - const dir = await context.downloadAndUnpack('cli-darwin-x64'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - if (context.platform === 'linux-arm64') { - it('cli-linux-arm64', async () => { - const dir = await context.downloadAndUnpack('cli-linux-arm64'); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - - if (context.platform === 'linux-arm') { - it('cli-linux-armhf', async () => { - const dir = await context.downloadAndUnpack('cli-linux-armhf'); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('cli-linux-x64', async () => { - const dir = await context.downloadAndUnpack('cli-linux-x64'); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - - if (context.platform === 'win32-arm64') { - it('cli-win32-arm64', async () => { - const dir = await context.downloadAndUnpack('cli-win32-arm64'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - - if (context.platform === 'win32-x64') { - it('cli-win32-x64', async () => { - const dir = await context.downloadAndUnpack('cli-win32-x64'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getEntryPoint('cli', dir); - await testCliApp(entryPoint); - }); - } - - async function testCliApp(entryPoint: string) { - const result = context.runNoErrors(entryPoint, '--version'); - const version = result.stdout.trim(); - assert.ok(version.includes(`(commit ${context.commit})`)); - - const workspaceDir = context.createTempDir(); - process.chdir(workspaceDir); - context.log(`Changed current directory to: ${workspaceDir}`); - - const args = [ - '--cli-data-dir', context.createTempDir(), - '--user-data-dir', context.createTempDir(), - 'tunnel', - '--accept-server-license-terms', - '--server-data-dir', context.createTempDir(), - '--extensions-dir', context.createTempDir(), - ]; - - context.log(`Running CLI ${entryPoint} with args ${args.join(' ')}`); - const cli = spawn(entryPoint, args, { detached: true }); - - cli.stderr.on('data', (data) => { - context.error(`[CLI Error] ${data.toString().trim()}`); - }); - - cli.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[CLI Output] ${line}`); - }); - - const match = /Using GitHub for authentication/.exec(text); - if (match !== null) { - context.log(`CLI started successfully and is waiting for authentication`); - context.killProcessTree(cli.pid!); - } - }); - - await new Promise((resolve, reject) => { - cli.on('error', reject); - cli.on('exit', resolve); - }); - } - }); + const match = /Using GitHub for authentication/.exec(text); + if (match !== null) { + context.log(`CLI started successfully and is waiting for authentication`); + context.killProcessTree(cli.pid!); + } + }); + + await new Promise((resolve, reject) => { + cli.on('error', reject); + cli.on('exit', resolve); + }); + } } diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts index 39f3e85438aad..82023c01c1a0d 100644 --- a/test/sanity/src/context.ts +++ b/test/sanity/src/context.ts @@ -33,19 +33,17 @@ export class TestContext { private static readonly codesignExclude = /node_modules\/(@parcel\/watcher\/build\/Release\/watcher\.node|@vscode\/deviceid\/build\/Release\/windows\.node|@vscode\/ripgrep\/bin\/rg|@vscode\/spdlog\/build\/Release\/spdlog.node|kerberos\/build\/Release\/kerberos.node|native-watchdog\/build\/Release\/watchdog\.node|node-pty\/build\/Release\/(pty\.node|spawn-helper)|vsda\/build\/Release\/vsda\.node)$/; private readonly tempDirs = new Set(); - private readonly logFile: string; private _currentTest?: Mocha.Test & { consoleOutputs?: string[] }; + private _osTempDir?: string; public constructor( public readonly quality: 'stable' | 'insider' | 'exploration', public readonly commit: string, public readonly verbose: boolean, public readonly skipSigningCheck: boolean, + public readonly headless: boolean, + public readonly skipRuntimeCheck: boolean, ) { - const osTempDir = fs.realpathSync(os.tmpdir()); - const logDir = fs.mkdtempSync(path.join(osTempDir, 'vscode-sanity-log')); - this.logFile = path.join(logDir, 'sanity.log'); - console.log(`Log file: ${this.logFile}`); } /** @@ -63,12 +61,31 @@ export class TestContext { return `${os.platform()}-${os.arch()}`; } + /** + * Returns the OS temp directory with expanded long names on Windows. + */ + public get osTempDir(): string { + if (this._osTempDir === undefined) { + let tempDir = fs.realpathSync(os.tmpdir()); + + // On Windows, expand short 8.3 file names to long names + if (os.platform() === 'win32') { + const result = spawnSync('powershell', ['-Command', `(Get-Item "${tempDir}").FullName`], { encoding: 'utf-8' }); + if (result.status === 0 && result.stdout) { + tempDir = result.stdout.trim(); + } + } + + this._osTempDir = tempDir; + } + return this._osTempDir; + } + /** * Logs a message with a timestamp. */ public log(message: string) { const line = `[${new Date().toISOString()}] ${message}`; - fs.appendFileSync(this.logFile, line + '\n'); this._currentTest?.consoleOutputs?.push(line); if (this.verbose) { console.log(line); @@ -80,7 +97,6 @@ export class TestContext { */ public error(message: string): never { const line = `[${new Date().toISOString()}] ERROR: ${message}`; - fs.appendFileSync(this.logFile, line + '\n'); this._currentTest?.consoleOutputs?.push(line); console.error(line); throw new Error(message); @@ -90,8 +106,7 @@ export class TestContext { * Creates a new temporary directory and returns its path. */ public createTempDir(): string { - const osTempDir = fs.realpathSync(os.tmpdir()); - const tempDir = fs.mkdtempSync(path.join(osTempDir, 'vscode-sanity')); + const tempDir = fs.mkdtempSync(path.join(this.osTempDir, 'vscode-sanity')); this.log(`Created temp directory: ${tempDir}`); this.tempDirs.add(tempDir); return tempDir; @@ -233,7 +248,7 @@ export class TestContext { * @param filePath The path to the file to validate. */ public validateAuthenticodeSignature(filePath: string) { - if (this.skipSigningCheck) { + if (this.skipSigningCheck || os.platform() !== 'win32') { this.log(`Skipping Authenticode signature validation for ${filePath} (signing checks disabled)`); return; } @@ -256,7 +271,7 @@ export class TestContext { * @param dir The directory to scan for executable files. */ public validateAllAuthenticodeSignatures(dir: string) { - if (this.skipSigningCheck) { + if (this.skipSigningCheck || os.platform() !== 'win32') { this.log(`Skipping Authenticode signature validation for ${dir} (signing checks disabled)`); return; } @@ -277,7 +292,7 @@ export class TestContext { * @param filePath The path to the file or app bundle to validate. */ public validateCodesignSignature(filePath: string) { - if (this.skipSigningCheck) { + if (this.skipSigningCheck || os.platform() !== 'darwin') { this.log(`Skipping codesign signature validation for ${filePath} (signing checks disabled)`); return; } @@ -299,7 +314,7 @@ export class TestContext { * @param dir The directory to scan for Mach-O binaries. */ public validateAllCodesignSignatures(dir: string) { - if (this.skipSigningCheck) { + if (this.skipSigningCheck || os.platform() !== 'darwin') { this.log(`Skipping codesign signature validation for ${dir} (signing checks disabled)`); return; } @@ -496,11 +511,11 @@ export class TestContext { } /** - * Prepares a macOS .app bundle for execution by removing the quarantine attribute. + * Returns the path to the VS Code Electron executable within a macOS .app bundle. * @param bundleDir The directory containing the .app bundle. * @returns The path to the VS Code Electron executable. */ - public installMacApp(bundleDir: string): string { + public getMacAppEntryPoint(bundleDir: string): string { let appName: string; switch (this.quality) { case 'stable': @@ -666,11 +681,11 @@ export class TestContext { this.log(`Launching web browser`); switch (os.platform()) { case 'darwin': - return await webkit.launch({ headless: false }); + return await webkit.launch({ headless: this.headless }); case 'win32': - return await chromium.launch({ channel: 'msedge', headless: false }); + return await chromium.launch({ channel: 'msedge', headless: this.headless }); default: - return await chromium.launch({ channel: 'chrome', headless: false }); + return await chromium.launch({ channel: 'chrome', headless: this.headless }); } } diff --git a/test/sanity/src/desktop.test.ts b/test/sanity/src/desktop.test.ts index 9e95585622579..832e3b0b0fc81 100644 --- a/test/sanity/src/desktop.test.ts +++ b/test/sanity/src/desktop.test.ts @@ -3,201 +3,226 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { test } from 'mocha'; import path from 'path'; import { _electron } from 'playwright'; import { TestContext } from './context'; import { UITest } from './uiTest'; export function setup(context: TestContext) { - describe('Desktop', () => { - if (context.platform === 'darwin-x64') { - it('desktop-darwin-x64', async () => { - const dir = await context.downloadAndUnpack('darwin'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.installMacApp(dir); - await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'darwin-arm64') { - it('desktop-darwin-arm64', async () => { - const dir = await context.downloadAndUnpack('darwin-arm64'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.installMacApp(dir); - await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'darwin-arm64' || context.platform === 'darwin-x64') { - it('desktop-darwin-universal', async () => { - const dir = await context.downloadAndUnpack('darwin-universal'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.installMacApp(dir); - await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-arm64') { - it('desktop-linux-arm64', async () => { - const dir = await context.downloadAndUnpack('linux-arm64'); - const entryPoint = context.getEntryPoint('desktop', dir); - const dataDir = context.createPortableDataDir(dir); - await testDesktopApp(entryPoint, dataDir); - }); - } - - if (context.platform === 'linux-arm') { - it('desktop-linux-armhf', async () => { - const dir = await context.downloadAndUnpack('linux-armhf'); - const entryPoint = context.getEntryPoint('desktop', dir); - const dataDir = context.createPortableDataDir(dir); - await testDesktopApp(entryPoint, dataDir); - }); - } - - if (context.platform === 'linux-arm64') { - it('desktop-linux-deb-arm64', async () => { - const packagePath = await context.downloadTarget('linux-deb-arm64'); + if (context.skipRuntimeCheck || context.platform === 'darwin-x64') { + test('desktop-darwin-x64', async () => { + const dir = await context.downloadAndUnpack('darwin'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getMacAppEntryPoint(dir); + await testDesktopApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-arm64') { + test('desktop-darwin-arm64', async () => { + const dir = await context.downloadAndUnpack('darwin-arm64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getMacAppEntryPoint(dir); + await testDesktopApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-arm64' || context.platform === 'darwin-x64') { + test('desktop-darwin-universal', async () => { + const dir = await context.downloadAndUnpack('darwin-universal'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getMacAppEntryPoint(dir); + await testDesktopApp(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('desktop-linux-arm64', async () => { + const dir = await context.downloadAndUnpack('linux-arm64'); + const entryPoint = context.getEntryPoint('desktop', dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm') { + test('desktop-linux-armhf', async () => { + const dir = await context.downloadAndUnpack('linux-armhf'); + const entryPoint = context.getEntryPoint('desktop', dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('desktop-linux-deb-arm64', async () => { + const packagePath = await context.downloadTarget('linux-deb-arm64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installDeb(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-arm') { - it('desktop-linux-deb-armhf', async () => { - const packagePath = await context.downloadTarget('linux-deb-armhf'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm') { + test('desktop-linux-deb-armhf', async () => { + const packagePath = await context.downloadTarget('linux-deb-armhf'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installDeb(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('desktop-linux-deb-x64', async () => { - const packagePath = await context.downloadTarget('linux-deb-x64'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('desktop-linux-deb-x64', async () => { + const packagePath = await context.downloadTarget('linux-deb-x64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installDeb(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-arm64') { - it('desktop-linux-rpm-arm64', async () => { - const packagePath = await context.downloadTarget('linux-rpm-arm64'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('desktop-linux-rpm-arm64', async () => { + const packagePath = await context.downloadTarget('linux-rpm-arm64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installRpm(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-arm') { - it('desktop-linux-rpm-armhf', async () => { - const packagePath = await context.downloadTarget('linux-rpm-armhf'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm') { + test('desktop-linux-rpm-armhf', async () => { + const packagePath = await context.downloadTarget('linux-rpm-armhf'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installRpm(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('desktop-linux-rpm-x64', async () => { - const packagePath = await context.downloadTarget('linux-rpm-x64'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('desktop-linux-rpm-x64', async () => { + const packagePath = await context.downloadTarget('linux-rpm-x64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installRpm(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('desktop-linux-snap-x64', async () => { - const packagePath = await context.downloadTarget('linux-snap-x64'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('desktop-linux-snap-x64', async () => { + const packagePath = await context.downloadTarget('linux-snap-x64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installSnap(packagePath); await testDesktopApp(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('desktop-linux-x64', async () => { - const dir = await context.downloadAndUnpack('linux-x64'); - const entryPoint = context.getEntryPoint('desktop', dir); - const dataDir = context.createPortableDataDir(dir); - await testDesktopApp(entryPoint, dataDir); - }); - } - - if (context.platform === 'win32-arm64') { - it('desktop-win32-arm64', async () => { - const packagePath = await context.downloadTarget('win32-arm64'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('desktop-linux-x64', async () => { + const dir = await context.downloadAndUnpack('linux-x64'); + const entryPoint = context.getEntryPoint('desktop', dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-arm64') { + test('desktop-win32-arm64', async () => { + const packagePath = await context.downloadTarget('win32-arm64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installWindowsApp('system', packagePath); context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); await testDesktopApp(entryPoint); await context.uninstallWindowsApp('system'); - }); - } - - if (context.platform === 'win32-arm64') { - it('desktop-win32-arm64-archive', async () => { - const dir = await context.downloadAndUnpack('win32-arm64-archive'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getEntryPoint('desktop', dir); - const dataDir = context.createPortableDataDir(dir); - await testDesktopApp(entryPoint, dataDir); - }); - } - - if (context.platform === 'win32-arm64') { - it('desktop-win32-arm64-user', async () => { - const packagePath = await context.downloadTarget('win32-arm64-user'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-arm64') { + test('desktop-win32-arm64-archive', async () => { + const dir = await context.downloadAndUnpack('win32-arm64-archive'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getEntryPoint('desktop', dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-arm64') { + test('desktop-win32-arm64-user', async () => { + const packagePath = await context.downloadTarget('win32-arm64-user'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installWindowsApp('user', packagePath); context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); await testDesktopApp(entryPoint); await context.uninstallWindowsApp('user'); - }); - } - - if (context.platform === 'win32-x64') { - it('desktop-win32-x64', async () => { - const packagePath = await context.downloadTarget('win32-x64'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-x64') { + test('desktop-win32-x64', async () => { + const packagePath = await context.downloadTarget('win32-x64'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installWindowsApp('system', packagePath); context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); await testDesktopApp(entryPoint); await context.uninstallWindowsApp('system'); - }); - } - - if (context.platform === 'win32-x64') { - it('desktop-win32-x64-archive', async () => { - const dir = await context.downloadAndUnpack('win32-x64-archive'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getEntryPoint('desktop', dir); - const dataDir = context.createPortableDataDir(dir); - await testDesktopApp(entryPoint, dataDir); - }); - } - - if (context.platform === 'win32-x64') { - it('desktop-win32-x64-user', async () => { - const packagePath = await context.downloadTarget('win32-x64-user'); + } + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-x64') { + test('desktop-win32-x64-archive', async () => { + const dir = await context.downloadAndUnpack('win32-x64-archive'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getEntryPoint('desktop', dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-x64') { + test('desktop-win32-x64-user', async () => { + const packagePath = await context.downloadTarget('win32-x64-user'); + if (!context.skipRuntimeCheck) { const entryPoint = context.installWindowsApp('user', packagePath); context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); await testDesktopApp(entryPoint); await context.uninstallWindowsApp('user'); - }); + } + }); + } + + async function testDesktopApp(entryPoint: string, dataDir?: string) { + if (context.skipRuntimeCheck) { + return; } - async function testDesktopApp(entryPoint: string, dataDir?: string) { - const test = new UITest(context, dataDir); - const args = dataDir ? [] : [ - '--extensions-dir', test.extensionsDir, - '--user-data-dir', test.userDataDir, - ]; - args.push(test.workspaceDir); + const test = new UITest(context, dataDir); + const args = dataDir ? [] : [ + '--extensions-dir', test.extensionsDir, + '--user-data-dir', test.userDataDir, + ]; + args.push(test.workspaceDir); - context.log(`Starting VS Code ${entryPoint} with args ${args.join(' ')}`); - const app = await _electron.launch({ executablePath: entryPoint, args }); - const window = await app.firstWindow(); + context.log(`Starting VS Code ${entryPoint} with args ${args.join(' ')}`); + const app = await _electron.launch({ executablePath: entryPoint, args }); + const window = await app.firstWindow(); - await test.run(window); + await test.run(window); - context.log('Closing the application'); - await app.close(); + context.log('Closing the application'); + await app.close(); - test.validate(); - } - }); + test.validate(); + } } diff --git a/test/sanity/src/index.ts b/test/sanity/src/index.ts index 8ce74cae96c9c..f88b0679953c3 100644 --- a/test/sanity/src/index.ts +++ b/test/sanity/src/index.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import fs from 'fs'; import minimist from 'minimist'; import Mocha, { MochaOptions } from 'mocha'; +import path from 'path'; const options = minimist(process.argv.slice(2), { string: ['fgrep', 'grep', 'test-results'], @@ -19,6 +21,8 @@ if (options.help) { console.info(` --quality, -q The quality to test (required, "stable", "insider" or "exploration")`); console.info(' --no-cleanup Do not cleanup downloaded files after each test'); console.info(' --no-signing-check Skip Authenticode and codesign signature checks'); + console.info(' --no-headless Run tests with a visible UI (desktop tests only)'); + console.info(' --no-runtime-check Enable all tests regardless of platform and skip executable runs'); console.info(' --grep, -g Only run tests matching the given '); console.info(' --fgrep, -f Only run tests containing the given '); console.info(' --test-results, -t Output test results in JUnit format to the specified path'); @@ -38,6 +42,10 @@ const mochaOptions: MochaOptions = { reporterOptions: testResults ? { mochaFile: testResults, outputs: true } : undefined, }; +if (testResults) { + fs.mkdirSync(path.dirname(testResults), { recursive: true }); +} + const mocha = new Mocha(mochaOptions); mocha.addFile(require.resolve('./main.js')); mocha.run(failures => { diff --git a/test/sanity/src/main.ts b/test/sanity/src/main.ts index e522356bc85a6..6ca2773d66c04 100644 --- a/test/sanity/src/main.ts +++ b/test/sanity/src/main.ts @@ -12,9 +12,9 @@ import { setup as setupServerWebTests } from './serverWeb.test'; const options = minimist(process.argv.slice(2), { string: ['commit', 'quality'], - boolean: ['cleanup', 'verbose', 'signing-check'], + boolean: ['cleanup', 'verbose', 'signing-check', 'headless', 'runtime-check'], alias: { commit: 'c', quality: 'q', verbose: 'v' }, - default: { cleanup: true, verbose: false, 'signing-check': true }, + default: { cleanup: true, verbose: false, 'signing-check': true, headless: true, 'runtime-check': true }, }); if (!options.commit) { @@ -25,24 +25,28 @@ if (!options.quality) { throw new Error('--quality is required'); } -const context = new TestContext(options.quality, options.commit, options.verbose, !options['signing-check']); +const context = new TestContext( + options.quality, + options.commit, + options.verbose, + !options['signing-check'], + options.headless, + !options['runtime-check']); + +beforeEach(function () { + context.currentTest = this.currentTest!; + const cwd = context.createTempDir(); + process.chdir(cwd); + context.log(`Changed working directory to: ${cwd}`); +}); -describe('VS Code Sanity Tests', () => { - beforeEach(function () { - context.currentTest = this.currentTest!; - const cwd = context.createTempDir(); - process.chdir(cwd); - context.log(`Changed working directory to: ${cwd}`); +if (options.cleanup) { + afterEach(() => { + context.cleanup(); }); +} - if (options.cleanup) { - afterEach(() => { - context.cleanup(); - }); - } - - setupCliTests(context); - setupDesktopTests(context); - setupServerTests(context); - setupServerWebTests(context); -}); +setupCliTests(context); +setupDesktopTests(context); +setupServerTests(context); +setupServerWebTests(context); diff --git a/test/sanity/src/server.test.ts b/test/sanity/src/server.test.ts index 70738ab6ddfb9..e9c662be2bc09 100644 --- a/test/sanity/src/server.test.ts +++ b/test/sanity/src/server.test.ts @@ -5,138 +5,141 @@ import assert from 'assert'; import { spawn } from 'child_process'; +import { test } from 'mocha'; import os from 'os'; import { TestContext } from './context'; export function setup(context: TestContext) { - describe('Server', () => { - if (context.platform === 'linux-arm64') { - it('server-alpine-arm64', async () => { - const dir = await context.downloadAndUnpack('server-alpine-arm64'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('server-alpine-arm64', async () => { + const dir = await context.downloadAndUnpack('server-alpine-arm64'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('server-alpine-x64', async () => { + const dir = await context.downloadAndUnpack('server-linux-alpine'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-arm64') { + test('server-darwin-arm64', async () => { + const dir = await context.downloadAndUnpack('server-darwin-arm64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-x64') { + test('server-darwin-x64', async () => { + const dir = await context.downloadAndUnpack('server-darwin'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('server-linux-arm64', async () => { + const dir = await context.downloadAndUnpack('server-linux-arm64'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm') { + test('server-linux-armhf', async () => { + const dir = await context.downloadAndUnpack('server-linux-armhf'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('server-linux-x64', async () => { + const dir = await context.downloadAndUnpack('server-linux-x64'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-arm64') { + test('server-win32-arm64', async () => { + const dir = await context.downloadAndUnpack('server-win32-arm64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-x64') { + test('server-win32-x64', async () => { + const dir = await context.downloadAndUnpack('server-win32-x64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + async function testServer(entryPoint: string) { + if (context.skipRuntimeCheck) { + return; } - if (context.platform === 'linux-x64') { - it('server-alpine-x64', async () => { - const dir = await context.downloadAndUnpack('server-linux-alpine'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } + const args = [ + '--accept-server-license-terms', + '--connection-token', context.getRandomToken(), + '--port', context.getRandomPort(), + '--server-data-dir', context.createTempDir(), + '--extensions-dir', context.createTempDir(), + ]; - if (context.platform === 'darwin-arm64') { - it('server-darwin-arm64', async () => { - const dir = await context.downloadAndUnpack('server-darwin-arm64'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } + context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`); + const server = spawn(entryPoint, args, { shell: true, detached: os.platform() !== 'win32' }); - if (context.platform === 'darwin-x64') { - it('server-darwin-x64', async () => { - const dir = await context.downloadAndUnpack('server-darwin'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } + let testError: Error | undefined; - if (context.platform === 'linux-arm64') { - it('server-linux-arm64', async () => { - const dir = await context.downloadAndUnpack('server-linux-arm64'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } + server.stderr.on('data', (data) => { + context.error(`[Server Error] ${data.toString().trim()}`); + }); - if (context.platform === 'linux-arm') { - it('server-linux-armhf', async () => { - const dir = await context.downloadAndUnpack('server-linux-armhf'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); + server.stdout.on('data', (data) => { + const text = data.toString().trim(); + text.split('\n').forEach((line: string) => { + context.log(`[Server Output] ${line}`); }); - } - if (context.platform === 'linux-x64') { - it('server-linux-x64', async () => { - const dir = await context.downloadAndUnpack('server-linux-x64'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'win32-arm64') { - it('server-win32-arm64', async () => { - const dir = await context.downloadAndUnpack('server-win32-arm64'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'win32-x64') { - it('server-win32-x64', async () => { - const dir = await context.downloadAndUnpack('server-win32-x64'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - async function testServer(entryPoint: string) { - const args = [ - '--accept-server-license-terms', - '--connection-token', context.getRandomToken(), - '--port', context.getRandomPort(), - '--server-data-dir', context.createTempDir(), - '--extensions-dir', context.createTempDir(), - ]; - - context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`); - const server = spawn(entryPoint, args, { shell: true, detached: os.platform() !== 'win32' }); - - let testError: Error | undefined; - - server.stderr.on('data', (data) => { - context.error(`[Server Error] ${data.toString().trim()}`); - }); + const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; + if (port) { + const url = context.getWebServerUrl(port); + url.pathname = '/version'; + runWebTest(url.toString()) + .catch((error) => { testError = error; }) + .finally(() => context.killProcessTree(server.pid!)); + } + }); - server.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[Server Output] ${line}`); - }); - - const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; - if (port) { - const url = context.getWebServerUrl(port); - url.pathname = '/version'; - runWebTest(url.toString()) - .catch((error) => { testError = error; }) - .finally(() => context.killProcessTree(server.pid!)); - } - }); + await new Promise((resolve, reject) => { + server.on('error', reject); + server.on('exit', resolve); + }); - await new Promise((resolve, reject) => { - server.on('error', reject); - server.on('exit', resolve); - }); - - if (testError) { - throw testError; - } + if (testError) { + throw testError; } + } - async function runWebTest(url: string) { - context.log(`Fetching ${url}`); - const response = await fetch(url); - assert.strictEqual(response.status, 200, `Expected status 200 but got ${response.status}`); + async function runWebTest(url: string) { + context.log(`Fetching ${url}`); + const response = await fetch(url); + assert.strictEqual(response.status, 200, `Expected status 200 but got ${response.status}`); - const text = await response.text(); - assert.strictEqual(text, context.commit, `Expected commit ${context.commit} but got ${text}`); - } - }); + const text = await response.text(); + assert.strictEqual(text, context.commit, `Expected commit ${context.commit} but got ${text}`); + } } diff --git a/test/sanity/src/serverWeb.test.ts b/test/sanity/src/serverWeb.test.ts index 5a769b8805d36..d35d0d4d0d5c8 100644 --- a/test/sanity/src/serverWeb.test.ts +++ b/test/sanity/src/serverWeb.test.ts @@ -4,150 +4,153 @@ *--------------------------------------------------------------------------------------------*/ import { spawn } from 'child_process'; +import { test } from 'mocha'; import os from 'os'; import { TestContext } from './context'; import { UITest } from './uiTest'; export function setup(context: TestContext) { - describe('Server Web', () => { - if (context.platform === 'linux-arm64') { - it('server-web-alpine-arm64', async () => { - const dir = await context.downloadAndUnpack('server-alpine-arm64-web'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('server-web-alpine-x64', async () => { - const dir = await context.downloadAndUnpack('server-linux-alpine-web'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'darwin-arm64') { - it('server-web-darwin-arm64', async () => { - const dir = await context.downloadAndUnpack('server-darwin-arm64-web'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'darwin-x64') { - it('server-web-darwin-x64', async () => { - const dir = await context.downloadAndUnpack('server-darwin-web'); - context.validateAllCodesignSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'linux-arm64') { - it('server-web-linux-arm64', async () => { - const dir = await context.downloadAndUnpack('server-linux-arm64-web'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'linux-arm') { - it('server-web-linux-armhf', async () => { - const dir = await context.downloadAndUnpack('server-linux-armhf-web'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - - if (context.platform === 'linux-x64') { - it('server-web-linux-x64', async () => { - const dir = await context.downloadAndUnpack('server-linux-x64-web'); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('server-web-alpine-arm64', async () => { + const dir = await context.downloadAndUnpack('server-alpine-arm64-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('server-web-alpine-x64', async () => { + const dir = await context.downloadAndUnpack('server-linux-alpine-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-arm64') { + test('server-web-darwin-arm64', async () => { + const dir = await context.downloadAndUnpack('server-darwin-arm64-web'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'darwin-x64') { + test('server-web-darwin-x64', async () => { + const dir = await context.downloadAndUnpack('server-darwin-web'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm64') { + test('server-web-linux-arm64', async () => { + const dir = await context.downloadAndUnpack('server-linux-arm64-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-arm') { + test('server-web-linux-armhf', async () => { + const dir = await context.downloadAndUnpack('server-linux-armhf-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'linux-x64') { + test('server-web-linux-x64', async () => { + const dir = await context.downloadAndUnpack('server-linux-x64-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-arm64') { + test('server-web-win32-arm64', async () => { + const dir = await context.downloadAndUnpack('server-win32-arm64-web'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + if (context.skipRuntimeCheck || context.platform === 'win32-x64') { + test('server-web-win32-x64', async () => { + const dir = await context.downloadAndUnpack('server-win32-x64-web'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + } + + async function testServer(entryPoint: string) { + if (context.skipRuntimeCheck) { + return; } - if (context.platform === 'win32-arm64') { - it('server-web-win32-arm64', async () => { - const dir = await context.downloadAndUnpack('server-win32-arm64-web'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); + const token = context.getRandomToken(); + const test = new UITest(context); + const args = [ + '--accept-server-license-terms', + '--port', context.getRandomPort(), + '--connection-token', token, + '--server-data-dir', context.createTempDir(), + '--extensions-dir', test.extensionsDir, + '--user-data-dir', test.userDataDir + ]; + + context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`); + const server = spawn(entryPoint, args, { shell: true, detached: os.platform() !== 'win32' }); + + let testError: Error | undefined; + + server.stderr.on('data', (data) => { + context.error(`[Server Error] ${data.toString().trim()}`); + }); + + server.stdout.on('data', (data) => { + const text = data.toString().trim(); + text.split('\n').forEach((line: string) => { + context.log(`[Server Output] ${line}`); }); - } - - if (context.platform === 'win32-x64') { - it('server-web-win32-x64', async () => { - const dir = await context.downloadAndUnpack('server-win32-x64-web'); - context.validateAllAuthenticodeSignatures(dir); - const entryPoint = context.getServerEntryPoint(dir); - await testServer(entryPoint); - }); - } - async function testServer(entryPoint: string) { - const token = context.getRandomToken(); - const test = new UITest(context); - const args = [ - '--accept-server-license-terms', - '--port', context.getRandomPort(), - '--connection-token', token, - '--server-data-dir', context.createTempDir(), - '--extensions-dir', test.extensionsDir, - '--user-data-dir', test.userDataDir - ]; - - context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`); - const server = spawn(entryPoint, args, { shell: true, detached: os.platform() !== 'win32' }); - - let testError: Error | undefined; - - server.stderr.on('data', (data) => { - context.error(`[Server Error] ${data.toString().trim()}`); - }); + const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; + if (port) { + const url = context.getWebServerUrl(port, token, test.workspaceDir).toString(); + runUITest(url, test) + .catch((error) => { testError = error; }) + .finally(() => context.killProcessTree(server.pid!)); + } + }); - server.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[Server Output] ${line}`); - }); - - const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; - if (port) { - const url = context.getWebServerUrl(port, token, test.workspaceDir).toString(); - runUITest(url, test) - .catch((error) => { testError = error; }) - .finally(() => context.killProcessTree(server.pid!)); - } - }); + await new Promise((resolve, reject) => { + server.on('error', reject); + server.on('exit', resolve); + }); - await new Promise((resolve, reject) => { - server.on('error', reject); - server.on('exit', resolve); - }); - - if (testError) { - throw testError; - } + if (testError) { + throw testError; } + } - async function runUITest(url: string, test: UITest) { - const browser = await context.launchBrowser(); - const page = await browser.newPage(); + async function runUITest(url: string, test: UITest) { + const browser = await context.launchBrowser(); + const page = await browser.newPage(); - context.log(`Navigating to ${url}`); - await page.goto(url, { waitUntil: 'networkidle' }); + context.log(`Navigating to ${url}`); + await page.goto(url, { waitUntil: 'networkidle' }); - context.log('Waiting for the workbench to load'); - await page.waitForSelector('.monaco-workbench'); + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); - await test.run(page); + await test.run(page); - context.log('Closing browser'); - await browser.close(); + context.log('Closing browser'); + await browser.close(); - test.validate(); - } - }); + test.validate(); + } }