punchblock 1.9.4 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -2
  4. data/CHANGELOG.md +17 -0
  5. data/Gemfile +1 -0
  6. data/Guardfile +4 -0
  7. data/README.markdown +6 -0
  8. data/Rakefile +16 -0
  9. data/benchmarks/ami_event_name_comparison.rb +14 -0
  10. data/benchmarks/channel.rb +27 -0
  11. data/lib/punchblock/client.rb +2 -6
  12. data/lib/punchblock/command/accept.rb +3 -24
  13. data/lib/punchblock/command/answer.rb +3 -24
  14. data/lib/punchblock/command/dial.rb +24 -76
  15. data/lib/punchblock/command/hangup.rb +3 -19
  16. data/lib/punchblock/command/join.rb +21 -70
  17. data/lib/punchblock/command/mute.rb +3 -3
  18. data/lib/punchblock/command/redirect.rb +6 -39
  19. data/lib/punchblock/command/reject.rb +14 -54
  20. data/lib/punchblock/command/unjoin.rb +8 -40
  21. data/lib/punchblock/command/unmute.rb +3 -3
  22. data/lib/punchblock/command_node.rb +0 -17
  23. data/lib/punchblock/component/asterisk/agi/command.rb +20 -127
  24. data/lib/punchblock/component/asterisk/ami/action.rb +30 -117
  25. data/lib/punchblock/component/component_node.rb +1 -1
  26. data/lib/punchblock/component/input.rb +89 -268
  27. data/lib/punchblock/component/output.rb +106 -154
  28. data/lib/punchblock/component/prompt.rb +51 -0
  29. data/lib/punchblock/component/record.rb +41 -130
  30. data/lib/punchblock/component.rb +1 -0
  31. data/lib/punchblock/connection/asterisk.rb +31 -4
  32. data/lib/punchblock/connection/xmpp.rb +6 -14
  33. data/lib/punchblock/core_ext/blather/stanza.rb +1 -1
  34. data/lib/punchblock/event/active_speaker.rb +2 -10
  35. data/lib/punchblock/event/answered.rb +3 -3
  36. data/lib/punchblock/event/asterisk/ami/event.rb +15 -47
  37. data/lib/punchblock/event/complete.rb +26 -48
  38. data/lib/punchblock/event/dtmf.rb +3 -13
  39. data/lib/punchblock/event/end.rb +10 -11
  40. data/lib/punchblock/event/joined.rb +5 -25
  41. data/lib/punchblock/event/offer.rb +4 -25
  42. data/lib/punchblock/event/ringing.rb +3 -3
  43. data/lib/punchblock/event/unjoined.rb +5 -25
  44. data/lib/punchblock/event.rb +0 -10
  45. data/lib/punchblock/has_headers.rb +20 -26
  46. data/lib/punchblock/rayo_node.rb +46 -23
  47. data/lib/punchblock/ref.rb +39 -18
  48. data/lib/punchblock/translator/asterisk/agi_app.rb +15 -0
  49. data/lib/punchblock/translator/asterisk/agi_command.rb +3 -1
  50. data/lib/punchblock/translator/asterisk/ami_error_converter.rb +20 -0
  51. data/lib/punchblock/translator/asterisk/call.rb +60 -39
  52. data/lib/punchblock/translator/asterisk/channel.rb +41 -0
  53. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +4 -1
  54. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +4 -4
  55. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +62 -0
  56. data/lib/punchblock/translator/asterisk/component/input.rb +1 -0
  57. data/lib/punchblock/translator/asterisk/component/mrcp_native_prompt.rb +56 -0
  58. data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +53 -0
  59. data/lib/punchblock/translator/asterisk/component/mrcp_recog_prompt.rb +99 -0
  60. data/lib/punchblock/translator/asterisk/component/output.rb +30 -22
  61. data/lib/punchblock/translator/asterisk/component/record.rb +8 -6
  62. data/lib/punchblock/translator/asterisk/component.rb +6 -5
  63. data/lib/punchblock/translator/asterisk/unimrcp_app.rb +26 -0
  64. data/lib/punchblock/translator/asterisk.rb +24 -28
  65. data/lib/punchblock/translator/dtmf_recognizer.rb +39 -20
  66. data/lib/punchblock/translator/freeswitch/call.rb +15 -14
  67. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +5 -4
  68. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
  69. data/lib/punchblock/translator/freeswitch/component/input.rb +5 -0
  70. data/lib/punchblock/translator/freeswitch/component/output.rb +2 -2
  71. data/lib/punchblock/translator/freeswitch/component/record.rb +19 -13
  72. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +2 -2
  73. data/lib/punchblock/translator/freeswitch/component.rb +2 -5
  74. data/lib/punchblock/translator/freeswitch.rb +2 -2
  75. data/lib/punchblock/translator/input_component.rb +33 -13
  76. data/lib/punchblock/uri_list.rb +21 -0
  77. data/lib/punchblock/version.rb +1 -1
  78. data/lib/punchblock.rb +4 -3
  79. data/punchblock.gemspec +7 -3
  80. data/spec/punchblock/client/component_registry_spec.rb +1 -1
  81. data/spec/punchblock/client_spec.rb +10 -26
  82. data/spec/punchblock/command/accept_spec.rb +41 -7
  83. data/spec/punchblock/command/answer_spec.rb +51 -7
  84. data/spec/punchblock/command/dial_spec.rb +56 -14
  85. data/spec/punchblock/command/hangup_spec.rb +41 -7
  86. data/spec/punchblock/command/join_spec.rb +53 -11
  87. data/spec/punchblock/command/mute_spec.rb +19 -4
  88. data/spec/punchblock/command/redirect_spec.rb +40 -10
  89. data/spec/punchblock/command/reject_spec.rb +43 -11
  90. data/spec/punchblock/command/unjoin_spec.rb +40 -9
  91. data/spec/punchblock/command/unmute_spec.rb +19 -4
  92. data/spec/punchblock/command_node_spec.rb +0 -4
  93. data/spec/punchblock/component/asterisk/agi/command_spec.rb +16 -39
  94. data/spec/punchblock/component/asterisk/ami/action_spec.rb +50 -53
  95. data/spec/punchblock/component/component_node_spec.rb +3 -5
  96. data/spec/punchblock/component/input_spec.rb +194 -61
  97. data/spec/punchblock/component/output_spec.rb +194 -62
  98. data/spec/punchblock/component/prompt_spec.rb +132 -0
  99. data/spec/punchblock/component/record_spec.rb +70 -32
  100. data/spec/punchblock/connection/asterisk_spec.rb +17 -3
  101. data/spec/punchblock/connection/freeswitch_spec.rb +4 -4
  102. data/spec/punchblock/connection/xmpp_spec.rb +20 -38
  103. data/spec/punchblock/event/answered_spec.rb +12 -10
  104. data/spec/punchblock/event/asterisk/ami/event_spec.rb +27 -22
  105. data/spec/punchblock/event/complete_spec.rb +15 -19
  106. data/spec/punchblock/event/dtmf_spec.rb +5 -6
  107. data/spec/punchblock/event/end_spec.rb +20 -10
  108. data/spec/punchblock/event/joined_spec.rb +8 -7
  109. data/spec/punchblock/event/offer_spec.rb +41 -12
  110. data/spec/punchblock/event/ringing_spec.rb +12 -10
  111. data/spec/punchblock/event/started_speaking_spec.rb +5 -6
  112. data/spec/punchblock/event/stopped_speaking_spec.rb +5 -6
  113. data/spec/punchblock/event/unjoined_spec.rb +7 -7
  114. data/spec/punchblock/ref_spec.rb +86 -9
  115. data/spec/punchblock/translator/asterisk/call_spec.rb +317 -154
  116. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +28 -5
  117. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +15 -13
  118. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +237 -0
  119. data/spec/punchblock/translator/asterisk/component/input_spec.rb +171 -14
  120. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +652 -0
  121. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +646 -0
  122. data/spec/punchblock/translator/asterisk/component/output_spec.rb +127 -77
  123. data/spec/punchblock/translator/asterisk/component/record_spec.rb +17 -8
  124. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +2 -2
  125. data/spec/punchblock/translator/asterisk/component_spec.rb +3 -7
  126. data/spec/punchblock/translator/asterisk_spec.rb +20 -24
  127. data/spec/punchblock/translator/freeswitch/call_spec.rb +103 -99
  128. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +17 -8
  129. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +26 -14
  130. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +30 -52
  131. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +23 -19
  132. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +18 -8
  133. data/spec/punchblock/translator/freeswitch/component_spec.rb +4 -8
  134. data/spec/punchblock/translator/freeswitch_spec.rb +11 -14
  135. data/spec/punchblock/uri_list_spec.rb +49 -0
  136. data/spec/punchblock_spec.rb +11 -1
  137. data/spec/spec_helper.rb +7 -11
  138. data/spec/support/mock_connection_with_event_handler.rb +1 -1
  139. metadata +104 -24
  140. data/lib/punchblock/header.rb +0 -9
  141. data/lib/punchblock/key_value_pair_node.rb +0 -51
  142. data/spec/punchblock/header_spec.rb +0 -11
