mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:51:58 +00:00
bfc0e88e9a
- 用到了遮罩列、最外层输入还原不了(容器的完整值找不回来、所在的查询快照读不出来)、卡上没记全输入的指标,以前不改动时 报 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 轮漏掉的三处和这次新加的 判断改掉后都变红
91 lines
4.4 KiB
Python
91 lines
4.4 KiB
Python
"""数字推演取哪一份:同一张口径卡执行过几次时推演哪一次、那次执行之前各节点的产出和跑完的时刻、快照什么时候交回。
|
||
|
||
照 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}
|