punchblock 1.9.4 → 2.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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