@@ -11,7 +11,7 @@ module Punchblock
11
11
  include HasMockCallbackConnection
12
12
 
13
13
  let(:channel) { 'SIP/foo' }
14
- let(:ami_client) { stub('AMI Client').as_null_object }
14
+ let(:ami_client) { double('AMI Client').as_null_object }
15
15
  let(:translator) { Punchblock::Translator::Asterisk.new ami_client, connection }
16
16
  let(:mock_call) { Punchblock::Translator::Asterisk::Call.new channel, translator, ami_client, connection }
17
17
  let(:component_id) { Punchblock.new_uuid }
@@ -56,7 +56,7 @@ module Punchblock
56
56
  end
57
57
 
58
58
  let :expected_response do
59
- Ref.new :id => component_id
59
+ Ref.new uri: component_id
60
60
  end
61
61
 
62
62
  let :response do
@@ -67,20 +67,42 @@ module Punchblock
67
67
  it 'should send the component node a ref with the action ID' do
68
68
  ami_client.should_receive(:send_action).once.and_return response
69
69
  subject.execute
70
- original_command.response(1).should eql(expected_response)
70
+ original_command.response(1).should == expected_response
71
71
  end
72
72
 
73
73
  context 'with an error' do
74
+ let(:message) { 'Action failed' }
74
75
  let :response do
