diff --git a/lib/clients/metric-client/index.js b/lib/clients/metric-client/index.js index 3e371a52..e7852d46 100644 --- a/lib/clients/metric-client/index.js +++ b/lib/clients/metric-client/index.js @@ -1,5 +1,8 @@ const uuid = require('uuid/v4'); const axios = require('axios'); +const AppConfig = require('@src/model/app-config'); +const { METRICS } = require('@src/utils/constants'); +const pck = require('../../../package.json'); const MetricActionResult = { SUCCESS: 'Success', @@ -57,37 +60,25 @@ class MetricAction { } class MetricClient { - /** - * A metric client options - * @typedef {Object} MetricClientOptions - * @property {string} version - The application version. Typically, version form package.json - * @property {string} machineId - The machine id - * @property {boolean} newUser - is new user - * @property {string} clientId - The client id. Typically, application name. For example, ask cli. - * @property {string} serverUrl - The server url where to send metrics data. - * @property {number} sendTimeout - The send timeout to send data to the metrics server. - */ - /** * @constructor - * @param {MetricClientOptions} options - The options for constructor */ - constructor(options) { - const { version, machineId, newUser, clientId, serverUrl, sendTimeout, enabled } = options; + constructor() { this.httpClient = axios.create({ - timeout: sendTimeout || 3000, + timeout: 3000, headers: { 'Content-Type': 'text/plain' } }); - this.serverUrl = serverUrl; + this.serverUrl = METRICS.ENDPOINT; this.postRetries = 3; - this.enabled = enabled !== false; + + this.enabled = this._isEnabled(); this.data = { - version, - machineId, + version: pck.version, + machineId: this._getMachineId(), timeStarted: new Date(), - newUser, + newUser: false, // default to false since unused. timeUploaded: null, - clientId, + clientId: pck.name, actions: [] }; } @@ -157,6 +148,25 @@ class MetricClient { _retry(retries, fn) { return fn().catch(err => (retries > 1 ? this._retry(retries - 1, fn) : Promise.reject(err))); } + + _isEnabled() { + if (process.env.ASK_SHARE_USAGE === 'false') return false; + if (!AppConfig.configFileExists()) return false; + + new AppConfig(); + return AppConfig.getInstance().getShareUsage(); + } + + _getMachineId() { + if (!this.enabled) return; + const appConfig = AppConfig.getInstance(); + if (!appConfig.getMachineId()) { + appConfig.setMachineId(uuid()); + appConfig.write(); + } + + return appConfig.getMachineId(); + } } module.exports = { MetricClient, MetricActionResult }; diff --git a/lib/commands/abstract-command.js b/lib/commands/abstract-command.js index 74a4473e..a7df0a06 100644 --- a/lib/commands/abstract-command.js +++ b/lib/commands/abstract-command.js @@ -76,7 +76,7 @@ class AbstractCommand { Messenger.getInstance().doDebug = commandInstance.debug; // Start metric client - metricClient.startAction(commandInstance._name, 'command'); + metricClient.startAction(commandInstance._name, ''); const isCredentialHelperCmd = commandInstance._name === 'git-credentials-helper'; // Check if a new CLI version is released diff --git a/lib/commands/smapi/smapi-commander.js b/lib/commands/smapi/smapi-commander.js index 76b68174..e901dda4 100644 --- a/lib/commands/smapi/smapi-commander.js +++ b/lib/commands/smapi/smapi-commander.js @@ -3,9 +3,10 @@ const { ModelIntrospector } = require('ask-smapi-sdk'); const { kebabCase } = require('@src/utils/string-utils'); const { CliCustomizationProcessor } = require('@src/commands/smapi/cli-customization-processor'); const optionModel = require('@src/commands/option-model.json'); -const { smapiCommandHandler } = require('@src/commands/smapi/smapi-command-handler'); +const handler = require('@src/commands/smapi/smapi-command-handler'); const aliases = require('@src/commands/smapi/customizations/aliases.json'); const { apiToCommanderMap } = require('@src/commands/smapi/customizations/parameters-map'); +const metricClient = require('@src/utils/metrics'); const uploadCatalog = require('./appended-commands/upload-catalog'); const exportPackage = require('./appended-commands/export-package'); @@ -99,13 +100,17 @@ const makeSmapiCommander = () => { defaultOptions.forEach(option => { commanderInstance.option(option.flags, option.description); }); - commanderInstance.action((inputCmdObj) => smapiCommandHandler( - apiOperationName, - flatParamsMap, - commanderToApiCustomizationMap, - inputCmdObj, - modelIntrospector - )); + commanderInstance.action((inputCmdObj) => { + metricClient.startAction('smapi', inputCmdObj.name()); + return handler.smapiCommandHandler( + apiOperationName, + flatParamsMap, + commanderToApiCustomizationMap, + inputCmdObj, + modelIntrospector + ).then(res => metricClient.sendData().then(() => res)) + .catch(err => metricClient.sendData(err).then(() => Promise.reject(err))); + }); }); // register hand-written appended commands diff --git a/lib/model/app-config.js b/lib/model/app-config.js index 32bc6283..3db43493 100644 --- a/lib/model/app-config.js +++ b/lib/model/app-config.js @@ -1,3 +1,4 @@ +const fs = require('fs-extra'); const os = require('os'); const path = require('path'); @@ -36,6 +37,10 @@ module.exports = class AppConfig extends ConfigFile { instance = null; } + static configFileExists() { + return fs.existsSync(defaultFilePath); + } + // getter and setter getAwsProfile(profile) { @@ -68,6 +73,21 @@ module.exports = class AppConfig extends ConfigFile { this.setProperty(['profiles', profile, 'vendor_id'], vendorId); } + setMachineId(machineId) { + this.setProperty(['machine_id'], machineId); + } + + getMachineId() { + return this.getProperty(['machine_id']); + } + + getShareUsage() { + const shareUsage = this.getProperty(['share_usage']); + if (shareUsage !== undefined) return shareUsage; + + return true; + } + /** * Returns all profile names and their associated aws profile names (if any) as list of objects. * return profilesList. Eg: [{ askProfile: 'askProfile1', awsProfile: 'awsProfile1'}, { askProfile: 'askProfile2', awsProfile: 'awsProfile2'}]. diff --git a/lib/model/metric-config.js b/lib/model/metric-config.js deleted file mode 100644 index 7df9ef0f..00000000 --- a/lib/model/metric-config.js +++ /dev/null @@ -1,44 +0,0 @@ -const fs = require('fs-extra'); -const jsonfile = require('jsonfile'); -const os = require('os'); -const path = require('path'); -const uuid = require('uuid/v4'); -const { FILE_PATH, CONFIGURATION, METRICS } = require('@src/utils/constants'); - -const askFolderPath = path.join(os.homedir(), FILE_PATH.ASK.HIDDEN_FOLDER); -const defaultMetricFilePath = path.join(askFolderPath, FILE_PATH.ASK.METRIC_FILE); - -class MetricConfig { - /** - * Constructor for MetricConfig class - * @param {string} filePath - */ - constructor(filePath = defaultMetricFilePath) { - // making file path if not exists - if (!fs.existsSync(filePath)) { - fs.ensureDirSync(askFolderPath); - jsonfile.writeFileSync(filePath, { machineId: uuid(), createdAt: new Date() }, { spaces: CONFIGURATION.JSON_DISPLAY_INDENT }); - } - this.data = JSON.parse(fs.readFileSync(filePath)); - } - - /** - * Gets machineId property - * @returns {string} - */ - get machineId() { - return this.data.machineId; - } - - /** - * Returns boolean indicating if user is new - * @returns {boolean} - */ - isNewUser() { - const { createdAt } = this.data; - const daysDiff = (new Date().getTime() - new Date(createdAt).getTime()) / (1000 * 3600 * 24); - return daysDiff <= METRICS.NEW_USER_LENGTH_DAYS; - } -} - -module.exports = MetricConfig; diff --git a/lib/utils/constants.js b/lib/utils/constants.js index c161f08e..e59eacdf 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -2,8 +2,7 @@ module.exports.APPLICATION_NAME = 'ask-cli'; module.exports.NPM_REGISTRY_URL_BASE = 'http://registry.npmjs.org'; module.exports.METRICS = { - ENDPOINT: '', // TODO add the official endpoint when we have it - NEW_USER_LENGTH_DAYS: 3 + ENDPOINT: 'https://client-telemetry.amazonalexa.com' }; module.exports.DEPLOYER_TYPE = { diff --git a/lib/utils/metrics.js b/lib/utils/metrics.js index 1923b8a9..6ba42df3 100644 --- a/lib/utils/metrics.js +++ b/lib/utils/metrics.js @@ -1,18 +1,6 @@ const { MetricClient } = require('@src/clients/metric-client'); -// const MetricConfig = require('@src/model/metric-config'); -const { METRICS } = require('@src/utils/constants'); -const { name, version } = require('./../../package.json'); -// TODO enable when we have configure command prompting for telemetry -// const metricConfig = new MetricConfig(); - -const metricClient = new MetricClient({ - version, - machineId: '', // metricConfig.machineId, - newUser: false, // metricConfig.isNewUser(), - clientId: name, - serverUrl: METRICS.ENDPOINT, - enabled: false // TODO make it dependent on configure command -}); +// metric client singleton +const metricClient = new MetricClient(); module.exports = metricClient; diff --git a/package.json b/package.json index a6134f0a..e36699c5 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "functional-test": "mocha -t 180000 test/functional/run-test.js", "lint": "eslint lib/builtins lib/clients lib/commands lib/controllers lib/model lib/view", "pre-release": "standard-version", - "prism": "prism" + "prism": "prism", + "postinstall": "node postinstall.js" }, "dependencies": { "adm-zip": "^0.4.13", diff --git a/postinstall.js b/postinstall.js new file mode 100644 index 00000000..31014c47 --- /dev/null +++ b/postinstall.js @@ -0,0 +1,9 @@ +console.log(` +================================================================================ +ASK CLI collects telemetry to better understand customer needs. You can +OPT OUT and disable telemetry by setting the 'share_usage' key to 'false' +in '~/.ask/cli_config'. + +Learn more: https://developer.amazon.com/docs/alexa/smapi/ask-cli-telemetry.html +================================================================================ +`); diff --git a/test/functional/run-test.js b/test/functional/run-test.js index bacdaa1c..b1ea8f82 100644 --- a/test/functional/run-test.js +++ b/test/functional/run-test.js @@ -1,5 +1,7 @@ require('module-alias/register'); +process.env.ASK_SHARE_USAGE = false; + [ '@test/functional/commands/high-level-commands-test.js' ].forEach((testFile) => { diff --git a/test/integration/run-test.js b/test/integration/run-test.js index 7cf77963..54d63b19 100644 --- a/test/integration/run-test.js +++ b/test/integration/run-test.js @@ -1,5 +1,7 @@ require('module-alias/register'); +process.env.ASK_SHARE_USAGE = false; + [ '@test/integration/commands/smapi-commands-test.js' ].forEach((testFile) => { diff --git a/test/unit/clients/metric-client-test.js b/test/unit/clients/metric-client-test.js index 1e789990..9ccd6b54 100644 --- a/test/unit/clients/metric-client-test.js +++ b/test/unit/clients/metric-client-test.js @@ -1,7 +1,10 @@ const chai = require('chai'); +const uuid = require('uuid/v4'); const chaiUuid = require('chai-uuid'); const chaiJsonSchema = require('chai-json-schema'); const sinon = require('sinon'); +const { METRICS } = require('@src/utils/constants'); +const AppConfig = require('@src/model/app-config'); const { MetricClient, MetricActionResult } = require('@src/clients/metric-client'); const jsonSchema = require('@test/fixture/ask-devtools-metrics.schema.json'); @@ -10,35 +13,62 @@ chai.use(chaiUuid); chai.use(chaiJsonSchema); describe('Clients test - cli metric client', () => { - const clientOptions = { - version: '1.0.1', - machineId: '8b2723f2-a25e-4d2c-89df-f9f590b8bb6e', - newUser: true, - clientId: 'ASK CLI', - serverUrl: 'https://somehost.com/dev/telemetry', - }; + const getShareUsageStub = sinon.stub(); + const getMachineIdStub = sinon.stub(); + const setMachineIdStub = sinon.stub(); + const writeConfigStub = sinon.stub(); + let configExistsStub; + let shareUsageVariableValue; + + beforeEach(() => { + shareUsageVariableValue = process.env.ASK_SHARE_USAGE; + delete process.env.ASK_SHARE_USAGE; + configExistsStub = sinon.stub(AppConfig, 'configFileExists').returns(true); + sinon.stub(AppConfig.prototype, 'read'); + sinon.stub(AppConfig, 'getInstance').returns({ + getShareUsage: getShareUsageStub.returns(true), + getMachineId: getMachineIdStub.returns(uuid()), + setMachineId: setMachineIdStub, + write: writeConfigStub, + read: sinon.stub() + }); + }); - const { version, machineId, newUser, clientId, serverUrl } = clientOptions; + afterEach(() => { + process.env.ASK_SHARE_USAGE = shareUsageVariableValue; + sinon.restore(); + }); describe('# constructor validation', () => { it('| creates instance of MetricClient, expect initial data to be set', () => { // set up - const client = new MetricClient(clientOptions); + const client = new MetricClient(); // call const data = client.getData(); // verify - expect(data).include({ version, machineId, newUser, clientId, timeUploaded: null }); + expect(data).have.keys(['actions', 'version', 'machineId', 'newUser', 'timeStarted', 'clientId', 'timeUploaded']); expect(data.actions).eql([]); expect(data.timeStarted).instanceof(Date); }); + + it('| creates machine id if it does not exist', () => { + getMachineIdStub.returns(undefined); + + // call + new MetricClient(); + + // verify + expect(setMachineIdStub.callCount).eql(1); + expect(writeConfigStub.callCount).eql(1); + }); }); describe('# start action validation', () => { let client; beforeEach(() => { - client = new MetricClient(clientOptions); + client = new MetricClient(); }); it('| adds action, expect action to be set', () => { @@ -88,7 +118,7 @@ describe('Clients test - cli metric client', () => { const name = 'ask.clone'; const type = 'userCommand'; beforeEach(() => { - client = new MetricClient(clientOptions); + client = new MetricClient(); action = client.startAction(name, type); }); @@ -151,8 +181,9 @@ describe('Clients test - cli metric client', () => { let httpClientPostStub; const name = 'ask.clone'; const type = 'userCommand'; + beforeEach(() => { - client = new MetricClient(clientOptions); + client = new MetricClient(); client.startAction(name, type); }); @@ -171,14 +202,29 @@ describe('Clients test - cli metric client', () => { expect(success).eql(true); expect(httpClientPostStub.calledOnce).eql(true); - expect(calledUrl).eql(serverUrl); + expect(calledUrl).eql(METRICS.ENDPOINT); expect(JSON.parse(calledPayload)).to.be.jsonSchema(jsonSchema); }); it('| sends metrics, expect metrics not to be send to metric server when enabled is false', async () => { + configExistsStub.returns(false); + httpClientPostStub = sinon.stub(client.httpClient, 'post'); + + const disabledClient = new MetricClient(); + + // call + const { success } = await disabledClient.sendData(); + + // verify + expect(success).eql(true); + expect(httpClientPostStub.called).eql(false); + }); + + it('| sends metrics, expect metrics not to be send to metric server when ASK_SHARE_USAGE is false', async () => { + process.env.ASK_SHARE_USAGE = false; httpClientPostStub = sinon.stub(client.httpClient, 'post'); - const disabledClient = new MetricClient({ enabled: false, ...clientOptions }); + const disabledClient = new MetricClient(); // call const { success } = await disabledClient.sendData(); @@ -199,7 +245,7 @@ describe('Clients test - cli metric client', () => { expect(success).eql(false); expect(httpClientPostStub.calledThrice).eql(true); - expect(calledUrl).eql(serverUrl); + expect(calledUrl).eql(METRICS.ENDPOINT); expect(JSON.parse(calledPayload)).to.be.jsonSchema(jsonSchema); }); }); diff --git a/test/unit/commands/abstract-command-test.js b/test/unit/commands/abstract-command-test.js index 63182e81..f27fdfc5 100644 --- a/test/unit/commands/abstract-command-test.js +++ b/test/unit/commands/abstract-command-test.js @@ -7,7 +7,6 @@ const httpClient = require('@src/clients/http-client'); const { AbstractCommand } = require('@src/commands/abstract-command'); const AppConfig = require('@src/model/app-config'); const CONSTANTS = require('@src/utils/constants'); -const metricClient = require('@src/utils/metrics'); const Messenger = require('@src/view/messenger'); const packageJson = require('@root/package.json'); @@ -60,7 +59,6 @@ describe('Command test - AbstractCommand class', () => { sinon.stub(path, 'join').returns(APP_CONFIG_NO_PROFILES_PATH); mockProcessExit = sinon.stub(process, 'exit'); mockConsoleError = sinon.stub(console, 'error'); - sinon.stub(metricClient, 'sendData').resolves(); sinon.stub(AbstractCommand.prototype, '_remindsIfNewVersion').callsArgWith(2); }); @@ -456,7 +454,6 @@ It is recommended to use the latest version. Please update using "npm upgrade -g beforeEach(() => { AppConfigReadStub = sinon.stub(AppConfig.prototype, 'read'); sinon.stub(process, 'exit'); - sinon.stub(metricClient, 'sendData').resolves(); }); it('| should not be null for non-configure commands', async () => { diff --git a/test/unit/commands/smapi/smapi-commander-test.js b/test/unit/commands/smapi/smapi-commander-test.js index 70bcd27e..8d7faa91 100644 --- a/test/unit/commands/smapi/smapi-commander-test.js +++ b/test/unit/commands/smapi/smapi-commander-test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const sinon = require('sinon'); const { makeSmapiCommander } = require('@src/commands/smapi/smapi-commander'); - +const handler = require('@src/commands/smapi/smapi-command-handler'); describe('Smapi test - makeSmapiCommander function', () => { beforeEach(() => { @@ -24,6 +24,23 @@ describe('Smapi test - makeSmapiCommander function', () => { expect(console.error.calledWithMatch('Command not recognized')).eql(true); }); + it('| should execute a command successfully', () => { + sinon.stub(handler, 'smapiCommandHandler').resolves('some data'); + const commander = makeSmapiCommander(); + + return commander.parseAsync(['', '', 'list-skills-for-vendor']) + .then(res => expect(res[0]).eql('some data')); + }); + + it('| should propagate error if handler fails', () => { + sinon.stub(handler, 'smapiCommandHandler').rejects(new Error('some error')); + const commander = makeSmapiCommander(); + + return commander.parseAsync(['', '', 'list-skills-for-vendor']) + .then(res => expect(res).eql(undefined)) + .catch(err => expect(err.message).eql('some error')); + }); + afterEach(() => { sinon.restore(); }); diff --git a/test/unit/fixture/model/app-config.json b/test/unit/fixture/model/app-config.json index d73e6822..346d73ce 100644 --- a/test/unit/fixture/model/app-config.json +++ b/test/unit/fixture/model/app-config.json @@ -1,4 +1,6 @@ { + "machine_id": "machineId", + "share_usage": false, "profiles": { "testProfile": { "aws_profile": "awsProfile", diff --git a/test/unit/fixture/model/app-config.yaml b/test/unit/fixture/model/app-config.yaml index 01fceab1..a04de766 100644 --- a/test/unit/fixture/model/app-config.yaml +++ b/test/unit/fixture/model/app-config.yaml @@ -1,3 +1,5 @@ +machine_id: machineId +share_usage: false profiles: testProfile: aws_profile: awsProfile diff --git a/test/unit/model/app-config-test.js b/test/unit/model/app-config-test.js index 32f82fd7..c36d1c09 100644 --- a/test/unit/model/app-config-test.js +++ b/test/unit/model/app-config-test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const sinon = require('sinon'); const path = require('path'); -const fs = require('fs'); +const fs = require('fs-extra'); const jsonfile = require('jsonfile'); const profileHelper = require('@src/utils/profile-helper'); @@ -141,6 +141,24 @@ describe('Model test - app config test', () => { }); }); + it('test getMachineId function successfully', () => { + expect(AppConfig.getInstance().getMachineId()).deep.equal('machineId'); + }); + + it('test setMachineId function successfully', () => { + AppConfig.getInstance().setMachineId('new Machine id'); + expect(AppConfig.getInstance().getMachineId()).equal('new Machine id'); + }); + + it('test getShareUsage function successfully', () => { + expect(AppConfig.getInstance().getShareUsage()).deep.equal(false); + }); + + it('test getShareUsage when property is not set', () => { + sinon.stub(AppConfig.prototype, 'getProperty').returns(); + expect(AppConfig.getInstance().getShareUsage()).deep.equal(true); + }); + afterEach(() => { AppConfig.dispose(); sinon.restore(); @@ -201,4 +219,22 @@ describe('Model test - app config test', () => { AppConfig.dispose(); }); }); + + describe('# inspect correctness of configFileExists', () => { + it('| returns true if config file exists', () => { + sinon.stub(fs, 'existsSync').returns(true); + + expect(AppConfig.configFileExists()).to.equal(true); + }); + + it('| returns false if config file does not exist', () => { + sinon.stub(fs, 'existsSync').returns(false); + + expect(AppConfig.configFileExists()).to.equal(false); + }); + + afterEach(() => { + sinon.restore(); + }); + }); }); diff --git a/test/unit/model/metric-config-test.js b/test/unit/model/metric-config-test.js deleted file mode 100644 index 94538366..00000000 --- a/test/unit/model/metric-config-test.js +++ /dev/null @@ -1,73 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const fs = require('fs-extra'); -const jsonfile = require('jsonfile'); -const { METRICS } = require('@src/utils/constants'); -const MetricConfig = require('@src/model/metric-config'); - -describe('Model test - metric config test', () => { - const configPath = 'somepath'; - - describe('# inspect correctness of getters when config file is present', () => { - const existingMachineId = 'existing-machine-id'; - - beforeEach(() => { - const createdAt = new Date(); - createdAt.setDate(createdAt.getDate() - METRICS.NEW_USER_LENGTH_DAYS - 1); - - sinon.stub(fs, 'existsSync').returns(true); - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ machineId: existingMachineId, createdAt })); - sinon.stub(jsonfile, 'writeFileSync'); - }); - - it('| returns correct machine id', () => { - const metricConfig = new MetricConfig(configPath); - const { machineId } = metricConfig; - expect(metricConfig).to.be.instanceof(MetricConfig); - expect(machineId).eq(existingMachineId); - }); - - it('| returns correct is new user flag', () => { - const metricConfig = new MetricConfig(configPath); - const isNewUser = metricConfig.isNewUser(); - expect(metricConfig).to.be.instanceof(MetricConfig); - expect(isNewUser).eq(false); - }); - - afterEach(() => { - sinon.restore(); - }); - }); - - describe('# inspect correctness of getters when config file is not present', () => { - const newMachineId = 'new-machine-id'; - - beforeEach(() => { - const createdAt = new Date(); - createdAt.setDate(createdAt.getDate() - METRICS.NEW_USER_LENGTH_DAYS + 1); - sinon.stub(fs, 'existsSync').returns(true); - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ machineId: newMachineId, createdAt })); - sinon.stub(jsonfile, 'writeFileSync'); - }); - - it('| returns correct machine id', () => { - const metricConfig = new MetricConfig(configPath); - const { machineId } = metricConfig; - expect(metricConfig).to.be.instanceof(MetricConfig); - expect(machineId).eq(newMachineId); - }); - - it('| returns correct is new user flag', () => { - const metricConfig = new MetricConfig(configPath); - const isNewUser = metricConfig.isNewUser(); - expect(metricConfig).to.be.instanceof(MetricConfig); - expect(isNewUser).eq(true); - }); - - afterEach(() => { - sinon.restore(); - }); - }); -}); diff --git a/test/unit/run-test.js b/test/unit/run-test.js index 2c3929dd..c7120ac7 100644 --- a/test/unit/run-test.js +++ b/test/unit/run-test.js @@ -1,5 +1,7 @@ require('module-alias/register'); +process.env.ASK_SHARE_USAGE = false; + /** * This list manages all the files we want to cover during the refactor. * Please also include the test-ready module in package.json's "nyc.include" list. @@ -76,7 +78,6 @@ require('module-alias/register'); '@test/unit/model/abstract-config-file-test', '@test/unit/model/app-config-test', '@test/unit/model/dialog-save-skill-io-file-test', - '@test/unit/model/metric-config-test', '@test/unit/model/manifest-test', '@test/unit/model/resources-config/resources-config-test', '@test/unit/model/resources-config/ask-resources-test',