Skip to content

Commit

Permalink
fix: enable interaction after record and support option to append qui…
Browse files Browse the repository at this point in the history
…t to replay file, continue session after .record (#89)
  • Loading branch information
pbheemag authored Mar 31, 2020
1 parent e6f0272 commit ea8da7a
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/concepts/Dialog-Command.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fileName>` or `.record <fileName> --append-quit`.

**.quit**: Exits the Interactive mode.

Expand Down
3 changes: 2 additions & 1 deletion lib/commands/dialog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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}`;
Expand Down
1 change: 1 addition & 0 deletions lib/commands/dialog/replay-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
59 changes: 45 additions & 14 deletions lib/controllers/dialog-controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ 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 <fileName>" or ".record <fileName> --append-quit"';
module.exports = class DialogController extends SkillSimulationController {
/**
* Constructor for DialogModeController.
* @param {Object} configuration | config object includes information such as skillId, locale, profile, stage.
*/
constructor(configuration) {
super(configuration);
this.newSession = true;
this.newSession = configuration.newSession === false ? configuration.newSession : true;
this.utteranceCache = [];
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion test/unit/commands/dialog/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
75 changes: 62 additions & 13 deletions test/unit/controller/dialog-controller/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fileName>" or ".record <fileName> --append-quit"';

describe('# test constructor', () => {
it('| constructor with config parameter returns DialogController object', () => {
Expand All @@ -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);
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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) => {
Expand All @@ -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 -', () => {
Expand Down

0 comments on commit ea8da7a

Please sign in to comment.