75
- RubyAMI::Error.new.tap { |e| e.message = 'Action failed' }
76
+ RubyAMI::Error.new.tap { |e| e.message = message }
76
77
  end
77
78
 
79
+ before { ami_client.should_receive(:send_action).once.and_raise response }
80
+
78
81
  it 'should send the component node false' do
79
- ami_client.should_receive(:send_action).once.and_raise response
80
82
  subject.execute
81
83
  original_command.response(1).should be_false
82
84
  subject.should_not be_alive
83
85
  end
86
+
87
+ context "which is 'No such channel'" do
88
+ let(:message) { 'No such channel' }
89
+
90
+ it "should return an :item_not_found error for the command" do
91
+ subject.execute
92
+ original_command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{mock_call.id}", mock_call.id)
93
+ subject.should_not be_alive
94
+ end
95
+ end
96
+
97
+ context "which is 'Channel SIP/nosuchchannel does not exist.'" do
98
+ let(:message) { 'Channel SIP/nosuchchannel does not exist.' }
99
+
100
+ it "should return an :item_not_found error for the command" do
101
+ subject.execute
102
+ original_command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{mock_call.id}", mock_call.id)
103
+ subject.should_not be_alive
104
+ end
105
+ end
84
106
  end
85
107
  end
86
108
 
