Files
daily_stock_analysis/tests/test_analysis_integration.py
Elvis Wang 972c314656 feat: Web/API 指数入口与共享 canonical 去重基础 (#2312)
* feat: Web/API 指数入口与共享 canonical 去重基础

为 Issue #2303 Phase 2 PR1 落地 Web/API 指数入口适配:

- API 使用 parse_analysis_target 构造结构化 AnalysisTarget 并贯通到 pipeline
- API 与 TaskQueue 去重按 asset_type 分支(指数用 canonical_id,个股用 legacy code)
- BatchTaskAcceptedResponse 追加可选 rejected 字段,未登记 CSI 单股 400、批量仅该目标失败
- TaskInfo 固化 dedupe_key,避免指数与同码个股折叠及 _analyzing_stocks 残留
- Web 移除 assetType=index 全局过滤,Chat 名称识别保护指数 canonical
- 补齐 Pipeline 指数 DecisionSignal market_override=cn 真实分支测试

* fix: 收敛指数 canonical 身份与批量响应契约

PR #2312 review 修复:报告 meta 补充 asset_type 隐藏指数自选;/analyze 在解析前限制非空原始 token;is_single 统一驱动 metadata/409/单任务 202;HomePage 三元计数继续后续 chunk。

* fix: avoid double space in index news search query

* fix: preserve canonical index identity

* fix: validate legacy task asset type

* fix: preserve canonical index identity in chat

* fix: preserve index identity across chat backends
2026-09-01 19:25:15 +08:00

164 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
===================================
Analysis Integration Tests
===================================
Covers:
- API endpoint /analyze
- Name resolution to code
- Task queue submission
- Metadata persistence (original_query, selection_source)
"""
import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from api.app import create_app
from src.services.task_queue import AnalysisTaskQueue, TaskStatus
from src.config import Config
import src.auth as auth
@pytest.fixture
def client():
app = create_app()
return TestClient(app)
@pytest.fixture(autouse=True)
def disable_auth():
"""Keep analysis integration tests independent from local auth env state."""
auth._auth_enabled = None
with patch("api.middlewares.auth.is_auth_enabled", return_value=False), \
patch("src.auth.is_auth_enabled", return_value=False):
yield
auth._auth_enabled = None
@pytest.fixture
def mock_task_queue():
with patch("api.v1.endpoints.analysis.get_task_queue") as mock_get:
queue = MagicMock(spec=AnalysisTaskQueue)
mock_get.return_value = queue
yield queue
class TestAnalysisIntegration:
"""End-to-end integration tests for the analysis flow."""
def test_trigger_analysis_flow_manual_name(self, client, mock_task_queue):
"""Test flow: User enters stock name -> resolved to code -> task submitted."""
# Setup mock behavior
mock_task_queue.submit_tasks_batch.return_value = (
[MagicMock(task_id="test_task_123", stock_code="600519", analysis_phase="auto")],
[]
)
# Trigger analysis with a stock name
response = client.post(
"/api/v1/analysis/analyze",
json={
"stock_code": "贵州茅台",
"async_mode": True,
"original_query": "贵州茅台",
"selection_source": "manual"
}
)
assert response.status_code == 202
data = response.json()
assert data["task_id"] == "test_task_123"
assert data["status"] == "pending"
# 单股 202TaskAccepted schema不包含可选的 asset_type 字段legacy
# MagicMock 任务即使 asset_type 是 MagicMock 子对象也必须保持该契约。
assert "asset_type" not in data
# Verify task queue received the correct resolved code and metadata.
# Use call_args so this integration test stays focused on analysis flow
# semantics even if the queue API gains orthogonal optional flags.
mock_task_queue.submit_tasks_batch.assert_called_once()
_, kwargs = mock_task_queue.submit_tasks_batch.call_args
assert kwargs["stock_codes"] == ["600519"]
assert kwargs["stock_name"] is None
assert kwargs["original_query"] == "贵州茅台"
assert kwargs["selection_source"] == "manual"
assert kwargs["report_type"] == "detailed"
assert kwargs["analysis_phase"] == "auto"
assert kwargs["force_refresh"] is False
assert kwargs["notify"] is True
def test_trigger_analysis_batch_deduplication(self, client, mock_task_queue):
"""Test de-duplication across different formats (600519 and 600519.SH)."""
mock_task_queue.submit_tasks_batch.return_value = ([], [])
client.post(
"/api/v1/analysis/analyze",
json={
"stock_codes": ["600519", "600519.SH"],
"async_mode": True
}
)
# Should only submit once after de-duplication
mock_task_queue.submit_tasks_batch.assert_called_once()
args, kwargs = mock_task_queue.submit_tasks_batch.call_args
assert len(kwargs["stock_codes"]) == 1
assert kwargs["stock_codes"] == ["600519"]
assert kwargs["analysis_phase"] == "auto"
def test_trigger_analysis_dos_protection(self, client):
"""Test that excessive stock codes are rejected."""
too_many_codes = [f"{i:06d}" for i in range(101)]
response = client.post(
"/api/v1/analysis/analyze",
json={
"stock_codes": too_many_codes,
"async_mode": True
}
)
assert response.status_code == 400
assert "最多支持" in response.json()["message"]
def test_trigger_analysis_metadata_isolation_in_batch(self, client, mock_task_queue):
"""Test that single-stock metadata isn't applied to batch tasks."""
mock_task_queue.submit_tasks_batch.return_value = ([], [])
client.post(
"/api/v1/analysis/analyze",
json={
"stock_codes": ["600519", "000001"],
"stock_name": "贵州茅台",
"original_query": "茅台",
"async_mode": True
}
)
# Batch request: metadata should be None
mock_task_queue.submit_tasks_batch.assert_called_once()
args, kwargs = mock_task_queue.submit_tasks_batch.call_args
assert kwargs["stock_name"] is None
assert kwargs["original_query"] is None
assert kwargs["selection_source"] is None
assert kwargs["analysis_phase"] == "auto"
def test_trigger_analysis_explicit_analysis_phase(self, client, mock_task_queue):
"""Explicit analysis_phase is passed through to the task queue."""
mock_task_queue.submit_tasks_batch.return_value = (
[MagicMock(task_id="test_task_phase", stock_code="600519", analysis_phase="intraday")],
[]
)
response = client.post(
"/api/v1/analysis/analyze",
json={
"stock_code": "600519",
"async_mode": True,
"analysis_phase": "intraday",
},
)
assert response.status_code == 202
assert response.json()["analysis_phase"] == "intraday"
mock_task_queue.submit_tasks_batch.assert_called_once()
_, kwargs = mock_task_queue.submit_tasks_batch.call_args
assert kwargs["analysis_phase"] == "intraday"