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
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Stealth::Controller::UnrecognizedMessage" do
6
+
7
+ let(:fb_message) { SampleMessage.new(service: 'facebook') }
8
+ let(:controller) { VadersController.new(service_message: fb_message.message_with_text) }
9
+
10
+ describe 'run_unrecognized_message' do
11
+ let(:e) {
12
+ e = OpenStruct.new
13
+ e.class = RuntimeError
14
+ e.message = 'oops'
15
+ e.backtrace = [
16
+ '/stealth/lib/stealth/controller/controller.rb',
17
+ '/stealth/lib/stealth/controller/catch_all.rb',
18
+ ]
19
+ e
20
+ }
21
+
22
+ describe 'when UnrecognizedMessagesController is not defined' do
23
+ before(:each) do
24
+ Object.send(:remove_const, :UnrecognizedMessagesController)
25
+ end
26
+
27
+ it "should log and run catch_all" do
28
+ expect(Stealth::Logger).to receive(:l).with(
29
+ topic: 'unrecognized_message',
30
+ message: "The message \"Hello World!\" was not recognized in the original context."
31
+ ).ordered
32
+
33
+ expect(Stealth::Logger).to receive(:l).with(
34
+ topic: 'unrecognized_message',
35
+ message: 'Running catch_all; UnrecognizedMessagesController not defined.'
36
+ ).ordered
37
+
38
+ expect(controller).to receive(:run_catch_all).with(err: e)
39
+ controller.run_unrecognized_message(err: e)
40
+ end
41
+ end
42
+
43
+ it "should call handle_unrecognized_message on the UnrecognizedMessagesController" do
44
+ class UnrecognizedMessagesController < Stealth::Controller
45
+ def handle_unrecognized_message
46
+ do_nothing
47
+ end
48
+ end
49
+
50
+ expect(Stealth::Logger).to receive(:l).with(
51
+ topic: 'unrecognized_message',
52
+ message: "The message \"Hello World!\" was not recognized in the original context."
53
+ ).ordered
54
+
55
+ expect(Stealth::Logger).to receive(:l).with(
56
+ topic: 'unrecognized_message',
57
+ message: 'A match was detected. Skipping catch-all.'
58
+ ).ordered
59
+
60
+ controller.run_unrecognized_message(err: e)
61
+ end
62
+
63
+ it "should log if the UnrecognizedMessagesController#handle_unrecognized_message does not progress the session" do
64
+ class UnrecognizedMessagesController < Stealth::Controller
65
+ def handle_unrecognized_message
66
+ # Oops
67
+ end
68
+ end
69
+
70
+ expect(Stealth::Logger).to receive(:l).with(
71
+ topic: 'unrecognized_message',
72
+ message: "The message \"Hello World!\" was not recognized in the original context."
73
+ ).ordered
74
+
75
+ expect(Stealth::Logger).to receive(:l).with(
76
+ topic: 'unrecognized_message',
77
+ message: 'Did not send replies, update session, or step'
78
+ ).ordered
79
+
80
+ expect(controller).to_not receive(:run_catch_all)
81
+
82
+ controller.run_unrecognized_message(err: e)
83
+ end
84
+
85
+ describe 'handoff to catch_all' do
86
+ before(:each) do
87
+ @session = Stealth::Session.new(id: controller.current_session_id)
88
+ @session.set_session(new_flow: 'vader', new_state: 'action_with_unrecognized_msg')
89
+
90
+ @error_slug = [
91
+ 'error',
92
+ controller.current_session_id,
93
+ 'vader',
94
+ 'action_with_unrecognized_msg'
95
+ ].join('-')
96
+
97
+ $redis.del(@error_slug)
98
+ end
99
+
100
+ it "should catch StandardError within UnrecognizedMessagesController and run catch_all" do
101
+ $err = Stealth::Errors::ReplyNotFound.new('oops')
102
+
103
+ class UnrecognizedMessagesController < Stealth::Controller
104
+ def handle_unrecognized_message
105
+ raise $err
106
+ end
107
+ end
108
+
109
+ expect(Stealth::Logger).to receive(:l).with(
110
+ topic: 'unrecognized_message',
111
+ message: "The message \"Hello World!\" was not recognized in the original context."
112
+ ).ordered
113
+
114
+ expect(controller).to receive(:run_catch_all).with(err: $err)
115
+
116
+ controller.run_unrecognized_message(err: e)
117
+ end
118
+
119
+ it "should track the catch_all level against the original session during exceptions" do
120
+ class UnrecognizedMessagesController < Stealth::Controller
121
+ def handle_unrecognized_message
122
+ raise 'oops'
123
+ end
124
+ end
125
+
126
+ expect($redis.get(@error_slug)).to be_nil
127
+ controller.run_unrecognized_message(err: e)
128
+ expect($redis.get(@error_slug)).to eq '1'
129
+ end
130
+
131
+ it "should track the catch_all level against the original session for UnrecognizedMessage errors" do
132
+ class UnrecognizedMessagesController < Stealth::Controller
133
+ def handle_unrecognized_message
134
+ handle_message(
135
+ 'x' => proc { do_nothing },
136
+ 'y' => proc { do_nothing }
137
+ )
138
+ end
139
+ end
140
+
141
+ expect($redis.get(@error_slug)).to be_nil
142
+ controller.action(action: :action_with_unrecognized_msg)
143
+ expect($redis.get(@error_slug)).to eq '1'
144
+ end
145
+
146
+ it "should NOT run catch_all if UnrecognizedMessagesController handles the message" do
147
+ $x = 0
148
+ class UnrecognizedMessagesController < Stealth::Controller
149
+ def handle_unrecognized_message
150
+ handle_message(
151
+ 'Hello World!' => proc {
152
+ $x = 1
153
+ do_nothing
154
+ },
155
+ 'y' => proc { do_nothing }
156
+ )
157
+ end
158
+ end
159
+
160
+ expect($redis.get(@error_slug)).to be_nil
161
+ controller.action(action: :action_with_unrecognized_msg)
162
+ expect($redis.get(@error_slug)).to be_nil
163
+ expect($x).to eq 1
164
+ end
165
+ end
166
+ end
167
+
168
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Stealth::Dispatcher" do
6
+
7
+ class Stealth::Services::Facebook::MessageHandler
8
+
9
+ end
10
+
11
+ describe 'coordinate' do
12
+ let(:dispatcher) {
13
+ Stealth::Dispatcher.new(
14
+ service: 'facebook',
15
+ params: {},
16
+ headers: {}
17
+ )
18
+ }
19
+
20
+ it 'should call coordinate on the message handler' do
21
+ message_handler = double
22
+ expect(Stealth::Services::Facebook::MessageHandler).to receive(:new).and_return(message_handler)
23
+ expect(message_handler).to receive(:coordinate)
24
+
25
+ dispatcher.coordinate
26
+ end
27
+ end
28
+
29
+ describe 'process' do
30
+ class StubbedBotController < Stealth::Controller
31
+ def route
32
+ true
33
+ end
34
+ end
35
+
36
+ let(:dispatcher) {
37
+ Stealth::Dispatcher.new(
38
+ service: 'facebook',
39
+ params: {},
40
+ headers: {}
41
+ )
42
+ }
43
+ let(:fb_message) { SampleMessage.new(service: 'facebook') }
44
+ let(:stubbed_controller) {
45
+ StubbedBotController.new(service_message: fb_message.message_with_text)
46
+ }
47
+
48
+ it 'should call process on the message handler' do
49
+ message_handler = double
50
+
51
+ # Stub out the message handler to return a service_message
52
+ expect(Stealth::Services::Facebook::MessageHandler).to receive(:new).and_return(message_handler)
53
+ expect(message_handler).to receive(:process).and_return(fb_message.message_with_text)
54
+
55
+ # Stub out BotController and set session
56
+ expect(BotController).to receive(:new).and_return(stubbed_controller)
57
+ stubbed_controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')
58
+
59
+ dispatcher.process
60
+ end
61
+
62
+ it 'should log the incoming message if transcript_logging is enabled' do
63
+ message_handler = double
64
+
65
+ # Stub out the message handler to return a service_message
66
+ expect(Stealth::Services::Facebook::MessageHandler).to receive(:new).and_return(message_handler)
67
+ expect(message_handler).to receive(:process).and_return(fb_message.message_with_text)
68
+
69
+ # Stub out BotController and set session
70
+ expect(BotController).to receive(:new).and_return(stubbed_controller)
71
+ stubbed_controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')
72
+
73
+ Stealth.config.transcript_logging = true
74
+ expect(dispatcher).to receive(:log_incoming_message).with(fb_message.message_with_text)
75
+ dispatcher.process
76
+ end
77
+ end
78
+
79
+ end
@@ -1,7 +1,6 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
+ require 'spec_helper'
5
4
 
