Merge pull request #163 from cantino/javascript-agent

Javascript agent

Andrew Cantino 11 ans auparavant
Parent
Commettre
d2d36a37f4

+ 3 - 0
Gemfile

@@ -24,6 +24,7 @@ gem 'coffee-rails', '~> 3.2.1'
24 24
 gem 'uglifier', '>= 1.0.3'
25 25
 gem 'select2-rails'
26 26
 gem 'jquery-rails'
27
+gem 'ace-rails-ap'
27 28
 
28 29
 gem 'geokit-rails3'
29 30
 gem 'kramdown'
@@ -37,6 +38,8 @@ gem 'twitter-stream', '>=0.1.16'
37 38
 gem 'em-http-request'
38 39
 gem 'weibo_2'
39 40
 
41
+gem 'therubyracer'
42
+
40 43
 platforms :ruby_18 do
41 44
   gem 'system_timer'
42 45
   gem 'fastercsv'

+ 8 - 0
Gemfile.lock

@@ -1,6 +1,7 @@
1 1
 GEM
2 2
   remote: https://rubygems.org/
3 3
   specs:
4
+    ace-rails-ap (2.0.1)
4 5
     actionmailer (3.2.13)
5 6
       actionpack (= 3.2.13)
6 7
       mail (~> 2.5.3)
@@ -124,6 +125,7 @@ GEM
124 125
       actionpack (>= 3.0.0)
125 126
       activesupport (>= 3.0.0)
126 127
     kramdown (1.1.0)
128
+    libv8 (3.16.14.3)
127 129
     mail (2.5.4)
128 130
       mime-types (~> 1.16)
129 131
       treetop (~> 1.4.8)
@@ -174,6 +176,7 @@ GEM
174 176
     rake (10.1.0)
175 177
     rdoc (3.12.2)
176 178
       json (~> 1.4)
179
+    ref (1.0.5)
177 180
     rest-client (1.6.7)
178 181
       mime-types (>= 1.16)
179 182
     rr (1.1.2)
@@ -224,6 +227,9 @@ GEM
224 227
     system_timer (1.2.4)
225 228
     term-ansicolor (1.2.2)
226 229
       tins (~> 0.8)
230
+    therubyracer (0.12.0)
231
+      libv8 (~> 3.16.14.0)
232
+      ref
227 233
     thor (0.18.1)
228 234
     tilt (1.4.1)
229 235
     tins (0.13.1)
@@ -267,6 +273,7 @@ PLATFORMS
267 273
   ruby
268 274
 
269 275
 DEPENDENCIES
276
+  ace-rails-ap
270 277
   better_errors
271 278
   binding_of_caller
272 279
   bootstrap-kaminari-views
@@ -300,6 +307,7 @@ DEPENDENCIES
300 307
   select2-rails
301 308
   shoulda-matchers
302 309
   system_timer
310
+  therubyracer
303 311
   twilio-ruby
304 312
   twitter
305 313
   twitter-stream (>= 0.1.16)

+ 27 - 0
app/assets/javascripts/user_credentials.js.coffee

@@ -0,0 +1,27 @@
1
+#= require ace/ace
2
+#= require ace/mode-javascript.js
3
+#= require ace/mode-markdown.js
4
+#= require_self
5
+
6
+$ ->
7
+  editor = ace.edit("ace-credential-value")
8
+  editor.getSession().setTabSize(2)
9
+  editor.getSession().setUseSoftTabs(true)
10
+  editor.getSession().setUseWrapMode(false)
11
+  editor.setTheme("ace/theme/chrome")
12
+
13
+  setMode = ->
14
+    mode = $("#user_credential_mode").val()
15
+    if mode == 'java_script'
16
+      editor.getSession().setMode("ace/mode/javascript")
17
+    else
18
+      editor.getSession().setMode("ace/mode/text")
19
+
20
+  setMode()
21
+  $("#user_credential_mode").on 'change', setMode
22
+
23
+  $textarea = $('#user_credential_credential_value').hide()
24
+  editor.getSession().setValue($textarea.val())
25
+
26
+  $textarea.closest('form').on 'submit', ->
27
+    $textarea.val(editor.getSession().getValue())

+ 8 - 0
app/assets/stylesheets/application.css.scss.erb

@@ -126,3 +126,11 @@ span.not-applicable:after {
126 126
 #show-tabs li a.recent-errors {
127 127
   font-weight: bold;
128 128
 }
