fix(agent): 修复规则中心表格版本和修改记录
补齐规则资产 JSON 读写接口和前端调用,修复 AuditView 导入缺失。 Excel 在线编辑改为比对所有页签并生成最近修改记录,版本快照统一保存到 rules/finance-rules/.versions。 隔离规则表测试存储,避免测试或旧入口写入真实规则目录与 storage/agent_assets。
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from openpyxl import Workbook, load_workbook
|
||||
@@ -9,6 +11,7 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.api.deps import CurrentUserContext
|
||||
from app.core.agent_enums import (
|
||||
AgentAssetContentType,
|
||||
AgentAssetDomain,
|
||||
@@ -19,13 +22,18 @@ from app.core.agent_enums import (
|
||||
AgentRunSource,
|
||||
AgentRunStatus,
|
||||
)
|
||||
from app.core.config import SERVER_DIR
|
||||
from app.db.base import Base
|
||||
from app.schemas.agent_asset import (
|
||||
AgentAssetCreate,
|
||||
AgentAssetReviewCreate,
|
||||
AgentAssetVersionCreate,
|
||||
)
|
||||
from app.api.deps import CurrentUserContext
|
||||
from app.services.agent_asset_spreadsheet import (
|
||||
COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
|
||||
COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
|
||||
FINANCE_RULES_LIBRARY,
|
||||
)
|
||||
from app.services.agent_assets import AgentAssetService
|
||||
from app.services.agent_runs import AgentRunService
|
||||
from app.services.audit import AuditLogService
|
||||
@@ -33,6 +41,38 @@ from app.services.expense_rule_runtime import ExpenseRuleRuntimeService
|
||||
from app.services.settings import OnlyOfficeRuntimeConfig
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_rule_file_storage(tmp_path, monkeypatch) -> None:
|
||||
temp_server_dir = tmp_path / "server"
|
||||
temp_rules_root = temp_server_dir / "rules"
|
||||
temp_finance_rules = temp_rules_root / FINANCE_RULES_LIBRARY
|
||||
temp_finance_rules.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
real_finance_rules = SERVER_DIR / "rules" / FINANCE_RULES_LIBRARY
|
||||
for file_name in (
|
||||
COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
|
||||
COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
|
||||
):
|
||||
source_path = real_finance_rules / file_name
|
||||
if source_path.exists():
|
||||
shutil.copy2(source_path, temp_finance_rules / file_name)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.agent_asset_spreadsheet.SERVER_DIR",
|
||||
temp_server_dir,
|
||||
)
|
||||
|
||||
def init_manager(self, storage_root=None, rule_root=None) -> None:
|
||||
self.storage_root = Path(storage_root or tmp_path / "storage").resolve()
|
||||
self.asset_root = (self.storage_root / "agent_assets").resolve()
|
||||
self.rule_root = Path(rule_root or temp_rules_root).resolve()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.agent_asset_spreadsheet.AgentAssetSpreadsheetManager.__init__",
|
||||
init_manager,
|
||||
)
|
||||
|
||||
|
||||
def build_session() -> Session:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
@@ -55,6 +95,19 @@ def build_workbook_bytes(rows: list[list[object]], *, sheet_name: str = "规则
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def build_multi_sheet_workbook_bytes(sheets: dict[str, list[list[object]]]) -> bytes:
|
||||
workbook = Workbook()
|
||||
default_sheet = workbook.active
|
||||
workbook.remove(default_sheet)
|
||||
for sheet_name, rows in sheets.items():
|
||||
sheet = workbook.create_sheet(sheet_name)
|
||||
for row in rows:
|
||||
sheet.append(row)
|
||||
buffer = BytesIO()
|
||||
workbook.save(buffer)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def test_agent_asset_service_seeds_assets_and_enforces_review_before_activation() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
@@ -62,7 +115,8 @@ def test_agent_asset_service_seeds_assets_and_enforces_review_before_activation(
|
||||
rules = service.list_assets(asset_type=AgentAssetType.RULE.value)
|
||||
assert len(rules) >= 3
|
||||
assert any(
|
||||
item.code == "rule.expense.travel_risk_control_standard" and item.status == AgentAssetStatus.ACTIVE.value
|
||||
item.code == "rule.expense.travel_risk_control_standard"
|
||||
and item.status == AgentAssetStatus.ACTIVE.value
|
||||
for item in rules
|
||||
)
|
||||
assert all(
|
||||
@@ -218,7 +272,10 @@ def test_expense_rule_runtime_uses_published_version_instead_of_working_version(
|
||||
rule.id,
|
||||
AgentAssetVersionCreate(
|
||||
version="v1.1.1",
|
||||
content="# 工作稿\n\n```expense-rule\n{\"kind\":\"travel_policy\",\"version\":1}\n```",
|
||||
content=(
|
||||
"# 工作稿\n\n"
|
||||
'```expense-rule\n{"kind":"travel_policy","version":1}\n```'
|
||||
),
|
||||
content_type=AgentAssetContentType.MARKDOWN,
|
||||
change_note="未上线草稿",
|
||||
created_by="finance_user",
|
||||
@@ -285,7 +342,10 @@ def test_spreadsheet_version_compare_returns_sheet_and_cell_changes() -> None:
|
||||
|
||||
assert diff.changed_sheet_count == 1
|
||||
assert diff.changed_cell_count == 3
|
||||
assert any(item.cell == "B2" and item.change_type == "modified" for item in diff.cell_changes)
|
||||
assert any(
|
||||
item.cell == "B2" and item.change_type == "modified"
|
||||
for item in diff.cell_changes
|
||||
)
|
||||
assert any(item.cell == "A3" and item.change_type == "added" for item in diff.cell_changes)
|
||||
|
||||
|
||||
@@ -311,7 +371,10 @@ def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_co
|
||||
current_asset = service.repository.get(rule.id)
|
||||
assert current_asset is not None
|
||||
live_storage_key = str((current_asset.config_json or {})["rule_document"]["storage_key"])
|
||||
assert live_storage_key.startswith(f"rules/{FINANCE_RULES_LIBRARY}/")
|
||||
assert "agent_assets" not in live_storage_key
|
||||
live_path = service.spreadsheet_manager.resolve_storage_path(live_storage_key)
|
||||
assert not service.spreadsheet_manager.asset_root.exists()
|
||||
original_live_bytes = live_path.read_bytes()
|
||||
try:
|
||||
live_path.write_bytes(build_workbook_bytes([["城市", "住宿"], ["北京", 999]]))
|
||||
@@ -322,6 +385,9 @@ def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_co
|
||||
)
|
||||
|
||||
assert snapshot_path != live_path
|
||||
assert FINANCE_RULES_LIBRARY in snapshot_path.parts
|
||||
assert ".versions" in snapshot_path.parts
|
||||
assert "agent_assets" not in snapshot_path.parts
|
||||
workbook = load_workbook(snapshot_path, data_only=False)
|
||||
assert workbook.active["B2"].value == 500
|
||||
finally:
|
||||
@@ -366,6 +432,55 @@ def test_spreadsheet_change_records_return_recent_edit_details() -> None:
|
||||
assert records[0].cell_changes[0].cell == "B2"
|
||||
|
||||
|
||||
def test_spreadsheet_change_records_include_all_modified_sheets() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
rule = next(
|
||||
item
|
||||
for item in service.list_assets(asset_type=AgentAssetType.RULE.value)
|
||||
if item.code == "rule.expense.company_travel_expense_reimbursement"
|
||||
)
|
||||
|
||||
service.upload_rule_spreadsheet(
|
||||
rule.id,
|
||||
filename="公司差旅费报销规则.xlsx",
|
||||
content=build_multi_sheet_workbook_bytes(
|
||||
{
|
||||
"差旅标准": [["城市", "住宿"], ["北京", 500]],
|
||||
"填表说明": [["字段", "说明"], ["住宿", "按城市标准"]],
|
||||
}
|
||||
),
|
||||
actor="finance_user",
|
||||
)
|
||||
detail = service.get_asset(rule.id)
|
||||
assert detail is not None
|
||||
first_version = detail.working_version
|
||||
|
||||
service.upload_rule_spreadsheet(
|
||||
rule.id,
|
||||
filename="公司差旅费报销规则.xlsx",
|
||||
content=build_multi_sheet_workbook_bytes(
|
||||
{
|
||||
"差旅标准": [["城市", "住宿"], ["北京", 550]],
|
||||
"填表说明": [["字段", "说明"], ["住宿", "按城市等级标准"]],
|
||||
}
|
||||
),
|
||||
actor="finance_user",
|
||||
)
|
||||
|
||||
records = service.list_spreadsheet_change_records(rule.id)
|
||||
latest = records[0]
|
||||
changed_sheets = {item.sheet_name for item in latest.sheet_changes}
|
||||
changed_cell_sheets = {item.sheet_name for item in latest.cell_changes}
|
||||
|
||||
assert latest.version != first_version
|
||||
assert latest.changed_sheet_count == 2
|
||||
assert {"差旅标准", "填表说明"}.issubset(changed_sheets)
|
||||
assert {"差旅标准", "填表说明"}.issubset(changed_cell_sheets)
|
||||
assert "差旅标准" in latest.summary
|
||||
assert "填表说明" in latest.summary
|
||||
|
||||
|
||||
def test_editable_spreadsheet_onlyoffice_config_enables_forcesave(monkeypatch) -> None:
|
||||
with build_session() as db:
|
||||
monkeypatch.setattr(
|
||||
|
||||
Reference in New Issue
Block a user