6
5
  describe Stealth::Flow do
7
6
 
@@ -1,7 +1,6 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
+ require 'spec_helper'
5
4
 
6
5
  describe Stealth::Flow::State do
7
6
 
@@ -15,6 +14,7 @@ describe Stealth::Flow::State do
15
14
  state :created2, fails_to: 'new_todo->new'
16
15
  state :deprecated, redirects_to: 'new'
17
16
  state :deprecated2, redirects_to: 'other_flow->say_hi'
17
+ state :new_opts, opt1: 'hello', opt2: 1
18
18
  state :error
19
19
  end
20
20
  end
@@ -69,6 +69,17 @@ describe Stealth::Flow::State do
69
69
  end
70
70
  end
71
71
 
72
+ describe "opts" do
73
+ it "should return {} for a state that has not specified any opts" do
74
+ expect(flow_map.current_state.opts).to eq({})
75
+ end
76
+
77
+ it "should return the opts if they were specified" do
78
+ flow_map.init(flow: :new_todo, state: :new_opts)
79
+ expect(flow_map.current_state.opts).to eq({ opt1: 'hello', opt2: 1 })
80
+ end
81
+ end
82
+
72
83
  describe "state incrementing and decrementing" do
73
84
  it "should increment the state" do
74
85
  flow_map.init(flow: :new_todo, state: :get_due_date)
@@ -78,7 +89,7 @@ describe Stealth::Flow::State do
78
89
 
79
90
  it "should decrement the state" do
80
91
  flow_map.init(flow: :new_todo, state: :error)
81
- new_state = flow_map.current_state - 5.states
92
+ new_state = flow_map.current_state - 6.states
82
93
  expect(new_state).to eq(:get_due_date)
83
94
  end
84
95
 
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Stealth::Redis" do
6
+
7
+ class RedisTester
8
+ include Stealth::Redis
9
+ end
10
+
11
+ let(:redis_tester) { RedisTester.new }
12
+ let(:key) { 'xyz' }
13
+
14
+ describe "get_key" do
15
+ it "should return the key from Redis if an expiration is not set" do
16
+ $redis.set(key, 'abc')
17
+ expect(redis_tester.send(:get_key, key)).to eq 'abc'
18
+ end
19
+
20
+ it "should call getex if an expiration is set" do
21
+ expect(redis_tester).to receive(:getex).with(key, 30)
22
+ redis_tester.send(:get_key, key, expiration: 30)
23
+ end
24
+ end
25
+
26
+ describe "delete_key" do
27
+ it 'should delete the key from Redis' do
28
+ $redis.set(key, 'abc')
29
+ expect(redis_tester.send(:get_key, key)).to eq 'abc'
30
+ redis_tester.send(:delete_key, key)
31
+ expect(redis_tester.send(:get_key, key)).to be_nil
32
+ end
33
+ end
34
+
35
+ describe "getex" do
36
+ it "should return the key from Redis" do
37
+ Stealth.config.session_ttl = 50
38
+ $redis.set(key, 'abc')
39
+ expect(redis_tester.send(:getex, key)).to eq 'abc'
40
+ end
41
+
42
+ it "should set the expiration of a key in Redis" do
43
+ Stealth.config.session_ttl = 50
44
+ $redis.set(key, 'abc')
45
+ redis_tester.send(:getex, key)
46
+ expect($redis.ttl(key)).to be_between(0, 50).inclusive
47
+ end
48
+
49
+ it "should update the expiration of a key in Redis" do
50
+ Stealth.config.session_ttl = 500
51
+ $redis.setex(key, 50, 'abc')
52
+ redis_tester.send(:getex, key)
53
+ expect($redis.ttl(key)).to be_between(400, 500).inclusive
54
+ end
55
+ end
56
+
57
+ describe "persist_key" do
58
+ it "should set the key in Redis" do
59
+ Stealth.config.session_ttl = 50
60
+ redis_tester.send(:persist_key, key: key, value: 'zzz')
61
+ expect($redis.get(key)).to eq 'zzz'
62
+ end
63
+
64
+ it "should set the expiration to session_ttl if none specified" do
65
+ Stealth.config.session_ttl = 50
66
+ redis_tester.send(:persist_key, key: key, value: 'zzz')
67
+ expect($redis.ttl(key)).to be_between(0, 50).inclusive
68
+ end
69
+
70
+ it "should set the expiration to the specified value when provided" do
71
+ Stealth.config.session_ttl = 50
72
+ redis_tester.send(:persist_key, key: key, value: 'zzz', expiration: 500)
73
+ expect($redis.ttl(key)).to be_between(400, 500).inclusive
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Stealth::Lock" do
6
+ let(:session_id) { SecureRandom.hex(14) }
7
+ let(:session_slug) { 'hello->say_hello' }
8
+
9
+ before(:each) do
10
+ Stealth.config.lock_autorelease = 30
11
+ end
12
+
13
+ describe "create" do
14
+ it "should raise an ArgumentError if the session_slug was not provided" do
15
+ lock = Stealth::Lock.new(session_id: session_id)
16
+ expect {
17
+ lock.create
18
+ }.to raise_error(ArgumentError)
19
+ end
20
+
21
+ it "should save the lock using a canonical key and value" do
22
+ lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug)
23
+ canonical_key = "#{session_id}-lock"
24
+ expected_value = "#{lock.tid}##{session_slug}"
25
+ lock.create
26
+ expect($redis.get(canonical_key)).to eq expected_value
27
+ end
28
+
29
+ it "should include the reply file position in the lock" do
30
+ lock = Stealth::Lock.new(
31
+ session_id: session_id,
32
+ session_slug: session_slug,
33
+ position: 3
34
+ )
35
+ canonical_key = "#{session_id}-lock"
36
+ expected_value = "#{lock.tid}##{session_slug}:3"
37
+ lock.create
38
+ expect($redis.get(canonical_key)).to eq expected_value
39
+ end
40
+
41
+ it "should set the lock expiration to lock_autorelease" do
42
+ lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug)
43
+ canonical_key = "#{session_id}-lock"
44
+ expected_value = "#{lock.tid}##{session_slug}"
45
+ lock.create
46
+ expect($redis.ttl(canonical_key)).to be_between(1, 30).inclusive
47
+ end
48
+ end
49
+
50
+ describe "release" do
51
+ it "should delete the key in Redis" do
52
+ lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug)
53
+ canonical_key = "#{session_id}-lock"
54
+ lock.create
55
+ expect($redis.get(canonical_key)).to_not be_nil
56
+ lock.release
57
+ expect($redis.get(canonical_key)).to be_nil
58
+ end
59
+ end
60
+
61
+ describe "slug" do
62
+ it "should return the lock slug from Redis" do
63
+ lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug)
64
+ lock.create
65
+ canonical_key = "#{session_id}-lock"
66
+ expect(lock.slug).to eq "#{lock.tid}##{session_slug}"
67
+ end
68
+ end
69
+
70
+ describe "flow_and_state" do
71
+ it "should return a hash containing the flow and state" do
72
+ lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug)
73
+ expect(lock.flow_and_state[:flow]).to eq 'hello'
74
+ expect(lock.flow_and_state[:state]).to eq 'say_hello'
75
+ end
76
+ end
77
+
78
+ describe "self.find_lock" do
79
+ it "should load the lock from Redis" do
80
+ lock_key = "#{session_id}-lock"
81
+ example_tid = 'ovefhgJvx'
82
+ example_session = 'goodbye->say_goodbye'
83
+ example_position = 2
84
+ example_lock = "#{example_tid}##{example_session}:#{example_position}"
85
+ $redis.set(lock_key, example_lock)
86
+
87
+ lock = Stealth::Lock.find_lock(session_id: session_id)
88
+ expect(lock.tid).to eq example_tid
89
+ expect(lock.session_slug).to eq example_session
90
+ expect(lock.position).to eq example_position
91
+ end
92
+
93
+ it "should return nil if the lock is not found" do
94
+ lock_key = "#{session_id}-lock"
95
+ lock = Stealth::Lock.find_lock(session_id: session_id)
96
+ expect($redis.get(lock_key)).to be_nil
97
+ expect(lock).to be_nil
98
+ end
99
+ end
100
+ end