@@ -142,6 +164,7 @@ module Punchblock
142
164
 
143
165
  it 'should send an end (hangup) event to the translator' do
144
166
  expected_end_event = Punchblock::Event::End.new reason: :hangup,
167
+ platform_code: 16,
145
168
  target_call_id: mock_call.id
146
169
 
147
170
  translator.should_receive(:handle_pb_event).once.with kind_of(Punchblock::Event::Complete)
@@ -10,7 +10,7 @@ module Punchblock
10
10
  describe AMIAction do
11
11
  include HasMockCallbackConnection
12
12
 
13
- let(:ami_client) { stub('AMI Client').as_null_object }
13
+ let(:ami_client) { double('AMI Client').as_null_object }
14
14
  let(:mock_translator) { Punchblock::Translator::Asterisk.new ami_client, connection }
15
15
 
16
16
  let :original_command do
@@ -27,7 +27,7 @@ module Punchblock
27
27
  let(:component_id) { Punchblock.new_uuid }
28
28
 
29
29
  let :expected_response do
30
- Ref.new :id => component_id
30
+ Ref.new uri: component_id
31
31
  end
32
32
 
33
33
  before { stub_uuids component_id }
@@ -49,8 +49,10 @@ module Punchblock
49
49
  'Status' => '-1'
50
50
  end
51
51
 
52
+ before { response.text_body = 'Some text body' }
53
+
52
54
  let :expected_complete_reason do
53
- Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new :message => 'Channel status will follow', :attributes => {:exten => "idonno", :context => "default", :hint => "", :status => "-1"}
55
+ Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new message: 'Channel status will follow', text_body: 'Some text body', headers: {'Exten' => "idonno", 'Context' => "default", 'Hint' => "", 'Status' => "-1"}
54
56
  end
55
57
 
56
58
  context 'for a non-causal action' do
@@ -88,19 +90,19 @@ module Punchblock
88
90
  end
89
91
 
90
92
  let :event_node do
