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