stealth 1.1.2 → 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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +18 -8
  3. data/CHANGELOG.md +100 -0
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +49 -43
  6. data/LICENSE +4 -17
  7. data/README.md +9 -17
  8. data/VERSION +1 -1
  9. data/lib/stealth/base.rb +62 -13
  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 +179 -41
  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 +4 -4
  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 -39
  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