fix: 避免空 Description 模板触发误报冲突
Deploy production / deploy (push) Successful in 49s

This commit is contained in:
yuxuanhui
2026-09-10 16:36:35 +08:00
parent 49bf8de9c8
commit f25161d624
2 changed files with 77 additions and 3 deletions
+22 -2
View File
@@ -72,8 +72,25 @@ def source(raw):
}
def comparable_description(text):
"""Canonicalize empty platform placeholders only; preserve all substantive text.
This comparison key must never replace the reviewed text sent to the platform.
Unknown or partially filled templates remain exact to protect concurrent edits.
"""
empty_template = r"\s*" + r"\s*".join(re.escape(heading.strip()) for heading in HEADINGS) + r"\s*"
return "" if not text.strip() or re.fullmatch(empty_template, text) else text
def fingerprint(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
comparable = {
**value,
"sections": {
key: {**item, "description": comparable_description(item["description"])}
for key, item in value["sections"].items()
},
}
return hashlib.sha256(json.dumps(comparable, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
def parse_description(text):
@@ -300,7 +317,10 @@ async def run_check(runner, job_id, payload):
patch = {}
for section, item in current["sections"].items():
before, target = expected["sections"][section], payload["descriptions"][section]
if item["code"] != before["code"] or item["description"] not in (before["description"], target):
if item["code"] != before["code"] or comparable_description(item["description"]) not in (
comparable_description(before["description"]),
comparable_description(target),
):
raise WqError("平台表达式或 Description 已变化,未覆盖;请刷新后重新核对", "conflict")
if item["description"] != target:
patch[section] = {"description": target}
+55 -1
View File
@@ -10,7 +10,7 @@ from pydantic_ai.models.function import FunctionModel
from app.alphas import upsert_alpha
from app.models import Account, AISettings, Alpha, Job, Research, SelfCorrelation
from app.security import cipher
from app.submission import HEADINGS, Description
from app.submission import HEADINGS, Description, comparable_description, fingerprint, source
from app.worldquant import WqClient
from tests.conftest import alpha
@@ -28,6 +28,26 @@ ACCRUAL_FIELDS = {
CHECKS = [{"name": "PROD_CORRELATION", "result": "FAIL", "value": 0.8, "limit": 0.7}]
@pytest.mark.parametrize(
"text",
[
"plain text",
"Idea: actual idea\nRationale for data used:\nRationale for operators used:",
"Idea:\nRationale for data used: actual data\nRationale for operators used:",
"Idea:\nRationale for data used:\nRationale for operators used: actual operator",
"Idea:\nRationale for data used:",
"Rationale for data used:\nIdea:\nRationale for operators used:",
],
)
def test_substantive_or_unknown_descriptions_remain_exact(text):
assert comparable_description(text) == text
original = source(alpha(regular={"code": "rank(close)", "description": text}))
changed = copy.deepcopy(original)
changed["sections"]["regular"]["description"] = text + " "
assert fingerprint(original) != fingerprint(changed)
assert original["sections"]["regular"]["description"] == text
class Platform:
def __init__(self, raw):
self.raw = copy.deepcopy(raw)
@@ -141,6 +161,40 @@ async def test_local_correlation_blocks_upstream(app, logged_in, status, stale):
assert platform.calls == []
@pytest.mark.parametrize("kind", ["REGULAR", "SUPER"])
@pytest.mark.parametrize("refresh", [False, True])
@pytest.mark.parametrize(
"before,after",
[
("", "Idea: \nRationale for data used: \nRationale for operators used:"),
("Idea:\r\n Rationale for data used:\t Rationale for operators used: ", ""),
(None, " \r\n\t\u00a0"),
],
)
async def test_empty_description_representations_allow_check(app, logged_in, kind, refresh, before, after):
sections = ["regular"] if kind == "REGULAR" else ["selection", "combo"]
raw = alpha(type=kind, **{key: {"code": "rank(close)", "description": before} for key in sections})
platform = await setup(app, raw)
state = (await logged_in.get("/api/v1/alphas/alpha1/submission")).json()
for key in sections:
platform.raw[key]["description"] = after
if refresh:
async with app.state.sessions.begin() as db:
await upsert_alpha(db, platform.raw)
target = Description(**FIELDS).text()
response = await logged_in.post(
"/api/v1/alphas/alpha1/submission-check",
json={"snapshot": state["snapshot"], "descriptions": dict.fromkeys(sections, target)},
)
assert response.status_code == 202, response.text
await app.state.runner.execute(response.json()["id"])
async with app.state.sessions() as db:
job = await db.get(Job, response.json()["id"])
assert job.status == "completed", job.error
assert platform.patches == [{key: {"description": target} for key in sections}]
assert ("GET", "/alphas/alpha1/check") in platform.calls
@pytest.mark.parametrize("change", ["description", "code", "settings", "status"])
async def test_remote_conflict_never_overwrites(app, logged_in, change):
platform = await setup(app)