```
refactor(Dockerfile): 使用requirements.txt替代硬编码依赖 将Dockerfile中的硬编码pip包列表替换为通过requirements.txt文件安装, 提高依赖管理的灵活性和可维护性。 feat(scheduling): 移除内置APScheduler,采用独立调度系统 移除app/core/background/scheduler.py中原来的APScheduler实现, 改为使用新的应用级调度系统app.application.scheduling。 refactor(task_queue): 调整任务队列模块结构和导入路径 将任务队列相关常量从app.core.background.task_queue.constants迁移至 app.tasks.constants,并更新所有相关导入路径和引用。 refactor(events): 将事件服务重构为独立的应用层组件 将事件分发逻辑从核心层迁移到应用层,使用app.application.events.EventDispatchService 替代原有的app.modules.events.services.EventService。 feat(ai_memory): 增强AI记忆自动写入的安全策略 新增ai_memory_blocked_content_terms配置项用于阻止敏感内容, 添加TTL过期机制控制自动写入条目的生命周期。 fix(security): 强化生产环境安全验证机制 增加model_validator确保生产环境中数据库连接、API密钥、CORS设置等 关键安全配置符合要求。 feat(risks): 优化风险事件操作动作的外键约束 为RiskEventAction模型的风险事件ID字段添加外键约束, 防止孤立记录并增强数据完整性。 refactor(audit): 优化审计服务方法命名和事务处理 将AuditService的log方法重命名为record以反映其阶段行为, 并调整事务提交时机以提高性能。 feat(events): 增强领域事件并发处理和响应模型 添加事件锁定机制防止重复处理,更新API响应模型以提供 更准确的数据类型定义。 ```
This commit is contained in:
@@ -30,12 +30,12 @@ os.environ["SCHEDULER_ENABLED"] = "false"
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.background.scheduler import create_scheduler
|
||||
from app.application.scheduling import create_scheduler
|
||||
from app.core.database import Base, SessionLocal, engine
|
||||
from app.core.http.pagination import bounded_limit, bounded_offset
|
||||
from app.core.http.responses import MaskedJSONResponse
|
||||
from app.core.security import require_api_key, require_audit_api_key
|
||||
from app.main import _allow_cors_credentials, app
|
||||
from app.modules.audit.constants import AUDIT_REDACTED_VALUE
|
||||
from app.modules.ai_memory.constants import (
|
||||
AIMemoryPayloadKey,
|
||||
AIMemoryResponseKey,
|
||||
@@ -48,7 +48,9 @@ from app.modules.events.constants import (
|
||||
EventStatus,
|
||||
EventType,
|
||||
)
|
||||
from app.modules.events.models import DomainEvent
|
||||
from app.modules.events.services import EventService
|
||||
from app.application.events import EventDispatchService
|
||||
from app.modules.business.registry import get_domain_model
|
||||
from app.modules.business.models import (
|
||||
Employee,
|
||||
@@ -101,17 +103,18 @@ from app.modules.reports.constants import (
|
||||
ReportTitle,
|
||||
ReportType,
|
||||
)
|
||||
from app.modules.reports.lifecycle_pipeline import LifecyclePipelineService
|
||||
from app.application.pipelines import LifecyclePipelineService
|
||||
from app.application.delivery import ReportDeliveryService
|
||||
from app.modules.reports.chart import render_lifecycle_chart
|
||||
from app.modules.reports.services import ReportService
|
||||
from app.modules.feishu.service import FeishuService
|
||||
from app.modules.feishu.commands import FeishuCommandService
|
||||
from app.modules.feishu.events import FeishuEventService
|
||||
from app.application.feishu import FeishuCommandService
|
||||
from app.application.feishu.events import FeishuEventService, _audit_event_metadata
|
||||
from app.modules.feishu.constants import FeishuEventSource
|
||||
from app.modules.risk.constants import RiskEventActionValue
|
||||
from app.modules.market.chart import render_market_chart
|
||||
from app.modules.market.service import MarketService, TushareClient, normalize_symbol
|
||||
from app.modules.market.pipeline import MarketPipelineService
|
||||
from app.application.pipelines import MarketPipelineService
|
||||
from app.modules.workflows.constants import WorkflowStatus, WorkflowType
|
||||
from app.modules.workflows.models import WorkflowInstance
|
||||
|
||||
@@ -208,6 +211,9 @@ def test_feishu_webhook_routes_message_event() -> None:
|
||||
data = response.json()
|
||||
assert data["handled"] is True
|
||||
assert data["result"]["command"] == "risk_summary"
|
||||
audit_metadata = _audit_event_metadata(payload)
|
||||
assert "content" not in json.dumps(audit_metadata)
|
||||
assert "test-feishu-token" not in json.dumps(audit_metadata)
|
||||
|
||||
duplicate_response = client.post("/api/v1/integrations/feishu/webhook", json=payload)
|
||||
assert duplicate_response.status_code == 200
|
||||
@@ -220,7 +226,7 @@ def test_feishu_webhook_routes_message_event() -> None:
|
||||
assert logs_response.status_code == 200
|
||||
audit_payload = json.dumps(logs_response.json(), ensure_ascii=False)
|
||||
assert "test-feishu-token" not in audit_payload
|
||||
assert AUDIT_REDACTED_VALUE in audit_payload
|
||||
assert "evt-smoke-risk-001" in audit_payload
|
||||
|
||||
|
||||
def test_feishu_rule_commands_create_list_disable_and_enable(monkeypatch) -> None:
|
||||
@@ -371,7 +377,10 @@ def test_v3_event_idempotency_and_workflow_dispatch() -> None:
|
||||
EventPayloadKey.STATUS: StatusValue.OPEN,
|
||||
},
|
||||
idempotency_key="v3-risk-idempotency",
|
||||
dispatch=True,
|
||||
)
|
||||
event = EventDispatchService(db).dispatch_event(
|
||||
event.event_id,
|
||||
worker_id="pytest-idempotency",
|
||||
)
|
||||
duplicate = service.emit(
|
||||
event_type=EventType.RISK_ACTION_RECORDED,
|
||||
@@ -384,7 +393,10 @@ def test_v3_event_idempotency_and_workflow_dispatch() -> None:
|
||||
EventPayloadKey.STATUS: StatusValue.OPEN,
|
||||
},
|
||||
idempotency_key="v3-risk-idempotency",
|
||||
dispatch=True,
|
||||
)
|
||||
duplicate = EventDispatchService(db).dispatch_event(
|
||||
duplicate.event_id,
|
||||
worker_id="pytest-idempotency-duplicate",
|
||||
)
|
||||
assert duplicate.event_id == event.event_id
|
||||
assert event.status == EventStatus.PROCESSED
|
||||
@@ -442,7 +454,7 @@ def test_v3_ai_memory_recall_and_auto_write() -> None:
|
||||
"/api/v1/ai/ask",
|
||||
headers=headers,
|
||||
json={
|
||||
"prompt": "Summarize quarterly cash planning for project memory smoke",
|
||||
"prompt": "Remember that project updates should use concise bullet summaries",
|
||||
"context": {
|
||||
AIMemoryPayloadKey.SCOPE: "project",
|
||||
AIMemoryPayloadKey.SUBJECT: "P-MEM-SMOKE",
|
||||
@@ -467,7 +479,7 @@ def test_v3_ai_memory_recall_and_auto_write() -> None:
|
||||
"/api/v1/ai/memory/recall",
|
||||
headers=headers,
|
||||
json={
|
||||
"query": "quarterly cash planning",
|
||||
"query": "concise bullet summaries",
|
||||
"scope": "project",
|
||||
"subject": "P-MEM-SMOKE",
|
||||
},
|
||||
@@ -476,6 +488,49 @@ def test_v3_ai_memory_recall_and_auto_write() -> None:
|
||||
assert recall_response.json()[AIMemoryResponseKey.ITEMS]
|
||||
|
||||
|
||||
def test_ai_memory_rejects_financial_facts_and_applies_retention() -> None:
|
||||
response = client.post(
|
||||
"/api/v1/ai/ask",
|
||||
headers=headers,
|
||||
json={
|
||||
"prompt": "Remember the project cash flow and budget details for next quarter",
|
||||
"context": {
|
||||
AIMemoryPayloadKey.SCOPE: "project",
|
||||
AIMemoryPayloadKey.SUBJECT: "P-MEM-FINANCIAL",
|
||||
},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
memory_write = response.json()[AIResponseKey.RAW][AIResponseKey.MEMORY_WRITE]
|
||||
assert memory_write[AIMemoryPayloadKey.STATUS] == AIMemoryStatus.REJECTED
|
||||
|
||||
rejected = client.get(
|
||||
"/api/v1/ai/memory",
|
||||
headers=headers,
|
||||
params={
|
||||
"scope": "project",
|
||||
"subject": "P-MEM-FINANCIAL",
|
||||
"status": AIMemoryStatus.REJECTED,
|
||||
},
|
||||
).json()[AIMemoryResponseKey.ITEMS]
|
||||
assert rejected
|
||||
assert rejected[0]["expires_at"] is not None
|
||||
assert "cash flow" not in rejected[0][AIMemoryPayloadKey.CONTENT].lower()
|
||||
|
||||
|
||||
def test_default_json_response_masks_sensitive_fields() -> None:
|
||||
response = MaskedJSONResponse(
|
||||
{
|
||||
"token": "provider-token",
|
||||
"nested": {"password": "provider-password", "status": "ok"},
|
||||
}
|
||||
)
|
||||
payload = json.loads(response.body)
|
||||
assert payload["token"] == "[MASKED]"
|
||||
assert payload["nested"]["password"] == "[MASKED]"
|
||||
assert payload["nested"]["status"] == "ok"
|
||||
|
||||
|
||||
def test_v3_risk_action_routes_are_disabled_in_read_only_mode() -> None:
|
||||
response = create_business_record(
|
||||
"risk-events",
|
||||
@@ -776,8 +831,8 @@ def test_new_ledgers_reports_and_risk_events() -> None:
|
||||
|
||||
|
||||
def test_independent_feishu_report_schedules_and_tasks_are_registered() -> None:
|
||||
from app.core.background.scheduler import create_scheduler
|
||||
from app.core.background.task_queue.constants import (
|
||||
from app.application.scheduling import create_scheduler
|
||||
from app.tasks.constants import (
|
||||
TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||
TASK_PUSH_DAILY_BRIEF,
|
||||
TASK_PUSH_PROJECT_WEEKLY,
|
||||
@@ -979,6 +1034,17 @@ def test_v3_enterprise_analytics_returns_read_only_sections() -> None:
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
event = db.execute(
|
||||
select(DomainEvent).where(
|
||||
DomainEvent.idempotency_key
|
||||
== f"enterprise-analytics:{data[EnterpriseAnalyticsKey.CODE]}"
|
||||
)
|
||||
).scalar_one()
|
||||
assert event.status == EventStatus.PENDING
|
||||
EventDispatchService(db).dispatch_event(
|
||||
event.event_id,
|
||||
worker_id="pytest-enterprise",
|
||||
)
|
||||
workflow = db.execute(
|
||||
select(WorkflowInstance).where(
|
||||
WorkflowInstance.workflow_type == WorkflowType.ENTERPRISE_ANALYTICS,
|
||||
@@ -1438,7 +1504,11 @@ def test_lifecycle_pipeline_is_idempotent(monkeypatch) -> None:
|
||||
"ai_analysis": {"ok": True, "answer": "analysis"},
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(ReportService, "push_report", lambda self, *args, **kwargs: {"ok": True})
|
||||
monkeypatch.setattr(
|
||||
ReportDeliveryService,
|
||||
"push_report",
|
||||
lambda self, *args, **kwargs: {"ok": True},
|
||||
)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -1506,7 +1576,7 @@ def test_ai_unavailable_sends_notice_without_business_report(monkeypatch) -> Non
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ReportService,
|
||||
ReportDeliveryService,
|
||||
"push_report",
|
||||
lambda self, *args, **kwargs: pytest.fail("business report must not be sent"),
|
||||
)
|
||||
@@ -1596,7 +1666,7 @@ def test_lifecycle_chart_is_uploaded_and_embedded_in_feishu_card(monkeypatch) ->
|
||||
monkeypatch.setattr(FeishuService, "send_card", fake_send_card)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
ReportService(db).push_report(
|
||||
ReportDeliveryService(db).push_report(
|
||||
{
|
||||
"title": "Lifecycle",
|
||||
"report_type": "daily",
|
||||
@@ -2190,6 +2260,7 @@ def test_market_closed_day_skips_quote_collection() -> None:
|
||||
|
||||
def test_market_scheduler_registers_close_and_weekly_jobs(monkeypatch) -> None:
|
||||
monkeypatch.setenv("MARKET_ANALYSIS_ENABLED", "true")
|
||||
monkeypatch.setenv("READ_ONLY_MODE", "false")
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
scheduler = create_scheduler()
|
||||
@@ -2199,6 +2270,7 @@ def test_market_scheduler_registers_close_and_weekly_jobs(monkeypatch) -> None:
|
||||
assert "market_weekly_analysis" in job_ids
|
||||
finally:
|
||||
monkeypatch.delenv("MARKET_ANALYSIS_ENABLED", raising=False)
|
||||
monkeypatch.delenv("READ_ONLY_MODE", raising=False)
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user