91
- Punchblock::Event::Asterisk::AMI::Event.new :name => 'CoreShowChannel', :component_id => subject.id, :attributes => {
92
- :channel => 'SIP/127.0.0.1-00000013',
93
- :uniqueid => '1287686437.19',
94
- :context => 'adhearsion',
95
- :extension => '23432',
96
- :priority => '2',
97
- :channelstate => '6',
98
- :channelstatedesc => 'Up'
93
+ Punchblock::Event::Asterisk::AMI::Event.new name: 'CoreShowChannel', component_id: subject.id, headers: {
94
+ 'Channel' => 'SIP/127.0.0.1-00000013',
95
+ 'UniqueID' => '1287686437.19',
96
+ 'Context' => 'adhearsion',
97
+ 'Extension' => '23432',
98
+ 'Priority' => '2',
99
+ 'ChannelState' => '6',
100
+ 'ChannelStateDesc' => 'Up'
99
101
  }
100
102
  end
101
103
 
102
104
  let :expected_complete_reason do
103
- Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new :message => 'Channel status will follow', :attributes => {:exten => "idonno", :context => "default", :hint => "", :status => "-1", :eventlist => 'Complete', :listitems => '3'}
105
+ Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new message: 'Channel status will follow', text_body: 'Some text body', headers: {'Exten' => "idonno", 'Context' => "default", 'Hint' => "", 'Status' => "-1", 'EventList' => 'Complete', 'ListItems' => '3'}
104
106
  end
105
107
 
106
108
  before { ami_client.should_receive(:send_action).once.and_return response }
@@ -130,7 +132,7 @@ module Punchblock
130
132
  end
131
133
 
132
134
  let :expected_complete_reason do
133
- Punchblock::Event::Complete::Error.new :details => 'Action failed', component_id: subject.id
135
+ Punchblock::Event::Complete::Error.new details: 'Action failed'
134
136
  end
135
137
 
136
138
  it 'should send a complete event to the component node' do
@@ -0,0 +1,237 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Asterisk
8
+ module Component
9
+ describe ComposedPrompt do
10
+ class MockConnection
11
+ attr_reader :events
12
+
13
+ def initialize
14
+ @events = []
15
+ end
16
+
17
+ def handle_event(event)
18
+ @events << event
19
+ end
20
+ end
21
+
22
+ let(:connection) { MockConnection.new }
23
+ let(:media_engine) { nil }
24
+ let(:ami_client) { double('AMI') }
25
+ let(:translator) { Punchblock::Translator::Asterisk.new ami_client, connection, media_engine }
26
+ let(:call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator, ami_client, connection }
27
+
28
+ let :ssml_doc do
29
+ RubySpeech::SSML.draw do
30
+ audio src: 'http://foo.com/bar.mp3'
31
+ end
32
+ end
33
+
34
+ let :dtmf_grammar do
35
+ RubySpeech::GRXML.draw mode: 'dtmf', root: 'digit' do
36
+ rule id: 'digit' do
37
+ one_of do
38
+ item { '1' }
39
+ item { '2' }
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ let :output_command_options do
46
+ { render_document: {value: ssml_doc} }
47
+ end
48
+
49
+ let :input_command_options do
50
+ {
51
+ mode: :dtmf,
52
+ grammar: {value: dtmf_grammar}
53
+ }
54
+ end
55
+
56
+ let(:command_options) { {} }
57
+
58
+ let :output_command do
59
+ Punchblock::Component::Output.new output_command_options
60
+ end
61
+
62
+ let :input_command do
63
+ Punchblock::Component::Input.new input_command_options
64
+ end
65
+
66
+ let :original_command do
67
+ Punchblock::Component::Prompt.new output_command, input_command, command_options
68
+ end
69
+
70
+ subject { described_class.new original_command, call }
71
+
72
+ let(:playbackstatus) { 'SUCCESS' }
73
+
74
+ before do
75
+ call.stub answered?: true, execute_agi_command: true
76
+ call.stub(:channel_var).with('PLAYBACKSTATUS').and_return playbackstatus
77
+ original_command.request!
78
+ end
79
+
80
+ def ami_event_for_dtmf(digit, position)
81
+ RubyAMI::Event.new 'DTMF',
82
+ 'Digit' => digit.to_s,
83
+ 'Start' => position == :start ? 'Yes' : 'No',
84
+ 'End' => position == :end ? 'Yes' : 'No'
85
+ end
86
+
87
+ def send_ami_events_for_dtmf(digit)
88
+ call.process_ami_event ami_event_for_dtmf(digit, :start)
89
+ call.process_ami_event ami_event_for_dtmf(digit, :end)
90
+ end
91
+
92
+ describe '#execute' do
93
+ context '#barge_in' do
94
+ context 'true' do
95
+ it "should execute an output component on the call and return a ref" do
96
+ call.should_receive(:execute_agi_command).once.with('EXEC Playback', 'http://foo.com/bar.mp3')
97
+ subject.execute
98
+ original_command.response(0.1).should be_a Ref
99
+ end
100
+
101
+ context "if output fails to start" do
102
+ it "should return the error returned by output"
103
+ end
104
+
105
+ context "receiving dtmf during output" do
106
+ it "should stop the output"
107
+
108
+ it "should contribute to the input result"
109
+
110
+ it "should return a match complete event"
111
+ end
112
+
113
+ context "when not receiving any DTMF input at all" do
114
+ it "should not start the initial timer until output completes"
115
+ end
116
+ end
117
+
118
+ context 'false' do
119
+ it "should execute an output component on the call" do
120
+ call.should_receive(:execute_agi_command).once.with('EXEC Playback', 'http://foo.com/bar.mp3')
121
+ subject.execute
122
+ original_command.response(0.1).should be_a Ref
123
+ end
124
+
125
+ context "if output fails to start" do
126
+ let(:output_response) { ProtocolError.new.setup 'unrenderable document error', 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.' }
127
+
128
+ let :ssml_doc do
129
+ RubySpeech::SSML.draw do
130
+ string "Foo Bar"
131
+ end
132
+ end
133
+
134
+ it "should return the error returned by output" do
135
+ subject.execute
136
+ original_command.response(0.1).should == output_response
137
+ end
138
+ end
139
+
140
+ context "receiving dtmf during output" do
141
+ it "should not stop the output"
142
+
143
+ it "should not contribute to the input result"
144
+ end
145
+
146
+ context "receiving matching dtmf after output completes" do
147
+ let :expected_nlsml do
148
+ RubySpeech::NLSML.draw do
149
+ interpretation confidence: 1 do
150
+ instance "dtmf-1"
151
+ input "1", mode: :dtmf
152
+ end
153
+ end
154
+ end
155
+
156
+ let :expected_event do
157
+ Punchblock::Event::Complete.new reason: expected_reason,
158
+ component_id: subject.id,
159
+ target_call_id: call.id
160
+ end
161
+
162
+ let :expected_reason do
163
+ Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml
164
+ end
165
+
166
+ it "should return a match complete event" do
167
+ expected_event
168
+ subject.execute
169
+ original_command.response(0.1).should be_a Ref
170
+ send_ami_events_for_dtmf 1
171
+
172
+ connection.events.should include(expected_event)
173
+ end
174
+ end
175
+
176
+ context "when not receiving any DTMF input at all" do
177
+ it "should not start the initial timer until output completes"
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ describe "#execute_command" do
184
+ context "with a command it does not understand" do
185
+ let(:command) { Punchblock::Component::Output::Pause.new }
186
+
187
+ before { command.request! }
188
+ it "returns a ProtocolError response" do
189
+ subject.execute_command command
190
+ command.response(0.1).should be_a ProtocolError
191
+ end
192
+ end
193
+
194
+ context "with a Stop command" do
195
+ let(:command) { Punchblock::Component::Stop.new }
196
+ let(:channel) { "SIP/1234-00000000" }
197
+ let :ami_event do
198
+ RubyAMI::Event.new 'AsyncAGI',
199
+ 'SubEvent' => "Start",
200
+ 'Channel' => channel,
201
+ '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"
202
+ end
203
+
204
+ before do
205
+ command.request!
206
+ original_command.execute!
207
+ end
208
+
209
+ it "sets the command response to true" do
210
+ call.async.should_receive(:redirect_back).once
211
+ subject.execute_command command
212
+ command.response(0.1).should be == true
213
+ end
214
+
215
+ it "sends the correct complete event" do
216
+ expected_reason = Punchblock::Event::Complete::Stop.new
217
+ expected_event = Punchblock::Event::Complete.new reason: expected_reason,
218
+ component_id: subject.id,
219
+ target_call_id: call.id
220
+
221
+ call.async.should_receive(:redirect_back)
222
+ subject.execute_command command
223
+ original_command.should_not be_complete
224
+ call.process_ami_event ami_event
225
+
226
+ sleep 0.2
227
+
228
+ connection.events.should include(expected_event)
229
+ end
230
+ end
231
+ end
232
+
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -10,7 +10,7 @@ module Punchblock
10
10
  include HasMockCallbackConnection
11
11
 
12
12
  let(:media_engine) { nil }
13
- let(:ami_client) { mock('AMI') }
13
+ let(:ami_client) { double('AMI') }
14
14
  let(:translator) { Punchblock::Translator::Asterisk.new ami_client, connection, media_engine }
15
15
  let(:call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator, ami_client, connection }
16
16
  let(:original_command_options) { {} }
@@ -79,13 +79,17 @@ module Punchblock
79
79
  send_ami_events_for_dtmf 2
80
80
  end
81
81
 
82
+ let :expected_nlsml do
83
+ RubySpeech::NLSML.draw do
84
+ interpretation confidence: 1 do
85
+ instance "dtmf-1 dtmf-2"
86
+ input "12", mode: :dtmf
87
+ end
88
+ end
89
+ end
90
+
82
91
  let :expected_event do
83
- Punchblock::Component::Input::Complete::Success.new :mode => :dtmf,
84
- :confidence => 1,
85
- :utterance => '12',
86
- :interpretation => 'dtmf-1 dtmf-2',
87
- :component_id => subject.id,
88
- :target_call_id => call.id
92
+ Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml
89
93
  end
90
94
 
91
95
  it "should send a success complete event with the relevant data" do
@@ -105,8 +109,7 @@ module Punchblock
105
109
  end
106
110
 
107
111
  let :expected_event do
108
- Punchblock::Component::Input::Complete::NoMatch.new :component_id => subject.id,
109
- :target_call_id => call.id
112
+ Punchblock::Component::Input::Complete::NoMatch.new
110
113
  end
111
114
 
112
115
  it "should send a nomatch complete event" do
@@ -124,6 +127,15 @@ module Punchblock
124
127
  original_command.response(0.1).should be == error
125
128
  end
126
129
  end
130
+
131
+ context 'with multiple grammars' do
132
+ let(:original_command_opts) { { :grammars => [{:value => grammar}, {:value => grammar}] } }
133
+ it "should return an error and not execute any actions" do
134
+ subject.execute
135
+ error = ProtocolError.new.setup 'option error', 'Only a single grammar is supported.'
136
+ original_command.response(0.1).should be == error
137
+ end
138
+ end
127
139
  end
128
140
 
129
141
  describe 'mode' do
@@ -145,8 +157,8 @@ module Punchblock
145
157
  end
146
158
  end
147
159
 
148
- context 'speech' do
149
- let(:original_command_opts) { { :mode => :speech } }
160
+ context 'voice' do
161
+ let(:original_command_opts) { { :mode => :voice } }
150
162
  it "should return an error and not execute any actions" do
151
163
  subject.execute
152
164
  error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
@@ -156,7 +168,85 @@ module Punchblock
156
168
  end
157
169
 
158
170
  describe 'terminator' do
159
- pending
171
+ context 'set' do
172
+ let(:original_command_opts) { { terminator: '#' } }
173
+
174
+ before do
175
+ subject.execute
176
+ expected_event
177
+ end
178
+
179
+ let :grammar do
180
+ RubySpeech::GRXML.draw mode: 'dtmf', root: 'digits' do
181
+ rule id: 'digits' do
182
+ item repeat: '2-5' do
183
+ one_of do
184
+ 0.upto(9) { |d| item { d.to_s } }
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ let :expected_nlsml do
192
+ RubySpeech::NLSML.draw do
193
+ interpretation confidence: 1 do
194
+ instance "dtmf-1 dtmf-2"
195
+ input "12", mode: :dtmf
196
+ end
197
+ end
198
+ end
199
+
200
+ let :expected_event do
201
+ Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml
202
+ end
203
+
204
+ context "when encountered with a match" do
205
+ before do
206
+ send_ami_events_for_dtmf 1
207
+ send_ami_events_for_dtmf 2
208
+ send_ami_events_for_dtmf '#'
209
+ end
210
+
211
+ it "should send a match complete event with the relevant data" do
212
+ reason.should be == expected_event
213
+ end
214
+
215
+ it "should not process further dtmf events" do
216
+ subject.async.should_receive(:process_dtmf).never
217
+ send_ami_events_for_dtmf 3
218
+ end
219
+ end
220
+
221
+ context "when encountered with a NoMatch" do
222
+ before do
223
+ send_ami_events_for_dtmf '#'
224
+ end
225
+
226
+ let :expected_event do
227
+ Punchblock::Component::Input::Complete::NoMatch.new
228
+ end
229
+
230
+ it "should send a nomatch complete event with the relevant data" do
231
+ reason.should be == expected_event
232
+ end
233
+ end
234
+
235
+ context "when encountered with a PotentialMatch" do
236
+ before do
237
+ send_ami_events_for_dtmf 1
238
+ send_ami_events_for_dtmf '#'
239
+ end
240
+
241
+ let :expected_event do
242
+ Punchblock::Component::Input::Complete::NoMatch.new
243
+ end
244
+
245
+ it "should send a nomatch complete event with the relevant data" do
246
+ reason.should be == expected_event
247
+ end
248
+ end
249
+ end
160
250
  end
161
251
 
162
252
  describe 'recognizer' do
@@ -172,7 +262,7 @@ module Punchblock
172
262
  send_ami_events_for_dtmf 1
173
263
  sleep 1.5
174
264
  send_ami_events_for_dtmf 2
175
- reason.should be_a Punchblock::Component::Input::Complete::Success
265
+ reason.should be_a Punchblock::Component::Input::Complete::Match
176
266
  end
177
267
 
178
268
  it "should cause a NoInput complete event to be sent after the timeout" do
@@ -223,7 +313,7 @@ module Punchblock
223
313
  send_ami_events_for_dtmf 1
224
314
  sleep 0.5
225
315
  send_ami_events_for_dtmf 2
226
- reason.should be_a Punchblock::Component::Input::Complete::Success
316
+ reason.should be_a Punchblock::Component::Input::Complete::Match
227
317
  end
228
318
 
229
319
  it "should cause a NoMatch complete event to be sent after the timeout" do
@@ -234,6 +324,73 @@ module Punchblock
234
324
  send_ami_events_for_dtmf 2
235
325
  reason.should be_a Punchblock::Component::Input::Complete::NoMatch
236
326
  end
327
+
328
+ context "with a trailing range repeat" do
329
+ let :grammar do
330
+ RubySpeech::GRXML.draw mode: 'dtmf', root: 'digits' do
331
+ rule id: 'digits', scope: 'public' do
332
+ item repeat: '2-5' do
333
+ '1'
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+ context "when the buffer potentially matches the grammar" do
340
+ it "should cause a NoMatch complete event to be sent after the timeout" do
341
+ subject.execute
342
+ sleep 1.5
343
+ send_ami_events_for_dtmf 1
344
+ sleep 1.5
345
+ reason.should be_a Punchblock::Component::Input::Complete::NoMatch
346
+ end
347
+ end
348
+
349
+ context "when the buffer matches the grammar" do
350
+ let :expected_nlsml do
351
+ RubySpeech::NLSML.draw do
352
+ interpretation confidence: 1 do
353
+ instance "dtmf-1 dtmf-1"
354
+ input '11', mode: :dtmf
355
+ end
356
+ end.root
357
+ end
358
+
359
+ it "should fire a match on timeout" do
360
+ subject.execute
361
+ sleep 1.5
362
+ send_ami_events_for_dtmf 1
363
+ sleep 0.5
364
+ send_ami_events_for_dtmf 1
365
+ sleep 1.5
366
+ reason.should be_a Punchblock::Component::Input::Complete::Match
367
+ reason.nlsml.should == expected_nlsml
368
+ end
369
+
370
+ context "on the first keypress" do
371
+ let :grammar do
372
+ RubySpeech::GRXML.draw mode: 'dtmf', root: 'digits' do
373
+ rule id: 'digits', scope: 'public' do
374
+ item repeat: '1-5' do
375
+ '1'
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ it "should fire a match on timeout" do
382
+ subject.execute
383
+ sleep 1.5
384
+ send_ami_events_for_dtmf 1
385
+ sleep 0.5
386
+ send_ami_events_for_dtmf 1
387
+ sleep 1.5
388
+ reason.should be_a Punchblock::Component::Input::Complete::Match
389
+ reason.nlsml.should == expected_nlsml
390
+ end
391
+ end
392
+ end
393
+ end
237
394
  end
238
395
 
239
396
  context '-1' do