• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

huginn / huginn / 22818305782

08 Mar 2026 09:28AM UTC coverage: 88.045% (-0.2%) from 88.255%
22818305782

Pull #3567

github

knu
Update the expectations following changes in Ruby 3.4
Pull Request #3567: Upgrade to Ruby 3.4

6886 of 7821 relevant lines covered (88.05%)

440.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.82
/app/models/agents/java_script_agent.rb
1
require 'date'
4✔
2
require 'cgi'
4✔
3

4
module Agents
4✔
5
  class JavaScriptAgent < Agent
4✔
6
    include FormConfigurable
4✔
7

8
    can_dry_run!
4✔
9

10
    default_schedule "never"
4✔
11

12
    gem_dependency_check { defined?(MiniRacer) }
8✔
13

14
    description <<~MD
4✔
15
      The JavaScript 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!
16

17
      #{'## Include `mini_racer` in your Gemfile to use this Agent!' if dependencies_missing?}
×
18

19
      You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended).
20

21
      You can implement `Agent.check` and `Agent.receive` as you see fit.  The following methods will be available on Agent in the JavaScript environment:
22

23
      * `this.createEvent(payload)`
24
      * `this.incomingEvents()` (the returned event objects will each have a `payload` property)
25
      * `this.memory()`
26
      * `this.memory(key)`
27
      * `this.memory(keyToSet, valueToSet)`
