test: rewrite services

This commit is contained in:
Shivam Mishra
2025-10-29 19:38:17 +05:30
parent 52271dc984
commit 990b58fe62
2 changed files with 499 additions and 0 deletions
@@ -0,0 +1,250 @@
require 'rails_helper'
RSpec.describe Captain::Assistant::ChangeToneService do
let(:text) { 'Hey! Just checking if you got my email about the project' }
let(:tone) { 'professional' }
let(:service) { described_class.new(text: text, tone: tone) }
let(:agent) { instance_double(Agents::Agent) }
let(:runner) { instance_double(Agents::Runner) }
let(:result) do
instance_double(Agents::RunResult,
output: {
'rewritten_text' => 'Good afternoon. I am following up regarding my previous email about the project.',
'tone_applied' => 'professional'
},
error: nil)
end
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4o-mini')
allow(Agents::Agent).to receive(:new).and_return(agent)
allow(Agents::Runner).to receive(:with_agents).and_return(runner)
allow(runner).to receive(:run).with(anything, context: anything).and_return(result)
allow(Captain::PromptRenderer).to receive(:render).and_return('tone prompt')
end
describe '#initialize' do
it 'initializes with text and tone' do
expect(service.instance_variable_get(:@text)).to eq(text)
expect(service.instance_variable_get(:@tone)).to eq('professional')
end
it 'converts tone to lowercase' do
service = described_class.new(text: text, tone: 'PROFESSIONAL')
expect(service.instance_variable_get(:@tone)).to eq('professional')
end
context 'with invalid tone' do
it 'raises ArgumentError' do
expect do
described_class.new(text: text, tone: 'invalid_tone')
end.to raise_error(ArgumentError, /Unsupported tone: invalid_tone/)
end
it 'includes supported tones in error message' do
expect do
described_class.new(text: text, tone: 'invalid')
end.to raise_error(ArgumentError, /professional, casual, straightforward, confident, friendly/)
end
end
end
describe '#execute' do
context 'when successful' do
it 'renders the tone prompt with correct context' do
expect(Captain::PromptRenderer).to receive(:render).with(
'rewrite/tone',
hash_including(tone: 'professional')
)
service.execute
end
it 'builds agent with correct parameters' do
expect(Agents::Agent).to receive(:new).with(
name: 'ToneChanger',
instructions: 'tone prompt',
model: 'gpt-4o-mini',
response_schema: service.send(:response_schema)
)
service.execute
end
it 'returns success response with rewritten text' do
response = service.execute
expect(response[:success]).to be true
expect(response[:rewritten_text]).to eq('Good afternoon. I am following up regarding my previous email about the project.')
expect(response[:tone]).to eq('professional')
expect(response[:original_text]).to eq(text)
end
end
context 'with different tones' do
%w[professional casual straightforward confident friendly].each do |test_tone|
it "works with #{test_tone} tone" do
service = described_class.new(text: text, tone: test_tone)
allow(Agents::Agent).to receive(:new).and_return(agent)
allow(Agents::Runner).to receive(:with_agents).and_return(runner)
allow(runner).to receive(:run).with(anything, context: anything).and_return(result)
expect(Captain::PromptRenderer).to receive(:render).with(
'rewrite/tone',
hash_including(tone: test_tone)
)
service.execute
end
end
end
context 'when agent returns an error' do
let(:result) { instance_double(Agents::RunResult, output: { error: 'Model error' }, error: nil) }
it 'returns error response' do
response = service.execute
expect(response[:success]).to be false
expect(response[:error]).to eq('Model error')
expect(response[:original_text]).to eq(text)
end
end
context 'when exception is raised' do
before do
allow(runner).to receive(:run).with(anything, context: anything).and_raise(StandardError.new('API timeout'))
end
it 'logs the error' do
expect(Rails.logger).to receive(:error).with(/ChangeToneService error: API timeout/)
expect(Rails.logger).to receive(:error).with(anything)
service.execute
end
it 'returns error response' do
response = service.execute
expect(response[:success]).to be false
expect(response[:error]).to eq('API timeout')
end
end
end
describe '#agent_name' do
it 'returns ToneChanger' do
expect(service.send(:agent_name)).to eq('ToneChanger')
end
end
describe '#build_instructions' do
it 'renders the tone template with tone context' do
expect(Captain::PromptRenderer).to receive(:render).with(
'rewrite/tone',
hash_including(tone: 'professional')
)
service.send(:build_instructions)
end
end
describe '#response_schema' do
let(:schema) { service.send(:response_schema) }
it 'defines object type' do
expect(schema[:type]).to eq('object')
end
it 'includes rewritten_text property' do
expect(schema[:properties][:rewritten_text]).to include(
type: 'string',
description: 'The rewritten text with the requested tone applied'
)
end
it 'includes tone_applied property' do
expect(schema[:properties][:tone_applied]).to include(
type: 'string',
description: 'The tone that was applied to the text'
)
end
it 'marks both fields as required' do
expect(schema[:required]).to match_array(%w[rewritten_text tone_applied])
end
it 'disallows additional properties' do
expect(schema[:additionalProperties]).to be false
end
end
describe '#build_success_response' do
let(:output) do
{
'rewritten_text' => 'Rewritten content',
'tone_applied' => 'professional'
}
end
it 'extracts rewritten_text from output' do
response = service.send(:build_success_response, output)
expect(response[:rewritten_text]).to eq('Rewritten content')
end
it 'includes the requested tone' do
response = service.send(:build_success_response, output)
expect(response[:tone]).to eq('professional')
end
it 'includes original text' do
response = service.send(:build_success_response, output)
expect(response[:original_text]).to eq(text)
end
it 'marks response as successful' do
response = service.send(:build_success_response, output)
expect(response[:success]).to be true
end
end
describe 'SUPPORTED_TONES constant' do
it 'includes all valid tones' do
expect(described_class::SUPPORTED_TONES).to match_array(
%w[professional casual straightforward confident friendly]
)
end
end
describe 'End-to-End Tests' do
context 'when making real API call (stubbed at HTTP level)' do
let(:openai_response) do
{
id: 'chatcmpl-123',
object: 'chat.completion',
created: 1_677_652_288,
model: 'gpt-4o-mini',
choices: [
{
index: 0,
message: {
role: 'assistant',
content: JSON.generate({
rewritten_text: 'Good afternoon. I am following up regarding my previous email about the project.',
tone_applied: 'professional'
})
},
finish_reason: 'stop'
}
]
}
end
before do
stub_request(:post, 'https://api.openai.com/v1/chat/completions')
.to_return(status: 200, body: openai_response.to_json, headers: { 'Content-Type' => 'application/json' })
end
it 'successfully changes tone through entire stack' do
response = service.execute
expect(response[:success]).to be true
expect(response[:rewritten_text]).to eq('Good afternoon. I am following up regarding my previous email about the project.')
expect(response[:tone]).to eq('professional')
expect(response[:original_text]).to eq(text)
end
end
end
end
@@ -0,0 +1,249 @@
require 'rails_helper'
RSpec.describe Captain::Assistant::FixGrammarService do
let(:text) { 'their going too the store tommorow and there bringing they\'re friends' }
let(:service) { described_class.new(text: text) }
let(:agent) { instance_double(Agents::Agent) }
let(:runner) { instance_double(Agents::Runner) }
let(:result) do
instance_double(Agents::RunResult,
output: {
'corrected_text' => 'They\'re going to the store tomorrow and they\'re bringing their friends.',
'corrections_made' => [
'Changed "their" to "They\'re"',
'Changed "too" to "to"',
'Fixed spelling: "tommorow" to "tomorrow"',
'Changed "there" to "they\'re"',
'Changed "they\'re" to "their"'
]
},
error: nil)
end
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4o-mini')
allow(Agents::Agent).to receive(:new).and_return(agent)
allow(Agents::Runner).to receive(:with_agents).and_return(runner)
allow(runner).to receive(:run).with(anything, context: anything).and_return(result)
allow(Captain::PromptRenderer).to receive(:render).and_return('grammar prompt')
end
describe '#initialize' do
it 'initializes with text only' do
expect(service.instance_variable_get(:@text)).to eq(text)
end
end
describe '#execute' do
context 'when successful' do
it 'renders the grammar prompt' do
expect(Captain::PromptRenderer).to receive(:render).with('rewrite/grammar', {})
service.execute
end
it 'builds agent with correct parameters' do
expect(Agents::Agent).to receive(:new).with(
name: 'GrammarFixer',
instructions: 'grammar prompt',
model: 'gpt-4o-mini',
response_schema: service.send(:response_schema)
)
service.execute
end
it 'returns success response with corrected text' do
response = service.execute
expect(response[:success]).to be true
expect(response[:corrected_text]).to eq('They\'re going to the store tomorrow and they\'re bringing their friends.')
expect(response[:original_text]).to eq(text)
end
it 'includes corrections made' do
response = service.execute
expect(response[:corrections_made]).to be_an(Array)
expect(response[:corrections_made]).to include('Changed "their" to "They\'re"')
end
end
context 'when output does not include corrections_made' do
let(:result) do
instance_double(Agents::RunResult,
output: {
'corrected_text' => 'Corrected text without corrections list'
},
error: nil)
end
it 'returns empty array for corrections_made' do
response = service.execute
expect(response[:success]).to be true
expect(response[:corrections_made]).to eq([])
end
end
context 'when agent returns an error' do
let(:result) { instance_double(Agents::RunResult, output: { error: 'Model error' }, error: nil) }
it 'returns error response' do
response = service.execute
expect(response[:success]).to be false
expect(response[:error]).to eq('Model error')
expect(response[:original_text]).to eq(text)
end
end
context 'when exception is raised' do
before do
allow(runner).to receive(:run).with(anything, context: anything).and_raise(StandardError.new('API timeout'))
end
it 'logs the error' do
expect(Rails.logger).to receive(:error).with(/FixGrammarService error: API timeout/)
expect(Rails.logger).to receive(:error).with(anything)
service.execute
end
it 'returns error response' do
response = service.execute
expect(response[:success]).to be false
expect(response[:error]).to eq('API timeout')
end
end
end
describe '#agent_name' do
it 'returns GrammarFixer' do
expect(service.send(:agent_name)).to eq('GrammarFixer')
end
end
describe '#build_instructions' do
it 'renders the grammar template with empty context' do
expect(Captain::PromptRenderer).to receive(:render).with('rewrite/grammar', {})
service.send(:build_instructions)
end
end
describe '#response_schema' do
let(:schema) { service.send(:response_schema) }
it 'defines object type' do
expect(schema[:type]).to eq('object')
end
it 'includes corrected_text property' do
expect(schema[:properties][:corrected_text]).to include(
type: 'string',
description: 'The text with corrected grammar, spelling, and punctuation'
)
end
it 'includes corrections_made property' do
expect(schema[:properties][:corrections_made]).to include(
type: 'array',
description: 'List of corrections that were made'
)
end
it 'marks corrected_text as required' do
expect(schema[:required]).to include('corrected_text')
end
it 'disallows additional properties' do
expect(schema[:additionalProperties]).to be false
end
end
describe '#build_success_response' do
let(:output) do
{
'corrected_text' => 'Corrected content',
'corrections_made' => ['Fix 1', 'Fix 2']
}
end
it 'extracts corrected_text from output' do
response = service.send(:build_success_response, output)
expect(response[:corrected_text]).to eq('Corrected content')
end
it 'includes corrections made' do
response = service.send(:build_success_response, output)
expect(response[:corrections_made]).to eq(['Fix 1', 'Fix 2'])
end
it 'includes original text' do
response = service.send(:build_success_response, output)
expect(response[:original_text]).to eq(text)
end
it 'marks response as successful' do
response = service.send(:build_success_response, output)
expect(response[:success]).to be true
end
end
describe '#extract_corrections' do
it 'returns array when output has corrections_made as symbol key' do
output = { corrections_made: ['Fix 1'] }
expect(service.send(:extract_corrections, output)).to eq(['Fix 1'])
end
it 'returns array when output has corrections_made as string key' do
output = { 'corrections_made' => ['Fix 1'] }
expect(service.send(:extract_corrections, output)).to eq(['Fix 1'])
end
it 'returns empty array when output is not a hash' do
expect(service.send(:extract_corrections, 'string')).to eq([])
end
it 'returns empty array when corrections_made is missing' do
expect(service.send(:extract_corrections, {})).to eq([])
end
end
describe 'End-to-End Tests' do
context 'when making real API call (stubbed at HTTP level)' do
let(:openai_response) do
{
id: 'chatcmpl-456',
object: 'chat.completion',
created: 1_677_652_300,
model: 'gpt-4o-mini',
choices: [
{
index: 0,
message: {
role: 'assistant',
content: JSON.generate({
corrected_text: 'They\'re going to the store tomorrow and they\'re bringing their friends.',
corrections_made: [
'Changed "their" to "They\'re"',
'Changed "too" to "to"',
'Fixed spelling: "tommorow" to "tomorrow"'
]
})
},
finish_reason: 'stop'
}
]
}
end
before do
stub_request(:post, 'https://api.openai.com/v1/chat/completions')
.to_return(status: 200, body: openai_response.to_json, headers: { 'Content-Type' => 'application/json' })
end
it 'successfully fixes grammar through entire stack' do
response = service.execute
expect(response[:success]).to be true
expect(response[:corrected_text]).to eq('They\'re going to the store tomorrow and they\'re bringing their friends.')
expect(response[:corrections_made]).to include('Changed "their" to "They\'re"')
expect(response[:original_text]).to eq(text)
end
end
end
end