stealth 1.1.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +15 -54
  3. data/CHANGELOG.md +72 -0
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +49 -44
  6. data/LICENSE +4 -17
  7. data/README.md +9 -17
  8. data/VERSION +1 -1
  9. data/lib/stealth/base.rb +62 -15
  10. data/lib/stealth/cli.rb +1 -2
  11. data/lib/stealth/commands/console.rb +1 -1
  12. data/lib/stealth/configuration.rb +0 -3
  13. data/lib/stealth/controller/callbacks.rb +1 -1
  14. data/lib/stealth/controller/catch_all.rb +27 -4
  15. data/lib/stealth/controller/controller.rb +168 -49
  16. data/lib/stealth/controller/dev_jumps.rb +41 -0
  17. data/lib/stealth/controller/dynamic_delay.rb +4 -6
  18. data/lib/stealth/controller/interrupt_detect.rb +100 -0
  19. data/lib/stealth/controller/messages.rb +283 -0
  20. data/lib/stealth/controller/nlp.rb +50 -0
  21. data/lib/stealth/controller/replies.rb +178 -40
  22. data/lib/stealth/controller/unrecognized_message.rb +62 -0
  23. data/lib/stealth/core_ext.rb +5 -0
  24. data/lib/stealth/{flow/core_ext.rb → core_ext/numeric.rb} +0 -1
  25. data/lib/stealth/core_ext/string.rb +18 -0
  26. data/lib/stealth/dispatcher.rb +21 -0
  27. data/lib/stealth/errors.rb +12 -0
  28. data/lib/stealth/flow/base.rb +1 -2
  29. data/lib/stealth/flow/specification.rb +3 -2
  30. data/lib/stealth/flow/state.rb +3 -3
  31. data/lib/stealth/generators/builder/Gemfile +4 -3
  32. data/lib/stealth/generators/builder/bot/controllers/bot_controller.rb +42 -0
  33. data/lib/stealth/generators/builder/bot/controllers/catch_alls_controller.rb +2 -0
  34. data/lib/stealth/generators/builder/bot/controllers/goodbyes_controller.rb +2 -0
  35. data/lib/stealth/generators/builder/bot/controllers/hellos_controller.rb +2 -0
  36. data/lib/stealth/generators/builder/bot/controllers/interrupts_controller.rb +9 -0
  37. data/lib/stealth/generators/builder/bot/controllers/unrecognized_messages_controller.rb +9 -0
  38. data/lib/stealth/generators/builder/config/flow_map.rb +8 -0
  39. data/lib/stealth/generators/builder/config/initializers/autoload.rb +8 -0
  40. data/lib/stealth/generators/builder/config/initializers/inflections.rb +16 -0
  41. data/lib/stealth/generators/builder/config/puma.rb +15 -0
  42. data/lib/stealth/helpers/redis.rb +40 -0
  43. data/lib/stealth/lock.rb +83 -0
  44. data/lib/stealth/logger.rb +27 -18
  45. data/lib/stealth/nlp/client.rb +22 -0
  46. data/lib/stealth/nlp/result.rb +57 -0
  47. data/lib/stealth/reloader.rb +90 -0
  48. data/lib/stealth/reply.rb +17 -0
  49. data/lib/stealth/scheduled_reply.rb +3 -3
  50. data/lib/stealth/server.rb +3 -3
  51. data/lib/stealth/service_message.rb +3 -2
  52. data/lib/stealth/service_reply.rb +5 -1
  53. data/lib/stealth/services/base_reply_handler.rb +2 -2
  54. data/lib/stealth/session.rb +106 -53
  55. data/spec/configuration_spec.rb +9 -2
  56. data/spec/controller/callbacks_spec.rb +23 -28
  57. data/spec/controller/catch_all_spec.rb +81 -29
  58. data/spec/controller/controller_spec.rb +444 -43
  59. data/spec/controller/dynamic_delay_spec.rb +16 -18
  60. data/spec/controller/helpers_spec.rb +1 -2
  61. data/spec/controller/interrupt_detect_spec.rb +171 -0
  62. data/spec/controller/messages_spec.rb +744 -0
  63. data/spec/controller/nlp_spec.rb +93 -0
  64. data/spec/controller/replies_spec.rb +446 -11
  65. data/spec/controller/unrecognized_message_spec.rb +168 -0
  66. data/spec/dispatcher_spec.rb +79 -0
  67. data/spec/flow/flow_spec.rb +1 -2
  68. data/spec/flow/state_spec.rb +14 -3
  69. data/spec/helpers/redis_spec.rb +77 -0
  70. data/spec/lock_spec.rb +100 -0
  71. data/spec/nlp/client_spec.rb +23 -0
  72. data/spec/nlp/result_spec.rb +57 -0
  73. data/spec/replies/messages/say_msgs_without_breaks.yml +4 -0
  74. data/spec/replies/messages/say_randomize_speech.yml +10 -0
  75. data/spec/replies/messages/say_randomize_text.yml +10 -0
  76. data/spec/replies/messages/sub1/sub2/say_nested.yml +10 -0
  77. data/spec/reply_spec.rb +61 -0
  78. data/spec/scheduled_reply_spec.rb +23 -0
  79. data/spec/service_reply_spec.rb +1 -2
  80. data/spec/session_spec.rb +251 -12
  81. data/spec/spec_helper.rb +21 -0
  82. data/spec/support/controllers/vaders_controller.rb +24 -0
  83. data/spec/support/nlp_clients/dialogflow.rb +9 -0
  84. data/spec/support/nlp_clients/luis.rb +9 -0
  85. data/spec/support/nlp_results/luis_result.rb +163 -0
  86. data/spec/version_spec.rb +1 -2
  87. data/stealth.gemspec +6 -6
  88. metadata +83 -38
  89. data/docs/00-introduction.md +0 -37
  90. data/docs/01-getting-started.md +0 -21
  91. data/docs/02-local-development.md +0 -40
  92. data/docs/03-basics.md +0 -171
  93. data/docs/04-sessions.md +0 -29
  94. data/docs/05-controllers.md +0 -179
  95. data/docs/06-models.md +0 -39
  96. data/docs/07-replies.md +0 -114
  97. data/docs/08-catchalls.md +0 -49
  98. data/docs/09-messaging-integrations.md +0 -80
  99. data/docs/10-nlp-integrations.md +0 -13
  100. data/docs/11-analytics.md +0 -13
  101. data/docs/12-commands.md +0 -62
  102. data/docs/13-deployment.md +0 -50
  103. data/lib/stealth/generators/builder/config/initializers/.keep +0 -0
@@ -1,72 +1,70 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- require File.expand_path(File.join(File.dirname(__FILE__), '..', '/spec_helper'))
3
+ require 'spec_helper'
5
4
 
6
5
  describe "Stealth::Controller::DynamicDelay" do
7
6
 
8
7
  let(:facebook_message) { SampleMessage.new(service: 'facebook') }
9
8
  let(:controller) { VadersController.new(service_message: facebook_message.message_with_text) }
10
- let(:service_replies) { YAML.load(File.read(File.expand_path("../replies/messages/say_howdy_with_dynamic.yml", __dir__))) }
9
+ let!(:service_replies) { YAML.load(File.read(File.expand_path("../replies/messages/say_howdy_with_dynamic.yml", __dir__))) }
11
10
 
12
11
  it "should return a SHORT_DELAY for a dynamic delay at position 0" do
13
- delay = controller.dynamic_delay(service_replies: service_replies, position: 0)
12
+ delay = controller.dynamic_delay(previous_reply: nil)
14
13
  expect(delay).to eq(Stealth::Controller::DynamicDelay::SHORT_DELAY)
15
14
  end
16
15
 
17
- it "should return a SHORT_DELAY for a dynamic delay at position -1" do
18
- delay = controller.dynamic_delay(service_replies: service_replies, position: -1)
19
- expect(delay).to eq(Stealth::Controller::DynamicDelay::SHORT_DELAY)
16
+ it "should return a STANDARD_DELAY for a dynamic delay at position -2" do
17
+ delay = controller.dynamic_delay(previous_reply: service_replies[-2])
18
+ expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
20
19
  end
21
20
 
22
21
  it "should return a SHORT_DELAY for text 35 chars long" do
23
- delay = controller.dynamic_delay(service_replies: service_replies, position: 2)
22
+ delay = controller.dynamic_delay(previous_reply: service_replies[1])
24
23
  expect(delay).to eq(Stealth::Controller::DynamicDelay::SHORT_DELAY)
25
24
  end
26
25
 
27
26
  it "should return a STANDARD_DELAY for text 120 chars long" do
28
- delay = controller.dynamic_delay(service_replies: service_replies, position: 4)
27
+ delay = controller.dynamic_delay(previous_reply: service_replies[3])
29
28
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
30
29
  end
31
30
 
32
31
  it "should return a (STANDARD_DELAY * 1.5) for text 230 chars long" do
33
- delay = controller.dynamic_delay(service_replies: service_replies, position: 6)
32
+ delay = controller.dynamic_delay(previous_reply: service_replies[5])
34
33
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY * 1.5)
35
34
  end
36
35
 
37
36
  it "should return a LONG_DELAY for text 350 chars long" do
38
- delay = controller.dynamic_delay(service_replies: service_replies, position: 8)
37
+ delay = controller.dynamic_delay(previous_reply: service_replies[7])
39
38
  expect(delay).to eq(Stealth::Controller::DynamicDelay::LONG_DELAY)
40
39
  end
41
40
 
42
41
  it "should return a STANDARD_DELAY for an image" do
43
- delay = controller.dynamic_delay(service_replies: service_replies, position: 10)
42
+ delay = controller.dynamic_delay(previous_reply: service_replies[9])
44
43
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
45
44
  end
46
45
 
47
46
  it "should return a STANDARD_DELAY for a video" do
48
- delay = controller.dynamic_delay(service_replies: service_replies, position: 12)
47
+ delay = controller.dynamic_delay(previous_reply: service_replies[11])
49
48
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
50
49
  end
51
50
 
52
51
  it "should return a STANDARD_DELAY for an audio" do
53
- delay = controller.dynamic_delay(service_replies: service_replies, position: 14)
52
+ delay = controller.dynamic_delay(previous_reply: service_replies[13])
54
53
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
55
54
  end
56
55
 
57
56
  it "should return a STANDARD_DELAY for a file" do
58
- delay = controller.dynamic_delay(service_replies: service_replies, position: 16)
57
+ delay = controller.dynamic_delay(previous_reply: service_replies[15])
59
58
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
60
59
  end
61
60
 
62
61
  it "should return a STANDARD_DELAY for cards" do
63
- delay = controller.dynamic_delay(service_replies: service_replies, position: 18)
62
+ delay = controller.dynamic_delay(previous_reply: service_replies[17])
64
63
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
65
64
  end
66
65
 
67
66
  it "should return a STANDARD_DELAY for a list" do
68
- delay = controller.dynamic_delay(service_replies: service_replies, position: 20)
67
+ delay = controller.dynamic_delay(previous_reply: service_replies[19])
69
68
  expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY)
70
69
  end
71
70
  end
72
-
@@ -1,7 +1,6 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- require File.expand_path(File.join(File.dirname(__FILE__), '..', '/spec_helper'))
3
+ require 'spec_helper'
5
4
 
6
5
  $:.unshift File.expand_path("../support/helpers", __dir__)
