This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user