Files
AgentLab/backend/tests/fixtures/analysis_ops.py
T
ThearchyHelios 6e38df4fbf 分析卡里随数据变化的调用错误记成算不了的一项,不让卡失败(复核 R6):analysis.run 加参数 refuse_data_errors(缺省假),为真时 DATA_ERRORS 里的调用错误——上一层没做成 parent_refused、下钻的段这期没有 parent_key_missing、期次这期没有 period_not_found(变化拆解、波动基线的本期、差异检验按期比、可比性检查)、总数那一格越界或为空 row_out_of_range / headline_empty、要比的组这期没有 unknown_group / too_few_groups、要用的那一格这期为空 empty_cell(乘积拆解的乘积和因子;差异检验的计数为空原来报的是 not_count,改报 empty_cell)——记成 status refused 的结论:契约 REASONS 加 parent_refused、input_unavailable,CallError 加 reason(照实说是哪一项、哪个值,不带改法),判定语「{叫法}没有做成:原因」;params 照调用原样(label 照样消毒),取过还没记用法的快照都记进输入(Inputs.settle,角色照第一次取时的),上一层在核对它做没做成之前就记进输入,复算按它们走到同一处;阈值和判定标准同各算子自己算不了时那一份(各模块的 USED / refusal_basis,算子本身也改用它)。recompute 一律按真复现;结构性的调用错误照旧抛。分析卡运行器传真,agent 工具不传。顺手:段数不够的原因里维度叫法和汉字之间照 say 的规矩补空格
2026-10-21 15:47:30 +08:00

62 lines
3.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.
"""测试用的分析算子替身(第 1 波各包并行:W1-a 的 app.analysis.run 落地前,入口的测试不等它)。
FakeOps 换掉模块上的 app.analysis.run(入口都按模块取 run),按请求交回 app.analysis.testing.fake_result 的假结论,
并记下每次调用拿到的请求、按名字解析到的输入快照、阈值、运行期依赖、上一层结论——测试据此断言入口交进去的东西。
"""
from __future__ import annotations
from typing import Any
import app.analysis as analysis_ops
from app.analysis.testing import fake_result
#: 每个算子缺省给的判定(fake_result 有现成判定语的那几个)
VERDICT = {"decompose": "driver", "baseline": "outside_band", "trend": "rising", "differ": "no_evidence",
"comparable": "comparable"}
class FakeOps:
"""errors:按调用的先后,每次要抛的异常(None 表示照常交回);用完了都照常交回。statuses / verdicts 同理。"""
def __init__(self, monkeypatch, *, errors: list[Any] | None = None, statuses: list[str] | None = None,
verdicts: list[str] | None = None, peek: list[str] | None = None, **overrides: Any) -> None:
self.calls: list[dict[str, Any]] = []
#: 每次调用时看一眼这几个名字能不能按 parents 取到上一层结论(visible)
self.peek = list(peek or [])
self.errors = list(errors or [])
self.statuses = list(statuses or [])
self.verdicts = list(verdicts or [])
self.overrides = overrides
monkeypatch.setattr(analysis_ops, "run", self.run)
async def run(self, request: dict[str, Any], resolve: Any, *, settings: Any, context: Any,
parents: Any = None, refuse_data_errors: bool = False) -> dict[str, Any]:
params = request.get("params") or {}
names = [params[k] for k in ("query", "base_query", "target_query") if params.get(k)]
snapshots = {name: resolve(name) for name in names}
parent = None
if isinstance(params.get("parent"), dict) and parents is not None:
parent = parents(params["parent"]["of"])
self.calls.append({"request": request, "snapshots": snapshots, "settings": dict(settings), "context": context,
"parent": parent, "parents": parents, "refuse_data_errors": refuse_data_errors,
"visible": {n: parents is not None and parents(n) is not None for n in self.peek}})
error = self.errors.pop(0) if self.errors else None
if error is not None:
raise error
status = self.statuses.pop(0) if self.statuses else "ok"
verdict = self.verdicts.pop(0) if self.verdicts else VERDICT[request["op"]]
roles = {"query": "data", "base_query": "base", "target_query": "target"}
inputs = [{"ref": name, "artifact": snap["artifact"], "role": roles[key], "columns": snap["columns"],
"rows": "all"}
for key in ("query", "base_query", "target_query")
if (name := params.get(key)) and (snap := snapshots.get(name)) is not None]
if parent is not None:
from app.core.artifact_store import canonical_json, content_hash
inputs.append({"ref": params["parent"]["of"], "artifact": content_hash(canonical_json(parent)),
"role": "parent"})
return fake_result(op=request["op"], mode=request.get("mode"), verdict=verdict, status=status,
reason_code="history_short" if status == "refused" else None, inputs=inputs,
params=dict(params), settings=dict(settings), hypothesis=request.get("hypothesis"),
**self.overrides)