7
6
 
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Stealth::Controller::InterruptDetect" do
6
+
7
+ let(:fb_message) { SampleMessage.new(service: 'facebook') }
8
+ let(:controller) { VadersController.new(service_message: fb_message.message_with_text) }
9
+ let(:lock_key) { "#{fb_message.sender_id}-lock" }
10
+ let(:example_tid) { 'ovefhgJvx' }
11
+ let(:example_session) { 'goodbye->say_goodbye' }
12
+ let(:example_position) { 2 }
13
+ let(:example_lock) { "#{example_tid}##{example_session}:#{example_position}" }
14
+
15
+ describe 'current_lock' do
16
+ it "should return the current lock for the session if it is locked" do
17
+ $redis.set(lock_key, example_lock)
18
+ current_lock = controller.current_lock
19
+ expect(current_lock).to be_a(Stealth::Lock)
20
+ expect(current_lock.session_id).to eq fb_message.sender_id
21
+ end
22
+
23
+ it "should return nil if the current session is not locked" do
24
+ random_lock_key = "xyz123-lock"
25
+
26
+ # clear the memoization
27
+ controller.instance_eval do
28
+ @current_lock = nil
29
+ @current_session_id = random_lock_key
30
+ end
31
+
32
+ expect($redis.get(random_lock_key)).to be_nil
33
+ expect(controller.current_lock).to be_nil
34
+ end
35
+ end
36
+
37
+ describe 'run_interrupt_action' do
38
+ let(:interrupts_controller) { InterruptController.new(service_message: fb_message) }
39
+
40
+ it "should return false if an InterruptsController is not defined" do
41
+ expect(Stealth::Logger).to receive(:l).with(
42
+ topic: 'interrupt',
43
+ message: "Interrupt detected for session #{fb_message.sender_id}"
44
+ ).ordered
45
+
46
+ expect(Stealth::Logger).to receive(:l).with(
47
+ topic: 'interrupt',
48
+ message: 'Ignoring interrupt; InterruptsController not defined.'
49
+ ).ordered
50
+
51
+ expect(controller.run_interrupt_action).to be false
52
+ end
53
+
54
+ it "should call say_interrupted on the InterruptsController" do
55
+ class InterruptsController < Stealth::Controller
56
+ def say_interrupted
57
+ end
58
+ end
59
+
60
+ expect_any_instance_of(InterruptsController).to receive(:say_interrupted)
61
+ controller.run_interrupt_action
62
+ end
63
+
64
+ it "should log if the InterruptsController#say_interrupted does not progress the session" do
65
+ class InterruptsController < Stealth::Controller
66
+ def say_interrupted
67
+ end
68
+ end
69
+
70
+ expect(Stealth::Logger).to receive(:l).with(
71
+ topic: 'interrupt',
72
+ message: "Interrupt detected for session #{fb_message.sender_id}"
73
+ ).ordered
74
+
75
+ expect(Stealth::Logger).to receive(:l).with(
76
+ topic: 'interrupt',
77
+ message: 'Did not send replies, update session, or step'
78
+ ).ordered
79
+
80
+ controller.run_interrupt_action
81
+ end
82
+
83
+ it "should catch StandardError from within InterruptController and log it" do
84
+ class InterruptsController < Stealth::Controller
85
+ def say_interrupted
86
+ raise Stealth::Errors::ReplyNotFound
87
+ end
88
+ end
89
+
90
+ # Once for the interrupt detection, once for the error
91
+ expect(Stealth::Logger).to receive(:l).exactly(2).times
92
+
93
+ controller.run_interrupt_action
94
+ end
95
+ end
96
+
97
+ describe 'interrupt_detected?' do
98
+ it "should return false if there is not a lock on the session" do
99
+ random_lock_key = "xyz123-lock"
100
+
101
+ # clear the memoization
102
+ controller.instance_eval do
103
+ @current_lock = nil
104
+ @current_session_id = random_lock_key
105
+ end
106
+
107
+ expect(controller.send(:interrupt_detected?)).to be false
108
+ end
109
+
110
+ it "should return false if the current thread owns the lock" do
111
+ $redis.set(lock_key, example_lock)
112
+ lock = controller.current_lock
113
+ expect(lock).to receive(:tid).and_return(Stealth.tid)
114
+
115
+ expect(controller.send(:interrupt_detected?)).to be false
116
+ end
117
+
118
+ it 'should return true if the session is locked by another thread' do
119
+ $redis.set(lock_key, example_lock)
120
+ # our mock tid will not match the real tid for this test
121
+ expect(controller.send(:interrupt_detected?)).to be true
122
+ end
123
+ end
124
+
125
+ describe 'current_thread_has_control?' do
126
+ it "should return true if the current tid matches the lock tid" do
127
+ $redis.set(lock_key, example_lock)
128
+ lock = controller.current_lock
129
+ expect(lock).to receive(:tid).and_return(Stealth.tid)
130
+ expect(controller.send(:current_thread_has_control?)).to be true
131
+ end
132
+
133
+ it "should return false if the current tid does not match the lock tid" do
134
+ $redis.set(lock_key, example_lock)
135
+ lock = controller.current_lock
136
+ expect(controller.send(:current_thread_has_control?)).to be false
137
+ end
138
+ end
139
+
140
+ describe 'lock_session!' do
141
+ it "should create a lock for the session" do
142
+ $redis.del(lock_key)
143
+ controller.send(:lock_session!, session_slug: example_session, position: example_position)
144
+ expect($redis.get(lock_key)).to match(/goodbye\-\>say_goodbye\:2/)
145
+ end
146
+ end
147
+
148
+ describe 'release_lock!' do
149
+ it "should not raise an error if current_lock is nil" do
150
+ expect(controller).to receive(:current_lock).and_return(nil)
151
+ controller.send(:release_lock!)
152
+ end
153
+
154
+ it "should not release the lock if we are in the InterruptsController" do
155
+ class InterruptsController
156
+ end
157
+
158
+ lock = controller.current_lock
159
+ expect(controller).to receive(:class).and_return InterruptsController
160
+ expect(lock).to_not receive(:release)
161
+ controller.send(:release_lock!)
162
+ end
163
+
164
+ it "should release the lock if there is one and we are not in the InterruptsController" do
165
+ $redis.set(lock_key, example_lock)
166
+ controller.send(:release_lock!)
167
+ expect($redis.get(lock_key)).to be_nil
168
+ end
169
+ end
170
+
171
+ end
@@ -0,0 +1,744 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Stealth::Controller::Messages do
6
+
7
+ class MrTronsController < Stealth::Controller
8
+
9
+ end
10
+
11
+ let(:facebook_message) { SampleMessage.new(service: 'facebook') }
12
+ let(:test_controller) {
13
+ MrTronsController.new(service_message: facebook_message.message_with_text)
14
+ }
15
+
16
+ describe "normalized_msg" do
17
+ let(:padded_msg) { ' Hello World! 👋 ' }
18
+ let(:weird_case_msg) { 'Oh BaBy Oh BaBy' }
19
+
20
+ it 'should normalize blank-padded messages' do
21
+ test_controller.current_message.message = padded_msg
22
+ expect(test_controller.normalized_msg).to eq('HELLO WORLD! 👋')
23
+ end
24
+
25
+ it 'should normalize differently cased messages' do
26
+ test_controller.current_message.message = weird_case_msg
27
+ expect(test_controller.normalized_msg).to eq('OH BABY OH BABY')
28
+ end
29
+ end
30
+
31
+ describe "homophone_translated_msg" do
32
+ it 'should convert homophones to their respective alpha ordinal' do
33
+ Stealth::Controller::Messages::HOMOPHONES.each do |homophone, ordinal|
34
+ test_controller.current_message.message = homophone
35
+ test_controller.normalized_msg = test_controller.homophone_translated_msg = nil
36
+ expect(test_controller.homophone_translated_msg).to eq(ordinal)
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "get_match" do
42
+ it "should match messages with different casing" do
43
+ test_controller.current_message.message = "NICE"
44
+ expect(
45
+ test_controller.get_match(['nice', 'woot'])
46
+ ).to eq('nice')
47
+ end
48
+
49
+ it "should match messages with blank padding" do
50
+ test_controller.current_message.message = " NiCe "
51
+ expect(
52
+ test_controller.get_match(['nice', 'woot'])
53
+ ).to eq('nice')
54
+ end
55
+
56
+ it "should match messages utilizing a lower case SMS quick reply" do
57
+ test_controller.current_message.message = "a "
58
+ expect(
59
+ test_controller.get_match(['nice', 'woot'])
60
+ ).to eq('nice')
61
+ end
62
+
63
+ it "should match messages utilizing an upper case SMS quick reply" do
64
+ test_controller.current_message.message = " B "
65
+ expect(
66
+ test_controller.get_match(['nice', 'woot'])
67
+ ).to eq('woot')
68
+ end
69
+
70
+ it "should match messages utilizing a single-quoted SMS quick reply" do
71
+ test_controller.current_message.message = "'B'"
72
+ expect(
73
+ test_controller.get_match(['nice', 'woot'])
74
+ ).to eq('woot')
75
+ end
76
+
77
+ it "should match messages utilizing a double-quoted SMS quick reply" do
78
+ test_controller.current_message.message = '"A"'
79
+ expect(
80
+ test_controller.get_match(['nice', 'woot'])
81
+ ).to eq('nice')
82
+ end
83
+
84
+ it "should match messages utilizing a double-smartquoted SMS quick reply" do
85
+ test_controller.current_message.message = '“A”'
86
+ expect(
87
+ test_controller.get_match(['nice', 'woot'])
88
+ ).to eq('nice')
89
+ end
90
+
91
+ it "should match messages utilizing a single-smartquoted SMS quick reply" do
92
+ test_controller.current_message.message = '‘A’'
93
+ expect(
94
+ test_controller.get_match(['nice', 'woot'])
95
+ ).to eq('nice')
96
+ end
97
+
98
+ it "should match messages with a period in the SMS quick reply" do
99
+ test_controller.current_message.message = 'A.'
100
+ expect(
101
+ test_controller.get_match(['nice', 'woot'])
102
+ ).to eq('nice')
103
+ end
104
+
105
+ it "should match messages with a question mark in the SMS quick reply" do
106
+ test_controller.current_message.message = 'B?'
107
+ expect(
108
+ test_controller.get_match(['nice', 'woot'])
109
+ ).to eq('woot')
110
+ end
111
+
112
+ it "should match messages in parens in the SMS quick reply" do
113
+ test_controller.current_message.message = '(B)'
114
+ expect(
115
+ test_controller.get_match(['nice', 'woot'])
116
+ ).to eq('woot')
117
+ end
118
+
119
+ it "should match messages with backticks in the SMS quick reply" do
120
+ test_controller.current_message.message = '`B`'
121
+ expect(
122
+ test_controller.get_match(['nice', 'woot'])
123
+ ).to eq('woot')
124
+ end
125
+
126
+ it "should match messages utilizing a homophone" do
127
+ test_controller.current_message.message = " bee "
128
+ expect(
129
+ test_controller.get_match(['nice', 'woot'])
130
+ ).to eq('woot')
131
+ end
132
+
133
+ it "should raise ReservedHomophoneUsed if a homophone is used" do
134
+ test_controller.current_message.message = " B "
135
+ expect {
136
+ test_controller.get_match(['nice', 'woot', 'sea', 'bee'])
137
+ }.to raise_error(Stealth::Errors::ReservedHomophoneUsed, 'Cannot use `SEA, BEE`. Reserved for homophones.')
138
+ end
139
+
140
+ it "should raise Stealth::Errors::UnrecognizedMessage if a response was not matched" do
141
+ test_controller.current_message.message = "uh oh"
142
+ expect {
143
+ test_controller.get_match(['nice', 'woot'])
144
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
145
+ end
146
+
147
+ it "should raise Stealth::Errors::UnrecognizedMessage if an SMS quick reply was not matched" do
148
+ test_controller.current_message.message = "C"
149
+ expect {
150
+ test_controller.get_match(['nice', 'woot'])
151
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
152
+ end
153
+
154
+ it "should not run NLP entity detection if an ordinal is entered by the user" do
155
+ test_controller.current_message.message = "C"
156
+
157
+ expect(test_controller).to_not receive(:perform_nlp!)
158
+ expect(
159
+ test_controller.get_match([:yes, :no, 'unsubscribe'])
160
+ ).to eq('unsubscribe')
161
+ end
162
+
163
+ describe "entity detection" do
164
+ let(:no_intent) { :no }
165
+ let(:yes_intent) { :yes }
166
+ let(:single_number_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :single_number_entity) }
167
+ let(:double_number_nlp_result) { TestNlpResult::Luis.new(intent: no_intent, entity: :double_number_entity) }
168
+ let(:triple_number_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :triple_number_entity) }
169
+
170
+ describe 'single nlp_result entity' do
171
+ it 'should return the :number entity' do
172
+ allow(test_controller).to receive(:perform_nlp!).and_return(single_number_nlp_result)
173
+ test_controller.nlp_result = single_number_nlp_result
174
+
175
+ test_controller.current_message.message = "hi"
176
+ expect(
177
+ test_controller.get_match(['nice', :number])
178
+ ).to eq(test_controller.nlp_result.entities[:number].first)
179
+ end
180
+
181
+ it 'should return the first :number entity if fuzzy_match=true' do
182
+ allow(test_controller).to receive(:perform_nlp!).and_return(double_number_nlp_result)
183
+ test_controller.nlp_result = double_number_nlp_result
184
+
185
+ test_controller.current_message.message = "hi"
186
+ expect(
187
+ test_controller.get_match(['nice', :number])
188
+ ).to eq(test_controller.nlp_result.entities[:number].first)
189
+ end
190
+
191
+ it 'should raise Stealth::Errors::UnrecognizedMessage if more than one :number entity is returned and fuzzy_match=false' do
192
+ allow(test_controller).to receive(:perform_nlp!).and_return(double_number_nlp_result)
193
+ test_controller.nlp_result = double_number_nlp_result
194
+
195
+ test_controller.current_message.message = "hi"
196
+ expect {
197
+ test_controller.get_match(['nice', :number], fuzzy_match: false)
198
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage, "Encountered 2 entity matches of type :number and expected 1. To allow, set fuzzy_match to true.")
199
+ end
200
+
201
+ it 'should log the NLP result if log_all_nlp_results=true' do
202
+ Stealth.config.log_all_nlp_results = true
203
+ Stealth.config.nlp_integration = :luis
204
+
205
+ luis_client = double('luis_client')
206
+ allow(luis_client).to receive(:understand).and_return(single_number_nlp_result)
207
+ allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(luis_client)
208
+
209
+ expect(Stealth::Logger).to receive(:l).with(
210
+ topic: :nlp,
211
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> Performing NLP."
212
+ )
213
+ expect(Stealth::Logger).to receive(:l).with(
214
+ topic: :nlp,
215
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: #{single_number_nlp_result.parsed_result.inspect}"
216
+ )
217
+ test_controller.current_message.message = "hi"
218
+ test_controller.get_match(['nice', :number])
219
+
220
+ Stealth.config.log_all_nlp_results = false
221
+ Stealth.config.nlp_integration = nil
222
+ end
223
+ end
224
+
225
+ describe 'multiple nlp_result entity matches' do
226
+ it 'should return the [:number, :number] entity' do
227
+ allow(test_controller).to receive(:perform_nlp!).and_return(double_number_nlp_result)
228
+ test_controller.nlp_result = double_number_nlp_result
229
+
230
+ test_controller.current_message.message = "hi"
231
+ expect(
232
+ test_controller.get_match(['nice', [:number, :number]])
233
+ ).to eq(double_number_nlp_result.entities[:number])
234
+ end
235
+
236
+ it 'should return the [:number, :number, :number] entity' do
237
+ allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result)
238
+ test_controller.nlp_result = triple_number_nlp_result
239
+
240
+ test_controller.current_message.message = "hi"
241
+ expect(
242
+ test_controller.get_match(['nice', [:number, :number, :number]])
243
+ ).to eq(triple_number_nlp_result.entities[:number])
244
+ end
245
+
246
+ it 'should return the [:number, :number] entity from a triple :number entity result' do
247
+ allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result)
248
+ test_controller.nlp_result = triple_number_nlp_result
249
+
250
+ test_controller.current_message.message = "hi"
251
+ expect(
252
+ test_controller.get_match(['nice', [:number, :number]])
253
+ ).to eq(triple_number_nlp_result.entities[:number].slice(0, 2))
254
+ end
255
+
256
+ it 'should return the :number entity from a triple :number entity result' do
257
+ allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result)
258
+ test_controller.nlp_result = triple_number_nlp_result
259
+
260
+ test_controller.current_message.message = "hi"
261
+ expect(
262
+ test_controller.get_match(['nice', :number])
263
+ ).to eq(triple_number_nlp_result.entities[:number].first)
264
+ end
265
+
266
+ it 'should return the [:number, :key_phrase] entities' do
267
+ allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result)
268
+ test_controller.nlp_result = triple_number_nlp_result
269
+
270
+ test_controller.current_message.message = "hi"
271
+ expect(
272
+ test_controller.get_match(['nice', [:number, :key_phrase]])
273
+ ).to eq([89, 'scores'])
274
+ end
275
+
276
+ it 'should raise Stealth::Errors::UnrecognizedMessage if more than one :number entity is returned and fuzzy_match=false' do
277
+ allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result)
278
+ test_controller.nlp_result = triple_number_nlp_result
279
+
280
+ test_controller.current_message.message = "hi"
281
+ expect {
282
+ test_controller.get_match(['nice', :number], fuzzy_match: false)
283
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage, "Encountered 3 entity matches of type :number and expected 1. To allow, set fuzzy_match to true.")
284
+ end
285
+
286
+ it 'should raise Stealth::Errors::UnrecognizedMessage if more than two :number entities are returned and fuzzy_match=false' do
287
+ allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result)
288
+ test_controller.nlp_result = triple_number_nlp_result
289
+
290
+ test_controller.current_message.message = "hi"
291
+ expect {
292
+ test_controller.get_match(['nice', [:number, :number]], fuzzy_match: false)
293
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage, "Encountered 1 additional entity matches of type :number for match [:number, :number]. To allow, set fuzzy_match to true.")
294
+ end
295
+
296
+ it 'should log the NLP result if log_all_nlp_results=true' do
297
+ Stealth.config.log_all_nlp_results = true
298
+ Stealth.config.nlp_integration = :luis
299
+
300
+ luis_client = double('luis_client')
301
+ allow(luis_client).to receive(:understand).and_return(triple_number_nlp_result)
302
+ allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(luis_client)
303
+
304
+ expect(Stealth::Logger).to receive(:l).with(
305
+ topic: :nlp,
306
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> Performing NLP."
307
+ )
308
+ expect(Stealth::Logger).to receive(:l).with(
309
+ topic: :nlp,
310
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: #{triple_number_nlp_result.parsed_result.inspect}"
311
+ )
312
+ test_controller.current_message.message = "hi"
313
+ test_controller.get_match(['nice', [:number, :number]])
314
+
315
+ Stealth.config.log_all_nlp_results = false
316
+ Stealth.config.nlp_integration = nil
317
+ end
318
+ end
319
+
320
+ describe 'custom entities' do
321
+ let(:custom_entity_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :custom_entity) }
322
+
323
+ it 'should return the text matched by the custom entity' do
324
+ allow(test_controller).to receive(:perform_nlp!).and_return(custom_entity_nlp_result)
325
+ test_controller.nlp_result = custom_entity_nlp_result
326
+
327
+ test_controller.current_message.message = "call me right away"
328
+ expect(
329
+ test_controller.get_match(['nice', :asap])
330
+ ).to eq 'right away'
331
+ end
332
+ end
333
+ end
334
+
335
+ describe "mismatch" do
336
+ describe 'raise_on_mismatch: true' do
337
+ it "should raise a Stealth::Errors::UnrecognizedMessage" do
338
+ test_controller.current_message.message = 'C'
339
+ expect {
340
+ test_controller.get_match(['nice', 'woot'])
341
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
342
+ end
343
+
344
+ it "should NOT log if an nlp_result is not present" do
345
+ test_controller.current_message.message = 'spicy'
346
+ expect(Stealth::Logger).to_not receive(:l)
347
+ expect {
348
+ test_controller.get_match(['nice', 'woot'])
349
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
350
+ end
351
+
352
+ it "should log if an nlp_result is present" do
353
+ test_controller.current_message.message = 'spicy'
354
+ nlp_result = double('nlp_result')
355
+ allow(nlp_result).to receive(:parsed_result).and_return({})
356
+
357
+ expect(Stealth::Logger).to receive(:l).with(
358
+ topic: :nlp,
359
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: {}"
360
+ )
361
+
362
+ test_controller.nlp_result = nlp_result
363
+
364
+ expect {
365
+ test_controller.get_match(['nice', 'woot'])
366
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
367
+ end
368
+ end
369
+
370
+ describe 'raise_on_mismatch: false' do
371
+ it "should not raise a Stealth::Errors::UnrecognizedMessage" do
372
+ test_controller.current_message.message = 'C'
373
+ expect {
374
+ test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false)
375
+ }.to_not raise_error(Stealth::Errors::UnrecognizedMessage)
376
+ end
377
+
378
+ it "should return the original message" do
379
+ test_controller.current_message.message = 'spicy'
380
+ expect(
381
+ test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false)
382
+ ).to eq 'spicy'
383
+ end
384
+
385
+ it "should NOT log if an nlp_result is not present" do
386
+ test_controller.current_message.message = 'spicy'
387
+ expect(Stealth::Logger).to_not receive(:l)
388
+ test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false)
389
+ end
390
+
391
+ it "should log if an nlp_result is present" do
392
+ test_controller.current_message.message = 'spicy'
393
+ nlp_result = double('nlp_result')
394
+ allow(nlp_result).to receive(:parsed_result).and_return({})
395
+
396
+ expect(Stealth::Logger).to receive(:l).with(
397
+ topic: :nlp,
398
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: {}"
399
+ )
400
+
401
+ test_controller.nlp_result = nlp_result
402
+
403
+ test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false)
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ describe "handle_message" do
410
+ it "should run the proc of the matched reply" do
411
+ expect(STDOUT).to receive(:puts).with('Cool, Refinance 👍')
412
+
413
+ test_controller.current_message.message = "B"
414
+ test_controller.handle_message(
415
+ 'Buy' => proc { puts 'Buy' },
416
+ 'Refinance' => proc { puts 'Cool, Refinance 👍' }
417
+ )
418
+ end
419
+
420
+ it "should run proc in the binding of the calling instance" do
421
+ test_controller.current_message.message = "B"
422
+ x = 0
423
+ test_controller.handle_message(
424
+ 'Buy' => proc { x += 1 },
425
+ 'Refinance' => proc { x += 2 }
426
+ )
427
+
428
+ expect(x).to eq 2
429
+ end
430
+
431
+ it "should match against single-quoted ordinals" do
432
+ test_controller.current_message.message = "'B'"
433
+ x = 0
434
+ test_controller.handle_message(
435
+ 'Buy' => proc { x += 1 },
436
+ 'Refinance' => proc { x += 2 }
437
+ )
438
+
439
+ expect(x).to eq 2
440
+ end
441
+
442
+ it "should match against double-quoted ordinals" do
443
+ test_controller.current_message.message = '"A"'
444
+ x = 0
445
+ test_controller.handle_message(
446
+ 'Buy' => proc { x += 1 },
447
+ 'Refinance' => proc { x += 2 }
448
+ )
449
+
450
+ expect(x).to eq 1
451
+ end
452
+
453
+ it "should match against double-smartquoted ordinals" do
454
+ test_controller.current_message.message = '“A”'
455
+ x = 0
456
+ test_controller.handle_message(
457
+ 'Buy' => proc { x += 1 },
458
+ 'Refinance' => proc { x += 2 }
459
+ )
460
+
461
+ expect(x).to eq 1
462
+ end
463
+
464
+ it "should match against single-smartquoted ordinals" do
465
+ test_controller.current_message.message = '‘A’'
466
+ x = 0
467
+ test_controller.handle_message(
468
+ 'Buy' => proc { x += 1 },
469
+ 'Refinance' => proc { x += 2 }
470
+ )
471
+
472
+ expect(x).to eq 1
473
+ end
474
+
475
+ it "should match against ordinals with periods" do
476
+ test_controller.current_message.message = 'A.'
477
+ x = 0
478
+ test_controller.handle_message(
479
+ 'Buy' => proc { x += 1 },
480
+ 'Refinance' => proc { x += 2 }
481
+ )
482
+
483
+ expect(x).to eq 1
484
+ end
485
+
486
+ it "should match against ordinals with question marks" do
487
+ test_controller.current_message.message = 'A?'
488
+ x = 0
489
+ test_controller.handle_message(
490
+ 'Buy' => proc { x += 1 },
491
+ 'Refinance' => proc { x += 2 }
492
+ )
493
+
494
+ expect(x).to eq 1
495
+ end
496
+
497
+ it "should match against ordinals with parens" do
498
+ test_controller.current_message.message = '(A)'
499
+ x = 0
500
+ test_controller.handle_message(
501
+ 'Buy' => proc { x += 1 },
502
+ 'Refinance' => proc { x += 2 }
503
+ )
504
+
505
+ expect(x).to eq 1
506
+ end
507
+
508
+ it "should match against ordinals with backticks" do
509
+ test_controller.current_message.message = '`A`'
510
+ x = 0
511
+ test_controller.handle_message(
512
+ 'Buy' => proc { x += 1 },
513
+ 'Refinance' => proc { x += 2 }
514
+ )
515
+
516
+ expect(x).to eq 1
517
+ end
518
+
519
+ it "should match homophones" do
520
+ test_controller.current_message.message = 'sea'
521
+ x = 0
522
+ test_controller.handle_message(
523
+ 'Buy' => proc { x += 1 },
524
+ 'Refinance' => proc { x += 2 },
525
+ 'Other' => proc { x += 3 }
526
+ )
527
+
528
+ expect(x).to eq 3
529
+ end
530
+
531
+ it "should raise ReservedHomophoneUsed error if an arm contains a reserved homophone" do
532
+ test_controller.current_message.message = "B"
533
+ x = 0
534
+
535
+ expect {
536
+ test_controller.handle_message(
537
+ 'Buy' => proc { x += 1 },
538
+ :woot => proc { x += 2 },
539
+ 'Sea' => proc { x += 3 }
540
+ )
541
+ }.to raise_error(Stealth::Errors::ReservedHomophoneUsed, 'Cannot use `SEA`. Reserved for homophones.')
542
+ end
543
+
544
+ it "should not run NLP if an ordinal is entered by the user" do
545
+ test_controller.current_message.message = "C"
546
+ x = 0
547
+ test_controller.handle_message(
548
+ :yes => proc { x += 1 },
549
+ :no => proc { x += 2 },
550
+ 'Unsubscribe' => proc { x += 3 }
551
+ )
552
+
553
+ expect(test_controller).to_not receive(:perform_nlp!)
554
+ expect(x).to eq 3
555
+ end
556
+
557
+ describe "intent detection" do
558
+ let(:no_intent) { :no }
559
+ let(:yes_intent) { :yes }
560
+ let(:yes_intent_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :single_number_entity) }
561
+ let(:no_intent_nlp_result) { TestNlpResult::Luis.new(intent: no_intent, entity: :double_number_entity) }
562
+
563
+ it 'should support :yes intent' do
564
+ test_controller.current_message.message = "YAS"
565
+ allow(test_controller).to receive(:perform_nlp!).and_return(yes_intent_nlp_result)
566
+ test_controller.nlp_result = yes_intent_nlp_result
567
+
568
+ x = 0
569
+ test_controller.send(
570
+ :handle_message, {
571
+ 'Buy' => proc { x += 1 },
572
+ :yes => proc { x += 9 },
573
+ :no => proc { x += 8 }
574
+ }
575
+ )
576
+
577
+ expect(x).to eq 9
578
+ end
579
+
580
+ it 'should support :no intent' do
581
+ test_controller.current_message.message = "NAH"
582
+ allow(test_controller).to receive(:perform_nlp!).and_return(no_intent_nlp_result)
583
+ test_controller.nlp_result = no_intent_nlp_result
584
+
585
+ x = 0
586
+ test_controller.send(
587
+ :handle_message, {
588
+ 'Buy' => proc { x += 1 },
589
+ :yes => proc { x += 9 },
590
+ :no => proc { x += 8 }
591
+ }
592
+ )
593
+
594
+ expect(x).to eq 8
595
+ end
596
+
597
+ it 'should log the NLP result if log_all_nlp_results=true' do
598
+ Stealth.config.log_all_nlp_results = true
599
+ Stealth.config.nlp_integration = :luis
600
+
601
+ luis_client = double('luis_client')
602
+ allow(luis_client).to receive(:understand).and_return(yes_intent_nlp_result)
603
+ allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(luis_client)
604
+
605
+ expect(Stealth::Logger).to receive(:l).with(
606
+ topic: :nlp,
607
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> Performing NLP."
608
+ )
609
+ expect(Stealth::Logger).to receive(:l).with(
610
+ topic: :nlp,
611
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: #{yes_intent_nlp_result.parsed_result.inspect}"
612
+ )
613
+ test_controller.current_message.message = "YAS"
614
+ x = 0
615
+ test_controller.send(
616
+ :handle_message, {
617
+ 'Buy' => proc { x += 1 },
618
+ :yes => proc { x += 9 },
619
+ :no => proc { x += 8 }
620
+ }
621
+ )
622
+
623
+ Stealth.config.log_all_nlp_results = false
624
+ Stealth.config.nlp_integration = nil
625
+ end
626
+ end
627
+
628
+ describe 'Regexp matcher' do
629
+ it "should match when the Regexp matches" do
630
+ test_controller.current_message.message = "About Encom"
631
+ x = 0
632
+ test_controller.handle_message(
633
+ 'Buy' => proc { x += 1 },
634
+ 'Refinance' => proc { x += 2 },
635
+ /about/i => proc { x += 10 }
636
+ )
637
+ expect(x).to eq 10
638
+ end
639
+
640
+ it "should match positional Regexes" do
641
+ test_controller.current_message.message = "Jump about"
642
+ x = 0
643
+ test_controller.handle_message(
644
+ 'Buy' => proc { x += 1 },
645
+ /\Aabout/i => proc { x += 2 },
646
+ /about/i => proc { x += 10 }
647
+ )
648
+ expect(x).to eq 10
649
+ end
650
+
651
+ it "should match as an alpha ordinal" do
652
+ test_controller.current_message.message = "C"
653
+ x = 0
654
+ test_controller.handle_message(
655
+ 'Buy' => proc { x += 1 },
656
+ 'Refinance' => proc { x += 2 },
657
+ /about/i => proc { x += 10 }
658
+ )
659
+ expect(x).to eq 10
660
+ end
661
+ end
662
+
663
+ describe 'nil matcher' do
664
+ it "should match the respective ordinal" do
665
+ test_controller.current_message.message = "C"
666
+ x = 0
667
+ test_controller.handle_message(
668
+ 'Buy' => proc { x += 1 },
669
+ 'Refinance' => proc { x += 2 },
670
+ nil => proc { x += 10 }
671
+ )
672
+ expect(x).to eq 10
673
+ end
674
+
675
+ it "should match an unknown ordinal" do
676
+ test_controller.current_message.message = "D"
677
+ x = 0
678
+ test_controller.handle_message(
679
+ 'Buy' => proc { x += 1 },
680
+ 'Refinance' => proc { x += 2 },
681
+ nil => proc { x += 10 }
682
+ )
683
+ expect(x).to eq 10
684
+ end
685
+
686
+ it "should match free-form text" do
687
+ test_controller.current_message.message = "Hello world!"
688
+ x = 0
689
+ test_controller.handle_message(
690
+ 'Buy' => proc { x += 1 },
691
+ 'Refinance' => proc { x += 2 },
692
+ nil => proc { x += 10 }
693
+ )
694
+ expect(x).to eq 10
695
+ end
696
+ end
697
+
698
+ it "should raise Stealth::Errors::UnrecognizedMessage if the reply does not match" do
699
+ test_controller.current_message.message = "C"
700
+ x = 0
701
+ expect {
702
+ test_controller.handle_message(
703
+ 'Buy' => proc { x += 1 },
704
+ 'Refinance' => proc { x += 2 }
705
+ )
706
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
707
+ end
708
+
709
+ it "should NOT log if an nlp_result is not present" do
710
+ test_controller.current_message.message = 'spicy'
711
+ expect(Stealth::Logger).to_not receive(:l)
712
+
713
+ x = 0
714
+ expect {
715
+ test_controller.handle_message(
716
+ 'Buy' => proc { x += 1 },
717
+ 'Refinance' => proc { x += 2 }
718
+ )
719
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
720
+ end
721
+
722
+ it "should log if an nlp_result is present" do
723
+ test_controller.current_message.message = 'spicy'
724
+ nlp_result = double('nlp_result')
725
+ allow(nlp_result).to receive(:parsed_result).and_return({})
726
+
727
+ expect(Stealth::Logger).to receive(:l).with(
728
+ topic: :nlp,
729
+ message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: {}"
730
+ )
731
+
732
+ test_controller.nlp_result = nlp_result
733
+
734
+ x = 0
735
+ expect {
736
+ test_controller.handle_message(
737
+ 'Buy' => proc { x += 1 },
738
+ 'Refinance' => proc { x += 2 }
739
+ )
740
+ }.to raise_error(Stealth::Errors::UnrecognizedMessage)
741
+ end
742
+ end
743
+
744
+ end