Files
AgentLab/backend/tests/test_positional_masks_elsewhere.py
T
ThearchyHelios 941723b17c 端到端验收修复与指标出具:
- 身份与工具:登录页建第一个管理员(认人接口的 401 说明能不能建);搭建者有只读的人和组名录,选人给候选并核对名字;授权三行带标题;端点请求头名校验(空名、非法名存不进、发不出时说清原因);写入能力在工具页试运行一律演练、不发送,HTTP 状态照实说成败;写记忆不算只读、查询能力只能选同一端点上的只读能力;审批与遮罩的说法和实际规则一致;开登录时不要署名、导入和发布记登录的人;使用者不显示推演等只给搭建者的操作,403 按是否开登录说话;登录页只认一次人
- 流程与实例:图里用到的岗位(派给谁、超时升级给谁、发送确认派给谁)全部派生、没绑人的发布时报错;写外部系统的参数按能力声明的类型收拢后再算哈希和发送,收不成就不发;发起人的实例页只看这一步的状态,处理人才给待办卡;会签进度带登录核实和时间,升级后照实写规则;实例状态写流程的结论(已退回、未确认、等待核实写入结果),核实生效后不再挂失败原因;流程应用首页不派生最近封存运行的结果块,看板没数据时是安静的空态;样例⑩的金额带类型和下限、待办标题带金额,表单上下限前后端一起生效
- 看板与证据:定时任务模板加 ISO 周、上一 ISO 周、本月、上月;出处链穿过原样解析查询结果的整形节点,追不到原始数据的照实说停在哪里,样例⑨改用 cell();应用首页和版本历史都能做成应用;指标块单位不重复;版本列表带退役说明;定时任务页头跟着刷新;删视图要确认、新视图能起名;编辑器属性用中文名、术语带解释
- 指标出具(用户 2026-10-15 拍板):契约只写「指标来自」、上游没有报告撰写节点、成果里没有模型写的话时按指标出具——必需指标齐、指标输入追到原始数据、成果字段里的数回指得上才完整出具;受管级要求每个指标静态地追到原始数据(governed.metric_untraced);出具横幅和应用印章写明按指标
- 受管工作流降为已发布只给管理员(Q8 顺延,服务端同样拦);工作流接口带当前发布版的级别,画布上改过之后也认得出受管工作流
2026-10-15 07:15:37 +08:00

