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): 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): def parse_description(text):
@@ -300,7 +317,10 @@ async def run_check(runner, job_id, payload):
patch = {} patch = {}
for section, item in current["sections"].items(): for section, item in current["sections"].items():
before, target = expected["sections"][section], payload["descriptions"][section] 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") raise WqError("平台表达式或 Description 已变化,未覆盖;请刷新后重新核对", "conflict")
if item["description"] != target: if item["description"] != target:
patch[section] = {"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.alphas import upsert_alpha
from app.models import Account, AISettings, Alpha, Job, Research, SelfCorrelation from app.models import Account, AISettings, Alpha, Job, Research, SelfCorrelation
from app.security import cipher 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 app.worldquant import WqClient
from tests.conftest import alpha from tests.conftest import alpha
@@ -28,6 +28,26 @@ ACCRUAL_FIELDS = {
CHECKS = [{"name": "PROD_CORRELATION", "result": "FAIL", "value": 0.8, "limit": 0.7}] 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: class Platform:
def __init__(self, raw): def __init__(self, raw):
self.raw = copy.deepcopy(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 == [] 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"]) @pytest.mark.parametrize("change", ["description", "code", "settings", "status"])
async def test_remote_conflict_never_overwrites(app, logged_in, change): async def test_remote_conflict_never_overwrites(app, logged_in, change):
platform = await setup(app) platform = await setup(app)