/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb

http://github.com/adhearsion/punchblock · Ruby · 1244 lines · 1040 code · 203 blank · 1 comment · 0 complexity · b3c81ee0f152e187bdf14ac3948a786c MD5 · raw file

  1. # encoding: utf-8
  2. require 'spec_helper'
  3. module Punchblock
  4. module Translator
  5. class Asterisk
  6. module Component
  7. describe MRCPPrompt do
  8. include HasMockCallbackConnection
  9. let(:ami_client) { double('AMI') }
  10. let(:translator) { Punchblock::Translator::Asterisk.new ami_client, connection }
  11. let(:mock_call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator, ami_client, connection }
  12. let :ssml_doc do
  13. RubySpeech::SSML.draw do
  14. say_as(:interpret_as => :cardinal) { 'FOO' }
  15. end
  16. end
  17. let :voice_grammar do
  18. RubySpeech::GRXML.draw :mode => 'voice', :root => 'color' do
  19. rule id: 'color' do
  20. one_of do
  21. item { 'red' }
  22. item { 'blue' }
  23. item { 'green' }
  24. end
  25. end
  26. end
  27. end
  28. let :dtmf_grammar do
  29. RubySpeech::GRXML.draw :mode => 'dtmf', :root => 'pin' do
  30. rule id: 'digit' do
  31. one_of do
  32. 0.upto(9) { |d| item { d.to_s } }
  33. end
  34. end
  35. rule id: 'pin', scope: 'public' do
  36. item repeat: '2' do
  37. ruleref uri: '#digit'
  38. end
  39. end
  40. end
  41. end
  42. let(:grammar) { voice_grammar }
  43. let(:output_command_opts) { {} }
  44. let :output_command_options do
  45. { render_document: {value: ssml_doc} }.merge(output_command_opts)
  46. end
  47. let(:input_command_opts) { {} }
  48. let :input_command_options do
  49. { grammar: {value: grammar} }.merge(input_command_opts)
  50. end
  51. let(:command_options) { {} }
  52. let :output_command do
  53. Punchblock::Component::Output.new output_command_options
  54. end
  55. let :input_command do
  56. Punchblock::Component::Input.new input_command_options
  57. end
  58. let :original_command do
  59. Punchblock::Component::Prompt.new output_command, input_command, command_options
  60. end
  61. let(:recog_status) { 'OK' }
  62. let(:recog_completion_cause) { '000' }
  63. let(:recog_result) { "%3C?xml%20version=%221.0%22?%3E%3Cresult%3E%0D%0A%3Cinterpretation%20grammar=%22session:grammar-0%22%20confidence=%220.43%22%3E%3Cinput%20mode=%22speech%22%3EHello%3C/input%3E%3Cinstance%3EHello%3C/instance%3E%3C/interpretation%3E%3C/result%3E" }
  64. subject { described_class.new original_command, mock_call }
  65. before do
  66. original_command.request!
  67. {
  68. 'RECOG_STATUS' => recog_status,
  69. 'RECOG_COMPLETION_CAUSE' => recog_completion_cause,
  70. 'RECOG_RESULT' => recog_result
  71. }.each do |var, val|
  72. allow(mock_call).to receive(:channel_var).with(var).and_return val
  73. end
  74. end
  75. context 'with an invalid recognizer' do
  76. let(:input_command_opts) { { recognizer: 'foobar' } }
  77. it "should return an error and not execute any actions" do
  78. subject.execute
  79. error = ProtocolError.new.setup 'option error', 'The recognizer foobar is unsupported.'
  80. expect(original_command.response(0.1)).to eq(error)
  81. end
  82. end
  83. [:asterisk].each do |recognizer|
  84. context "with a recognizer #{recognizer.inspect}" do
  85. let(:input_command_opts) { { recognizer: recognizer } }
  86. it "should return an error and not execute any actions" do
  87. subject.execute
  88. error = ProtocolError.new.setup 'option error', "The recognizer #{recognizer} is unsupported."
  89. expect(original_command.response(0.1)).to eq(error)
  90. end
  91. end
  92. end
  93. def expect_mrcpsynth_with_options(options)
  94. expect_app_with_options 'MRCPSynth', options
  95. end
  96. def expect_synthandrecog_with_options(options)
  97. expect_app_with_options 'SynthAndRecog', options
  98. end
  99. def expect_app_with_options(app, options)
  100. expect(mock_call).to receive(:execute_agi_command).once { |*args|
  101. expect(args[0]).to eq("EXEC #{app}")
  102. expect(args[1]).to match options
  103. {code: 200, result: 1}
  104. }
  105. end
  106. describe 'Output#document' do
  107. context 'with multiple inline documents' do
  108. let(:output_command_options) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
  109. it "should return an error and not execute any actions" do
  110. subject.execute
  111. error = ProtocolError.new.setup 'option error', 'Only one document is allowed.'
  112. expect(original_command.response(0.1)).to eq(error)
  113. end
  114. end
  115. context 'with multiple documents by URI' do
  116. let(:output_command_options) { { render_documents: [{url: 'http://example.com/doc1.ssml'}, {url: 'http://example.com/doc2.ssml'}] } }
  117. it "should return an error and not execute any actions" do
  118. subject.execute
  119. error = ProtocolError.new.setup 'option error', 'Only one document is allowed.'
  120. expect(original_command.response(0.1)).to eq(error)
  121. end
  122. end
  123. context 'unset' do
  124. let(:output_command_options) { {} }
  125. it "should return an error and not execute any actions" do
  126. subject.execute
  127. error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
  128. expect(original_command.response(0.1)).to eq(error)
  129. end
  130. end
  131. end
  132. describe 'Output#renderer' do
  133. [nil, :unimrcp].each do |renderer|
  134. context renderer.to_s do
  135. let(:output_command_opts) { { renderer: renderer } }
  136. it "should return a ref and execute SynthAndRecog" do
  137. param = [ssml_doc.to_doc, grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
  138. expect(mock_call).to receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
  139. subject.execute
  140. expect(original_command.response(0.1)).to be_a Ref
  141. end
  142. context "when SynthAndRecog completes" do
  143. shared_context "with a match" do
  144. let :expected_nlsml do
  145. RubySpeech::NLSML.draw do
  146. interpretation grammar: 'session:grammar-0', confidence: 0.43 do
  147. input 'Hello', mode: :speech
  148. instance 'Hello'
  149. end
  150. end
  151. end
  152. it 'should send a match complete event' do
  153. expected_complete_reason = Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml
  154. expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
  155. subject.execute
  156. expect(original_command.complete_event(0.1).reason).to eq(expected_complete_reason)
  157. end
  158. end
  159. context "with a match cause" do
  160. %w{000 008 012}.each do |code|
  161. context "when the MRCP recognition response code is #{code}" do
  162. let(:recog_completion_cause) { code }
  163. it_behaves_like 'with a match'
  164. end
  165. end
  166. end
  167. context "with a nomatch cause" do
  168. %w{001 003 013 014 015}.each do |code|
  169. context "with value #{code}" do
  170. let(:recog_completion_cause) { code }
  171. it 'should send a nomatch complete event' do
  172. expected_complete_reason = Punchblock::Component::Input::Complete::NoMatch.new
  173. expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
  174. subject.execute
  175. expect(original_command.complete_event(0.1).reason).to eq(expected_complete_reason)
  176. end
  177. end
  178. end
  179. end
  180. context "with a noinput cause" do
  181. %w{002 011}.each do |code|
  182. context "with value #{code}" do
  183. let(:recog_completion_cause) { code }
  184. specify do
  185. expected_complete_reason = Punchblock::Component::Input::Complete::NoInput.new
  186. expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
  187. subject.execute
  188. expect(original_command.complete_event(0.1).reason).to eq(expected_complete_reason)
  189. end
  190. end
  191. end
  192. end
  193. shared_context 'should send an error complete event' do
  194. specify do
  195. expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
  196. subject.execute
  197. complete_reason = original_command.complete_event(0.1).reason
  198. expect(complete_reason).to be_a Punchblock::Event::Complete::Error
  199. end
  200. end
  201. context 'with an error cause' do
  202. %w{004 005 006 007 009 010 016}.each do |code|
  203. context "when the MRCP recognition response code is #{code}" do
  204. let(:recog_completion_cause) { code }
  205. it_behaves_like 'should send an error complete event'
  206. end
  207. end
  208. end
  209. context 'with an invalid cause' do
  210. let(:recog_completion_cause) { '999' }
  211. it_behaves_like 'should send an error complete event'
  212. end
  213. context "when the RECOG_STATUS variable is set to 'ERROR'" do
  214. let(:recog_status) { 'ERROR' }
  215. it_behaves_like 'should send an error complete event'
  216. end
  217. end
  218. context "when we get a RubyAMI Error" do
  219. it "should send an error complete event" do
  220. error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
  221. expect(mock_call).to receive(:execute_agi_command).and_raise error
  222. subject.execute
  223. complete_reason = original_command.complete_event(0.1).reason
  224. expect(complete_reason).to be_a Punchblock::Event::Complete::Error
  225. expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
  226. end
  227. end
  228. context "when the channel is gone" do
  229. it "should send an error complete event" do
  230. error = ChannelGoneError.new 'FooBar'
  231. expect(mock_call).to receive(:execute_agi_command).and_raise error
  232. subject.execute
  233. complete_reason = original_command.complete_event(0.1).reason
  234. expect(complete_reason).to be_a Punchblock::Event::Complete::Hangup
  235. end
  236. end
  237. end
  238. end
  239. [:foobar, :swift, :asterisk].each do |renderer|
  240. context renderer.to_s do
  241. let(:output_command_opts) { { renderer: renderer } }
  242. it "should return an error and not execute any actions" do
  243. subject.execute
  244. error = ProtocolError.new.setup 'option error', "The renderer #{renderer} is unsupported."
  245. expect(original_command.response(0.1)).to eq(error)
  246. end
  247. end
  248. end
  249. end
  250. describe 'barge_in' do
  251. context 'unset' do
  252. let(:command_options) { { barge_in: nil } }
  253. it 'should pass the b=1 option to SynthAndRecog' do
  254. expect_synthandrecog_with_options(/b=1/)
  255. subject.execute
  256. end
  257. end
  258. context 'true' do
  259. let(:command_options) { { barge_in: true } }
  260. it 'should pass the b=1 option to SynthAndRecog' do
  261. expect_synthandrecog_with_options(/b=1/)
  262. subject.execute
  263. end
  264. end
  265. context 'false' do
  266. let(:command_options) { { barge_in: false } }
  267. it 'should pass the b=0 option to SynthAndRecog' do
  268. expect_synthandrecog_with_options(/b=0/)
  269. subject.execute
  270. end
  271. end
  272. end
  273. describe 'Output#voice' do
  274. context 'unset' do
  275. let(:output_command_opts) { { voice: nil } }
  276. it 'should not pass the vn option to SynthAndRecog' do
  277. expect_synthandrecog_with_options(//)
  278. subject.execute
  279. end
  280. end
  281. context 'set' do
  282. let(:output_command_opts) { { voice: 'alison' } }
  283. it 'should pass the vn option to SynthAndRecog' do
  284. expect_synthandrecog_with_options(/vn=alison/)
  285. subject.execute
  286. end
  287. end
  288. end
  289. describe 'Output#start-offset' do
  290. context 'unset' do
  291. let(:output_command_opts) { { start_offset: nil } }
  292. it 'should not pass any options to SynthAndRecog' do
  293. expect_synthandrecog_with_options(//)
  294. subject.execute
  295. end
  296. end
  297. context 'set' do
  298. let(:output_command_opts) { { start_offset: 10 } }
  299. it "should return an error and not execute any actions" do
  300. subject.execute
  301. error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
  302. expect(original_command.response(0.1)).to eq(error)
  303. end
  304. end
  305. end
  306. describe 'Output#start-paused' do
  307. context 'false' do
  308. let(:output_command_opts) { { start_paused: false } }
  309. it 'should not pass any options to SynthAndRecog' do
  310. expect_synthandrecog_with_options(//)
  311. subject.execute
  312. end
  313. end
  314. context 'true' do
  315. let(:output_command_opts) { { start_paused: true } }
  316. it "should return an error and not execute any actions" do
  317. subject.execute
  318. error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
  319. expect(original_command.response(0.1)).to eq(error)
  320. end
  321. end
  322. end
  323. describe 'Output#repeat-interval' do
  324. context 'unset' do
  325. let(:output_command_opts) { { repeat_interval: nil } }
  326. it 'should not pass any options to SynthAndRecog' do
  327. expect_synthandrecog_with_options(//)
  328. subject.execute
  329. end
  330. end
  331. context 'set' do
  332. let(:output_command_opts) { { repeat_interval: 10 } }
  333. it "should return an error and not execute any actions" do
  334. subject.execute
  335. error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
  336. expect(original_command.response(0.1)).to eq(error)
  337. end
  338. end
  339. end
  340. describe 'Output#repeat-times' do
  341. context 'unset' do
  342. let(:output_command_opts) { { repeat_times: nil } }
  343. it 'should not pass any options to SynthAndRecog' do
  344. expect_synthandrecog_with_options(//)
  345. subject.execute
  346. end
  347. end
  348. context 'set' do
  349. let(:output_command_opts) { { repeat_times: 2 } }
  350. it "should return an error and not execute any actions" do
  351. subject.execute
  352. error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported on Asterisk.'
  353. expect(original_command.response(0.1)).to eq(error)
  354. end
  355. end
  356. end
  357. describe 'Output#max-time' do
  358. context 'unset' do
  359. let(:output_command_opts) { { max_time: nil } }
  360. it 'should not pass any options to SynthAndRecog' do
  361. expect_synthandrecog_with_options(//)
  362. subject.execute
  363. end
  364. end
  365. context 'set' do
  366. let(:output_command_opts) { { max_time: 30 } }
  367. it "should return an error and not execute any actions" do
  368. subject.execute
  369. error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
  370. expect(original_command.response(0.1)).to eq(error)
  371. end
  372. end
  373. end
  374. describe 'Output#interrupt_on' do
  375. context 'unset' do
  376. let(:output_command_opts) { { interrupt_on: nil } }
  377. it 'should not pass any options to SynthAndRecog' do
  378. expect_synthandrecog_with_options(//)
  379. subject.execute
  380. end
  381. end
  382. context 'set' do
  383. let(:output_command_opts) { { interrupt_on: :dtmf } }
  384. it "should return an error and not execute any actions" do
  385. subject.execute
  386. error = ProtocolError.new.setup 'option error', 'A interrupt_on value is unsupported on Asterisk.'
  387. expect(original_command.response(0.1)).to eq(error)
  388. end
  389. end
  390. end
  391. describe 'Output#grammar' do
  392. context 'with multiple inline grammars' do
  393. let(:input_command_options) { { grammars: [{value: voice_grammar}, {value: dtmf_grammar}] } }
  394. it "should return a ref and execute SynthAndRecog" do
  395. param = [ssml_doc.to_doc, [voice_grammar.to_doc.to_s, dtmf_grammar.to_doc.to_s].join(',')].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
  396. expect(mock_call).to receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
  397. subject.execute
  398. expect(original_command.response(0.1)).to be_a Ref
  399. end
  400. end
  401. context 'with multiple grammars by URI' do
  402. let(:input_command_options) { { grammars: [{url: 'http://example.com/grammar1.grxml'}, {url: 'http://example.com/grammar2.grxml'}] } }
  403. it "should return a ref and execute SynthAndRecog" do
  404. param = [ssml_doc.to_doc, ['http://example.com/grammar1.grxml', 'http://example.com/grammar2.grxml'].join(',')].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
  405. expect(mock_call).to receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
  406. subject.execute
  407. expect(original_command.response(0.1)).to be_a Ref
  408. end
  409. end
  410. context 'unset' do
  411. let(:input_command_options) { {} }
  412. it "should return an error and not execute any actions" do
  413. subject.execute
  414. error = ProtocolError.new.setup 'option error', 'A grammar is required.'
  415. expect(original_command.response(0.1)).to eq(error)
  416. end
  417. end
  418. end
  419. describe 'Input#initial-timeout' do
  420. context 'a positive number' do
  421. let(:input_command_opts) { { initial_timeout: 1000 } }
  422. it 'should pass the nit option to SynthAndRecog' do
  423. expect_synthandrecog_with_options(/nit=1000/)
  424. subject.execute
  425. end
  426. end
  427. context '-1' do
  428. let(:input_command_opts) { { initial_timeout: -1 } }
  429. it 'should not pass any options to SynthAndRecog' do
  430. expect_synthandrecog_with_options(//)
  431. subject.execute
  432. end
  433. end
  434. context 'unset' do
  435. let(:input_command_opts) { { initial_timeout: nil } }
  436. it 'should not pass any options to SynthAndRecog' do
  437. expect_synthandrecog_with_options(//)
  438. subject.execute
  439. end
  440. end
  441. context 'a negative number other than -1' do
  442. let(:input_command_opts) { { initial_timeout: -1000 } }
  443. it "should return an error and not execute any actions" do
  444. subject.execute
  445. error = ProtocolError.new.setup 'option error', 'An initial-timeout value must be -1 or a positive integer.'
  446. expect(original_command.response(0.1)).to eq(error)
  447. end
  448. end
  449. end
  450. describe 'Input#recognition-timeout' do
  451. context 'a positive number' do
  452. let(:input_command_opts) { { recognition_timeout: 1000 } }
  453. it 'should pass the t option to SynthAndRecog' do
  454. expect_synthandrecog_with_options(/t=1000/)
  455. subject.execute
  456. end
  457. end
  458. context '0' do
  459. let(:input_command_opts) { { recognition_timeout: 0 } }
  460. it 'should pass the t option to SynthAndRecog' do
  461. expect_synthandrecog_with_options(/t=0/)
  462. subject.execute
  463. end
  464. end
  465. context 'a negative number' do
  466. let(:input_command_opts) { { recognition_timeout: -1000 } }
  467. it "should return an error and not execute any actions" do
  468. subject.execute
  469. error = ProtocolError.new.setup 'option error', 'A recognition-timeout value must be -1, 0, or a positive integer.'
  470. expect(original_command.response(0.1)).to eq(error)
  471. end
  472. end
  473. context 'unset' do
  474. let(:input_command_opts) { { recognition_timeout: nil } }
  475. it 'should not pass any options to SynthAndRecog' do
  476. expect_synthandrecog_with_options(//)
  477. subject.execute
  478. end
  479. end
  480. end
  481. describe 'Input#inter-digit-timeout' do
  482. context 'a positive number' do
  483. let(:input_command_opts) { { inter_digit_timeout: 1000 } }
  484. it 'should pass the dit option to SynthAndRecog' do
  485. expect_synthandrecog_with_options(/dit=1000/)
  486. subject.execute
  487. end
  488. end
  489. context '-1' do
  490. let(:input_command_opts) { { inter_digit_timeout: -1 } }
  491. it 'should not pass any options to SynthAndRecog' do
  492. expect_synthandrecog_with_options(//)
  493. subject.execute
  494. end
  495. end
  496. context 'unset' do
  497. let(:input_command_opts) { { inter_digit_timeout: nil } }
  498. it 'should not pass any options to SynthAndRecog' do
  499. expect_synthandrecog_with_options(//)
  500. subject.execute
  501. end
  502. end
  503. context 'a negative number other than -1' do
  504. let(:input_command_opts) { { inter_digit_timeout: -1000 } }
  505. it "should return an error and not execute any actions" do
  506. subject.execute
  507. error = ProtocolError.new.setup 'option error', 'An inter-digit-timeout value must be -1 or a positive integer.'
  508. expect(original_command.response(0.1)).to eq(error)
  509. end
  510. end
  511. end
  512. describe 'Input#max-silence' do
  513. context 'a positive number' do
  514. let(:input_command_opts) { { max_silence: 1000 } }
  515. it 'should pass the sint option to SynthAndRecog' do
  516. expect_synthandrecog_with_options(/sint=1000/)
  517. subject.execute
  518. end
  519. end
  520. context '0' do
  521. let(:input_command_opts) { { max_silence: 0 } }
  522. it 'should pass the sint option to SynthAndRecog' do
  523. expect_synthandrecog_with_options(/sint=0/)
  524. subject.execute
  525. end
  526. end
  527. context 'a negative number' do
  528. let(:input_command_opts) { { max_silence: -1000 } }
  529. it "should return an error and not execute any actions" do
  530. subject.execute
  531. error = ProtocolError.new.setup 'option error', 'A max-silence value must be -1, 0, or a positive integer.'
  532. expect(original_command.response(0.1)).to eq(error)
  533. end
  534. end
  535. context 'unset' do
  536. let(:input_command_opts) { { max_silence: nil } }
  537. it 'should not pass any options to SynthAndRecog' do
  538. expect_synthandrecog_with_options(//)
  539. subject.execute
  540. end
  541. end
  542. end
  543. describe 'Input#speech-complete-timeout' do
  544. context 'a positive number' do
  545. let(:input_command_opts) { { headers: {"Speech-Complete-Timeout" => 1000 } } }
  546. it 'should pass the sct option to SynthAndRecog' do
  547. expect_synthandrecog_with_options(/sct=1000/)
  548. subject.execute
  549. end
  550. end
  551. context '0' do
  552. let(:input_command_opts) { { headers: {"Speech-Complete-Timeout" => 0 } } }
  553. it 'should pass the sct option to SynthAndRecog' do
  554. expect_synthandrecog_with_options(/sct=0/)
  555. subject.execute
  556. end
  557. end
  558. context 'a negative number' do
  559. let(:input_command_opts) { { headers: {"Speech-Complete-Timeout" => -1000 } } }
  560. it "should return an error and not execute any actions" do
  561. subject.execute
  562. error = ProtocolError.new.setup 'option error', 'A speech-complete-timeout value must be -1, 0, or a positive integer.'
  563. expect(original_command.response(0.1)).to eq(error)
  564. end
  565. end
  566. context 'unset' do
  567. let(:input_command_opts) { { headers: {"Speech-Complete-Timeout" => nil } } }
  568. it 'should not pass any options to SynthAndRecog' do
  569. expect_synthandrecog_with_options(//)
  570. subject.execute
  571. end
  572. end
  573. end
  574. describe 'Input#headers(Speed-Vs-Accuracy)' do
  575. context 'a positive number' do
  576. let(:input_command_opts) { { headers: {"Speed-Vs-Accuracy" => 1 } } }
  577. it 'should pass the sva option to SynthAndRecog' do
  578. expect_synthandrecog_with_options(/sva=1/)
  579. subject.execute
  580. end
  581. end
  582. context '0' do
  583. let(:input_command_opts) { { headers: {"Speed-Vs-Accuracy" => 0 } } }
  584. it 'should pass the sva option to SynthAndRecog' do
  585. expect_synthandrecog_with_options(/sva=0/)
  586. subject.execute
  587. end
  588. end
  589. context 'a negative number' do
  590. let(:input_command_opts) { { headers: {"Speed-Vs-Accuracy" => -1000 } } }
  591. it "should return an error and not execute any actions" do
  592. subject.execute
  593. error = ProtocolError.new.setup 'option error', 'A speed-vs-accuracy value must be a positive integer.'
  594. expect(original_command.response(0.1)).to eq(error)
  595. end
  596. end
  597. context 'unset' do
  598. let(:input_command_opts) { { headers: {"Speed-Vs-Accuracy" => nil } } }
  599. it 'should not pass any options to SynthAndRecog' do
  600. expect_synthandrecog_with_options(//)
  601. subject.execute
  602. end
  603. end
  604. end
  605. describe 'Input#headers(N-Best-List-Length)' do
  606. context 'a positive number' do
  607. let(:input_command_opts) { { headers: {"N-Best-List-Length" => 5 } } }
  608. it 'should pass the nb option to SynthAndRecog' do
  609. expect_synthandrecog_with_options(/nb=5/)
  610. subject.execute
  611. end
  612. end
  613. context '0' do
  614. let(:input_command_opts) { { headers: {"N-Best-List-Length" => 0 } } }
  615. it "should return an error and not execute any actions" do
  616. subject.execute
  617. error = ProtocolError.new.setup 'option error', 'An n-best-list-length value must be a positive integer.'
  618. expect(original_command.response(0.1)).to eq(error)
  619. end
  620. end
  621. context 'a negative number' do
  622. let(:input_command_opts) { { headers: {"N-Best-List-Length" => -1000 } } }
  623. it "should return an error and not execute any actions" do
  624. subject.execute
  625. error = ProtocolError.new.setup 'option error', 'An n-best-list-length value must be a positive integer.'
  626. expect(original_command.response(0.1)).to eq(error)
  627. end
  628. end
  629. context 'unset' do
  630. let(:input_command_opts) { { headers: {"N-Best-List-Length" => nil } } }
  631. it 'should not pass any options to SynthAndRecog' do
  632. expect_synthandrecog_with_options(//)
  633. subject.execute
  634. end
  635. end
  636. end
  637. describe 'Input#headers(Start-Input-Timers)' do
  638. context 'true' do
  639. let(:input_command_opts) { { headers: {"Start-Input-Timers" => true } } }
  640. it 'should pass the sit option to SynthAndRecog' do
  641. expect_synthandrecog_with_options(/sit=true/)
  642. subject.execute
  643. end
  644. end
  645. context 'false' do
  646. let(:input_command_opts) { { headers: {"Start-Input-Timers" => false } } }
  647. it 'should pass the sit option to SynthAndRecog' do
  648. expect_synthandrecog_with_options(/sit=false/)
  649. subject.execute
  650. end
  651. end
  652. context 'unset' do
  653. let(:input_command_opts) { { headers: {"Start-Input-Timers" => nil } } }
  654. it 'should not pass any options to SynthAndRecog' do
  655. expect_synthandrecog_with_options(//)
  656. subject.execute
  657. end
  658. end
  659. end
  660. describe 'Input#headers(DTMF-Terminate-Timeout)' do
  661. context 'a positive number' do
  662. let(:input_command_opts) { { headers: {"DTMF-Terminate-Timeout" => 1000 } } }
  663. it 'should pass the dtt option to SynthAndRecog' do
  664. expect_synthandrecog_with_options(/dtt=1000/)
  665. subject.execute
  666. end
  667. end
  668. context '0' do
  669. let(:input_command_opts) { { headers: {"DTMF-Terminate-Timeout" => 0 } } }
  670. it 'should pass the dtt option to SynthAndRecog' do
  671. expect_synthandrecog_with_options(/dtt=0/)
  672. subject.execute
  673. end
  674. end
  675. context 'a negative number' do
  676. let(:input_command_opts) { { headers: {"DTMF-Terminate-Timeout" => -1000 } } }
  677. it "should return an error and not execute any actions" do
  678. subject.execute
  679. error = ProtocolError.new.setup 'option error', 'A dtmf-terminate-timeout value must be -1, 0, or a positive integer.'
  680. expect(original_command.response(0.1)).to eq(error)
  681. end
  682. end
  683. context 'unset' do
  684. let(:input_command_opts) { { headers: {"DTMF-Terminate-Timeout" => nil } } }
  685. it 'should not pass any options to SynthAndRecog' do
  686. expect_synthandrecog_with_options(//)
  687. subject.execute
  688. end
  689. end
  690. end
  691. describe 'Input#headers(Save-Waveform)' do
  692. context 'true' do
  693. let(:input_command_opts) { { headers: {"Save-Waveform" => true } } }
  694. it 'should pass the sw option to SynthAndRecog' do
  695. expect_synthandrecog_with_options(/sw=true/)
  696. subject.execute
  697. end
  698. end
  699. context 'false' do
  700. let(:input_command_opts) { { headers: {"Save-Waveform" => false } } }
  701. it 'should pass the sw option to SynthAndRecog' do
  702. expect_synthandrecog_with_options(/sw=false/)
  703. subject.execute
  704. end
  705. end
  706. context 'unset' do
  707. let(:input_command_opts) { { headers: {"Save-Waveform" => nil } } }
  708. it 'should not pass any options to SynthAndRecog' do
  709. expect_synthandrecog_with_options(//)
  710. subject.execute
  711. end
  712. end
  713. end
  714. describe 'Input#headers(New-Audio-Channel)' do
  715. context 'true' do
  716. let(:input_command_opts) { { headers: {"New-Audio-Channel" => true } } }
  717. it 'should pass the nac option to SynthAndRecog' do
  718. expect_synthandrecog_with_options(/nac=true/)
  719. subject.execute
  720. end
  721. end
  722. context 'false' do
  723. let(:input_command_opts) { { headers: {"New-Audio-Channel" => false } } }
  724. it 'should pass the nac option to SynthAndRecog' do
  725. expect_synthandrecog_with_options(/nac=false/)
  726. subject.execute
  727. end
  728. end
  729. context 'unset' do
  730. let(:input_command_opts) { { headers: {"New-Audio-Channel" => nil } } }
  731. it 'should not pass any options to SynthAndRecog' do
  732. expect_synthandrecog_with_options(//)
  733. subject.execute
  734. end
  735. end
  736. end
  737. describe 'Input#headers(Recognition-Mode)' do
  738. context 'a string' do
  739. let(:input_command_opts) { { headers: {"Recognition-Mode" => "hotword" } } }
  740. it 'should pass the rm option to SynthAndRecog' do
  741. expect_synthandrecog_with_options(/rm=hotword/)
  742. subject.execute
  743. end
  744. end
  745. context 'unset' do
  746. let(:input_command_opts) { { headers: {"Recognition-Mode" => nil } } }
  747. it 'should not pass any options to SynthAndRecog' do
  748. expect_synthandrecog_with_options(//)
  749. subject.execute
  750. end
  751. end
  752. end
  753. describe 'Input#headers(Hotword-Max-Duration)' do
  754. context 'a positive number' do
  755. let(:input_command_opts) { { headers: {"Hotword-Max-Duration" => 1000 } } }
  756. it 'should pass the hmaxd option to SynthAndRecog' do
  757. expect_synthandrecog_with_options(/hmaxd=1000/)
  758. subject.execute
  759. end
  760. end
  761. context '0' do
  762. let(:input_command_opts) { { headers: {"Hotword-Max-Duration" => 0 } } }
  763. it 'should pass the hmaxd option to SynthAndRecog' do
  764. expect_synthandrecog_with_options(/hmaxd=0/)
  765. subject.execute
  766. end
  767. end
  768. context 'a negative number' do
  769. let(:input_command_opts) { { headers: {"Hotword-Max-Duration" => -1000 } } }
  770. it "should return an error and not execute any actions" do
  771. subject.execute
  772. error = ProtocolError.new.setup 'option error', 'A hotword-max-duration value must be -1, 0, or a positive integer.'
  773. expect(original_command.response(0.1)).to eq(error)
  774. end
  775. end
  776. context 'unset' do
  777. let(:input_command_opts) { { headers: {"Hotword-Max-Duration" => nil } } }
  778. it 'should not pass any options to SynthAndRecog' do
  779. expect_synthandrecog_with_options(//)
  780. subject.execute
  781. end
  782. end
  783. end
  784. describe 'Input#headers(Hotword-Min-Duration)' do
  785. context 'a positive number' do
  786. let(:input_command_opts) { { headers: {"Hotword-Min-Duration" => 1000 } } }
  787. it 'should pass the hmind option to SynthAndRecog' do
  788. expect_synthandrecog_with_options(/hmind=1000/)
  789. subject.execute
  790. end
  791. end
  792. context '0' do
  793. let(:input_command_opts) { { headers: {"Hotword-Min-Duration" => 0 } } }
  794. it 'should pass the hmind option to SynthAndRecog' do
  795. expect_synthandrecog_with_options(/hmind=0/)
  796. subject.execute
  797. end
  798. end
  799. context 'a negative number' do
  800. let(:input_command_opts) { { headers: {"Hotword-Min-Duration" => -1000 } } }
  801. it "should return an error and not execute any actions" do
  802. subject.execute
  803. error = ProtocolError.new.setup 'option error', 'A hotword-min-duration value must be -1, 0, or a positive integer.'
  804. expect(original_command.response(0.1)).to eq(error)
  805. end
  806. end
  807. context 'unset' do
  808. let(:input_command_opts) { { headers: {"Hotword-Min-Duration" => nil } } }
  809. it 'should not pass any options to SynthAndRecog' do
  810. expect_synthandrecog_with_options(//)
  811. subject.execute
  812. end
  813. end
  814. end
  815. describe 'Input#headers(Clear-DTMF-Buffer)' do
  816. context 'true' do
  817. let(:input_command_opts) { { headers: {"Clear-DTMF-Buffer" => true } } }
  818. it 'should pass the cdb option to SynthAndRecog' do
  819. expect_synthandrecog_with_options(/cdb=true/)
  820. subject.execute
  821. end
  822. end
  823. context 'false' do
  824. let(:input_command_opts) { { headers: {"Clear-DTMF-Buffer" => false } } }
  825. it 'should pass the cdb option to SynthAndRecog' do
  826. expect_synthandrecog_with_options(/cdb=false/)
  827. subject.execute
  828. end
  829. end
  830. context 'unset' do
  831. let(:input_command_opts) { { headers: {"Clear-DTMF-Buffer" => nil } } }
  832. it 'should not pass any options to SynthAndRecog' do
  833. expect_synthandrecog_with_options(//)
  834. subject.execute
  835. end
  836. end
  837. end
  838. describe 'Input#headers(Early-No-Match)' do
  839. context 'true' do
  840. let(:input_command_opts) { { headers: {"Early-No-Match" => true } } }
  841. it 'should pass the enm option to SynthAndRecog' do
  842. expect_synthandrecog_with_options(/enm=true/)
  843. subject.execute
  844. end
  845. end
  846. context 'a negative number' do
  847. let(:input_command_opts) { { headers: {"Early-No-Match" => false } } }
  848. it "should return an error and not execute any actions" do
  849. expect_synthandrecog_with_options(/enm=false/)
  850. subject.execute
  851. end
  852. end
  853. context 'unset' do
  854. let(:input_command_opts) { { headers: {"Early-No-Match" => nil } } }
  855. it 'should not pass any options to SynthAndRecog' do
  856. expect_synthandrecog_with_options(//)
  857. subject.execute
  858. end
  859. end
  860. end
  861. describe 'Input#headers(Input-Waveform-URI)' do
  862. context 'a positive number' do
  863. let(:input_command_opts) { { headers: {"Input-Waveform-URI" => 'http://wave.form.com' } } }
  864. it 'should pass the iwu option to SynthAndRecog' do
  865. expect_synthandrecog_with_options(/iwu=http:\/\/wave\.form\.com/)
  866. subject.execute
  867. end
  868. end
  869. context 'unset' do
  870. let(:input_command_opts) { { headers: {"Input-Waveform-URI" => nil } } }
  871. it 'should not pass any options to SynthAndRecog' do
  872. expect_synthandrecog_with_options(//)
  873. subject.execute
  874. end
  875. end
  876. end
  877. describe 'Input#headers(Media-Type)' do
  878. context 'a string' do
  879. let(:input_command_opts) { { headers: {"Media-Type" => "foo/type" } } }
  880. it 'should pass the mt option to SynthAndRecog' do
  881. expect_synthandrecog_with_options(/mt=foo\/type/)
  882. subject.execute
  883. end
  884. end
  885. context 'unset' do
  886. let(:input_command_opts) { { headers: {"Media-Type" => nil } } }
  887. it 'should not pass any options to SynthAndRecog' do
  888. expect_synthandrecog_with_options(//)
  889. subject.execute
  890. end
  891. end
  892. end
  893. describe 'Input#mode' do
  894. skip
  895. end
  896. describe 'Input#terminator' do
  897. context 'a string' do
  898. let(:input_command_opts) { { terminator: '#' } }
  899. it 'should pass the dttc option to SynthAndRecog' do
  900. expect_synthandrecog_with_options(/dttc=#/)
  901. subject.execute
  902. end
  903. end
  904. context 'unset' do
  905. let(:input_command_opts) { { terminator: nil } }
  906. it 'should not pass any options to SynthAndRecog' do
  907. expect_synthandrecog_with_options(//)
  908. subject.execute
  909. end
  910. end
  911. end
  912. describe 'Input#recognizer' do
  913. skip
  914. end
  915. describe 'Input#sensitivity' do
  916. context 'a string' do
  917. let(:input_command_opts) { { sensitivity: '0.2' } }
  918. it 'should pass the sl option to SynthAndRecog' do
  919. expect_synthandrecog_with_options(/sl=0.2/)
  920. subject.execute
  921. end
  922. end
  923. context 'unset' do
  924. let(:input_command_opts) { { sensitivity: nil } }
  925. it 'should not pass any options to SynthAndRecog' do
  926. expect_synthandrecog_with_options(//)
  927. subject.execute
  928. end
  929. end
  930. end
  931. describe 'Input#min-confidence' do
  932. context 'a string' do
  933. let(:input_command_opts) { { min_confidence: '0.5' } }
  934. it 'should pass the ct option to SynthAndRecog' do
  935. expect_synthandrecog_with_options(/ct=0.5/)
  936. subject.execute
  937. end
  938. end
  939. context 'unset' do
  940. let(:input_command_opts) { { min_confidence: nil } }
  941. it 'should not pass any options to SynthAndRecog' do
  942. expect_synthandrecog_with_options(//)
  943. subject.execute
  944. end
  945. end
  946. end
  947. describe 'Input#max-silence' do
  948. skip
  949. end
  950. describe 'Input#match-content-type' do
  951. skip
  952. end
  953. describe 'Input#language' do
  954. context 'a string' do
  955. let(:input_command_opts) { { language: 'en-GB' } }
  956. it 'should pass the spl option to SynthAndRecog' do
  957. expect_synthandrecog_with_options(/spl=en-GB/)
  958. subject.execute
  959. end
  960. end
  961. context 'unset' do
  962. let(:input_command_opts) { { language: nil } }
  963. it 'should not pass any options to SynthAndRecog' do
  964. expect_synthandrecog_with_options(//)
  965. subject.execute
  966. end
  967. end
  968. end
  969. describe "#execute_command" do
  970. context "with a command it does not understand" do
  971. let(:command) { Punchblock::Component::Output::Pause.new }
  972. before { command.request! }
  973. it "returns a ProtocolError response" do
  974. subject.execute_command command
  975. expect(command.response(0.1)).to be_a ProtocolError
  976. end
  977. end
  978. context "with a Stop command" do
  979. let(:command) { Punchblock::Component::Stop.new }
  980. let(:reason) { original_command.complete_event(5).reason }
  981. let(:channel) { "SIP/1234-00000000" }
  982. let :ami_event do
  983. RubyAMI::Event.new 'AsyncAGI',
  984. 'SubEvent' => "Start",
  985. 'Channel' => channel,
  986. 'Env' => "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A"
  987. end
  988. before do
  989. command.request!
  990. original_command.execute!
  991. end
  992. it "sets the command response to true" do
  993. expect(mock_call).to receive(:redirect_back)
  994. subject.execute_command command
  995. expect(command.response(0.1)).to eq(true)
  996. end
  997. it "sends the correct complete event" do
  998. expect(mock_call).to receive(:redirect_back)
  999. subject.execute_command command
  1000. expect(original_command).not_to be_complete
  1001. mock_call.process_ami_event ami_event
  1002. expect(reason).to be_a Punchblock::Event::Complete::Stop
  1003. expect(original_command).to be_complete
  1004. end
  1005. it "redirects the call by unjoining it" do
  1006. expect(mock_call).to receive(:redirect_back)
  1007. subject.execute_command command
  1008. end
  1009. end
  1010. end
  1011. end
  1012. end
  1013. end
  1014. end
  1015. end