* Port unit tests for JS CLU Recognizer * Change return of mocked functions
This commit is contained in:
Родитель
7eef04cc83
Коммит
9894bffcc1
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@microsoft/bot-components-clu-recognizer-tests",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:package": "yarn workspace @microsoft/bot-components-clu-recognizer build",
|
||||
"test": "yarn run build:package && mocha --require ts-node/register tests/*.test.ts",
|
||||
"lint": "eslint . --ext .js,.ts --config ../../../../../packages/Recognizers/ConversationLanguageUnderstanding/js/.eslintrc.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/bot-components-clu-recognizer": "workspace:packages/Recognizers/ConversationLanguageUnderstanding/js",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/sinon": "^10.0.16",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"botbuilder": "4.19.3",
|
||||
"botbuilder-dialogs-adaptive": "4.19.3-preview",
|
||||
"botbuilder-dialogs-adaptive-testing": "4.19.3-preview",
|
||||
"botframework-connector": "4.19.3",
|
||||
"mocha": "^9.0.2",
|
||||
"nock": "^13.1.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.30.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
// Licensed under the MIT License.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
import 'mocha';
|
||||
import assert from 'assert';
|
||||
import { CluApplication } from '@microsoft/bot-components-clu-recognizer';
|
||||
|
||||
const ProjectName = 'MockProjectName';
|
||||
const EndpointKey = '4da536f842114fa68c657115d7312026';
|
||||
const Endpoint = 'https://mockcluservice.cognitiveservices.azure.com';
|
||||
const DeploymentName = 'MockDeploymentName';
|
||||
|
||||
const badArgumentCases = [
|
||||
{
|
||||
it: 'empty arguments',
|
||||
projectName: '',
|
||||
endpointKey: '',
|
||||
endpoint: '',
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'only ProjectName',
|
||||
projectName: ProjectName,
|
||||
endpointKey: '',
|
||||
endpoint: '',
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'only EndpointKey',
|
||||
projectName: '',
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: '',
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'only Endpoint',
|
||||
projectName: '',
|
||||
endpointKey: '',
|
||||
endpoint: Endpoint,
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'only DeploymentName',
|
||||
projectName: '',
|
||||
endpointKey: '',
|
||||
endpoint: '',
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no Endpoint or DeploymentName',
|
||||
projectName: ProjectName,
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: '',
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'no EndpointKey or DeploymentName',
|
||||
projectName: ProjectName,
|
||||
endpointKey: '',
|
||||
endpoint: Endpoint,
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'no EndpointKey or Endpoint',
|
||||
projectName: ProjectName,
|
||||
endpointKey: '',
|
||||
endpoint: '',
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no ProjectName or DeploymentName',
|
||||
projectName: '',
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: Endpoint,
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'no ProjectName or Endpoint',
|
||||
projectName: '',
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: '',
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no ProjectName or EndpointKey',
|
||||
projectName: '',
|
||||
endpointKey: '',
|
||||
endpoint: Endpoint,
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no DeploymentName',
|
||||
projectName: ProjectName,
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: Endpoint,
|
||||
deploymentName: '',
|
||||
},
|
||||
{
|
||||
it: 'no Endpoint',
|
||||
projectName: ProjectName,
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: '',
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no EndpointKey',
|
||||
projectName: ProjectName,
|
||||
endpointKey: '',
|
||||
endpoint: Endpoint,
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no ProjectName',
|
||||
projectName: '',
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: Endpoint,
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no valid EndpointKey',
|
||||
projectName: ProjectName,
|
||||
endpointKey: 'NotValidGuid',
|
||||
endpoint: Endpoint,
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
{
|
||||
it: 'no valid Endpoint',
|
||||
projectName: ProjectName,
|
||||
endpointKey: EndpointKey,
|
||||
endpoint: 'NotValidEndpoint',
|
||||
deploymentName: DeploymentName,
|
||||
},
|
||||
];
|
||||
|
||||
describe('CluApplication Tests', function () {
|
||||
describe('Constructor should throw with bad arguments', function () {
|
||||
badArgumentCases.forEach((testCase) => {
|
||||
it(`${testCase.it}`, () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
new CluApplication(
|
||||
testCase.projectName,
|
||||
testCase.endpointKey,
|
||||
testCase.endpoint,
|
||||
testCase.deploymentName
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Constructor should work when using valid arguments', () => {
|
||||
const cluApplication = new CluApplication(
|
||||
ProjectName,
|
||||
EndpointKey,
|
||||
Endpoint,
|
||||
DeploymentName
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(cluApplication.projectName, ProjectName);
|
||||
assert.deepStrictEqual(cluApplication.endpointKey, EndpointKey);
|
||||
assert.deepStrictEqual(cluApplication.endpoint, Endpoint);
|
||||
assert.deepStrictEqual(cluApplication.deploymentName, DeploymentName);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
// Licensed under the MIT License.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
import 'mocha';
|
||||
import assert from 'assert';
|
||||
import { CluExtensions } from '@microsoft/bot-components-clu-recognizer/lib/cluExtensions';
|
||||
|
||||
const expectedMappedIntents: Record<string, number> = {
|
||||
OrderPizza: 0.79148775,
|
||||
Help: 0.51214343,
|
||||
CancelOrder: 0.44985053,
|
||||
None: 0,
|
||||
};
|
||||
|
||||
const sut = {
|
||||
topIntent: 'OrderPizza',
|
||||
projectKind: 'Conversation',
|
||||
intents: [
|
||||
{
|
||||
category: 'OrderPizza',
|
||||
confidenceScore: 0.79148775,
|
||||
},
|
||||
{
|
||||
category: 'Help',
|
||||
confidenceScore: 0.51214343,
|
||||
},
|
||||
{
|
||||
category: 'CancelOrder',
|
||||
confidenceScore: 0.44985053,
|
||||
},
|
||||
{
|
||||
category: 'None',
|
||||
confidenceScore: 0,
|
||||
},
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
category: 'DateTimeOfOrder',
|
||||
text: 'tomorrow',
|
||||
offset: 29,
|
||||
length: 8,
|
||||
confidenceScore: 1,
|
||||
resolutions: [
|
||||
{
|
||||
resolutionKind: 'DateTimeResolution',
|
||||
dateTimeSubKind: 'Date',
|
||||
timex: '2023-02-03',
|
||||
value: '2023-02-03',
|
||||
},
|
||||
],
|
||||
extraInformation: [
|
||||
{
|
||||
extraInformationKind: 'EntitySubtype',
|
||||
value: 'datetime.date',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Ingredients',
|
||||
text: 'ham',
|
||||
offset: 43,
|
||||
length: 3,
|
||||
confidenceScore: 1,
|
||||
extraInformation: [
|
||||
{
|
||||
extraInformationKind: 'ListKey',
|
||||
key: 'Ham',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Ingredients',
|
||||
text: 'cheese and onions',
|
||||
offset: 48,
|
||||
length: 17,
|
||||
confidenceScore: 1,
|
||||
},
|
||||
{
|
||||
category: 'DateTimeOfOrder',
|
||||
text: 'next week',
|
||||
offset: 89,
|
||||
length: 9,
|
||||
confidenceScore: 1,
|
||||
resolutions: [
|
||||
{
|
||||
resolutionKind: 'TemporalSpanResolution',
|
||||
begin: '2023-02-06',
|
||||
end: '2023-02-13',
|
||||
},
|
||||
],
|
||||
extraInformation: [
|
||||
{
|
||||
extraInformationKind: 'EntitySubtype',
|
||||
value: 'datetime.daterange',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('CluExtensions Tests', function () {
|
||||
it('ExtractIntents should extract intents from clu result', () => {
|
||||
const result = CluExtensions.extractIntents(sut);
|
||||
|
||||
for (const key in result) {
|
||||
const expectedKeys = Object.keys(expectedMappedIntents);
|
||||
if (expectedKeys.includes(key)) {
|
||||
const expectedIntentKey = expectedKeys.find((expKey) => expKey === key);
|
||||
|
||||
assert.strictEqual(key, expectedIntentKey);
|
||||
assert.strictEqual(result[key].score, expectedMappedIntents[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('ExtractEntities should extract entities from clu result', () => {
|
||||
const result = CluExtensions.extractEntities(sut);
|
||||
const resultArray = Object.entries(result);
|
||||
|
||||
assert.strictEqual(resultArray.length, 2);
|
||||
assert.strictEqual(resultArray[0][0], 'DateTimeOfOrder');
|
||||
assert.strictEqual(resultArray[1][0], 'Ingredients');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
// Licensed under the MIT License.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
import 'mocha';
|
||||
import assert from 'assert';
|
||||
import {
|
||||
HttpClient,
|
||||
HttpHeaders,
|
||||
HttpOperationResponse,
|
||||
WebResourceLike,
|
||||
} from '@azure/ms-rest-js';
|
||||
import {
|
||||
CluApplication,
|
||||
CluRecognizerOptions,
|
||||
} from '@microsoft/bot-components-clu-recognizer';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const responseContentStr = {
|
||||
kind: 'ConversationResult',
|
||||
result: {
|
||||
query: 'I want to order 3 pizzas with ham tomorrow',
|
||||
prediction: {
|
||||
topIntent: 'OrderPizza',
|
||||
projectKind: 'Conversation',
|
||||
intents: [
|
||||
{
|
||||
category: 'OrderPizza',
|
||||
confidenceScore: 0.9043113,
|
||||
},
|
||||
{
|
||||
category: 'None',
|
||||
confidenceScore: 0,
|
||||
},
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
category: 'Ingredients',
|
||||
text: 'ham',
|
||||
offset: 30,
|
||||
length: 3,
|
||||
confidenceScore: 1,
|
||||
extraInformation: [
|
||||
{
|
||||
extraInformationKind: 'ListKey',
|
||||
key: 'Ham',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'DateTimeOfOrder',
|
||||
text: 'tomorrow',
|
||||
offset: 34,
|
||||
length: 8,
|
||||
confidenceScore: 1,
|
||||
resolutions: [
|
||||
{
|
||||
resolutionKind: 'DateTimeResolution',
|
||||
dateTimeSubKind: 'Date',
|
||||
timex: '2023-02-04',
|
||||
value: '2023-02-04',
|
||||
},
|
||||
],
|
||||
extraInformation: [
|
||||
{
|
||||
extraInformationKind: 'EntitySubtype',
|
||||
value: 'datetime.date',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('CluRecognizerOptions Tests', function () {
|
||||
const options = new CluRecognizerOptions(
|
||||
new CluApplication(
|
||||
'MockProjectName',
|
||||
v4(),
|
||||
'https://mockendpoint.com',
|
||||
'MockDeploymentName'
|
||||
)
|
||||
);
|
||||
|
||||
it('Recognize should return recognizeResult when called with utterance', async () => {
|
||||
const httpClient = new HttpClientMock();
|
||||
|
||||
const result = await options.recognize('test', httpClient);
|
||||
|
||||
assert(result);
|
||||
assert.strictEqual(result.text, 'test');
|
||||
assert.strictEqual(result.alteredText, 'test');
|
||||
assert(result.intents);
|
||||
|
||||
const intents = Object.keys(result.intents);
|
||||
assert.strictEqual(intents.length, 2);
|
||||
|
||||
intents.forEach((intent) => {
|
||||
if (intent == 'OrderPizza') {
|
||||
assert.strictEqual(result.intents[intent].score, 0.9043113);
|
||||
} else if (intent == 'None') {
|
||||
assert.strictEqual(result.intents[intent].score, 0);
|
||||
}
|
||||
});
|
||||
|
||||
assert(result.entities);
|
||||
const expectedKeys = Object.keys(result.entities);
|
||||
assert.strictEqual(expectedKeys.length, 2);
|
||||
assert.strictEqual(expectedKeys.includes('DateTimeOfOrder'), true);
|
||||
assert.strictEqual(expectedKeys.includes('Ingredients'), true);
|
||||
});
|
||||
});
|
||||
|
||||
class HttpClientMock implements HttpClient {
|
||||
sendRequest(httpRequest: WebResourceLike): Promise<HttpOperationResponse> {
|
||||
return Promise.resolve({
|
||||
parsedBody: responseContentStr,
|
||||
headers: new HttpHeaders(),
|
||||
request: httpRequest,
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// Licensed under the MIT License.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
import 'mocha';
|
||||
import assert from 'assert';
|
||||
import {
|
||||
CluApplication,
|
||||
CluConstants,
|
||||
CluRecognizerOptionsBase,
|
||||
} from '@microsoft/bot-components-clu-recognizer';
|
||||
import { HttpClient } from '@azure/ms-rest-js';
|
||||
import {
|
||||
TurnContext,
|
||||
RecognizerResult,
|
||||
Activity,
|
||||
NullTelemetryClient,
|
||||
} from 'botbuilder';
|
||||
import { DialogContext } from 'botbuilder-dialogs';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('CluRecognizerOptionsBase Tests', function () {
|
||||
it('Should throw when application is undefined', () => {
|
||||
let application: CluApplication;
|
||||
|
||||
assert.throws(() => new CluRecognizerOptionsBaseMock(application));
|
||||
});
|
||||
|
||||
it('Should set application when application is defined', () => {
|
||||
const application: CluApplication = sinon.createStubInstance(
|
||||
CluApplication
|
||||
);
|
||||
|
||||
const options = new CluRecognizerOptionsBaseMock(application);
|
||||
|
||||
assert.deepStrictEqual(options.application, application);
|
||||
});
|
||||
|
||||
it('Should default properties with correct values', () => {
|
||||
const application: CluApplication = sinon.createStubInstance(
|
||||
CluApplication
|
||||
);
|
||||
|
||||
const options = new CluRecognizerOptionsBaseMock(application);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
options.timeout,
|
||||
CluConstants.HttpClientOptions.Timeout
|
||||
);
|
||||
assert.deepStrictEqual(options.telemetryClient, new NullTelemetryClient());
|
||||
assert.deepStrictEqual(
|
||||
options.cluRequestBodyStringIndexType,
|
||||
CluConstants.RequestOptions.StringIndexType
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
options.cluApiVersion,
|
||||
CluConstants.RequestOptions.ApiVersion
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
class CluRecognizerOptionsBaseMock extends CluRecognizerOptionsBase {
|
||||
constructor(application: CluApplication) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
recognize(
|
||||
turnContext: TurnContext,
|
||||
httpClient: HttpClient
|
||||
): Promise<RecognizerResult>;
|
||||
recognize(
|
||||
dialogContext: DialogContext,
|
||||
activity: Activity,
|
||||
httpClient: HttpClient
|
||||
): Promise<RecognizerResult>;
|
||||
recognize(
|
||||
utterance: string,
|
||||
httpClient: HttpClient
|
||||
): Promise<RecognizerResult>;
|
||||
recognize(
|
||||
_dialogContext: unknown,
|
||||
_activity: unknown,
|
||||
_httpClient?: unknown
|
||||
): Promise<RecognizerResult> {
|
||||
return Promise.resolve({ text: 'text', intents: {}, entities: {} });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// Licensed under the MIT License.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
import 'mocha';
|
||||
import assert from 'assert';
|
||||
import { DefaultHttpClientFactory } from '@microsoft/bot-components-clu-recognizer/lib/defaultHttpClientFactory';
|
||||
import { ActivityTypes, TestAdapter, TurnContext } from 'botbuilder';
|
||||
import {
|
||||
ConnectorClient,
|
||||
MicrosoftAppCredentials,
|
||||
} from 'botframework-connector';
|
||||
|
||||
describe('DefaultHttpClientFactory Tests', function () {
|
||||
const connectorClient = new ConnectorClient(
|
||||
new MicrosoftAppCredentials('abc', '123'),
|
||||
{
|
||||
baseUri: 'https://smba.trafficmanager.net/amer/',
|
||||
}
|
||||
);
|
||||
|
||||
const adapter = new TestAdapter();
|
||||
const activity = {
|
||||
type: ActivityTypes.Message,
|
||||
text: 'test',
|
||||
};
|
||||
const context = new TurnContext(adapter, activity);
|
||||
context.turnState.set(context.adapter.ConnectorClientKey, connectorClient);
|
||||
|
||||
it('Create should return same http client instance', async () => {
|
||||
const factory = new DefaultHttpClientFactory(context);
|
||||
|
||||
const firstClient = factory.create();
|
||||
const secondClient = factory.create();
|
||||
|
||||
assert.strictEqual(firstClient === secondClient, true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"composite": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
43
yarn.lock
43
yarn.lock
|
@ -918,6 +918,26 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@microsoft/bot-components-clu-recognizer-tests@workspace:tests/unit/packages/Recognizers/js":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@microsoft/bot-components-clu-recognizer-tests@workspace:tests/unit/packages/Recognizers/js"
|
||||
dependencies:
|
||||
"@microsoft/bot-components-clu-recognizer": "workspace:packages/Recognizers/ConversationLanguageUnderstanding/js"
|
||||
"@types/mocha": ^8.2.2
|
||||
"@types/sinon": ^10.0.16
|
||||
"@types/uuid": ^9.0.2
|
||||
botbuilder: 4.19.3
|
||||
botbuilder-dialogs-adaptive: 4.19.3-preview
|
||||
botbuilder-dialogs-adaptive-testing: 4.19.3-preview
|
||||
botframework-connector: 4.19.3
|
||||
eslint: ^7.30.0
|
||||
mocha: ^9.0.2
|
||||
nock: ^13.1.1
|
||||
ts-node: ^10.0.0
|
||||
uuid: ^8.3.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@microsoft/bot-components-clu-recognizer@workspace:packages/Recognizers/ConversationLanguageUnderstanding/js":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@microsoft/bot-components-clu-recognizer@workspace:packages/Recognizers/ConversationLanguageUnderstanding/js"
|
||||
|
@ -1660,6 +1680,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/sinon@npm:^10.0.16":
|
||||
version: 10.0.16
|
||||
resolution: "@types/sinon@npm:10.0.16"
|
||||
dependencies:
|
||||
"@types/sinonjs__fake-timers": "*"
|
||||
checksum: fbce0708cf11f4962fa17a57e0cc27c93c381a2220764a3f059d23263875063047c9a8cdf3d5a7b69bb19a0d909c9c04710cddf6ce89243499712a85469170eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/sinonjs__fake-timers@npm:*":
|
||||
version: 8.1.2
|
||||
resolution: "@types/sinonjs__fake-timers@npm:8.1.2"
|
||||
checksum: 27e88de175c4ee8e95338be9f8bfef9ba29bfa505f67addd227242aa0d57deb0cfd237e1596105da4b71fd389a8eb1c6030f0322cda350fd7aa6f2bb2736ecbf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stack-utils@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@types/stack-utils@npm:1.0.1"
|
||||
|
@ -1676,6 +1712,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/uuid@npm:^9.0.2":
|
||||
version: 9.0.2
|
||||
resolution: "@types/uuid@npm:9.0.2"
|
||||
checksum: c4f5a8e284a3daca4638384445824246f862ed021526598481fa85f8ee0208aa5bed44db32258e39c0981d2b00666d025359956087f1972b6d8ec3c14290995f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^6.0.3":
|
||||
version: 6.0.4
|
||||
resolution: "@types/ws@npm:6.0.4"
|
||||
|
|
Загрузка…
Ссылка в новой задаче