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

huginn / huginn / 22884042014

10 Mar 2026 02:19AM UTC coverage: 87.938% (-0.4%) from 88.319%
22884042014

Pull #3560

github

web-flow
Merge 1f2015dd5 into 421a5a046
Pull Request #3560: OpenAI API Agents

340 of 392 new or added lines in 5 files covered. (86.73%)

43 existing lines in 31 files now uncovered.

7225 of 8216 relevant lines covered (87.94%)

219.11 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'
2✔
2
require 'cgi'
2✔
3

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

8
    can_dry_run!
2✔
9

10
    default_schedule "never"
2✔
11

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

14
    description <<~MD
2✔
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

UNCOV
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]
2✔
42
    form_configurable :code, type: :text, ace: true
2✔
43
    form_configurable :expected_receive_period_in_days
2✔
44
    form_configurable :expected_update_period_in_days
2✔
45

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

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

60
    def working?
2✔
61
      return false if recent_error_logs?
12✔
62

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

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

71
      true
4✔
72
    end
73

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

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

86
    def default_options
2✔
87
      js_code = <<-JS
2✔
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),
2✔
106
        'language' => 'JavaScript',
107
        'expected_receive_period_in_days' => '2',
108
        'expected_update_period_in_days' => '2'
109
      }
110
    end
111

112
    private
2✔
113

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

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

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

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

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

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

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

166
    def setup_javascript
2✔
167
      <<-JS
64✔
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
2✔
234
      yield
64✔
235
    rescue MiniRacer::Error => e
236
      error "JavaScript error: #{e.message}"
4✔
237
    end
238

239
    def clean_nans(input)
2✔
240
      case input
118✔
241
      when Array
242
        input.map { |v| clean_nans(v) }
12✔
243
      when Hash
244
        input.transform_values { |v| clean_nans(v) }
102✔
245
      when Float
246
        input.nan? ? 'NaN' : input
2✔
247
      else
248
        input
72✔
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