129
+
130
+// Credentials
131
+
132
+#ace-credential-value {
133
+  position: relative;
134
+  width: 940px;
135
+  height: 400px;
136
+}

+ 2 - 1
app/models/agent.rb

@@ -16,7 +16,7 @@ class Agent < ActiveRecord::Base
16 16
   load_types_in "Agents"
17 17
 
18 18
   SCHEDULES = %w[every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d
19
-                 midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm]
19
+                 midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm never]
20 20
 
21 21
   EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })]
22 22
 
@@ -296,6 +296,7 @@ class Agent < ActiveRecord::Base
296 296
     # Given a schedule name, run `check` via `bulk_check` on all Agents with that schedule.
297 297
     # This is called by bin/schedule.rb for each schedule in `SCHEDULES`.
298 298
     def run_schedule(schedule)
299
+      return if schedule == 'never'
299 300
       types = where(:schedule => schedule).group(:type).pluck(:type)
300 301
       types.each do |type|
301 302
         type.constantize.bulk_check(schedule)

+ 186 - 0
app/models/agents/java_script_agent.rb

@@ -0,0 +1,186 @@
1
+require 'date'
2
+require 'cgi'
3
+
4
+module Agents
5
+  class JavaScriptAgent < Agent
6
+    default_schedule "never"
7
+
8
+    description <<-MD
9
+      This Agent allows you to write code in JavaScript that can create and receive events.  If other Agents aren't meeting your needs, try this one!
10
+
11
+      You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended).
12
+
13
+      You can implement `Agent.check` and `Agent.receive` as you see fit.  The following methods will be available on Agent in the JavaScript environment:
14
+
15
+      * `this.createEvent(payload)`
16
+      * `this.incomingEvents()`
17
+      * `this.memory()`
18
+      * `this.memory(key)`
19
+      * `this.memory(keyToSet, valueToSet)`
20
+      * `this.options()`
21
+      * `this.options(key)`
22
+      * `this.log(message)`
23
+      * `this.error(message)`
24
+    MD
25
+
26
+    def validate_options
27
+      cred_name = credential_referenced_by_code
28
+      if cred_name
29
+        errors.add(:base, "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
30
+      else
31
+        errors.add(:base, "The 'code' option is required") unless options['code'].present?
32
+      end
33
+    end
34
+
35
+    def working?
36
+      return false if recent_error_logs?
37
+
38
+      if options['expected_update_period_in_days'].present?
39
+        return false unless event_created_within?(options['expected_update_period_in_days'])
40
+      end
41
+
42
+      if options['expected_receive_period_in_days'].present?
43
+        return false unless last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago
44
+      end
45
+
46
+      true
47
+    end
48
+
49
+    def check
50
+      log_errors do
51
+        execute_js("check")
52
+      end
53
+    end
54
+
55
+    def receive(incoming_events)
56
+      log_errors do
57
+        execute_js("receive", incoming_events)
58
+      end
59
+    end
60
+
61
+    def default_options
62
+      js_code = <<-JS
63
+        Agent.check = function() {
64
+          if (this.options('make_event')) {
65
+            this.createEvent({ 'message': 'I made an event!' });
66
+            var callCount = this.memory('callCount') || 0;
67
+            this.memory('callCount', callCount + 1);
68
+          }
69
+        };
70
+        
71
+        Agent.receive = function() {
72
+          var events = this.incomingEvents();
73
+          for(var i = 0; i < events.length; i++) {
74
+            this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
75
+          }
76
+        }
77
+      JS
78
+
79
+      {
80
+        "code" => js_code.gsub(/[\n\r\t]/, '').strip,
81
+        'expected_receive_period_in_days' => "2",
82
+        'expected_update_period_in_days' => "2"
83
+      }
84
+    end
85
+
86
+    private
87
+
88
+    def execute_js(js_function, incoming_events = [])
89
+      js_function = js_function == "check" ? "check" : "receive"
90
+      context = V8::Context.new
91
+      context.eval(setup_javascript)
92
+
93
+      context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json }
94
+      context["getIncomingEvents"] = lambda { |a| incoming_events.to_json }
95
+      context["getOptions"] = lambda { |a, x| options.to_json }
96
+      context["doLog"] = lambda { |a, x| log x }
97
+      context["doError"] = lambda { |a, x| error x }
98
+      context["getMemory"] = lambda do |a, x, y|
99
+        if x && y
100
+          memory[x] = clean_nans(y)
101
+        else
102
+          memory.to_json
103
+        end
104
+      end
105
+
106
+      context.eval(code)
107
+      context.eval("Agent.#{js_function}();")
108
+    end
109
+
110
+    def code
111
+      cred = credential_referenced_by_code
112
+      if cred
113
+        credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
114
+      else
115
+        options['code']
116
+      end
117
+    end
118
+
119
+    def credential_referenced_by_code
120
+      options['code'] =~ /\Acredential:(.*)\Z/ && $1
121
+    end
122
+
123
+    def setup_javascript
124
+      <<-JS
125
+        function Agent() {};
126
+
127
+        Agent.createEvent = function(opts) {
128
+          return JSON.parse(doCreateEvent(JSON.stringify(opts)));
129
+        }
130
+
131
+        Agent.incomingEvents = function() {
132
+          return JSON.parse(getIncomingEvents());
133
+        }
134
+
135
+        Agent.memory = function(key, value) {
136
+          if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
137
+            getMemory(key, value);
138
+          } else if (typeof(key) !== "undefined") {
139
+            return JSON.parse(getMemory())[key];
140
+          } else {
141
+            return JSON.parse(getMemory());
142
+          }
143
+        }
144
+
145
+        Agent.options = function(key) {
146
+          if (typeof(key) !== "undefined") {
147
+            return JSON.parse(getOptions())[key];
148
+          } else {
149
+            return JSON.parse(getOptions());
150
+          }
151
+        }
152
+
153
+        Agent.log = function(message) {
154
+          doLog(message);
155
+        }
156
+
157
+        Agent.error = function(message) {
158
+          doError(message);
159
+        }
160
+
161
+        Agent.check = function(){};
162
+        Agent.receive = function(){};
163
+      JS
164
+    end
165
+
166
+    def log_errors
167
+      begin
168
+        yield
169
+      rescue V8::Error => e
170
+        error "JavaScript error: #{e.message}"
171
+      end
172
+    end
173
+
174
+    def clean_nans(input)
175
+      if input.is_a?(Array)
176
+        input.map {|v| clean_nans(v) }
177
+      elsif input.is_a?(Hash)
178
+        input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m }
179
+      elsif input.is_a?(Float) && input.nan?
180
+        'NaN'
181
+      else
182
+        input
183
+      end
184
+    end
185
+  end
186
+end