28
      * `this.setMemory(object)` (replaces the Agent's memory with the provided object)
29
      * `this.deleteKey(key)` (deletes a key from memory and returns the value)
30
      * `this.credential(name)`
31
      * `this.credential(name, valueToSet)`
32
      * `this.options()`
33
      * `this.options(key)`
34
      * `this.log(message)`
35
      * `this.error(message)`
36
      * `this.kvs` (whose properties are variables provided by KeyValueStoreAgents)
37
      * `this.escapeHtml(htmlToEscape)`
38
      * `this.unescapeHtml(htmlToUnescape)`
39
    MD
40

41
    form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
4✔
42
    form_configurable :code, type: :text, ace: true
4✔
43
    form_configurable :expected_receive_period_in_days
4✔
44
    form_configurable :expected_update_period_in_days
4✔
45

46
    def validate_options
4✔
47
      cred_name = credential_referenced_by_code
332✔
48
      if cred_name
332✔
49
        errors.add(:base,
50
                   "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
12✔
51
      else
52
        errors.add(:base, "The 'code' option is required") unless options['code'].present?
320✔
53
      end
54

55
      if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
332✔
56
        errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
4✔
57
      end
58
    end
59

60
    def working?
4✔
61
      return false if recent_error_logs?
24✔
62

63
      if interpolated['expected_update_period_in_days'].present?
24✔
64
        return false unless event_created_within?(interpolated['expected_update_period_in_days'])
12✔
65
      end
66

67
      if interpolated['expected_receive_period_in_days'].present?
16✔
68
        return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago
12✔
69
      end
70

71
      true
8✔
72
    end
73

74
    def check
4✔
75
      log_errors do
116✔
76
        execute_js("check")
116✔
77
      end
78
    end
79

80
    def receive(incoming_events)
4✔
81
      log_errors do
12✔
82
        execute_js("receive", incoming_events)
12✔
83
      end
84
    end
85

86
    def default_options
4✔
87
      js_code = <<-JS
4✔
88
        Agent.check = function() {
89
          if (this.options('make_event')) {
90
            this.createEvent({ 'message': 'I made an event!' });
91
            var callCount = this.memory('callCount') || 0;
92
            this.memory('callCount', callCount + 1);
93
          }
94
        };
95

96
        Agent.receive = function() {
97
          var events = this.incomingEvents();
98
          for(var i = 0; i < events.length; i++) {
99
            this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
100
          }
101
        }
102
      JS
103

104
      {
105
        'code' => Utils.unindent(js_code),
4✔
106
        'language' => 'JavaScript',
107
        'expected_receive_period_in_days' => '2',
108
        'expected_update_period_in_days' => '2'
109
      }
110
    end
111

112
    private
4✔
113

114
    def execute_js(js_function, incoming_events = [])
4✔
115
      js_function = js_function == "check" ? "check" : "receive"
128✔
116
      context = MiniRacer::Context.new
128✔
117
      context.eval(setup_javascript)
128✔
118

119
      context.attach("doCreateEvent", ->(y) { create_event(payload: clean_nans(JSON.parse(y))).payload.to_json })
172✔
120
      context.attach("getIncomingEvents", -> { incoming_events.to_json })
136✔
121
      context.attach("getOptions", -> { interpolated.to_json })
148✔
122
      context.attach("doLog", ->(x) { log x; nil })
144✔
123
      context.attach("doError", ->(x) { error x; nil })
136✔
124
      context.attach("getMemory", -> { memory.to_json })
148✔
125
      context.attach("setMemoryKey", ->(x, y) { memory[x] = clean_nans(y) })
176✔
126
      context.attach("setMemory", ->(x) { memory.replace(clean_nans(x)) })
132✔
127
      context.attach("deleteKey", ->(x) { memory.delete(x).to_json })
140✔
128
      context.attach("escapeHtml", ->(x) { CGI.escapeHTML(x) })
132✔
129
      context.attach("unescapeHtml", ->(x) { CGI.unescapeHTML(x) })
132✔
130
      context.attach('getCredential', ->(k) { credential(k); })
132✔
131
      context.attach('setCredential', ->(k, v) { set_credential(k, v) })
136✔
132

133
      kvs = Agents::KeyValueStoreAgent.merge(controllers).find_each.to_h { |kvs|
128✔
134
        [kvs.options[:variable], kvs.memory.as_json]
8✔
135
      }
136
      context.attach("getKeyValueStores", -> { kvs })
136✔
137
      context.eval("Object.defineProperty(Agent, 'kvs', { get: getKeyValueStores })")
128✔
138

139
      if (options['language'] || '').downcase == 'coffeescript'
128✔
140
        context.eval(CoffeeScript.compile(code))
4✔
141
      else
142
        context.eval(code)
124✔
143
      end
144
      context.eval("Agent.#{js_function}();")
124✔
145
    end
146

147
    def code
4✔
148
      cred = credential_referenced_by_code
128✔
149
      if cred
128✔
150
        credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
8✔
151
      else
152
        interpolated['code']
120✔
153
      end
154
    end
155

156
    def credential_referenced_by_code
4✔
157
      (interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1
460✔
158
    end
159

160
    def set_credential(name, value)
4✔
161
      c = user.user_credentials.find_or_initialize_by(credential_name: name)
16✔
162
      c.credential_value = value
16✔
163
      c.save!
16✔
164
    end
165

166
    def setup_javascript
4✔
167
      <<-JS
128✔
168
        function Agent() {};
169

170
        Agent.createEvent = function(opts) {
171
          return JSON.parse(doCreateEvent(JSON.stringify(opts)));
172
        }
173

174
        Agent.incomingEvents = function() {
175
          return JSON.parse(getIncomingEvents());
176
        }
177

178
        Agent.memory = function(key, value) {
179
          if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
180
            setMemoryKey(key, value);
181
          } else if (typeof(key) !== "undefined") {
182
            return JSON.parse(getMemory())[key];
183
          } else {
184
            return JSON.parse(getMemory());
185
          }
186
        }
187

188
        Agent.setMemory = function(obj) {
189
          setMemory(obj);
190
        }
191

192
        Agent.credential = function(name, value) {
193
          if (typeof(value) !== "undefined") {
194
            setCredential(name, value);
195
          } else {
196
            return getCredential(name);
197
          }
198
        }
199

200
        Agent.options = function(key) {
201
          if (typeof(key) !== "undefined") {
202
            return JSON.parse(getOptions())[key];
203
          } else {
204
            return JSON.parse(getOptions());
205
          }
206
        }
207

208
        Agent.log = function(message) {
209
          doLog(message);
210
        }
211

212
        Agent.error = function(message) {
213
          doError(message);
214
        }
215

216
        Agent.deleteKey = function(key) {
217
          return JSON.parse(deleteKey(key));
218
        }
219

220
        Agent.escapeHtml = function(html) {
221
          return escapeHtml(html);
222
        }
223

224
        Agent.unescapeHtml = function(html) {
225
          return unescapeHtml(html);
226
        }
227

228
        Agent.check = function(){};
229
        Agent.receive = function(){};
230
      JS
231
    end
232

233
    def log_errors
4✔
234
      yield
128✔
235
    rescue MiniRacer::Error => e
236
      error "JavaScript error: #{e.message}"
8✔
237
    end
238

239
    def clean_nans(input)
4✔
240
      case input
236✔
241
      when Array
242
        input.map { |v| clean_nans(v) }
24✔
243
      when Hash
244
        input.transform_values { |v| clean_nans(v) }
204✔
245
      when Float
246
        input.nan? ? 'NaN' : input
4✔
247
      else
248
        input
144✔
249
      end
250
    end
251
  end
252
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc