Files
AgentLab/backend/tests/test_whatif_selection.py
T
ThearchyHelios bfc0e88e9a 数字推演复核修正(第 2 轮,后端):整个指标推演不了的不改动也照实说,完整值找不回来的数组排除行后不再报「无变化」
- 用到了遮罩列、最外层输入还原不了(容器的完整值找不回来、所在的查询快照读不出来)、卡上没记全输入的指标,以前不改动时
  报 unchanged(界面写「无变化」),输入照样可改、行照样可勾,改了、勾了才说推演不了。现在不论有没有改动都交回
  unsupported / masked 和原因,所有输入 editable 为假、不给 row_span,unsupported 里另有一条 input 为空的(界面据此只写
  原因),之后只列卡住它的输入。同一格既遮着、快照又读不出来时说遮罩
- 下游口径卡改看重算新给的 maybe_changed(改动碰到了某个指标,它又变了或说不准):推演不了的指标 changed 一律为空,
  只看 changed 分不出「什么都没改」,一打开就会把下游标成可能受影响
- agent 数组字段的完整值找不回来(节点产出不在、被改过,另存的完整值读不出来)时,排除它行段里的行以前因为值为空被忽略,
  指标报「无变化」、报告引用处报「不受影响」。现在算碰到了它(excluded_rows 照记,输入 changed 为真、new 为空),
  完整值找不回来的输入一律不给行段
- 决定取哪一格的容器(cell(nodes.q, len(vars.res.rows) - 1, …) 里的 vars.res.rows)少了行时,以前拿卡上记的那一格算出
  「无变化」,现在说不准(unsupported)
- 补测试:四类整个推演不了的指标不改动、改动后都照实说(单元和真跑);数组完整值找不回来时排除行段内一行(单元和真跑,
  含报告引用处);查询当时记下、跑完又从数据源去掉的遮罩(推演和勾行接口);写输入的节点和快照在同一条事件里交回
  (合并查询的边界);给了新值的输入不再说不准;数组只认行段内被排除的行;同一份快照交回两次按第一次;读得出来却不是
  查询结果的快照;嵌在里面、读不出来的那一格不进代入式。逐个做过变异:复核列出的四处、第 1 轮漏掉的三处和这次新加的
  判断改掉后都变红
2026-10-05 06:49:33 +08:00

91 lines
4.4 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.
"""数字推演取哪一份:同一张口径卡执行过几次时推演哪一次、那次执行之前各节点的产出和跑完的时刻、快照什么时候交回。
照 api/evidence.py 的 _Sealed 手写事件(虚构的工件 id),不起运行、不碰库:真跑的循环图里报告总是引用最后一次执行,
节点也常常不会在口径卡之后再跑,这几条边界只有手写的事件才拿得到。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import pytest
from app.api.coded import CodedHTTPException
from app.api.evidence import _Sealed
from app.api.whatif import _card_of, _finished_before, _outputs_before, _returned
FILL1, FILL2, A1, A2, OUT1, OUT2, Q1 = ("f1" * 32, "f2" * 32, "a1" * 32, "a2" * 32, "c1" * 32, "c2" * 32, "d1" * 32)
@dataclass
class Report:
node_id: str
doc: dict[str, Any] | None = field(default_factory=dict)
def sealed(events: list[tuple[int, str, str | None, dict[str, Any]]]) -> _Sealed:
return _Sealed(run_id="r1", graph={"nodes": [{"id": "card", "type": "metrics"}]},
seal={"sealed": True, "ok": True, "manifest_seq": 99, "legacy": False}, events=events)
#: 循环两轮:fill → card,fill → card。第一轮的卡读到 FILL1,第二轮读到 FILL2
LOOP = [
(1, "node.started", "fill", {}),
(2, "tool.end", "fill", {"query_artifact": Q1}),
(3, "node.finished", "fill", {"artifact": FILL1}),
(4, "node.started", "card", {}),
(5, "node.finished", "card", {"artifact": OUT1, "evidence": [{"kind": "metric_set", "artifact": A1}]}),
(6, "node.started", "fill", {}),
(7, "node.finished", "fill", {"artifact": FILL2, "evidence": [{"kind": "query", "artifact": Q1}]}),
(8, "node.started", "card", {}),
(9, "node.finished", "card", {"artifact": OUT2, "evidence": [{"kind": "metric_set", "artifact": A2}]}),
]
CARDS = [("card", A1, {"metrics": [{"id": "s", "value": 1}]}), ("card", A2, {"metrics": [{"id": "s", "value": 2}]})]
def cites(*artifacts: str, node: str = "write") -> Report:
return Report(node, {"catalog": {f"m:{i}": {"kind": "metric", "node_id": "card", "artifact": a}
for i, a in enumerate(artifacts)}})
def test_the_execution_the_report_cites_is_chosen_over_the_last_one():
artifact, card, started = _card_of(sealed(LOOP), CARDS, "card", [cites(A1)], None)
assert (artifact, card["metrics"][0]["value"], started) == (A1, 1, 4)
def test_without_a_citation_the_last_execution_is_used():
assert _card_of(sealed(LOOP), CARDS, "card", [cites()], None)[0] == A2
assert _card_of(sealed(LOOP), CARDS, "card", [], None)[0] == A2
def test_a_chosen_report_decides_which_execution_is_used():
"""两份报告各引用一次执行:指定了 report 就按那一份,不指定按任意一份引用的(取后一次)。"""
reports = [cites(A1, node="weekly"), cites(A2, node="daily")]
assert _card_of(sealed(LOOP), CARDS, "card", reports, "weekly")[0] == A1
assert _card_of(sealed(LOOP), CARDS, "card", reports, "daily")[0] == A2
assert _card_of(sealed(LOOP), CARDS, "card", reports, None)[0] == A2
def test_a_card_whose_content_does_not_verify_is_refused():
with pytest.raises(CodedHTTPException) as caught:
_card_of(sealed(LOOP), CARDS[1:], "card", [cites(A1)], None)
assert caught.value.status_code == 409
def test_node_outputs_and_finish_times_stop_at_the_chosen_execution():
"""第一轮的卡(第 4 条开始)读到的是 FILL1:第二轮 fill 的产出在它之后,不算。"""
events = sealed(LOOP)
assert _outputs_before(events, 4) == {"fill": FILL1}
assert _finished_before(events, 4) == {"fill": 3}
assert _outputs_before(events, 8) == {"fill": FILL2, "card": OUT1}
assert _finished_before(events, 8) == {"fill": 7, "card": 5}
def test_a_snapshot_counts_as_returned_the_first_time_it_shows_up():
assert _returned(sealed(LOOP)) == {Q1: 2}
merged_only = [(3, "node.finished", "merged", {"artifact": OUT1, "evidence": [{"kind": "query", "artifact": Q1}]})]
assert _returned(sealed(merged_only)) == {Q1: 3}
# 同一条查询又跑了一次(结果一样,工件 id 也一样):仍按第一次交回算,第一次之后算出来的值都可能受影响
again = [(2, "tool.end", "fill", {"query_artifact": Q1}), (6, "tool.end", "fill", {"query_artifact": Q1})]
assert _returned(sealed(again)) == {Q1: 2}