+ 9 - 1
app/models/user_credential.rb

@@ -1,13 +1,17 @@
1 1
 class UserCredential < ActiveRecord::Base
2
-  attr_accessible :credential_name, :credential_value
2
+  MODES = %w[text java_script]
3
+
4
+  attr_accessible :credential_name, :credential_value, :mode
3 5
 
4 6
   belongs_to :user
5 7
 
6 8
   validates_presence_of :credential_name
7 9
   validates_presence_of :credential_value
10
+  validates_inclusion_of :mode, :in => MODES
8 11
   validates_presence_of :user_id
9 12
   validates_uniqueness_of :credential_name, :scope => :user_id
10 13
 
14
+  before_validation :default_mode_to_text
11 15
   before_save :trim_fields
12 16
 
13 17
   protected
@@ -16,4 +20,8 @@ class UserCredential < ActiveRecord::Base
16 20
     credential_name.strip!
17 21
     credential_value.strip!
18 22
   end
23
+
24
+  def default_mode_to_text
25
+    self.mode = 'text' unless mode.present?
26
+  end
19 27
 end

+ 0 - 1
app/views/agents/_form.html.erb

@@ -44,7 +44,6 @@
44 44
     </div>
45 45
   </div>
46 46
 
47
-
48 47
   <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>">
49 48
     <div class="control-group">
50 49
       <%= f.label :keep_events_for, "Keep events", :class => 'control-label' %>

+ 10 - 0
app/views/user_credentials/_form.html.erb

@@ -18,9 +18,17 @@
18 18
   </div>
19 19
 
20 20
   <div class="control-group">
21
+    <%= f.label :mode, :class => 'control-label' %>
22
+    <div class="controls">
23
+      <%= f.select :mode, options_for_select(UserCredential::MODES.map {|s| [s.classify, s] }, @user_credential.mode), {}, :class => 'span4' %>
24
+    </div>
25
+  </div>
26
+
27
+  <div class="control-group">
21 28
     <%= f.label :credential_value, :class => 'control-label' %>
