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
@@ -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