<commit_before>require 'rails_helper'

describe Agents::JavaScriptAgent do
  before do
    @valid_params = {
      :name => "somename",
      :options => {
        :code => "Agent.check = function() { this.createEvent({ 'message': 'hi' }); };",
      }
    }

    @agent = Agents::JavaScriptAgent.new(@valid_params)
    @agent.user = users(:jane)
    @agent.save!
  end

  describe "validations" do
    it "requires 'code'" do
      expect(@agent).to be_valid
      @agent.options['code'] = ''
      expect(@agent).not_to be_valid
      @agent.options.delete('code')
      expect(@agent).not_to be_valid
    end

    it "checks for a valid 'language', but allows nil" do
      expect(@agent).to be_valid
      @agent.options['language'] = ''
      expect(@agent).to be_valid
      @agent.options.delete('language')
      expect(@agent).to be_valid
      @agent.options['language'] = 'foo'
      expect(@agent).not_to be_valid

      %w[javascript JavaScript coffeescript CoffeeScript].each do |valid_language|
        @agent.options['language'] = valid_language
        expect(@agent).to be_valid
      end
    end

    it "accepts a credential, but it must exist" do
      expect(@agent).to be_valid
      @agent.options['code'] = 'credential:foo'
      expect(@agent).not_to be_valid
      users(:jane).user_credentials.create! :credential_name => "foo", :credential_value => "bar"
      expect(@agent.reload).to be_valid
    end
  end

  describe "#working?" do
    describe "when expected_update_period_in_days is set" do
      it "returns false when more than expected_update_period_in_days have passed since the last event creation" do
        @agent.options['expected_update_period_in_days'] = 1
        @agent.save!
        expect(@agent).not_to be_working
        @agent.check
        expect(@agent.reload).to be_working
        three_days_from_now = 3.days.from_now
        stub(Time).now { three_days_from_now }
        expect(@agent).not_to be_working
      end
    end

    describe "when expected_receive_period_in_days is set" do
      it "returns false when more than expected_receive_period_in_days have passed since the last event was received" do
        @agent.options['expected_receive_period_in_days'] = 1
        @agent.save!
        expect(@agent).not_to be_working
        Agents::JavaScriptAgent.async_receive @agent.id, [events(:bob_website_agent_event).id]
        expect(@agent.reload).to be_working
        two_days_from_now = 2.days.from_now
        stub(Time).now { two_days_from_now }
        expect(@agent.reload).not_to be_working
      end
    end
  end

  describe "executing code" do
    it "works by default" do
      @agent.options = @agent.default_options
      @agent.options['make_event'] = true
      @agent.save!

      expect {
        expect {
          @agent.receive([events(:bob_website_agent_event)])
          @agent.check
        }.not_to change { AgentLog.count }
      }.to change { Event.count }.by(2)
    end

    describe "using credentials as code" do
      before do
        @agent.user.user_credentials.create :credential_name => 'code-foo', :credential_value => 'Agent.check = function() { this.log("ran it"); };'
        @agent.options['code'] = "credential:code-foo\n\n"
        @agent.save!
      end

      it "accepts credentials" do
        @agent.check
        expect(AgentLog.last.message).to eq("ran it")
      end

      it "logs an error when the credential goes away" do
        @agent.user.user_credentials.delete_all
        @agent.reload.check
        expect(AgentLog.last.message).to eq("Unable to find credential")
      end
    end

    describe "error handling" do
      it "should log an error when V8 has issues" do
        @agent.options['code'] = 'syntax error!'
        @agent.save!
        expect {
          expect {
            @agent.check
          }.not_to raise_error
        }.to change { AgentLog.count }.by(1)
        expect(AgentLog.last.message).to match(/Unexpected identifier/)
        expect(AgentLog.last.level).to eq(4)
      end

      it "should log an error when JavaScript throws" do
        @agent.options['code'] = 'Agent.check = function() { throw "oh no"; };'
        @agent.save!
        expect {
          expect {
            @agent.check
          }.not_to raise_error
        }.to change { AgentLog.count }.by(1)
        expect(AgentLog.last.message).to match(/oh no/)
        expect(AgentLog.last.level).to eq(4)
      end
    end

    describe "getMemory" do
      it "won't store NaNs" do
        @agent.options['code'] = 'Agent.check = function() { this.memory("foo", NaN); };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq('NaN') # string
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores an Array" do
        @agent.options['code'] = 'Agent.check = function() {
          var arr = [1,2];
          this.memory("foo", arr);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq([1,2])
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores a Hash" do
        @agent.options['code'] = 'Agent.check = function() {
          var obj = {};
          obj["one"] = 1;
          obj["two"] = [1,2];
          this.memory("foo", obj);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq({"one"=>1, "two"=> [1,2]})
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores a nested Hash" do
        @agent.options['code'] = 'Agent.check = function() {
          var u = {};
          u["one"] = 1;
          u["two"] = 2;
          var obj = {};
          obj["three"] = 3;
          obj["four"] = u;
          this.memory("foo", obj);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq({"three"=>3, "four"=>{"one"=>1, "two"=>2}})
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end
    end

    describe "deleteKey" do
      it "deletes a memory key" do
        @agent.memory = { foo: "baz" }
        @agent.options['code'] = 'Agent.check = function() {
          this.deleteKey("foo");
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to be_nil
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "returns the string value of the deleted key" do
        @agent.memory = { foo: "baz" }
        @agent.options['code'] = 'Agent.check = function() {
          this.createEvent({ message: this.deleteKey("foo")});
          };'
        @agent.save!
        @agent.check<commit_msg>Add specs that `this.memory(key, falsy)` should work<commit_after>require 'rails_helper'

describe Agents::JavaScriptAgent do
  before do
    @valid_params = {
      :name => "somename",
      :options => {
        :code => "Agent.check = function() { this.createEvent({ 'message': 'hi' }); };",
      }
    }

    @agent = Agents::JavaScriptAgent.new(@valid_params)
    @agent.user = users(:jane)
    @agent.save!
  end

  describe "validations" do
    it "requires 'code'" do
      expect(@agent).to be_valid
      @agent.options['code'] = ''
      expect(@agent).not_to be_valid
      @agent.options.delete('code')
      expect(@agent).not_to be_valid
    end

    it "checks for a valid 'language', but allows nil" do
      expect(@agent).to be_valid
      @agent.options['language'] = ''
      expect(@agent).to be_valid
      @agent.options.delete('language')
      expect(@agent).to be_valid
      @agent.options['language'] = 'foo'
      expect(@agent).not_to be_valid

      %w[javascript JavaScript coffeescript CoffeeScript].each do |valid_language|
        @agent.options['language'] = valid_language
        expect(@agent).to be_valid
      end
    end

    it "accepts a credential, but it must exist" do
      expect(@agent).to be_valid
      @agent.options['code'] = 'credential:foo'
      expect(@agent).not_to be_valid
      users(:jane).user_credentials.create! :credential_name => "foo", :credential_value => "bar"
      expect(@agent.reload).to be_valid
    end
  end

  describe "#working?" do
    describe "when expected_update_period_in_days is set" do
      it "returns false when more than expected_update_period_in_days have passed since the last event creation" do
        @agent.options['expected_update_period_in_days'] = 1
        @agent.save!
        expect(@agent).not_to be_working
        @agent.check
        expect(@agent.reload).to be_working
        three_days_from_now = 3.days.from_now
        stub(Time).now { three_days_from_now }
        expect(@agent).not_to be_working
      end
    end

    describe "when expected_receive_period_in_days is set" do
      it "returns false when more than expected_receive_period_in_days have passed since the last event was received" do
        @agent.options['expected_receive_period_in_days'] = 1
        @agent.save!
        expect(@agent).not_to be_working
        Agents::JavaScriptAgent.async_receive @agent.id, [events(:bob_website_agent_event).id]
        expect(@agent.reload).to be_working
        two_days_from_now = 2.days.from_now
        stub(Time).now { two_days_from_now }
        expect(@agent.reload).not_to be_working
      end
    end
  end

  describe "executing code" do
    it "works by default" do
      @agent.options = @agent.default_options
      @agent.options['make_event'] = true
      @agent.save!

      expect {
        expect {
          @agent.receive([events(:bob_website_agent_event)])
          @agent.check
        }.not_to change { AgentLog.count }
      }.to change { Event.count }.by(2)
    end

    describe "using credentials as code" do
      before do
        @agent.user.user_credentials.create :credential_name => 'code-foo', :credential_value => 'Agent.check = function() { this.log("ran it"); };'
        @agent.options['code'] = "credential:code-foo\n\n"
        @agent.save!
      end

      it "accepts credentials" do
        @agent.check
        expect(AgentLog.last.message).to eq("ran it")
      end

      it "logs an error when the credential goes away" do
        @agent.user.user_credentials.delete_all
        @agent.reload.check
        expect(AgentLog.last.message).to eq("Unable to find credential")
      end
    end

    describe "error handling" do
      it "should log an error when V8 has issues" do
        @agent.options['code'] = 'syntax error!'
        @agent.save!
        expect {
          expect {
            @agent.check
          }.not_to raise_error
        }.to change { AgentLog.count }.by(1)
        expect(AgentLog.last.message).to match(/Unexpected identifier/)
        expect(AgentLog.last.level).to eq(4)
      end

      it "should log an error when JavaScript throws" do
        @agent.options['code'] = 'Agent.check = function() { throw "oh no"; };'
        @agent.save!
        expect {
          expect {
            @agent.check
          }.not_to raise_error
        }.to change { AgentLog.count }.by(1)
        expect(AgentLog.last.message).to match(/oh no/)
        expect(AgentLog.last.level).to eq(4)
      end
    end

    describe "getMemory" do
      it "won't store NaNs" do
        @agent.options['code'] = 'Agent.check = function() { this.memory("foo", NaN); };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq('NaN') # string
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores an Array" do
        @agent.options['code'] = 'Agent.check = function() {
          var arr = [1,2];
          this.memory("foo", arr);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq([1,2])
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores a Hash" do
        @agent.options['code'] = 'Agent.check = function() {
          var obj = {};
          obj["one"] = 1;
          obj["two"] = [1,2];
          this.memory("foo", obj);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq({"one"=>1, "two"=> [1,2]})
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores a nested Hash" do
        @agent.options['code'] = 'Agent.check = function() {
          var u = {};
          u["one"] = 1;
          u["two"] = 2;
          var obj = {};
          obj["three"] = 3;
          obj["four"] = u;
          this.memory("foo", obj);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq({"three"=>3, "four"=>{"one"=>1, "two"=>2}})
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores null" do
        @agent.options['code'] = 'Agent.check = function() {
          this.memory("foo", "test");
          this.memory("foo", null);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq(nil)
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "it stores false" do
        @agent.options['code'] = 'Agent.check = function() {
          this.memory("foo", "test");
          this.memory("foo", false);
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to eq(false)
        @agent.save!
        expect { @agent.reload.memory }.not_to raise_error
      end
    end

    describe "deleteKey" do
      it "deletes a memory key" do
        @agent.memory = { foo: "baz" }
        @agent.options['code'] = 'Agent.check = function() {
          this.deleteKey("foo");
          };'
        @agent.save!
        @agent.check
        expect(@agent.memory['foo']).to be_nil
        expect { @agent.reload.memory }.not_to raise_error
      end

      it "returns the string value of the deleted key" do
        @agent.memory = { foo: "baz" }
        @agent.options['code'] = 'Agent.check = function() {
          this.createEvent({ message: this.deleteKey("foo")});
          };'
        @agent.save!
        @agent.check
{'fraction_non_alphanumeric': 0.1368959955116067, 'fraction_numerical': 0.0036468195525632932, 'mean_word_length': 2.2117117117117115, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3424215', 'n_tokens_mistral': 4455, 'n_tokens_neox': 4343, 'n_words': 1003}