22 29
     <div class="controls">
23 30
       <%= f.text_area :credential_value, :class => 'span8', :rows => 10 %>
31
+      <div id="ace-credential-value"></div>
24 32
     </div>
25 33
   </div>
26 34
 
@@ -28,3 +36,5 @@
28 36
     <%= f.submit "Save Credential", :class => "btn btn-primary" %>
29 37
   </div>
30 38
 <% end %>
39
+
40
+<%= javascript_include_tag "user_credentials" %>

+ 1 - 1
config/environments/production.rb

@@ -48,7 +48,7 @@ Huginn::Application.configure do
48 48
   end
49 49
 
50 50
   # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added)
51
-  config.assets.precompile += %w( graphing.js )
51
+  config.assets.precompile += %w( graphing.js user_credentials.js )
52 52
 
53 53
   # Enable threaded mode
54 54
   # config.threadsafe!

+ 5 - 0
db/migrate/20140210062747_add_mode_to_user_credentials.rb

@@ -0,0 +1,5 @@
1
+class AddModeToUserCredentials < ActiveRecord::Migration
2
+  def change
3
+    add_column :user_credentials, :mode, :string, :default => 'text', :null => false
4
+  end
5
+end

+ 7 - 6
db/schema.rb

@@ -11,7 +11,7 @@
11 11
 #
12 12
 # It's strongly recommended to check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(:version => 20140127164931) do
14
+ActiveRecord::Schema.define(:version => 20140210062747) do
15 15
 
16 16
   create_table "agent_logs", :force => true do |t|
17 17
     t.integer  "agent_id",                                             :null => false
@@ -96,11 +96,12 @@ ActiveRecord::Schema.define(:version => 20140127164931) do
96 96
   add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id"
97 97
 
98 98
   create_table "user_credentials", :force => true do |t|
99
-    t.integer  "user_id",          :null => false
100
-    t.string   "credential_name",  :null => false
101
-    t.text     "credential_value", :null => false
102
-    t.datetime "created_at",       :null => false
103
-    t.datetime "updated_at",       :null => false
99
+    t.integer  "user_id",                              :null => false
100
+    t.string   "credential_name",                      :null => false
101
+    t.text     "credential_value",                     :null => false
102
+    t.datetime "created_at",                           :null => false
103
+    t.datetime "updated_at",                           :null => false
104
+    t.string   "mode",             :default => "text", :null => false
104 105
   end
105 106
 
106 107
   add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true

+ 2 - 2
spec/fixtures/events.yml

@@ -1,9 +1,9 @@
1 1
 bob_website_agent_event:
2 2
   user: bob
3 3
   agent: bob_website_agent
4
-  payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_json.inspect %>
4
+  payload: <%= { :title => "foo", :url => "http://foo.com" }.to_json.inspect %>
5 5
 
6 6
 jane_website_agent_event:
7 7
   user: jane
8 8
   agent: jane_website_agent
9
-  payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_json.inspect %>
9
+  payload: <%= { :title => "foo", :url => "http://foo.com" }.to_json.inspect %>

+ 4 - 0
spec/fixtures/user_credentials.yml

@@ -2,15 +2,19 @@ bob_aws_key:
2 2
   user: bob
3 3
   credential_name: aws_key
4 4
   credential_value: 2222222222-bob
5
+  mode: text
5 6
 bob_aws_secret:
6 7
   user: bob
7 8
   credential_name: aws_secret
8 9
   credential_value: 1111111111-bob
10
+  mode: text
9 11
 jane_aws_key:
10 12
   user: jane
11 13
   credential_name: aws_key
12 14
   credential_value: 2222222222-jane
15
+  mode: text
13 16
 jane_aws_secret:
14 17
   user: jane
15 18
   credential_name: aws_secret
16 19
   credential_value: 1111111111-jabe
20
+  mode: text

+ 6 - 0
spec/models/agent_spec.rb

@@ -25,6 +25,12 @@ describe Agent do
25 25
       do_not_allow(Agents::WebsiteAgent).async_check
26 26
       Agent.run_schedule("blah")
27 27
     end
