Add manual AI analysis trigger for existing feedback
Implement dashboard functionality to manually trigger AI analysis for
feedback that was submitted before Phase 4 or failed analysis. Adds
detection mechanism to identify which feedback has been analyzed.
Features:
- Manual analysis trigger route: POST /feedback/{id}/analyze
- Detection of whether feedback has been analyzed (analysis.md exists)
- Dashboard UI button showing "Analyze" or "Re-analyze"
- Visual feedback for feedback without text content
- Comprehensive error handling and flash messages
Implementation:
- app/routes/dashboard.py: Added trigger_analysis() route handler
- app/routes/dashboard.py: Updated detail() to pass analysis status
- app/services/feedback_storage.py: Added has_analysis() helper method
- app/templates/dashboard/detail.html: Added analyze button UI
- tests/contract/test_dashboard_routes.py: Added 3 new contract tests
Testing:
- test_post_trigger_analysis_success: Successful manual analysis
- test_post_trigger_analysis_no_content: Reject empty content
- test_post_trigger_analysis_unauthenticated: Auth required
- All 49 tests passing (1 skipped)
User Experience:
- Green box with "Analyze" button for unanalyzed feedback
- Blue box with "Re-analyze" button for already analyzed feedback
- Red box with info message for feedback without text content
- Flash messages show success/error after analysis
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import pytest
|
||||
import os
|
||||
import yaml
|
||||
import io
|
||||
from unittest.mock import Mock, patch
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@@ -322,3 +323,114 @@ def test_access_control_owner_products(client, app, test_users, test_product):
|
||||
# Should NOT have access to feedback from test-product
|
||||
response = client.get(f'/feedback/{feedback_id}')
|
||||
assert response.status_code == 403 # Forbidden
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_post_trigger_analysis_success(client, app, test_users, test_product):
|
||||
"""Contract test for POST /feedback/{id}/analyze - manual analysis trigger
|
||||
|
||||
Expected: 302 redirect with success message, status updated to 'analyzed'
|
||||
"""
|
||||
# Login first
|
||||
client.post('/login', data={'username': 'admin', 'password': 'admin123'})
|
||||
|
||||
feedback_id = test_product['feedback_id']
|
||||
|
||||
# Mock the Claude API response
|
||||
mock_api_response = Mock()
|
||||
mock_api_response.content = [Mock(text="""
|
||||
# Feedback Analysis
|
||||
|
||||
**Category**: bug
|
||||
|
||||
**Original Language**: en
|
||||
|
||||
**Summary**: User reports a test feedback issue
|
||||
|
||||
**Translation**: (same as original)
|
||||
""")]
|
||||
|
||||
# Patch AI analyzer
|
||||
with patch('app.routes.dashboard.ClaudeAnalyzer') as mock_analyzer_class:
|
||||
mock_analyzer = Mock()
|
||||
mock_analyzer.analyze_feedback.return_value = Mock(
|
||||
category='bug',
|
||||
original_language='en',
|
||||
summary='User reports a test feedback issue',
|
||||
translation='(same as original)',
|
||||
raw_analysis=mock_api_response.content[0].text
|
||||
)
|
||||
mock_analyzer_class.return_value = mock_analyzer
|
||||
|
||||
with patch('app.routes.dashboard.os.getenv', return_value='test-api-key'):
|
||||
response = client.post(f'/feedback/{feedback_id}/analyze', follow_redirects=False)
|
||||
|
||||
# Should redirect
|
||||
assert response.status_code == 302
|
||||
assert f'/feedback/{feedback_id}' in response.location
|
||||
|
||||
# Verify analysis was saved
|
||||
with app.app_context():
|
||||
data_dir = app.config['DATA_DIR']
|
||||
analysis_file = os.path.join(
|
||||
data_dir, 'products', 'test-product', 'feedback', feedback_id, 'analysis.md'
|
||||
)
|
||||
assert os.path.exists(analysis_file)
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_post_trigger_analysis_no_content(client, app, test_users, test_product):
|
||||
"""Contract test for POST /feedback/{id}/analyze - feedback with empty text content
|
||||
|
||||
Expected: 302 redirect with error message
|
||||
"""
|
||||
# Create feedback with empty content.txt
|
||||
with app.app_context():
|
||||
product_dir = os.path.join(app.config['DATA_DIR'], 'products', 'test-product')
|
||||
feedback_id = 'test-feedback-empty-text'
|
||||
feedback_path = os.path.join(product_dir, 'feedback', feedback_id)
|
||||
os.makedirs(feedback_path, exist_ok=True)
|
||||
|
||||
metadata = {
|
||||
'feedback_id': feedback_id,
|
||||
'product_id': 'test-product',
|
||||
'status': 'new',
|
||||
'submitted_at': '2025-10-16T10:00:00Z',
|
||||
'has_attachments': True,
|
||||
'attachment_count': 1
|
||||
}
|
||||
|
||||
with open(os.path.join(feedback_path, 'metadata.yaml'), 'w') as f:
|
||||
yaml.dump(metadata, f)
|
||||
|
||||
# Create empty content.txt
|
||||
with open(os.path.join(feedback_path, 'content.txt'), 'w') as f:
|
||||
f.write('')
|
||||
|
||||
# Create attachments directory to show this has attachments only
|
||||
attachments_dir = os.path.join(feedback_path, 'attachments')
|
||||
os.makedirs(attachments_dir, exist_ok=True)
|
||||
with open(os.path.join(attachments_dir, 'image.png'), 'w') as f:
|
||||
f.write('fake image data')
|
||||
|
||||
# Login and try to analyze
|
||||
client.post('/login', data={'username': 'admin', 'password': 'admin123'})
|
||||
response = client.post(f'/feedback/{feedback_id}/analyze', follow_redirects=True)
|
||||
|
||||
# Should show error message
|
||||
assert response.status_code == 200
|
||||
assert b'Cannot analyze' in response.data or b'no text content' in response.data
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_post_trigger_analysis_unauthenticated(client, app, test_product):
|
||||
"""Contract test for POST /feedback/{id}/analyze - unauthenticated access
|
||||
|
||||
Expected: 302 redirect to login
|
||||
"""
|
||||
feedback_id = test_product['feedback_id']
|
||||
response = client.post(f'/feedback/{feedback_id}/analyze', follow_redirects=False)
|
||||
|
||||
# Should redirect to login
|
||||
assert response.status_code == 302
|
||||
assert '/login' in response.location
|
||||
|
||||
Reference in New Issue
Block a user