130 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""按位置取自遮罩列的一格(vars.res.rows[1][2]、nodes.fetch.rows[0][0])在推演之外的两处:期 4 I2a 把它接进了
recompute 的还原(_positional),用 recompute 的地方都跟着变,这里把变化钉住。
- 双盲复算的预检(dual_check._plan_metric 什么都不改先重算一遍):以前这种指标照常盲算,遮着的那一格还会作为字面量
写进给盲算模型的计算式;现在预检判它用到了遮罩列,按无法复算记,原因不说「推演」(这里不是推演)。数据整形只是原样
解析查询结果时(模板整串就是 {{ nodes.fetch }}),这一格卡上直接记成单元格取数(engine/lineage.py),和 cell() 取的
遮罩格同一个说法;模板带了过滤器的不算原样解析,走顺着取值链认遮罩的那条路——两种写法都守
- 运行对比(engine/compare.py):这种输入两边的值都不给,标 masked,和 cell() 取的遮罩格一样
库和数字都是编的:号码写成带横线的文本,区域名是「东区」这类。
"""
from __future__ import annotations
import json
import pytest
from app.engine import compare
from app.engine.runner import run_manager
from tests.fixtures.compare_runs import BASE_ROWS, NEXT_ROWS, metric, scalar_in, week
from tests.test_evidence_metric_masks import ( # noqa: F401 - 夹具按名字注入(engine_up 自动生效)
METRICS,
PHONES,
SQL,
client,
engine_up,
node,
set_source,
shop,
)
#: 数据整形的两种写法:原样解析、模板带过滤器(不算原样解析)
PARSED = "{{ nodes.fetch }}"
FILTERED = "{{ nodes.fetch | trim }}"
def dual_graph(template: str = PARSED):
"""取数 → 数据整形(vars.res)→ 口径卡 → 双盲复算(只复算 mix)→ 成果。"""
nodes = [
node("start", "input"),
node("fetch", "tool", tool="db_query__shop", args={"sql": SQL}),
node("parse", "transform", mode="json", template=template, assign_to="res"),
node("card", "metrics", caliber="遮罩口径", caliber_version="v1", metrics=METRICS),
node("dual", "dual_check", metrics=["mix"]),
node("out", "output", fields=[{"name": "结果", "value": "{{ nodes.card.text }}"}]),
]
return {"nodes": nodes, "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]}
async def dual_output(run_id: str) -> dict:
from sqlalchemy import select
from app.core import artifact_store
from app.db.base import SessionLocal
from app.db.models import RunEvent
async with SessionLocal() as session:
[finished] = (await session.execute(select(RunEvent).where(
RunEvent.run_id == run_id, RunEvent.type == "node.finished", RunEvent.node_id == "dual"))).scalars()
return artifact_store.load(finished.data["artifact"])
@pytest.mark.parametrize("template", [PARSED, FILTERED])
async def test_the_dual_check_does_not_blind_compute_a_metric_reading_a_masked_cell_by_position(shop, template):
"""mix = round(cell(nodes.fetch, 1, 'amount') + len(vars.res.rows[1][2]), 1):cell() 那一格不在遮罩列上,按位置取的
号码在。认出它取自遮罩列,按无法复算记缺口,原因点名是哪个输入、不说「推演」。没遮罩时:原样解析的那一格是查询来的
文本,和 cell() 取的文本格一样不能单独复算,照实说不是数值;模板带了过滤器的,预检照常通过、指标进入盲算。"""
import asyncio
from app.db.base import SessionLocal
from app.db.models import Run
async def run(options):
await set_source(shop, options)
run_id = (await run_manager.start(graph=dual_graph(template), input_payload={})).id
for _ in range(400):
await asyncio.sleep(0.05)
async with SessionLocal() as session:
row = await session.get(Run, run_id)
if row.status in ("succeeded", "failed") and row.manifest_seq is not None:
await run_manager.wait_idle(run_id)
assert row.status == "succeeded", row.error
return await dual_output(run_id)
raise AssertionError(f"运行 {run_id} 没跑完:{row.status}")
masked = await run({"mask_columns": ["phone"]})
[mix] = masked["metrics"]
assert mix["metric_id"] == "mix" and mix["status"] == "unchecked"
assert mix["reason"] == "输入 vars.res.rows[1][2] 取自设置了遮罩的列,不能用于复算"
assert "推演" not in json.dumps(masked, ensure_ascii=False)
assert not [p for p in PHONES if p in json.dumps(masked, ensure_ascii=False)]
assert any("遮罩" in gap for gap in masked["issuance_gaps"])
plain = await run({})
[mix] = plain["metrics"]
if template == PARSED:
# 原样解析:号码那一格是查询快照里的文本,和 cell() 取的文本格同一个说法
assert mix["status"] == "unchecked" and mix["reason"] == "输入 vars.res.rows[1][2] 不是数值", mix
else:
# 没遮罩:预检通过,原因不再是遮罩(盲算模型没配,按盲算没交回数记,但已经走到了盲算这一步)
assert "遮罩" not in (mix.get("reason") or "") and "无法按原表达式重算" not in (mix.get("reason") or "")
assert "不是数值" not in (mix.get("reason") or ""), mix
def region_len(fetch, bot, rows, bot_rows):
"""东区名称的字数:按位置取自取数结果的第 0 行第 0 列(region)。"""
return [metric("rlen", "len(nodes.fetch.rows[0][0])", len(rows[0][0]),
[scalar_in("nodes.fetch.rows[0][0]", rows[0][0], "fetch", "tool")], name="首个区域名字数",
unit="字", decimals=0)]
def test_the_compare_page_withholds_a_masked_cell_read_by_position():
"""运行对比:按位置取自遮罩列(region)的一格两边都不给值、标 masked,和 cell() 取的遮罩格一样;没遮罩时照给。"""
def masked(run):
run.masked = {a: frozenset({"region"}) for a in run.queries}
return run
result = compare.compare(masked(week("r1", metrics=region_len)).side(),
masked(week("r2", NEXT_ROWS, metrics=region_len)).side())
[rlen] = [m for m in result["metrics"] if m["metric_id"] == "rlen"]
[inp] = rlen["inputs"]
assert inp["masked"] is True and inp["base"] is None and inp["target"] is None
assert "东区" not in json.dumps(result, ensure_ascii=False)
shown = compare.compare(week("r1", metrics=region_len).side(), week("r2", NEXT_ROWS, metrics=region_len).side())
[rlen] = [m for m in shown["metrics"] if m["metric_id"] == "rlen"]
[inp] = rlen["inputs"]
assert not inp.get("masked") and inp["base"] == BASE_ROWS[0][0] and inp["target"] == NEXT_ROWS[0][0]