28
+
29
+    it "will not run the 'never' schedule" do
30
+      agents(:bob_weather_agent).update_attribute 'schedule', 'never'
31
+      do_not_allow(Agents::WebsiteAgent).async_check
32
+      Agent.run_schedule("never")
33
+    end
28 34
   end
29 35
 
30 36
   describe "credential" do

+ 228 - 0
spec/models/agents/java_script_agent_spec.rb

@@ -0,0 +1,228 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::JavaScriptAgent do
4
+  before do
5
+    @valid_params = {
6
+      :name => "somename",
7
+      :options => {
8
+        :code => "Agent.check = function() { this.createEvent({ 'message': 'hi' }); };",
9
+      }
10
+    }
11
+
12
+    @agent = Agents::JavaScriptAgent.new(@valid_params)
13
+    @agent.user = users(:jane)
14
+    @agent.save!
15
+  end
16
+
17
+  describe "validations" do
18
+    it "requires 'code'" do
19
+      @agent.should be_valid
20
+      @agent.options['code'] = ''
21
+      @agent.should_not be_valid
22
+      @agent.options.delete('code')
23
+      @agent.should_not be_valid
24
+    end
25
+
26
+    it "accepts a credential, but it must exist" do
27
+      @agent.should be_valid
28
+      @agent.options['code'] = 'credential:foo'
29
+      @agent.should_not be_valid
30
+      users(:jane).user_credentials.create! :credential_name => "foo", :credential_value => "bar"
31
+      @agent.reload.should be_valid
32
+    end
33
+  end
34
+
35
+  describe "#working?" do
36
+    describe "when expected_update_period_in_days is set" do
37
+      it "returns false when more than expected_update_period_in_days have passed since the last event creation" do
38
+        @agent.options['expected_update_period_in_days'] = 1
39
+        @agent.save!
40
+        @agent.should_not be_working
41
+        @agent.check
42
+        @agent.reload.should be_working
43
+        three_days_from_now = 3.days.from_now
44
+        stub(Time).now { three_days_from_now }
45
+        @agent.should_not be_working
46
+      end
47
+    end
48
+
49
+    describe "when expected_receive_period_in_days is set" do
50
+      it "returns false when more than expected_receive_period_in_days have passed since the last event was received" do
51
+        @agent.options['expected_receive_period_in_days'] = 1
52
+        @agent.save!
53
+        @agent.should_not be_working
54
+        Agents::JavaScriptAgent.async_receive @agent.id, [events(:bob_website_agent_event).id]
55
+        @agent.reload.should be_working
56
+        two_days_from_now = 2.days.from_now
57
+        stub(Time).now { two_days_from_now }
58
+        @agent.reload.should_not be_working
59
+      end
60
+    end
61
+  end
62
+
63
+  describe "executing code" do
64
+    it "works by default" do
65
+      @agent.options = @agent.default_options
66
+      @agent.options['make_event'] = true
67
+      @agent.save!
68
+
69
+      lambda {
70
+        lambda {
71
+          @agent.receive([events(:bob_website_agent_event)])
72
+          @agent.check
73
+        }.should_not change { AgentLog.count }
74
+      }.should change { Event.count }.by(2)
75
+    end
76
+
77
+
78
+    describe "using credentials as code" do
79
+      before do
80
+        @agent.user.user_credentials.create :credential_name => 'code-foo', :credential_value => 'Agent.check = function() { this.log("ran it"); };'
81
+        @agent.options['code'] = 'credential:code-foo'
82
+        @agent.save!
83
+      end
84
+
85
+      it "accepts credentials" do
86
+        @agent.check
87
+        AgentLog.last.message.should == "ran it"
88
+      end
89
+
90
+      it "logs an error when the credential goes away" do
91
+        @agent.user.user_credentials.delete_all
92
+        @agent.reload.check
93
+        AgentLog.last.message.should == "Unable to find credential"
94
+      end
95
+    end
96
+
97
+    describe "error handling" do
98
+      it "should log an error when V8 has issues" do
99
+        @agent.options['code'] = 'syntax error!'
100
+        @agent.save!
101
+        lambda {
102
+          lambda {
103
+            @agent.check
104
+          }.should_not raise_error
105
+        }.should change { AgentLog.count }.by(1)
106
+        AgentLog.last.message.should =~ /Unexpected identifier/
107
+        AgentLog.last.level.should == 4
108
+      end
109
+
110
+      it "should log an error when JavaScript throws" do
111
+        @agent.options['code'] = 'Agent.check = function() { throw "oh no"; };'
112
+        @agent.save!
113
+        lambda {
114
+          lambda {
115
+            @agent.check
116
+          }.should_not raise_error
117
+        }.should change { AgentLog.count }.by(1)
118
+        AgentLog.last.message.should =~ /oh no/
119
+        AgentLog.last.level.should == 4
120
+      end
121
+
122
+      it "won't store NaNs" do
123
+        @agent.options['code'] = 'Agent.check = function() { this.memory("foo", NaN); };'
124
+        @agent.save!
125
+        @agent.check
126
+        @agent.memory['foo'].should == 'NaN' # string
127
+        @agent.save!
128
+        lambda { @agent.reload.memory }.should_not raise_error
129
+      end
130
+    end
131
+
132
+    describe "creating events" do
133
+      it "creates events with this.createEvent in the JavaScript environment" do
134
+        @agent.options['code'] = 'Agent.check = function() { this.createEvent({ message: "This is an event!", stuff: { foo: 5 } }); };'
135
+        @agent.save!
136
+        lambda {
137
+          lambda {
138
+            @agent.check
139
+          }.should_not change { AgentLog.count }
140
+        }.should change { Event.count }.by(1)
141
+        created_event = @agent.events.last
142
+        created_event.payload.should == { 'message' => "This is an event!", 'stuff' => { 'foo' => 5 } }
143
+      end
144
+    end
145
+
146
+    describe "logging" do
147
+      it "can output AgentLogs with this.log and this.error in the JavaScript environment" do
148
+        @agent.options['code'] = 'Agent.check = function() { this.log("woah"); this.error("WOAH!"); };'
149
+        @agent.save!
150
+        lambda {
151
+          lambda {
152
+            @agent.check
153
+          }.should_not raise_error
154
+        }.should change { AgentLog.count }.by(2)
155
+
156
+        log1, log2 = AgentLog.last(2)
157
+
158
+        log1.message.should == "woah"
159
+        log1.level.should == 3
160
+        log2.message.should == "WOAH!"
161
+        log2.level.should == 4
162
+      end
163
+    end
164
+
165
+    describe "getting incoming events" do
166
+      it "can access incoming events in the JavaScript enviroment via this.incomingEvents" do
167
+        event = Event.new
168
+        event.agent = agents(:bob_rain_notifier_agent)
169
+        event.payload = { :data => "Something you should know about" }
170
+        event.save!
171
+        event.reload
172
+
173
+        @agent.options['code'] = <<-JS
174
+          Agent.receive = function() {
175
+            var events = this.incomingEvents();
176
+            for(var i = 0; i < events.length; i++) {
177
+              this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
178
+            }
179
+          }
180
+        JS
181
+
182
+        @agent.save!
183
+        lambda {
184
+          lambda {
185
+            @agent.receive([events(:bob_website_agent_event), event])
186
+          }.should_not change { AgentLog.count }
187
+        }.should change { Event.count }.by(2)
188
+        created_event = @agent.events.first
189
+        created_event.payload.should == { 'message' => "I got an event!", 'event_was' => { 'data' => "Something you should know about" } }
190
+      end
191
+    end
192
+
193
+    describe "getting and setting memory, getting options" do
194
+      it "can access options via this.options and work with memory via this.memory" do
195
+        @agent.options['code'] = <<-JS
196
+          Agent.check = function() {
197
+            if (this.options('make_event')) {
198
+              var callCount = this.memory('callCount') || 0;
199
+              this.memory('callCount', callCount + 1);
200
+            }
201
+          };
202
+        JS
203
+
204
+        @agent.save!
205
+
206
+        lambda {
207
+          lambda {
208
+
209
+            @agent.check
210
+            @agent.memory['callCount'].should_not be_present
211
+
212
+            @agent.options['make_event'] = true
213
+            @agent.check
214
+            @agent.memory['callCount'].should == 1
215
+
216
+            @agent.check
217
+            @agent.memory['callCount'].should == 2
218
+
219
+            @agent.memory['callCount'] = 20
220
+            @agent.check
221
+            @agent.memory['callCount'].should == 21
222
+
223
+          }.should_not change { AgentLog.count }
224
+        }.should_not change { Event.count }
225
+      end
226
+    end
227
+  end
228
+end