Files
AgentLab/backend/tests/test_autofix_governance.py
T
ThearchyHelios 03c18c4d78 界面文案整改:口语改书面、术语统一、不再露出实现细节;修掉几处说错的提示;新增文案检查防回潮
一、文案(前端约 1600 处,后端传到界面的约 880 处)
- 术语统一:工作流层面用「运行」(继续运行、重新运行、试运行),节点和工具层面用「执行」,不再用「跑」;
  总时长 / 执行时长 / 等待审批;服务端;助手;Agent;token;工作流;固定版本;模型接入;系统提示;批准 / 驳回;受管级别
- 口语改书面:取不到、没取回来、读不懂、连不上、多半、照样、兜底、揉成这类说法改成「无法 / 未能 / 失败 / 请」;去掉拟人写法
- 不再露出实现细节:界面上不写 checkpoint、同一拍、哈希(凭证处除外),不写改版历史(「以前…」「老数据」);
  报错、校验、门禁、自动修复里裸写的配置键名和枚举值换成界面上的中文标签,由新的 backend/app/engine/labels.py 统一提供;
  发布弹窗里结论句的选项不再是 off / require_citation
- 同一件事只留一种说法:生成阶段文案、出具档位说明、画布锁定提示各合成一份
- 去掉界面上的工业场景示例(MES、ERP、「张工」「王工」)和真实产品名,换成通用示例

二、说错的提示(都有测试证明)
- 数据库测连接失败时的专用提示以前从不出现(按报错文字全等比较,永远比不上):改为按错误类型判断,不再拼出重复的前缀
- 「设置 → 供应商」改指「设置 → 模型接入」;「在设置里建数据源」改指「数据」页
- 缺少 API Key、模型接入已停用的报错不再夹英文,写明去哪里填
- 两处原因说错(「这种引用在后续版本支持」「这一版还不支持」)改成真实原因
- 节点说明里不准确的说法改正:代码节点在 microVM 或系统沙箱中执行;报告撰写、口径卡的说明不再说过头
- 导出的证据 CSV 里两列同名,第三列改为「片段编号」

三、前后端对齐
- 前端按后端原文识别日志、报错的地方,新旧两种写法都认得(历史运行里存的是旧文字),逐条补了新旧原文的核对
- 后端交回模型的原指令放进单独的 for_model 字段,前端在解析响应时去掉,不会显示到界面上

四、检查
- 检查脚本里按原文匹配的断言改成新文案;改完会变成永远成立的反向断言逐条修正,并做了变异验证
- 新增 check-copy(静态扫描前端界面字符串和后端上界面的文案):再出现「跑」「后端」「多半」「令牌」、
  界面上的 Copilot、裸写的配置键名、半角标点等就报错;确需保留的写进 scripts/copy-allow.json 并注明理由
2026-09-30 08:05:15 +08:00

