diff --git a/docs/concepts/Dialog-Command.md b/docs/concepts/Dialog-Command.md index 6b918568..a9c1bc0d 100644 --- a/docs/concepts/Dialog-Command.md +++ b/docs/concepts/Dialog-Command.md @@ -56,7 +56,7 @@ User > .quit ``` #### Special sub-commands: -**.record**: To record the list of utterances so far in a JSON file. User can continue to interact with the skill once the replay file has been created. +**.record**: To record the list of utterances so far in a JSON file. User can continue to interact with the skill once the replay file has been created. This command provides user an option `--append-quit`, which the user can append to record command, to add `.quit` to list of utterances before creation of replay file. Format: `.record ` or `.record --append-quit`. **.quit**: Exits the Interactive mode. diff --git a/lib/commands/dialog/index.js b/lib/commands/dialog/index.js index db666d7a..d519c754 100644 --- a/lib/commands/dialog/index.js +++ b/lib/commands/dialog/index.js @@ -3,6 +3,7 @@ const path = require('path'); const SmapiClient = require('@src/clients/smapi-client'); const { AbstractCommand } = require('@src/commands/abstract-command'); const optionModel = require('@src/commands/option-model'); +const CliError = require('@src/exceptions/cli-error'); const DialogReplayFile = require('@src/model/dialog-replay-file'); const ResourcesConfig = require('@src/model/resources-config'); const CONSTANTS = require('@src/utils/constants'); @@ -89,7 +90,7 @@ class DialogCommand extends AbstractCommand { new ResourcesConfig(path.join(process.cwd(), CONSTANTS.FILE_PATH.ASK_RESOURCES_JSON_CONFIG)); skillId = ResourcesConfig.getInstance().getSkillId(profile); } catch (err) { - throw 'Failed to read project resource file.'; + throw new CliError('Failed to read project resource file. Please run the command within a ask-cli project.'); } if (!stringUtils.isNonBlankString(skillId)) { throw `Failed to obtain skill-id from project resource file ${CONSTANTS.FILE_PATH.ASK_RESOURCES_JSON_CONFIG}`; diff --git a/lib/commands/dialog/replay-mode.js b/lib/commands/dialog/replay-mode.js index f5d50fb2..50650466 100644 --- a/lib/commands/dialog/replay-mode.js +++ b/lib/commands/dialog/replay-mode.js @@ -54,6 +54,7 @@ module.exports = class ReplayMode extends DialogController { replayReplView.close(); this.config.header = 'Switching to interactive dialog.\n' + 'To automatically quit after replay, append \'.quit\' to the userInput of your replay file.'; + this.config.session = false; const interactiveReplView = new InteractiveMode(this.config); interactiveReplView.start(callback); } diff --git a/lib/controllers/dialog-controller/index.js b/lib/controllers/dialog-controller/index.js index 307c0070..fd7b1337 100644 --- a/lib/controllers/dialog-controller/index.js +++ b/lib/controllers/dialog-controller/index.js @@ -8,6 +8,7 @@ const Messenger = require('@src/view/messenger'); const responseParser = require('@src/controllers/dialog-controller/simulation-response-parser'); const SkillSimulationController = require('@src/controllers/skill-simulation-controller'); +const RECORD_FORMAT = 'Please use the format: ".record " or ".record --append-quit"'; module.exports = class DialogController extends SkillSimulationController { /** * Constructor for DialogModeController. @@ -15,7 +16,7 @@ module.exports = class DialogController extends SkillSimulationController { */ constructor(configuration) { super(configuration); - this.newSession = true; + this.newSession = configuration.newSession === false ? configuration.newSession : true; this.utteranceCache = []; } @@ -67,24 +68,54 @@ module.exports = class DialogController extends SkillSimulationController { * @param {Function} callback */ setupSpecialCommands(dialogReplView, callback) { - dialogReplView.registerRecordCommand((filePath) => { - if (!stringUtils.isNonBlankString(filePath)) { - return callback('A file name has not been specified'); + dialogReplView.registerRecordCommand((recordArgs) => { + const recordArgsList = recordArgs.trim().split(' '); + if (!stringUtils.isNonBlankString(recordArgs) || recordArgsList.length > 2) { + return Messenger.getInstance().warn(`Incorrect format. ${RECORD_FORMAT}`); } - const JSON_FILE_EXTENSION = '.json'; - if (path.extname(filePath).toLowerCase() !== JSON_FILE_EXTENSION) { - return callback(`File should be of type '${JSON_FILE_EXTENSION}'`); + const { filePath, shouldAppendQuit } = this._validateRecordCommandInput(recordArgsList, RECORD_FORMAT); + const utteranceCacheCopy = [...this.utteranceCache]; + if (shouldAppendQuit) { + utteranceCacheCopy.push('.quit'); } - try { - this.createReplayFile(filePath); - Messenger.getInstance().info(`Created replay file at ${filePath}`); - } catch (err) { - return callback(err); + if (filePath) { + try { + this.createReplayFile(filePath, utteranceCacheCopy); + Messenger.getInstance().info(`Created replay file at ${filePath}` + + `${shouldAppendQuit ? ' (appended ".quit" to list of utterances).' : ''}`); + } catch (replayFileCreationError) { + return callback(replayFileCreationError); + } } }); dialogReplView.registerQuitCommand(() => {}); } + /** + * Validate record command arguments. + * @param {Array} recordArgsList + * @param {String} recordCommandFormat + */ + _validateRecordCommandInput(recordArgsList) { + const filePath = recordArgsList[0]; + const appendQuitArgument = recordArgsList[1]; + let shouldAppendQuit = false; + const JSON_FILE_EXTENSION = '.json'; + + if (path.extname(filePath).toLowerCase() !== JSON_FILE_EXTENSION) { + Messenger.getInstance().warn(`File should be of type '${JSON_FILE_EXTENSION}'`); + return {}; + } + if (stringUtils.isNonBlankString(appendQuitArgument)) { + if (appendQuitArgument !== '--append-quit') { + Messenger.getInstance().warn(`Unable to validate arguments: "${appendQuitArgument}". ${RECORD_FORMAT}`); + return {}; + } + shouldAppendQuit = true; + } + return { filePath, shouldAppendQuit }; + } + /** * Start skill simulation by calling SMAPI POST skill simulation endpoint. * @param {String} utterance text utterance to simulate against. @@ -136,13 +167,13 @@ module.exports = class DialogController extends SkillSimulationController { * Function to create replay file. * @param {String} filename name of file to save replay JSON. */ - createReplayFile(filename) { + createReplayFile(filename, utterances) { if (stringUtils.isNonBlankString(filename)) { const content = { skillId: this.skillId, locale: this.locale, type: 'text', - userInput: this.utteranceCache + userInput: utterances }; fs.outputJSONSync(filename, content); } diff --git a/test/unit/commands/dialog/index-test.js b/test/unit/commands/dialog/index-test.js index 01a924ea..06412e68 100644 --- a/test/unit/commands/dialog/index-test.js +++ b/test/unit/commands/dialog/index-test.js @@ -234,7 +234,7 @@ describe('Commands Dialog test - command class test', () => { // call instance.handle(TEST_CMD_WITH_VALUES, (err) => { // verify - expect(err).equal('Failed to read project resource file.'); + expect(err.message).equal('Failed to read project resource file. Please run the command within a ask-cli project.'); done(); }); }); diff --git a/test/unit/controller/dialog-controller/index-test.js b/test/unit/controller/dialog-controller/index-test.js index 6969bf75..88d3a3a1 100644 --- a/test/unit/controller/dialog-controller/index-test.js +++ b/test/unit/controller/dialog-controller/index-test.js @@ -4,12 +4,12 @@ const fs = require('fs-extra'); const sinon = require('sinon'); const DialogController = require('@src/controllers/dialog-controller'); -const stringUtils = require('@src/utils/string-utils'); const DialogReplView = require('@src/view/dialog-repl-view'); const Messenger = require('@src/view/messenger'); describe('Controller test - dialog controller test', () => { const TEST_MSG = 'TEST_MSG'; + const RECORD_FORMAT = 'Please use the format: ".record " or ".record --append-quit"'; describe('# test constructor', () => { it('| constructor with config parameter returns DialogController object', () => { @@ -19,7 +19,8 @@ describe('Controller test - dialog controller test', () => { debug: true, skillId: 'a1b2c3', locale: 'en-US', - stage: 'DEVELOPMENT' + stage: 'DEVELOPMENT', + newSession: false }); // verify expect(dialogController).to.be.instanceOf(DialogController); @@ -178,14 +179,16 @@ describe('Controller test - dialog controller test', () => { it('| successful replay file creation', () => { // setup + const utterances = ['userUtterance']; const dialogController = new DialogController({}); const fileSystemStub = sinon.stub(fs, 'outputJSONSync'); // call - dialogController.createReplayFile('random_file_name'); + dialogController.createReplayFile('random_file_name', utterances); // verify expect(fileSystemStub.callCount).equal(1); + expect(fileSystemStub.args[0][1].userInput).deep.equal(utterances); }); it('| file name is empty', () => { @@ -208,29 +211,53 @@ describe('Controller test - dialog controller test', () => { sinon.restore(); }); - it('| file path is empty', (done) => { + it('| Invalid record command format', () => { // setup sinon.stub(DialogReplView.prototype, 'registerRecordCommand'); DialogReplView.prototype.registerRecordCommand.callsArgWith(0, ''); - sinon.stub(stringUtils, 'isNonBlankString').returns(false); + const warnStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + warn: warnStub + }); // call - dialogController.setupSpecialCommands(dialogReplView, (error) => { - expect(error).equal('A file name has not been specified'); - done(); + dialogController.setupSpecialCommands(dialogReplView, () => {}); + + // verify + expect(warnStub.args[0][0]).equal(`Incorrect format. ${RECORD_FORMAT}`); + }); + + it('| Invalid record command format, malformed --append-quit argument', () => { + // setup + const malFormedAppendQuitArgument = '--append'; + sinon.stub(DialogReplView.prototype, 'registerRecordCommand'); + DialogReplView.prototype.registerRecordCommand.callsArgWith(0, `history.json ${malFormedAppendQuitArgument}`); + const warnStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + warn: warnStub }); + + // call + dialogController.setupSpecialCommands(dialogReplView, () => {}); + + // verify + expect(warnStub.args[0][0]).equal(`Unable to validate arguments: "${malFormedAppendQuitArgument}". ${RECORD_FORMAT}`); }); - it('| file is not of JSON type', (done) => { + it('| file is not of JSON type', () => { // setup + const warnStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + warn: warnStub + }); sinon.stub(DialogReplView.prototype, 'registerRecordCommand'); DialogReplView.prototype.registerRecordCommand.callsArgWith(0, 'file.yml'); // call - dialogController.setupSpecialCommands(dialogReplView, (error) => { - expect(error).equal("File should be of type '.json'"); - done(); - }); + dialogController.setupSpecialCommands(dialogReplView, () => {}); + + // verify + expect(warnStub.args[0][0]).equal("File should be of type '.json'"); }); it('| replay file creation throws error', (done) => { @@ -251,6 +278,28 @@ describe('Controller test - dialog controller test', () => { done(); }); }); + + it('| Valid record command format, with --append-quit argument', () => { + // setup + const appendQuitArgument = '--append-quit'; + const filePath = 'history.json'; + sinon.stub(DialogReplView.prototype, 'registerRecordCommand'); + DialogReplView.prototype.registerRecordCommand.callsArgWith(0, `${filePath} ${appendQuitArgument}`); + const infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + const replayStub = sinon.stub(DialogController.prototype, 'createReplayFile'); + + // call + dialogController.setupSpecialCommands(dialogReplView, () => {}); + + // verify + expect(infoStub.args[0][0]).equal(`Created replay file at ${filePath} (appended ".quit" to list of utterances).`); + expect(replayStub.calledOnce).equal(true); + expect(replayStub.args[0][0]).equal(filePath); + expect(replayStub.args[0][1][0]).equal('.quit'); + }); }); describe('# test evaluateUtterance -', () => {