mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 09:31:59 +00:00
可验证报告里有输入取自遮罩列的口径卡只给 id,核对页把它当正常的无法核对
口径卡上记着每个输入的值和代入式:cell() 取的遮罩格、按位置取的 vars.res.rows[0][1],以前原样嵌进这份要发给别人的文件, 证据面板上已经遮住的号码在这里照样看得到。现在口径卡有一个输入取自遮罩列(遮没遮核对不了的也算),整张卡和带遮罩的快照一样 只给 id——没法只拿掉几个值,拿掉了哈希就对不上。认法和证据面板、推演同一个(recompute.input_origin):按定位里的列认,按位置 取的一格顺着取值链从写变量的节点在口径卡那次执行之前的产出起走;快照或产出读不出来、结果没存快照只带着数据源名的宁可多挡。 核对页(verifier.js)把带遮罩的口径卡当成正常的无法核对(「含遮罩列,未随文件附带,只能核对到哈希」),引用它的数字同样无法 核对;只有报告文档标着遮罩才判被改过。check-bundle 原来那项「口径卡标成遮罩判不通过」相应改成「报告文档标成遮罩判不通过」, 另加样本页 masked-card:口径卡在 ③ 无法核对、引用它的数字无法核对、总览通过、页面里找不到那个号码。去掉核对页的改动,这几项变红。
This commit is contained in:
@@ -8,7 +8,9 @@
|
||||
浏览器先核它的哈希,再从里面读出报告文档、口径卡、查询快照的 id。其余事件只给哈希:工具返回、节点产出的
|
||||
预览里可能有遮罩列的原值和提示词。
|
||||
- 工件:每份报告文档(各报告节点最后一次自查记下的 doc_artifact)、它引用到的口径卡和查询快照,按原字节
|
||||
base64 嵌入,浏览器重算 sha256 和 id 比;带遮罩的快照不嵌内容、只给 id。没被报告引用的工件不嵌。
|
||||
base64 嵌入,浏览器重算 sha256 和 id 比;带遮罩的快照不嵌内容、只给 id。口径卡上记着每个输入的值和代入式,
|
||||
有输入取自遮罩列(cell() 的格、Agent 字段、按位置取的 vars.res.rows[0][2])的整张卡同样只给 id,认法和证据
|
||||
面板、推演同一个(recompute.input_origin)。没被报告引用的工件不嵌。
|
||||
- 封存之后追加的事件(按需裁判、审批质询这类批注)单列:类型、序号、内容摘要,不进清单。
|
||||
|
||||
它只证明「内部一致」:数字、文档、运行记录彼此对得上,而且没被改过。真伪要拿页面上的封存哈希和系统里这次
|
||||
@@ -34,6 +36,7 @@ from app.core.artifact_store import canonical_json, content_hash
|
||||
from app.core.events import EventType
|
||||
from app.engine.evidence import iter_units, query_entry_fields
|
||||
from app.engine.merge_query import MERGE_SOURCE
|
||||
from app.engine.recompute import input_origin
|
||||
from app.engine.schema import NodeType
|
||||
from app.engine.toolcalls import QUERY_PREFIX
|
||||
|
||||
@@ -106,6 +109,44 @@ class _Store:
|
||||
self._masked[artifact] = _snapshot_masked(self.content(artifact), self._source_masks)
|
||||
return self._masked[artifact]
|
||||
|
||||
def card_masked(self, artifact: str, events: Sequence[tuple[int, str, str | None, Any]]) -> bool:
|
||||
"""这张口径卡算不算带遮罩:卡上有一个输入取自遮罩列,或者遮没遮核对不了。卡上记着每个输入的值(cell() 取的格、
|
||||
Agent 字段、按位置取的 vars.res.rows[0][2])和代入式,整张卡不随文件附带——没法只拿掉几个值,拿掉了哈希就对不上。
|
||||
|
||||
认哪个输入取自哪份查询结果的哪几列,和证据面板、推演同一个认法(recompute.input_origin);按位置取的一格顺着
|
||||
取值链从写变量的节点在口径卡算这一次之前的产出起走。核对不了的宁可多挡:快照读不出来、取值链上的产出读不出来、
|
||||
那份结果没存快照只带着数据源名、定位里没写列。认得出、可结果既没带快照的 id 也没带数据源名的(数据整形改过形状)
|
||||
认不出出处,不算。读不出来的卡本来就不嵌,这里不管。"""
|
||||
key = f"card:{artifact}"
|
||||
if key not in self._masked:
|
||||
card = self.content(artifact) if self.intact(artifact) else None
|
||||
outputs = _card_outputs(events, artifact) if isinstance(card, dict) else {}
|
||||
|
||||
def output(node_id: Any) -> Any:
|
||||
found = outputs.get(str(node_id or ""))
|
||||
return self.content(found) if found and self.intact(found) else None
|
||||
|
||||
items = [item for metric in (card or {}).get("metrics") or [] if isinstance(metric, dict)
|
||||
for item in metric.get("inputs") or [] if isinstance(item, dict)]
|
||||
self._masked[key] = any(self._input_masked(item, output) for item in items)
|
||||
return self._masked[key]
|
||||
|
||||
def _input_masked(self, item: dict[str, Any], output: Callable[[Any], Any]) -> bool:
|
||||
origin = input_origin(item, output)
|
||||
if origin is None:
|
||||
return False
|
||||
if origin.unverified is not None:
|
||||
return True
|
||||
if origin.artifact is None:
|
||||
return origin.source is not None # 没存快照:查询当时记下的遮罩无从核对
|
||||
content = self.content(origin.artifact)
|
||||
if not self.intact(origin.artifact) or not isinstance(content, dict):
|
||||
return True
|
||||
if not origin.columns:
|
||||
return self.masked(origin.artifact)
|
||||
hidden = _snapshot_hidden(content, self._source_masks)
|
||||
return any(c.lower() in hidden for c in origin.columns)
|
||||
|
||||
|
||||
def _snapshot_masked(content: Any, source_masks: Mapping[str, Any]) -> bool:
|
||||
if not isinstance(content, dict):
|
||||
@@ -117,6 +158,37 @@ def _snapshot_masked(content: Any, source_masks: Mapping[str, Any]) -> bool:
|
||||
return any(source_masks.get(str(name or "")) for name in sources or [])
|
||||
|
||||
|
||||
def _snapshot_hidden(content: dict[str, Any], source_masks: Mapping[str, Any]) -> set[str]:
|
||||
"""一份读得出来的快照要遮的列(小写):查询当时记下的,加上它的数据源(合并查询看它合并的那几个)现在设的。"""
|
||||
recorded = content.get("mask_columns")
|
||||
hidden = {c.lower() for c in recorded if isinstance(c, str)} if isinstance(recorded, list) else set()
|
||||
sources = content.get("sources") if content.get("source") == MERGE_SOURCE else [content.get("source")]
|
||||
for name in sources if isinstance(sources, list) else []:
|
||||
now = source_masks.get(str(name or ""))
|
||||
hidden |= {str(c).lower() for c in now} if isinstance(now, (list, tuple, set, frozenset)) else set()
|
||||
return hidden
|
||||
|
||||
|
||||
def _card_outputs(events: Sequence[tuple[int, str, str | None, Any]], card: str) -> dict[str, str]:
|
||||
"""算出这份指标集的那一次口径卡执行开始之前,每个节点最后一次的产出工件:卡上 vars.*、nodes.* 取到的就是它们
|
||||
(和证据接口 api/evidence._card_outputs 同一个认法)。认不出是哪一次执行时取封存范围内每个节点最后一次的产出。"""
|
||||
found = next(((seq, node_id) for seq, etype, node_id, data in events
|
||||
if etype == EventType.NODE_FINISHED and isinstance(data, dict)
|
||||
and any(isinstance(e, dict) and e.get("kind") == "metric_set" and e.get("artifact") == card
|
||||
for e in data.get("evidence") or [])), None)
|
||||
bound = None
|
||||
if found is not None:
|
||||
bound = max((seq for seq, etype, node_id, _ in events
|
||||
if etype == EventType.NODE_STARTED and node_id == found[1] and seq < found[0]), default=found[0])
|
||||
out: dict[str, str] = {}
|
||||
for seq, etype, node_id, data in events:
|
||||
if bound is not None and seq >= bound:
|
||||
break
|
||||
if etype == EventType.NODE_FINISHED and node_id and isinstance(data, dict) and data.get("artifact"):
|
||||
out[node_id] = str(data["artifact"])
|
||||
return out
|
||||
|
||||
|
||||
def _query_artifacts(events: Iterable[tuple[int, str, str | None, Any]]) -> list[str]:
|
||||
"""封存范围内交回过的查询快照:tool.end / merge.end 的 query_artifact,节点台账里的 query 条目。"""
|
||||
out: list[str] = []
|
||||
@@ -234,10 +306,12 @@ def _cited_artifacts(doc: dict[str, Any]) -> list[tuple[str, str, str | None]]:
|
||||
|
||||
|
||||
class _Artifacts:
|
||||
"""嵌进页面的工件清单,按先后:报告文档在前,接着是它引用到的。同一份只列一次。"""
|
||||
"""嵌进页面的工件清单,按先后:报告文档在前,接着是它引用到的。同一份只列一次。events 是封存范围内的事件
|
||||
(认口径卡带不带遮罩时找写变量的节点当时的产出)。"""
|
||||
|
||||
def __init__(self, store: _Store) -> None:
|
||||
def __init__(self, store: _Store, events: Sequence[tuple[int, str, str | None, Any]] = ()) -> None:
|
||||
self.store = store
|
||||
self.events = events
|
||||
self.items: list[dict[str, Any]] = []
|
||||
self._seen: set[str] = set()
|
||||
|
||||
@@ -252,7 +326,8 @@ class _Artifacts:
|
||||
if raw is None:
|
||||
# 服务端的文件不在了:只给 id,浏览器照实说核对不了
|
||||
item["missing"] = True
|
||||
elif kind == "query" and self.store.masked(artifact):
|
||||
elif (kind == "query" and self.store.masked(artifact)) \
|
||||
or (kind == "metric_set" and self.store.card_masked(artifact, self.events)):
|
||||
item["masked"] = True
|
||||
else:
|
||||
# 原样嵌入,不在这里复验哈希:被改过的文件照实交出去,浏览器自己算出对不上
|
||||
@@ -380,7 +455,7 @@ def build_bundle(
|
||||
later = [e for e in events if e[0] > manifest_seq]
|
||||
store = _Store(read, source_masks or {})
|
||||
|
||||
artifacts = _Artifacts(store)
|
||||
artifacts = _Artifacts(store, sealed)
|
||||
reports = _report_events(sealed)
|
||||
cited_queries: list[str] = []
|
||||
for report in reports:
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
artifactUndecodable: '内容无法解码',
|
||||
artifactUnparsable: '哈希一致,但内容无法解析',
|
||||
masked: '含遮罩列,未随文件附带,只能核对到哈希',
|
||||
maskedOdd: '只有查询快照会因遮罩不随文件附带,这份工件却标着遮罩:导出时不会出现这种情况,文件可能被改过',
|
||||
maskedOdd: '只有查询快照和口径卡会因遮罩不随文件附带,这份工件却标着遮罩:导出时不会出现这种情况,文件可能被改过',
|
||||
artifactMissing: '导出时服务端已没有这份工件的文件',
|
||||
artifactAbsent: '内容未随文件附带:导出时不会出现这种情况,文件可能被改过',
|
||||
chainDoc: (label) => `报告「${label}」的文档`,
|
||||
@@ -485,7 +485,8 @@
|
||||
}
|
||||
}
|
||||
} else if (a.masked) {
|
||||
if (a.kind === 'query') {
|
||||
// 查询快照带遮罩列;口径卡上有输入取自遮罩列(卡上记着输入的值和代入式)。两种都只给 id,报告文档不会
|
||||
if (a.kind === 'query' || a.kind === 'metric_set') {
|
||||
item.reason = T.masked
|
||||
} else {
|
||||
item.result = FAIL
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
- continued:早期运行先失败、经继续运行跑到成功:范围里有那条运行失败,之后运行又继续了,成功结束的那条在范围外
|
||||
- degraded:两条出具判定,先完整出具、后降档出具,认最后一条
|
||||
- missing-doc / missing-card:导出时服务端已没有报告文档 / 口径卡的文件
|
||||
- masked-card:口径卡上一个指标的输入取自会员消费的号码列(设了遮罩),卡上记着号码原值:整张卡只给 id,引用它的
|
||||
数字无法核对
|
||||
- nan:运行开始事件和门店流水快照里有 json.dumps 写出的裸 NaN(快照里那一列报告没有引用)
|
||||
- stray:报告自查记下的文档是另一次运行写的
|
||||
"""
|
||||
@@ -70,14 +72,25 @@ def shop(nan: bool = False) -> dict[str, Any]:
|
||||
"sql": "SELECT store, amount, ratio FROM orders ORDER BY id", "truncated": False}
|
||||
|
||||
|
||||
class Sample:
|
||||
"""一次虚构的已封存运行:工件(原字节)、证据目录、报告文档、事件。"""
|
||||
def masked_card(crm_id: str) -> dict[str, Any]:
|
||||
"""口径卡多一个指标:联系电话位数,输入是会员消费第 0 行的号码(phone 设了遮罩),卡上记着号码原值和代入式。"""
|
||||
cell = "cell(nodes.members, 0, 'phone')"
|
||||
contact = {"id": "contact", "name": "联系电话位数", "value": 8, "unit": "位", "decimals": 0, "format": "thousands",
|
||||
"expression": f"len({cell})", "substituted": f"len('{CRM['rows'][0][1]}')",
|
||||
"inputs": [{"path": cell, "value": CRM["rows"][0][1], "node_id": "members", "via": "tool_cell",
|
||||
"artifact": crm_id, "locator": {"row": 0, "column": "phone"}, "status": "verified"}]}
|
||||
return {**CARD, "metrics": [*CARD["metrics"], contact]}
|
||||
|
||||
def __init__(self, *, nan: bool = False) -> None:
|
||||
|
||||
class Sample:
|
||||
"""一次虚构的已封存运行:工件(原字节)、证据目录、报告文档、事件。masked=True 时口径卡带着取自遮罩列的输入。"""
|
||||
|
||||
def __init__(self, *, nan: bool = False, masked: bool = False) -> None:
|
||||
self.files: dict[str, bytes] = {}
|
||||
self.shop_id = self.put(shop(nan))
|
||||
self.crm_id = self.put(CRM)
|
||||
self.card_id = self.put(CARD)
|
||||
self.card = masked_card(self.crm_id) if masked else CARD
|
||||
self.card_id = self.put(self.card)
|
||||
self.ledger = [
|
||||
{"kind": "query", "node_id": "fetch", "exec": 1, "artifact": self.shop_id, "tool": "db_query__shop",
|
||||
"source": "shop", "columns": ["store", "amount", "ratio"], "rows": len(STORES), "truncated": False},
|
||||
@@ -85,7 +98,7 @@ class Sample:
|
||||
"source": "crm", "columns": CRM["columns"], "rows": len(CRM["rows"]), "truncated": False},
|
||||
{"kind": "metric_set", "node_id": "card", "artifact": self.card_id},
|
||||
]
|
||||
self.catalog = build_catalog(nodes={"card": CARD}, ledger=self.ledger)
|
||||
self.catalog = build_catalog(nodes={"card": self.card}, ledger=self.ledger)
|
||||
|
||||
def put(self, obj: Any) -> str:
|
||||
artifact, raw = stored(obj)
|
||||
@@ -198,6 +211,10 @@ def pages() -> dict[str, str]:
|
||||
n = Sample(nan=True)
|
||||
events, bound = n.events({"write": n.doc(WEEKLY, "write")}, nan=True)
|
||||
out["nan"] = render(n, events, bound)
|
||||
|
||||
m = Sample(masked=True)
|
||||
events, bound = m.events({"write": m.doc(WEEKLY, "write")})
|
||||
out["masked-card"] = render(m, events, bound)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -463,7 +463,7 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
|
||||
|
||||
got = {name: json.loads(data_block(page)) for name, page in pages().items()}
|
||||
assert set(got) == {"base", "two-reports", "legacy", "failed", "continued", "degraded", "missing-doc",
|
||||
"missing-card", "nan", "stray"}
|
||||
"missing-card", "nan", "stray", "masked-card"}
|
||||
for name, bundle in got.items():
|
||||
assert browser_manifest(bundle["events"]) == bundle["meta"]["manifest_hash"], name
|
||||
|
||||
@@ -499,6 +499,9 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
|
||||
issuances = [e["seq"] for e in got["degraded"]["events"] if e["type"] == "issuance"]
|
||||
assert len(issuances) == 2 and got["degraded"]["meta"]["tier"] == {"value": "degraded", "seq": issuances[-1]}
|
||||
assert [a["kind"] for a in got["stray"]["artifacts"]] == ["report_doc"]
|
||||
# 口径卡上有输入取自遮罩列:整张卡只给 id,页面里找不到那个号码
|
||||
card = next(a for a in got["masked-card"]["artifacts"] if a["kind"] == "metric_set")
|
||||
assert card.get("masked") is True and "b64" not in card
|
||||
nan = got["nan"]
|
||||
assert "NaN" in next(e["canonical"] for e in nan["events"] if e["type"] == "run.started")
|
||||
assert b"NaN" in base64.b64decode(next(a["b64"] for a in nan["artifacts"] if a.get("node_id") == "fetch"))
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""可验证报告里的口径卡和遮罩(期 4 I2a 复核):口径卡上记着每个输入的值和代入式,有输入取自遮罩列时整张卡只给 id。
|
||||
|
||||
以前只有查询快照会因遮罩不随文件附带,口径卡一律原样嵌入:证据面板上已经遮住的号码(cell() 取的遮罩格、按位置取的
|
||||
vars.res.rows[0][1]),在这份要发给别人的文件里照样看得到。认哪个输入取自哪份查询结果的哪几列,和证据面板、推演同一个
|
||||
认法(recompute.input_origin);核对不了的宁可多挡。
|
||||
|
||||
工件、事件都是手搭的(会员、号码都是编的),照 tests/test_bundle_build.py 的样子。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
|
||||
from app.bundle.build import build_bundle, render_html
|
||||
from tests.test_bundle_build import GRAPH, MASKED_ID, RUN, SNAP_MASKED, STORE, doc_of, stored
|
||||
|
||||
#: SNAP_MASKED 里的号码(phone 设了遮罩)
|
||||
PHONE = SNAP_MASKED["rows"][0][1]
|
||||
CELL = "cell(nodes.members, 0, 'phone')"
|
||||
|
||||
|
||||
def card_with(inputs, substituted="len('000-0001')", expression=f"len({CELL})"):
|
||||
"""会员号码位数一张卡:报告引用的是它(m:gmv 指过来)。"""
|
||||
return {"kind": "metric_set", "caliber": "会员口径", "caliber_version": "v1", "metrics": [
|
||||
{"id": "gmv", "name": "号码位数", "value": 1280.5, "unit": "元", "decimals": 1, "expression": expression,
|
||||
"substituted": substituted, "inputs": inputs}]}
|
||||
|
||||
|
||||
def bundle_of(card, *, outputs=None, source_masks=None):
|
||||
"""一次手搭的运行:会员查询(带遮罩的快照)→ 数据整形(outputs 里的产出)→ 口径卡 → 报告。"""
|
||||
card_id, card_raw = stored(card)
|
||||
doc = doc_of(extra_catalog={"m:gmv": {"alias": "m:gmv", "kind": "metric", "artifact": card_id, "node_id": "card",
|
||||
"locator": {"metric": "gmv"}, "value": 1280.5}})
|
||||
doc_id, doc_raw = stored(doc)
|
||||
files = {**STORE, card_id: card_raw, doc_id: doc_raw}
|
||||
events = [
|
||||
(1, "run.started", None, {"nodes": 7}),
|
||||
(2, "tool.end", "members", {"tool": "db_query__crm", "query_artifact": MASKED_ID}),
|
||||
(3, "node.finished", "members", {"evidence": [{"kind": "query", "node_id": "members", "artifact": MASKED_ID}]}),
|
||||
]
|
||||
for node_id, content in (outputs or {}).items():
|
||||
artifact, raw = stored(content) if content is not None else ("ee" * 32, None)
|
||||
if raw is not None:
|
||||
files[artifact] = raw
|
||||
events.append((len(events) + 1, "node.finished", node_id, {"artifact": artifact}))
|
||||
events += [
|
||||
(len(events) + 1, "node.started", "card", {}),
|
||||
(len(events) + 2, "node.finished", "card", {"evidence": [{"kind": "metric_set", "node_id": "card",
|
||||
"artifact": card_id}]}),
|
||||
(len(events) + 3, "report.checked", "write", {"doc_artifact": doc_id, "ok": True}),
|
||||
(len(events) + 4, "run.finished", None, {"output": {}}),
|
||||
]
|
||||
bundle = build_bundle(RUN, events, manifest_seq=len(events), manifest_hash="0" * 64, graph=GRAPH,
|
||||
source_masks=source_masks or {}, read=files.get, exported_at="2026-10-06T08:00:00+00:00")
|
||||
entry = next(a for a in bundle["artifacts"] if a["id"] == card_id)
|
||||
return bundle, entry
|
||||
|
||||
|
||||
def cell_input(column, value):
|
||||
return {"path": f"cell(nodes.members, 0, '{column}')", "value": value, "node_id": "members", "via": "tool_cell",
|
||||
"artifact": MASKED_ID, "locator": {"row": 0, "column": column}, "status": "verified"}
|
||||
|
||||
|
||||
def test_a_card_reading_a_masked_cell_ships_only_its_id():
|
||||
bundle, entry = bundle_of(card_with([cell_input("phone", PHONE)]))
|
||||
assert entry == {"id": entry["id"], "kind": "metric_set", "node_id": "card", "masked": True}
|
||||
assert PHONE not in render_html(bundle)
|
||||
|
||||
|
||||
def test_a_card_reading_only_unmasked_columns_is_embedded():
|
||||
"""同一份快照里没遮的列(消费额):卡上没有遮罩列的值,照常嵌入、照常核对。"""
|
||||
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
|
||||
_, entry = bundle_of(card)
|
||||
assert "masked" not in entry and base64.b64decode(entry["b64"]) == stored(card)[1]
|
||||
|
||||
|
||||
def test_a_mask_set_on_the_source_now_counts_too():
|
||||
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
|
||||
_, entry = bundle_of(card, source_masks={"crm": ["spend"]})
|
||||
assert entry.get("masked") is True and "b64" not in entry
|
||||
|
||||
|
||||
PARSED = {"columns": SNAP_MASKED["columns"], "rows": SNAP_MASKED["rows"]}
|
||||
POSITIONAL = "vars.res.rows[0][1]"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parsed, masked", [
|
||||
# 数据整形原样解析的查询结果(带着快照的 id):认得出取自 phone 列
|
||||
({**PARSED, "artifact": MASKED_ID, "source": "crm"}, True),
|
||||
# 写变量的节点的产出读不出来:核对不了,宁可多挡
|
||||
(None, True),
|
||||
# 结果没存快照、只带着数据源名:查询当时记下的遮罩无从核对,宁可多挡
|
||||
({**PARSED, "source": "crm"}, True),
|
||||
# 改过形状、既没带快照的 id 也没带数据源名:认不出出处,照常嵌入(和证据面板、推演一样,是已知的缺口)
|
||||
(PARSED, False),
|
||||
])
|
||||
def test_a_card_reading_a_cell_by_position_is_judged_the_same_way(parsed, masked):
|
||||
item = {"path": POSITIONAL, "value": PHONE, "node_id": "parse", "via": "transform", "field": "rows[0][1]",
|
||||
"status": "ok"}
|
||||
card = card_with([item], substituted=f"len('{PHONE}')", expression=f"len({POSITIONAL})")
|
||||
bundle, entry = bundle_of(card, outputs={"parse": parsed})
|
||||
assert (entry.get("masked") is True) is masked and ("b64" in entry) is not masked
|
||||
if masked:
|
||||
assert PHONE not in render_html(bundle)
|
||||
else:
|
||||
assert PHONE in base64.b64decode(entry["b64"]).decode()
|
||||
+24
-10
@@ -15,9 +15,9 @@
|
||||
// - 深浅色、窄屏:390 宽不横向溢出,点开的数字面板在窄屏上从底部抽出,深色下底是深的、字是浅的;
|
||||
// - 样本页:tests/bundle_samples.py 用虚构数据真实生成的几份页面(遮罩快照、两份报告、早期运行、失败结束、先失败后
|
||||
// 经继续运行跑到成功的早期运行、两条出具
|
||||
// 判定、服务端缺文件、裸 NaN、别的运行的文档),逐份断言各项结果和原因文案;在上面篡改 ④ 引用链(换文档、拿掉报告、
|
||||
// 多一份没人引用的工件)、⑥ 运行信息(运行结果、类别、出具档位)、拿掉被引用的口径卡、把口径卡标成遮罩、只改页面顶部
|
||||
// 的封存哈希;宽屏点开表格里右对齐的数字不被面板盖住;不执行脚本时封存哈希、运行 ID 照样看得到,状态写「尚未核对」;
|
||||
// 判定、服务端缺文件、裸 NaN、别的运行的文档、口径卡有输入取自遮罩列),逐份断言各项结果和原因文案;在上面篡改 ④
|
||||
// 引用链(换文档、拿掉报告、多一份没人引用的工件)、⑥ 运行信息(运行结果、类别、出具档位)、拿掉被引用的口径卡、把
|
||||
// 报告文档标成遮罩、只改页面顶部的封存哈希;宽屏点开表格里右对齐的数字不被面板盖住;不执行脚本时封存哈希、运行 ID 照样看得到,状态写「尚未核对」;
|
||||
// - 记录页的导出按钮(预览用例 ?syn=bundle-provenance-*,page.route 假装导出接口):已封存时可点、下载的文件名同服务端;
|
||||
// 未封存时不可点并说明原因(左边写着「已封存」却不能点时原因写在屏幕上);服务端回 409 时提示原因;失败结束的运行
|
||||
// 提示之后继续运行会重新封存。
|
||||
@@ -487,7 +487,7 @@ await section('样本页:各种形状', async () => {
|
||||
check('生成样本页(tests/bundle_samples.py)', false, String(e?.message ?? e))
|
||||
return
|
||||
}
|
||||
check('生成样本页(tests/bundle_samples.py)', Object.keys(samples).length === 10, Object.keys(samples).join('、'))
|
||||
check('生成样本页(tests/bundle_samples.py)', Object.keys(samples).length === 11, Object.keys(samples).join('、'))
|
||||
|
||||
await sample('base', async (s, page) => {
|
||||
check('base:总览通过,有无法核对的项时写「可核对的项全部通过」', s.overall === 'pass' && s.text === '可核对的项全部通过',
|
||||
@@ -601,6 +601,19 @@ await section('样本页:各种形状', async () => {
|
||||
check('missing-card:引用口径卡的数字无法核对,写明服务端已没有文件', hasItem(s, 'numbers', 'na', '导出时服务端已没有这份工件的文件'))
|
||||
check('missing-card:服务端缺文件是正常的无法核对,总览通过', s.overall === 'pass', `${s.overall}:${s.text}`)
|
||||
})
|
||||
// 口径卡上有输入取自遮罩列(会员号码):卡上记着号码原值和代入式,整张卡只给 id。这是正常的无法核对,不是被改过
|
||||
await sample('masked-card', async (s, page) => {
|
||||
const card = s.items.find((i) => i.check === 'artifacts' && i.text.includes('口径卡'))
|
||||
check('masked-card:口径卡在 ③ 无法核对,写明「含遮罩列,未随文件附带,只能核对到哈希」,不判不通过',
|
||||
card?.result === 'na' && card.text.includes('含遮罩列,未随文件附带,只能核对到哈希'), card?.text)
|
||||
const metricNumber = s.items.find((i) => i.check === 'numbers' && i.text.includes('销售额'))
|
||||
check('masked-card:引用这张卡的数字无法核对,原因同上', metricNumber?.result === 'na'
|
||||
&& metricNumber.text.includes('含遮罩列'), metricNumber?.text)
|
||||
check('masked-card:总览通过(可核对的项全部通过),没有一项不通过', s.overall === 'pass' && s.text === '可核对的项全部通过'
|
||||
&& !s.items.some((i) => i.result === 'fail'), `${s.overall}:${s.text}`)
|
||||
const html = await page.content()
|
||||
check('masked-card:页面里找不到卡上那个遮罩列的号码', !html.includes('000-0001') && !html.includes('000-0002'))
|
||||
})
|
||||
await sample('nan', async (s) => {
|
||||
check('nan:内容里有裸 NaN 的事件和工件照常通过', s.overall === 'pass' && s.checks.events === 'pass'
|
||||
&& !s.items.some((i) => i.result === 'fail'), describe(s))
|
||||
@@ -655,12 +668,13 @@ await section('样本页:篡改', async () => {
|
||||
data.artifacts = data.artifacts.filter((a) => a.kind !== 'metric_set')
|
||||
return '删掉口径卡条目'
|
||||
}, (s) => [...turnsRed('numbers')(s), ['原因写明', hasItem(s, 'numbers', 'fail', '引用的工件不在文件的工件清单里')]])
|
||||
await tamper(base, '口径卡标成遮罩', (data) => {
|
||||
const card = byKind(data, 'metric_set')
|
||||
delete card.b64
|
||||
card.masked = true
|
||||
return '口径卡去掉内容、标遮罩'
|
||||
}, (s) => [...turnsRed('artifacts')(s), ['原因写明', hasItem(s, 'artifacts', 'fail', '只有查询快照会因遮罩不随文件附带')]])
|
||||
// 口径卡有输入取自遮罩列时服务端照样只给 id(见样本页 masked-card),报告文档从来不会:把它标成遮罩只能是改过
|
||||
await tamper(base, '报告文档标成遮罩', (data) => {
|
||||
const doc = byKind(data, 'report_doc')
|
||||
delete doc.b64
|
||||
doc.masked = true
|
||||
return '报告文档去掉内容、标遮罩'
|
||||
}, (s) => [...turnsRed('artifacts')(s), ['原因写明', hasItem(s, 'artifacts', 'fail', '只有查询快照和口径卡会因遮罩不随文件附带')]])
|
||||
await tamper(base, '报告文档的一个字节', (data) => flipByte(byKind(data, 'report_doc')), (s) => [
|
||||
...turnsRed('artifacts')(s),
|
||||
['数字不予核对', s.checks.numbers === 'na' && hasItem(s, 'numbers', 'na', '所在的报告文档没有通过核对')],
|
||||
|
||||
Reference in New Issue
Block a user