# frozen_string_literal: true require 'rails_helper' RSpec.describe Concerns::Agentable do let(:dummy_class) do Class.new do include Concerns::Agentable attr_reader :account attr_accessor :temperature def initialize(name: 'Test Agent', temperature: 0.8, account: nil) @name = name @temperature = temperature @account = account end def self.name 'DummyClass' end private def agent_name @name end def prompt_context { base_key: 'base_value' } end end end let(:account) { create(:account) } let(:dummy_instance) { dummy_class.new(account: account) } let(:mock_agents_agent) { instance_double(Agents::Agent) } before do InstallationConfig.where(name: 'CAPTAIN_OPEN_AI_MODEL').destroy_all allow(Agents::Agent).to receive(:new).and_return(mock_agents_agent) allow(Captain::PromptRenderer).to receive(:render).and_return('rendered_template') end describe '#agent' do it 'creates an Agents::Agent with correct parameters' do expect(Agents::Agent).to receive(:new).with( name: 'Test Agent', instructions: instance_of(Proc), tools: [], model: Llm::Models.default_model_for('assistant'), temperature: 0.8, response_schema: Captain::ResponseSchema ) dummy_instance.agent end it 'uses default temperature when temperature is nil' do dummy_instance.temperature = nil expect(Agents::Agent).to receive(:new).with( hash_including(temperature: 0.5) ) dummy_instance.agent end it 'converts temperature to float' do dummy_instance.temperature = '0.5' expect(Agents::Agent).to receive(:new).with( hash_including(temperature: 0.5) ) dummy_instance.agent end end describe '#agent_instructions' do it 'calls Captain::PromptRenderer with base context' do expect(Captain::PromptRenderer).to receive(:render).with( 'dummy_class', hash_including(base_key: 'base_value') ) dummy_instance.agent_instructions end it 'merges context state when provided' do context_double = instance_double(Agents::RunContext, context: { state: { assistant_config: { 'feature_contact_attributes' => true }, conversation: { id: 123 }, contact: { name: 'John' } } }) expected_context = { base_key: 'base_value', conversation: { id: 123 }, contact: { name: 'John' }, campaign: {} } expect(Captain::PromptRenderer).to receive(:render).with( 'dummy_class', hash_including(expected_context) ) dummy_instance.agent_instructions(context_double) end it 'merges campaign data from context state' do context_double = instance_double(Agents::RunContext, context: { state: { conversation: { id: 123 }, contact: { name: 'John' }, campaign: { id: 10, title: 'Summer Sale', message: 'Check it out' } } }) expect(Captain::PromptRenderer).to receive(:render).with( 'dummy_class', hash_including( campaign: { id: 10, title: 'Summer Sale', message: 'Check it out' } ) ) dummy_instance.agent_instructions(context_double) end it 'handles context without state' do context_double = instance_double(Agents::RunContext, context: {}) expect(Captain::PromptRenderer).to receive(:render).with( 'dummy_class', hash_including( base_key: 'base_value', conversation: {}, contact: nil, campaign: {} ) ) dummy_instance.agent_instructions(context_double) end end describe '#template_name' do it 'returns underscored class name' do expect(dummy_instance.send(:template_name)).to eq('dummy_class') end end describe '#agent_tools' do it 'returns empty array by default' do expect(dummy_instance.send(:agent_tools)).to eq([]) end end describe '#agent_model' do it 'returns the assistant feature default model' do expect(dummy_instance.send(:agent_model)).to eq(Llm::Models.default_model_for('assistant')) end it 'returns account override model when present' do create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4.1-nano') account.update!(captain_models: { 'assistant' => 'gpt-5.2' }) expect(dummy_instance.send(:agent_model)).to eq('gpt-5.2') end it 'returns the installation model when account override is absent' do create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4.1-nano') expect(dummy_instance.send(:agent_model)).to eq('gpt-4.1-nano') end it 'returns the Captain V2 default when Captain V2 is enabled' do create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4.1-nano') account.enable_features!('captain_integration_v2') expect(dummy_instance.send(:agent_model)).to eq('gpt-5.2') expect(account.reload.captain_models).to be_nil end it 'returns the assistant feature default model when account is nil' do agent = dummy_class.new(account: nil) expect(agent.send(:agent_model)).to eq(Llm::Models.default_model_for('assistant')) end end describe '#agent_response_schema' do it 'returns Captain::ResponseSchema' do expect(dummy_instance.send(:agent_response_schema)).to eq(Captain::ResponseSchema) end end describe 'required methods' do let(:incomplete_class) do Class.new do include Concerns::Agentable end end let(:incomplete_instance) { incomplete_class.new } describe '#agent_name' do it 'raises NotImplementedError when not implemented' do expect { incomplete_instance.send(:agent_name) } .to raise_error(NotImplementedError, /must implement agent_name/) end end describe '#prompt_context' do it 'raises NotImplementedError when not implemented' do expect { incomplete_instance.send(:prompt_context) } .to raise_error(NotImplementedError, /must implement prompt_context/) end end end end