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