497 lines
24 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.
"""证据门禁 G1–G5 接进发布前自动修复(engine/autofix.py)。
照发布前自动修复的原则:答案唯一、只提高要求的一键修(auto);要人拿主意的列候选、不替人选(choice);
结构性的交给 Copilot(assist)。修完重跑 validate 和门禁,错误必须变少、不能冒出新的;降低要求的改动
(numbers、on_violation、claims 往宽里改,关掉 cite_fields,替人把沙箱代码标成取数)一律拒绝。
"""
from __future__ import annotations
import copy
import json
import pytest
from httpx import ASGITransport, AsyncClient
from langchain_core.messages import AIMessageChunk
from app.engine.autofix import HANDOFF, apply_fixes, forbidden_changes, plan_fixes
from app.engine.governance import publish_issues
from app.engine.schema import GraphSpec
from app.main import app
STRICT = {"numbers": "strict", "on_violation": "fail", "claims": "require_citation"}
SCHEMA = {"type": "object", "properties": {"gmv": {"type": "number"}}}
def node(nid, ntype, label=None, **config):
return {"id": nid, "type": ntype, "position": {"x": 10, "y": 20}, "width": 240,
"data": {"label": label or nid, "config": config}}
def traceable(*, report=None, **contract_overrides) -> dict:
"""input → 查库 → 口径卡 → 报告撰写 → 出具:受管要求(含 G1–G5)全部满足。"""
contract = {"report_from": "write", "metrics_from": ["card"], "required": ["gmv"], "strict": True}
contract.update(contract_overrides)
nodes = [
node("start", "input", "周期"),
node("fetch", "tool", "取数", tool="db_query__shop",
args={"sql": "SELECT SUM(amount) AS gmv, COUNT(*) AS orders FROM orders"}),
node("card", "metrics", "周报口径卡", caliber="周报口径", metrics=[
{"id": "gmv", "name": "销售额", "unit": "元", "expression": "cell(nodes.fetch, 0, 'gmv')"}]),
node("write", "report", "报告撰写", instructions="写周报", **(STRICT if report is None else report)),
node("done", "output", "出具", fields=[{"name": "周报", "value": "{{ nodes.write.text }}"},
{"name": "指标清单", "value": "{{ nodes.card.text }}"}],
contract={k: v for k, v in contract.items() if v is not None}),
]
edges = [{"id": f"e{i}", "source": a["id"], "target": b["id"]} for i, (a, b) in enumerate(zip(nodes, nodes[1:]))]
return {"nodes": nodes, "edges": edges, "viewport": {"x": 1, "y": 2, "zoom": 1}}
def find(graph: dict, nid: str) -> dict:
return next(n for n in graph["nodes"] if n["id"] == nid)
def config(graph: dict, nid: str) -> dict:
return find(graph, nid)["data"]["config"]
def insert(graph: dict, new: dict, *, before: str, after: str) -> dict:
graph["nodes"].insert(len(graph["nodes"]) - 1, new)
graph["edges"] += [{"source": after, "target": new["id"]}, {"source": new["id"], "target": before}]
return graph
def plan(graph: dict, level: str = "governed") -> dict[str, dict]:
spec = GraphSpec.model_validate(graph)
return {f["id"]: f for f in plan_fixes(spec, publish_issues(spec, level=level), level=level)}
def errors(graph: dict, level: str = "governed") -> list:
return [i for i in publish_issues(GraphSpec.model_validate(graph), level=level) if i.level == "error"]
def codes(issues) -> set[str]:
return {i["code"] if isinstance(i, dict) else i.code for i in issues}
def test_the_baseline_needs_no_fix():
assert errors(traceable()) == [] and plan(traceable()) == {}
# --------------------------------------------------------------------------
# G3:一键写明 numbers / on_violation / claims
# --------------------------------------------------------------------------
def test_report_policy_is_filled_in_with_one_auto_fix():
graph = traceable(report={"numbers": "off"})
fid = "governed.report_policy:write"
fix = plan(graph)[fid]
assert fix["kind"] == "auto" and fix["node_id"] == "write"
assert fix["preview"]["after"] == STRICT and fix["preview"]["before"] == {
"numbers": "off", "on_violation": None, "claims": None}
out = apply_fixes(graph, [fid], level="governed")
assert out["applied"] == [fid] and out["rejected"] == [] and out["ok"] is True
assert {k: config(out["graph"], "write")[k] for k in STRICT} == STRICT
assert config(out["graph"], "write")["instructions"] == "写周报" # 别的配置原样留着
assert [(c["field"], c["before"], c["after"]) for c in out["changes"]] == [
("numbers", "off", "strict"), ("on_violation", None, "fail"), ("claims", None, "require_citation")]
assert out["ops"] == [{"op": "update_node", "id": "write", "config": STRICT}]
# 坐标、视口这些前端的东西原样留着
assert find(out["graph"], "write")["position"] == {"x": 10, "y": 20} and out["graph"]["viewport"]["zoom"] == 1
def test_only_the_missing_settings_are_written():
graph = traceable(report={"numbers": "strict", "claims": "require_citation"})
fix = plan(graph)["governed.report_policy:write"]
assert fix["preview"] == {"field": "on_violation", "before": None, "after": "fail"}
out = apply_fixes(graph, [fix["id"]], level="governed")
assert [c["field"] for c in out["changes"]] == ["on_violation"] and out["ok"] is True
def test_published_level_warnings_get_no_fix():
graph = traceable(report={})
assert plan(graph, "published") == {}
# --------------------------------------------------------------------------
# validate:claims 写了不支持的值
# --------------------------------------------------------------------------
def test_an_invalid_claims_value_becomes_require_citation_under_governed():
graph = traceable(report={**STRICT, "claims": "sometimes"})
fid = "report.claims_invalid:write"
fix = plan(graph)[fid]
assert fix["kind"] == "auto" and fix["preview"] == {"field": "claims", "before": "sometimes",
"after": "require_citation"}
out = apply_fixes(graph, [fid], level="governed")
assert out["ok"] is True and config(out["graph"], "write")["claims"] == "require_citation"
def test_an_invalid_claims_value_is_a_choice_under_published():
graph = traceable(report={"claims": "sometimes"})
fid = "report.claims_invalid:write"
fix = plan(graph, "published")[fid]
assert fix["kind"] == "choice" and [o["value"] for o in fix["options"]] == ["off", "require_citation"]
assert apply_fixes(graph, [fid], level="published")["applied"] == []
out = apply_fixes(graph, [fid], {fid: "off"}, level="published")
assert out["applied"] == [fid] and config(out["graph"], "write")["claims"] == "off" and out["ok"] is True
# 乱写的值没有「最接近的」,不给建议
assert "default" not in plan(traceable(report={"claims": "strict"}), "published")[fid]
# --------------------------------------------------------------------------
# G5:给口径卡供数的 agent
# --------------------------------------------------------------------------
def feeding_agent(**config_) -> dict:
graph = insert(traceable(), node("ask", "agent", "查数员", tools=["db_query__shop"], prompt="查 orders",
assign_to="facts", **config_), before="card", after="start")
config(graph, "card")["metrics"].append({"id": "gmv2", "expression": "vars.facts.gmv"})
return graph
def test_cite_fields_is_switched_on_when_the_schema_exists():
graph = feeding_agent(output_schema=SCHEMA)
fid = "governed.caliber_agent_cite_fields:ask"
fix = plan(graph)[fid]
assert fix["kind"] == "auto" and fix["preview"] == {"field": "cite_fields", "before": None, "after": True}
out = apply_fixes(graph, [fid], level="governed")
assert out["ok"] is True and config(out["graph"], "ask")["cite_fields"] is True
assert config(out["graph"], "ask")["output_schema"] == SCHEMA
def test_a_missing_schema_is_not_invented():
graph = feeding_agent()
fid = "governed.caliber_agent_schema:ask"
fix = plan(graph)[fid]
assert fix["kind"] == "assist" and "「结构化输出 Schema」" in fix["label"]
out = apply_fixes(graph, [fid], level="governed")
assert out["applied"] == [] and out["rejected"][0]["fix_id"] == fid
assert "output_schema" not in config(out["graph"], "ask") and out["graph"] == graph
def test_a_model_call_feeding_a_card_goes_to_copilot():
graph = insert(traceable(), node("guess", "llm", "估算", prompt="估一个数", assign_to="g"), before="card",
after="start")
config(graph, "card")["metrics"].append({"id": "g", "expression": "vars.g"})
assert plan(graph)["governed.caliber_model_input:guess"]["kind"] == "assist"
# --------------------------------------------------------------------------
# G4:沙箱代码喂口径卡——它到底是在取数还是在算,要人说
# --------------------------------------------------------------------------
def compute_feeding() -> dict:
graph = insert(traceable(), node("calc", "code", "计算", code="print(1)", assign_to="calc"), before="card",
after="fetch")
config(graph, "card")["metrics"].append({"id": "ratio", "expression": "vars.calc.ratio"})
return graph
def test_compute_code_is_a_choice_not_an_auto_fix():
graph = compute_feeding()
fid = "governed.caliber_compute_input:calc"
fix = plan(graph)[fid]
assert fix["kind"] == "choice" and fix["node_id"] == "calc" and fix["field"] == "evidence_role"
assert [o["value"] for o in fix["options"]] == ["source", HANDOFF]
assert "default" not in fix and "preview" not in fix
# 不选就不动
out = apply_fixes(graph, [fid], level="governed")
assert out["applied"] == [] and "evidence_role" not in config(out["graph"], "calc")
assert out["rejected"][0]["fix_id"] == fid
def test_choosing_source_marks_the_node():
graph = compute_feeding()
fid = "governed.caliber_compute_input:calc"
out = apply_fixes(graph, [fid], {fid: "source"}, level="governed")
assert out["applied"] == [fid] and out["ok"] is True
assert config(out["graph"], "calc")["evidence_role"] == "source"
def test_choosing_copilot_hands_the_node_over_without_touching_it():
graph = compute_feeding()
fid = "governed.caliber_compute_input:calc"
out = apply_fixes(graph, [fid], {fid: HANDOFF}, level="governed")
assert out["applied"] == [] and out["rejected"] == [] and out["handoff"] == [fid]
assert out["graph"] == graph and out["ok"] is False
def test_copilot_may_not_mark_a_code_node_as_source_on_its_own():
before = compute_feeding()
after = copy.deepcopy(before)
config(after, "calc")["evidence_role"] = "source"
[reason] = forbidden_changes(before, after)
assert "「计算」" in reason and "「取数」" in reason
# 新加的沙箱代码直接标成取数也一样
after = copy.deepcopy(before)
after["nodes"].append(node("raw", "code", "取原始数据", code="print(2)", evidence_role="source"))
assert any("「取原始数据」" in r for r in forbidden_changes(before, after))
# --------------------------------------------------------------------------
# G1:契约和成果字段接到报告撰写节点
# --------------------------------------------------------------------------
def narrative() -> dict:
return traceable(report_from=None, narrative="{{ nodes.write.text }}")
def test_report_from_takes_the_only_report_upstream():
graph = narrative()
fid = "governed.report_from_required:done"
fix = plan(graph)[fid]
assert fix["kind"] == "auto" and fix["preview"] == {"field": "contract.report_from", "before": None,
"after": "write"}
out = apply_fixes(graph, [fid], level="governed")
assert out["ok"] is True and config(out["graph"], "done")["contract"]["report_from"] == "write"
def test_several_reports_make_report_from_a_choice():
graph = insert(narrative(), node("write2", "report", "第二份报告", instructions="写月报", **STRICT),
before="done", after="card")
fid = "governed.report_from_required:done"
fix = plan(graph)[fid]
assert fix["kind"] == "choice" and [o["value"] for o in fix["options"]] == ["write", "write2"]
assert apply_fixes(graph, [fid], level="governed")["applied"] == []
out = apply_fixes(graph, [fid], {fid: "write2"}, level="governed")
assert out["applied"] == [fid] and config(out["graph"], "done")["contract"]["report_from"] == "write2"
def test_without_a_report_node_report_from_goes_to_copilot():
graph = narrative()
write = find(graph, "write")
write.update(type="llm", data={"label": "写周报", "config": {"prompt": "写周报"}})
# 受管级别的叙述模式过不了 G1,不再给「选 narrative」这种修了也发不出去的候选
for fid in ("governed.report_from_required:done",):
assert plan(graph)[fid]["kind"] == "assist"
bare = traceable(report_from=None)
find(bare, "write").update(type="llm", data={"label": "写周报", "config": {"prompt": "写周报"}})
assert plan(bare)["contract.report_from_missing:done"]["kind"] == "assist"
def with_story(graph: dict) -> dict:
graph = insert(graph, node("narrate", "llm", "叙述", prompt="写周报", assign_to="story"), before="done",
after="card")
config(graph, "done")["fields"] += [{"name": "摘要", "value": "{{ vars.story }}"},
{"name": "点评", "value": "点评:{{ nodes.narrate.text }}"}]
return graph
def test_exit_fields_are_repointed_at_the_chosen_report():
graph = with_story(traceable())
fid = "governed.exit_text_source:done"
fix = plan(graph)[fid]
assert fix["kind"] == "choice" and fix["field"] == "fields"
assert [o["value"] for o in fix["options"]] == ["write"] and fix["default"] == "write"
assert "「摘要」" in fix["label"] and "「点评」" in fix["label"]
# 就一个候选也不自动改:字段里放什么是作者的事
out = apply_fixes(graph, [fid], level="governed")
assert out["applied"] == [] and out["graph"] == graph
out = apply_fixes(graph, [fid], {fid: "write"}, level="governed")
assert out["applied"] == [fid] and out["ok"] is True
fields = config(out["graph"], "done")["fields"]
assert fields == [{"name": "周报", "value": "{{ nodes.write.text }}"},
{"name": "指标清单", "value": "{{ nodes.card.text }}"},
{"name": "摘要", "value": "{{ nodes.write.text }}"},
{"name": "点评", "value": "{{ nodes.write.text }}"}]
assert [(c["field"], c["before"], c["after"]) for c in out["changes"]] == [
("fields[2].value", "{{ vars.story }}", "{{ nodes.write.text }}"),
("fields[3].value", "点评:{{ nodes.narrate.text }}", "{{ nodes.write.text }}")]
assert out["ops"] == [{"op": "update_node", "id": "done", "config": {"fields": fields}}]
def test_an_exit_without_fields_gets_one_that_takes_the_report():
graph = traceable()
config(graph, "done")["fields"] = []
graph = insert(graph, node("polish", "llm", "润色", prompt="{{ nodes.write.text }}"), before="done",
after="write")
graph["edges"] = [e for e in graph["edges"] if (e["source"], e["target"]) != ("write", "done")]
fid = "governed.exit_text_source:done"
out = apply_fixes(graph, [fid], {fid: "write"}, level="governed")
assert out["applied"] == [fid] and out["ok"] is True
assert config(out["graph"], "done")["fields"] == [{"name": "result", "value": "{{ nodes.write.text }}"}]
def test_exit_fields_without_any_report_go_to_copilot():
graph = with_story(traceable())
find(graph, "write").update(type="llm", data={"label": "写周报", "config": {"prompt": "写周报"}})
config(graph, "done")["contract"].pop("report_from")
assert plan(graph)["governed.exit_text_source:done"]["kind"] == "assist"
# --------------------------------------------------------------------------
# G2:结构性的,交给 Copilot
# --------------------------------------------------------------------------
def test_text_bypass_is_an_assist():
graph = traceable()
graph["nodes"] += [node("chat", "llm", "闲聊", prompt="你好"),
node("side", "output", "附带成果", fields=[{"name": "回答", "value": "{{ nodes.chat.text }}"}])]
graph["edges"] += [{"source": "start", "target": "chat"}, {"source": "chat", "target": "side"}]
fid = "governed.text_bypass:chat"
fix = plan(graph)[fid]
assert fix["kind"] == "assist" and "报告撰写" in fix["label"]
out = apply_fixes(graph, [fid], level="governed")
assert out["applied"] == [] and out["rejected"][0]["fix_id"] == fid
# --------------------------------------------------------------------------
# 修完更好、不降低要求
# --------------------------------------------------------------------------
def test_all_auto_fixes_together_leave_only_the_decisions():
graph = with_story(feeding_agent(output_schema=SCHEMA))
graph = insert(graph, node("calc", "code", "计算", code="print(1)", assign_to="calc"), before="card",
after="fetch")
config(graph, "card")["metrics"].append({"id": "ratio", "expression": "vars.calc.ratio"})
config(graph, "write").clear()
config(graph, "done")["contract"].update(report_from=None, narrative="{{ nodes.write.text }}")
fixes = plan(graph)
autos = [f["id"] for f in fixes.values() if f["kind"] == "auto"]
assert set(autos) == {"governed.report_policy:write", "governed.caliber_agent_cite_fields:ask",
"governed.report_from_required:done"}
before = len(errors(graph))
out = apply_fixes(graph, autos, level="governed")
assert sorted(out["applied"]) == sorted(autos) and out["rejected"] == []
assert len([i for i in out["remaining"] if i["level"] == "error"]) < before
left = {i["code"] for i in out["remaining"] if i["level"] == "error"}
assert left == {"governed.exit_text_source", "governed.caliber_compute_input"}
# 剩下的都要人拿主意,没有一条被替人应用
assert all(fixes[f]["kind"] != "auto" for f in fixes if f.split(":")[0] in left)
@pytest.mark.parametrize("key, before, after", [
("numbers", "strict", "off"), ("on_violation", "fail", "flag"), ("on_violation", "fail", None),
("claims", "require_citation", "off"), ("claims", "require_citation", None),
])
def test_loosening_the_report_policy_is_refused(key, before, after):
old = traceable(report={**STRICT, key: before})
new = copy.deepcopy(old)
if after is None:
config(new, "write").pop(key)
else:
config(new, "write")[key] = after
from app.engine.labels import field_label
[reason] = forbidden_changes(old, new)
assert "「报告撰写」" in reason and f"「{field_label(key)}」" in reason
def test_turning_cite_fields_off_is_refused():
old = feeding_agent(output_schema=SCHEMA, cite_fields=True)
new = copy.deepcopy(old)
config(new, "ask")["cite_fields"] = False
[reason] = forbidden_changes(old, new)
assert "「查数员」" in reason and "「按出处核对字段」" in reason
def test_tightening_is_not_refused():
old = traceable(report={"numbers": "off", "on_violation": "flag", "claims": "off"})
new = copy.deepcopy(old)
config(new, "write").update(STRICT)
assert forbidden_changes(old, new) == []
# --------------------------------------------------------------------------
# 接口:发布前检查带上 G 规则的修复;选了「交给 Copilot」就真的交给 Copilot
# --------------------------------------------------------------------------
@pytest.fixture
async def client():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
class Scripted:
"""按剧本吐操作流的 Copilot 模型;记下每次收到的请求。"""
def __init__(self, ops: list[dict]) -> None:
self.lines = [json.dumps(o, ensure_ascii=False) for o in ops]
self.calls: list[list] = []
async def astream(self, messages):
self.calls.append(messages)
for line in self.lines:
yield AIMessageChunk(content=line + "\n")
def script(monkeypatch, ops: list[dict]) -> Scripted:
import app.api.copilot as copilot
model = Scripted([{"op": "plan", "summary": "改图"}, *ops, {"op": "done", "explanation": "改好了"}])
async def _model(*_a, **_k):
return model, "mock-fast"
monkeypatch.setattr(copilot, "get_chat_model", _model)
return model
async def create(client, graph: dict) -> str:
return (await client.post("/api/workflows", json={"name": "证据门禁-修复", "graph": graph})).json()["id"]
async def test_publish_check_lists_the_g_fixes(client):
graph = with_story(compute_feeding())
config(graph, "write").clear()
wf = await create(client, graph)
body = (await client.post(f"/api/workflows/{wf}/publish-check", json={"level": "governed"})).json()
assert body["ok"] is False
kinds = {f["id"]: f["kind"] for f in body["fixes"]}
assert kinds["governed.report_policy:write"] == "auto"
assert kinds["governed.caliber_compute_input:calc"] == "choice"
assert kinds["governed.exit_text_source:done"] == "choice"
assert all(not any(k.startswith("_") for k in f) for f in body["fixes"]) # 内部信息不外露
by_code = {i["code"]: i for i in body["issues"]}
assert by_code["governed.report_policy"]["fix"] == "governed.report_policy:write"
assert by_code["governed.exit_text_source"]["fix"] == "governed.exit_text_source:done"
# 已发布级别:G 规则只是警告,不给修复
body = (await client.post(f"/api/workflows/{wf}/publish-check", json={"level": "published"})).json()
assert {i["level"] for i in body["issues"] if (i["code"] or "").startswith("governed.")} == {"warning"}
assert not [f for f in body["fixes"] if f["code"].startswith("governed.")]
async def test_choosing_copilot_in_the_choice_runs_the_assist(client, monkeypatch):
# Copilot 把口径卡里读沙箱代码的那条指标改成直接读查询结果:计算挪进了口径卡
card = [{"id": "gmv", "name": "销售额", "unit": "元", "expression": "cell(nodes.fetch, 0, 'gmv')"},
{"id": "ratio", "expression": "cell(nodes.fetch, 0, 'gmv') / 100"}]
model = script(monkeypatch, [{"op": "update_node", "id": "card", "config": {"metrics": card}}])
wf = await create(client, compute_feeding())
fid = "governed.caliber_compute_input:calc"
out = (await client.post(f"/api/workflows/{wf}/autofix", json={
"level": "governed", "apply": [fid], "choices": {fid: HANDOFF}})).json()
assert out["handoff"] == [fid] and len(model.calls) == 1
human = next(text for role, text in model.calls[0] if role == "human")
assert "「计算」" in human and "evidence_role" in human
assert out["assist"]["ok"] is True and out["applied"] == ["assist"] and out["ok"] is True, out
assert "evidence_role" not in config(out["graph"], "calc")
# 没选「交给 Copilot」、也没带 assist 的,不调模型
model.calls.clear()
out = (await client.post(f"/api/workflows/{wf}/autofix", json={
"level": "governed", "apply": [fid], "choices": {fid: "source"}})).json()
assert model.calls == [] and out["assist"] is None and out["applied"] == [fid]
@pytest.mark.parametrize("ops, word", [
([{"op": "update_node", "id": "calc", "config": {"evidence_role": "source"}}], "「证据角色」"),
([{"op": "update_node", "id": "write", "config": {"numbers": "off"}},
{"op": "update_node", "id": "calc", "config": {"evidence_role": "source"}}], "「未引用的数字」"),
])
async def test_copilot_cannot_decide_for_the_author_or_loosen_the_report(client, monkeypatch, ops, word):
script(monkeypatch, ops)
wf = await create(client, compute_feeding())
out = (await client.post(f"/api/workflows/{wf}/autofix", json={
"level": "governed", "apply": [], "assist": True})).json()
assert out["assist"]["ok"] is False and out["applied"] == []
[refused] = [r for r in out["rejected"] if r["fix_id"] == "assist"]
assert "降低了要求" in refused["reason"] and word in refused["reason"]
assert out["graph"] == (await client.get(f"/api/workflows/{wf}")).json()["graph"]