mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:51:59 +00:00
可验证报告、报告导出写「被驳回」:元信息带 end_reason,核对仍按运行取消
为什么:被驳回的运行导出后,可验证报告的运行结果写「已取消」,报告导出的运行状态也写「已取消」,和系统里「被驳回」两种 说法。封存里被驳回和手动停止是同一种结束事件(运行取消),分开它们的原因在那条事件的内容里,而运行取消的内容不随文件 附带(里面有驳回人和备注)——光看封存分不出来。 做了什么: - 两种导出的元信息都带 end_reason(bundle/build、report_export/build、对应的接口)。 - verifier.js:运行结果按运行记录写「被驳回」,核对的仍是「最后一条结束类事件是运行取消」,旁边写「已核对运行取消,被驳回 取自运行记录」,原因里写明这一半要去系统里对照;手动停止、老运行照旧「已取消」;文件里写被驳回、事件却是运行结束的照旧 不通过。runtime.js 的运行状态照 end_reason 写「被驳回」。 - 样本页加一份 rejected(check-bundle 样本数 17 → 18),断言写被驳回、写明取自运行记录、不提重新封存。 测试:test_bundle_rejected 用 node 载入 verifier.js 核运行信息(被驳回、手动停止、老运行、伪造);test_bundle_build 核 元信息和样本页;test_bundle_api 核两种导出接口都带 end_reason。把实现退回去回跑,被驳回、伪造两条和元信息、样本页、 接口的断言都失败(手动停止、老运行两条守的是原样,照旧通过);改完通过。
This commit is contained in:
@@ -53,7 +53,8 @@ async def export_bundle(run_id: str) -> Response:
|
||||
raise CodedHTTPException(404, "运行记录不存在", RUN_NOT_FOUND)
|
||||
info = {"id": run.id, "workflow_id": run.workflow_id, "workflow_name": run.workflow_name,
|
||||
"version": run.version, "version_hash": run.version_hash, "run_class": run.run_class,
|
||||
"status": run.status, "started_at": run.started_at, "finished_at": run.finished_at}
|
||||
"status": run.status, "end_reason": run.end_reason, "started_at": run.started_at,
|
||||
"finished_at": run.finished_at}
|
||||
# 封存之后追加的事件(按需裁判、审批质询):_sealed 只留了封存范围内的,这几条另取,单列在页面上
|
||||
later = [tuple(r) for r in await session.execute(
|
||||
select(RunEvent.seq, RunEvent.type, RunEvent.node_id, RunEvent.data)
|
||||
|
||||
@@ -2851,7 +2851,7 @@ async def export_report_html(run_id: str, report: str | None = None) -> Response
|
||||
if run is None: # 两次读之间被删掉了
|
||||
raise CodedHTTPException(404, "运行记录不存在", RUN_NOT_FOUND)
|
||||
info = {"id": run.id, "workflow_id": run.workflow_id, "workflow_name": run.workflow_name,
|
||||
"run_class": run.run_class, "status": run.status,
|
||||
"run_class": run.run_class, "status": run.status, "end_reason": run.end_reason,
|
||||
"started_at": run.started_at, "finished_at": run.finished_at}
|
||||
data = build_report(info, report_node=chosen.node_id, doc_artifact=chosen.doc_artifact,
|
||||
doc_hash_ok=chosen.hash_ok, doc=doc, seal=sealed.seal, chains=chains, refs=refs,
|
||||
|
||||
@@ -1237,8 +1237,8 @@ def build_bundle(
|
||||
) -> dict[str, Any]:
|
||||
"""一次已封存运行 → 页面里的数据(bundle-data)。
|
||||
|
||||
- run:运行记录的几样(id、workflow_id、workflow_name、version、version_hash、run_class、status、
|
||||
started_at、finished_at)
|
||||
- run:运行记录的几样(id、workflow_id、workflow_name、version、version_hash、run_class、status、end_reason、
|
||||
started_at、finished_at)。end_reason 分开同是 cancelled 的被驳回和手动停止(05-已知遗留 (a)),不在封存范围内
|
||||
- events:这次运行的全部事件 (seq, type, node_id, data),按数据库里 seq 的顺序。和封存核对
|
||||
(runner.verify_manifest)读的是同一个顺序:seq 相同的几条,浏览器按这里的先后排,清单才算得一样
|
||||
- manifest_seq / manifest_hash:封存范围(老运行是按结束事件推算的那个,legacy 为真)和封存哈希
|
||||
@@ -1306,7 +1306,7 @@ def build_bundle(
|
||||
"meta": {
|
||||
"run_id": run.get("id"), "workflow_id": run.get("workflow_id"), "workflow_name": display_name(run),
|
||||
"version": run.get("version"), "version_hash": run.get("version_hash"),
|
||||
"run_class": run.get("run_class"), "status": run.get("status"),
|
||||
"run_class": run.get("run_class"), "status": run.get("status"), "end_reason": run.get("end_reason"),
|
||||
"started_at": _iso(run.get("started_at")), "finished_at": _iso(run.get("finished_at")),
|
||||
"manifest_seq": manifest_seq, "manifest_hash": manifest_hash, "legacy": bool(legacy),
|
||||
"tier": _tier(sealed), "exported_at": exported_at,
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
const KIND_LABEL = { report_doc: '报告文档', metric_set: '口径卡', query: '查询快照' }
|
||||
const TIER_LABEL = { formal: '完整出具', degraded: '降档出具', withheld: '不予出具' }
|
||||
const CLASS_LABEL = { formal: '正式运行', exploratory: '探索运行' }
|
||||
const STATUS_LABEL = { succeeded: '已完成', failed: '已失败', cancelled: '已取消' }
|
||||
// rejected:被审批人驳回结束(运行记录 cancelled + end_reason=rejected)。封存里它和手动停止是同一种结束事件(运行取消),
|
||||
// 分开二者的原因在那条事件的内容里,内容不随文件附带:见 checkMeta
|
||||
const STATUS_LABEL = { succeeded: '已完成', failed: '已失败', cancelled: '已取消', rejected: '被驳回' }
|
||||
/** 结束类事件 → 它说明的运行结果(runner:成功、失败、取消各发一条,落库之后才封存) */
|
||||
const TERMINAL = { 'run.finished': 'succeeded', 'run.failed': 'failed', 'run.cancelled': 'cancelled' }
|
||||
const EVENT_LABEL = {
|
||||
@@ -84,6 +86,7 @@
|
||||
range: (seq, count) => `第 1–${seq} 条事件(范围内 ${count} 条)`,
|
||||
rangeLegacy: '(早期运行,范围按结束事件推算)',
|
||||
markPass: '已核对',
|
||||
markPassRejected: '已核对运行取消,被驳回取自运行记录',
|
||||
markFail: (claim) => `文件里写的是「${claim}」,见核对明细 ⑥`,
|
||||
markNa: '无法核对',
|
||||
noReports: '这次运行没有报告文档,下方只核对运行记录。',
|
||||
@@ -208,6 +211,8 @@
|
||||
inputFrom: (query, row, column) => (query ? `查询 ${query}` : '') + (Number.isInteger(row) ? ` · 第 ${row + 1} 行` : '') + (column ? ` · ${column}` : ''),
|
||||
inputNotAttached: '快照未随文件附带(见核对明细 ③),输入的值按口径卡的记录展示',
|
||||
statusOk: (seq, what) => `与第 ${seq} 条事件(${what})一致`,
|
||||
statusOkRejected: (seq, what) => `与第 ${seq} 条事件(${what})一致。被驳回和手动停止在封存里是同一种结束事件,`
|
||||
+ '「被驳回」取自运行记录(这条事件的内容不随文件附带),需要在系统中对照',
|
||||
statusMismatch: (claim, seq, what) => `文件里写的是「${claim}」,与封存范围内最后一条结束类事件(第 ${seq} 条,${what})不符`,
|
||||
statusLegacy: '早期运行在结束事件落库之前封存,结束事件不在封存范围内,运行结果无法核对',
|
||||
statusNoEvent: '封存范围内没有运行结束、失败或取消的事件,运行结果无法核对',
|
||||
@@ -1509,7 +1514,8 @@
|
||||
*/
|
||||
function checkMeta(meta, out) {
|
||||
const items = []
|
||||
const add = (field, shown, claim, result, reason) => items.push({ field, shown, claim, result, reason })
|
||||
// mark:显示值旁边那一句的特例(默认按结果写已核对 / 不通过 / 无法核对)
|
||||
const add = (field, shown, claim, result, reason, mark) => items.push({ field, shown, claim, result, reason, mark })
|
||||
const order = out.order
|
||||
const sealOk = out.manifest[0].result === PASS
|
||||
const label = (map, value) => map[value] || (value ? String(value) : '—')
|
||||
@@ -1519,7 +1525,10 @@
|
||||
// 它就是范围内的最后一条;早期运行在 run.finished 落库之前封存,成功结束的那条在范围外,失败、取消的那条在范围的最后。
|
||||
// 先失败、经继续运行接着跑的,范围里那条运行失败之后运行又继续了,它不是运行的结束:拿它去对,元信息改成「已失败」
|
||||
// 也会被标成已核对。真正截止的那条不在范围里时,只有早期运行成功结束说得通(无法核对),别的说法都没有事件印证
|
||||
const statusClaim = label(STATUS_LABEL, meta.status)
|
||||
// 被驳回(cancelled + end_reason=rejected)照运行记录写「被驳回」,不写成手动停止的「已取消」;能核对的只有结束事件
|
||||
// 是运行取消这一半,另一半写明取自运行记录
|
||||
const rejected = meta.status === 'cancelled' && meta.end_reason === 'rejected'
|
||||
const statusClaim = rejected ? STATUS_LABEL.rejected : label(STATUS_LABEL, meta.status)
|
||||
const lastSeq = order.length ? order[order.length - 1].seq : null
|
||||
const final = [...order].reverse().find((ev) => TERMINAL[ev.type])
|
||||
// 同一序号的几条(老数据里有)都算最后:结束事件和别的事件撞了序号,不能因此说它之后运行又继续了
|
||||
@@ -1527,8 +1536,12 @@
|
||||
if (!sealOk) add('status', statusClaim, statusClaim, NA, T.metaNeedsManifest)
|
||||
else if (end) {
|
||||
const ok = TERMINAL[end.type] === meta.status
|
||||
add('status', label(STATUS_LABEL, TERMINAL[end.type]), statusClaim, ok ? PASS : FAIL,
|
||||
ok ? T.statusOk(end.seq, what(end)) : T.statusMismatch(statusClaim, end.seq, what(end)))
|
||||
if (ok && rejected) {
|
||||
add('status', statusClaim, statusClaim, PASS, T.statusOkRejected(end.seq, what(end)), T.markPassRejected)
|
||||
} else {
|
||||
add('status', label(STATUS_LABEL, TERMINAL[end.type]), statusClaim, ok ? PASS : FAIL,
|
||||
ok ? T.statusOk(end.seq, what(end)) : T.statusMismatch(statusClaim, end.seq, what(end)))
|
||||
}
|
||||
} else if (meta.legacy && meta.status === 'succeeded') add('status', statusClaim, statusClaim, NA, T.statusLegacy)
|
||||
// 显示的值写「—」:范围里没有说明运行怎样结束的事件,不能把文件里写的当成核对出来的
|
||||
else if (final) add('status', '—', statusClaim, FAIL, T.statusContinued(statusClaim, final.seq, what(final)))
|
||||
@@ -1899,7 +1912,7 @@
|
||||
const checked = (field, fallback) => {
|
||||
const item = out.meta.find((m) => m.field === field)
|
||||
if (!item) return fallback
|
||||
const note = item.result === PASS ? T.markPass : item.result === FAIL ? T.markFail(item.claim) : T.markNa
|
||||
const note = item.mark ?? (item.result === PASS ? T.markPass : item.result === FAIL ? T.markFail(item.claim) : T.markNa)
|
||||
return h('span', { 'data-meta': field, 'data-result': item.result }, item.shown,
|
||||
h('span', { class: item.result === FAIL ? 'bad' : 'muted' }, `(${note})`))
|
||||
}
|
||||
|
||||
@@ -320,7 +320,8 @@ def build_report(
|
||||
) -> dict[str, Any]:
|
||||
"""页面里的数据(report-data)。
|
||||
|
||||
- run:运行记录的几样(id、workflow_id、workflow_name、run_class、status、started_at、finished_at)
|
||||
- run:运行记录的几样(id、workflow_id、workflow_name、run_class、status、end_reason、started_at、finished_at);
|
||||
end_reason 分开同是 cancelled 的被驳回和手动停止(05-已知遗留 (a))
|
||||
- report_node / doc_artifact / doc_hash_ok:选中的报告节点、它的文档工件、取回时哈希核对的结果
|
||||
- doc:报告文档(正文的服务端渲染另走 render_body,这里不给:数据块只装点开数字要用的东西)
|
||||
- seal:{_sealed 的 seal 字段里的几样}(sealed、ok、manifest_hash、manifest_seq)
|
||||
@@ -334,6 +335,7 @@ def build_report(
|
||||
"meta": {
|
||||
"run_id": run.get("id"), "workflow_id": run.get("workflow_id"),
|
||||
"workflow_name": display_name(run), "run_class": run.get("run_class"), "status": run.get("status"),
|
||||
"end_reason": run.get("end_reason"),
|
||||
"started_at": _iso(run.get("started_at")), "finished_at": _iso(run.get("finished_at")),
|
||||
"report": {"node_id": report_node, "label": (nodes.get(report_node) or {}).get("label") or report_node},
|
||||
"doc_artifact": doc_artifact, "doc_hash_ok": doc_hash_ok,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
metaRun: '运行 ID', metaClass: '运行类别', metaStatus: '运行状态', metaReport: '报告节点',
|
||||
metaStarted: '开始', metaFinished: '结束', metaExported: '导出时间',
|
||||
classLabel: { formal: '正式运行', exploratory: '探索运行' },
|
||||
statusLabel: { succeeded: '已完成', failed: '已失败', cancelled: '已取消', running: '进行中', waiting: '等待审批' },
|
||||
statusLabel: { succeeded: '已完成', failed: '已失败', cancelled: '已取消', rejected: '被驳回', running: '进行中', waiting: '等待审批' },
|
||||
docHashBad: '这份报告文档与哈希不一致(疑似被修改),正文仅供参考,其中的口径和出处未经核对。',
|
||||
sealNone: '这次运行导出时尚未封存:下列出处来自导出那一刻的系统记录,未经封存核对。',
|
||||
sealOk: (hash) => `运行已封存,清单哈希 ${hash}。要向系统外的人证明文件未被修改,请导出「可验证报告」(记录页 · 凭证详情),拿封存哈希去系统对照。`,
|
||||
@@ -147,7 +147,8 @@
|
||||
$('meta').replaceChildren(...[...[
|
||||
[T.metaRun, h('code', {}, String(meta.run_id || ''))],
|
||||
[T.metaClass, label(T.classLabel, meta.run_class)],
|
||||
[T.metaStatus, label(T.statusLabel, meta.status)],
|
||||
// 被驳回(cancelled + end_reason=rejected)照运行记录写「被驳回」,不写成手动停止的「已取消」
|
||||
[T.metaStatus, label(T.statusLabel, meta.status === 'cancelled' && meta.end_reason === 'rejected' ? 'rejected' : meta.status)],
|
||||
[T.metaReport, String(meta.report?.label || meta.report?.node_id || NONE)],
|
||||
[T.metaStarted, when(meta.started_at)],
|
||||
[T.metaFinished, when(meta.finished_at)],
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
引用链(④)按「已核对的口径卡的输入」认它;点开数字看得到指标、原式、代入式、输入和复算
|
||||
- legacy:早期运行,封存范围按结束事件推算(运行结束事件在范围外,运行结果无法核对)
|
||||
- failed:失败结束的运行(没有出具判定),封存说明多一句「继续运行会重新封存」
|
||||
- rejected:被审批人驳回结束的运行(运行记录 cancelled + end_reason=rejected,最后一条是运行取消):运行结果写「被驳回」,
|
||||
写明能核对的只是「运行取消」这一半、被驳回取自运行记录;不提重新封存
|
||||
- continued:早期运行先失败、经继续运行跑到成功:范围里有那条运行失败,之后运行又继续了,成功结束的那条在范围外
|
||||
- degraded:两条出具判定,先完整出具、后降档出具,认最后一条
|
||||
- missing-doc / missing-card:导出时服务端已没有报告文档 / 口径卡的文件
|
||||
@@ -278,6 +280,11 @@ class Sample:
|
||||
add("issuance", "out", {"tier": tier, "mode": "citations"})
|
||||
add("node.finished", "out", {"duration_ms": 2, "attempt": 1, "preview": {"周报": "(报告正文)"}})
|
||||
add("run.finished", None, {"output": {"周报": "(报告正文)"}})
|
||||
elif ending == "rejected":
|
||||
# 审批人在成果之前驳回(engine/runner:run.cancelled 带 reason=rejected、在哪一步、谁、备注;内容不随文件附带)
|
||||
add("node.failed", "out", {"error": "人工驳回:数字对不上"})
|
||||
add("run.cancelled", None, {"reason": "rejected", "node_id": "out", "label": "成果", "kind": "approve",
|
||||
"decision": "reject", "actor": "值班分析员", "note": "数字对不上"})
|
||||
else:
|
||||
add("node.failed", "out", {"error": "出口复核没有通过"})
|
||||
add("run.failed", None, {"error": "出口复核没有通过"})
|
||||
@@ -326,6 +333,9 @@ def bundles() -> dict[str, dict[str, Any]]:
|
||||
events, bound = s.events({"write": weekly}, ending="failed")
|
||||
out["failed"] = build(s, events, bound, run={**RUN, "status": "failed"})
|
||||
|
||||
events, bound = s.events({"write": weekly}, ending="rejected")
|
||||
out["rejected"] = build(s, events, bound, run={**RUN, "status": "cancelled", "end_reason": "rejected"})
|
||||
|
||||
# 早期运行先失败、经继续运行跑到成功:封存范围同样按结束事件推算,到成功结束的前一条为止
|
||||
events, _ = s.events({"write": weekly}, ending="continued")
|
||||
finished = next(seq for seq, etype, _, _ in events if etype == "run.finished")
|
||||
|
||||
@@ -353,6 +353,24 @@ async def test_a_failed_run_is_exportable(client, monkeypatch):
|
||||
assert data_of(resp.text)["meta"]["status"] == "failed"
|
||||
|
||||
|
||||
async def test_a_rejected_run_carries_its_end_reason(client, monkeypatch):
|
||||
"""被审批人驳回结束的运行(cancelled + end_reason=rejected,05-已知遗留 (a)):可验证报告和报告导出的元信息都带上
|
||||
end_reason,页面照它写「被驳回」,不写成手动停止的「已取消」。"""
|
||||
row = await sealed_run(monkeypatch)
|
||||
async with SessionLocal() as session:
|
||||
run = await session.get(Run, row.id)
|
||||
run.status, run.end_reason = "cancelled", "rejected"
|
||||
await session.commit()
|
||||
bundle = await client.get(f"/api/runs/{row.id}/bundle.html")
|
||||
assert bundle.status_code == 200, bundle.text
|
||||
meta = data_of(bundle.text)["meta"]
|
||||
assert (meta["status"], meta["end_reason"]) == ("cancelled", "rejected")
|
||||
report = await client.get(f"/api/runs/{row.id}/report.html")
|
||||
assert report.status_code == 200, report.text
|
||||
m = re.search(r'<script type="application/json" id="report-data">(.*?)</script>', report.text, re.S)
|
||||
assert m and json.loads(m.group(1))["meta"]["end_reason"] == "rejected"
|
||||
|
||||
|
||||
async def test_a_broken_seal_is_refused(client, monkeypatch):
|
||||
row = await sealed_run(monkeypatch)
|
||||
async with SessionLocal() as session:
|
||||
|
||||
@@ -340,6 +340,11 @@ def test_meta_names_the_seal_and_the_run():
|
||||
assert meta["manifest_seq"] == 15 and meta["manifest_hash"] == "ab" * 32 and meta["legacy"] is False
|
||||
assert meta["started_at"] == RUN["started_at"] and meta["finished_at"] == RUN["finished_at"]
|
||||
assert meta["exported_at"] == "2026-10-05T08:00:00+00:00"
|
||||
# 同是 cancelled 的被驳回和手动停止靠 end_reason 分开;没有的(成功、失败、老运行)是 None
|
||||
assert meta["end_reason"] is None
|
||||
rejected = build_bundle({**RUN, "status": "cancelled", "end_reason": "rejected"}, sample_events(), manifest_seq=15,
|
||||
manifest_hash="ab" * 32, graph=GRAPH, read=STORE.get, exported_at="x")
|
||||
assert (rejected["meta"]["status"], rejected["meta"]["end_reason"]) == ("cancelled", "rejected")
|
||||
# 出具档位取封存范围内最后一条出具判定事件,带上序号,浏览器拿已核对的事件对照
|
||||
assert meta["tier"] == {"value": "formal", "seq": 13}
|
||||
assert bundle["nodes"]["write"] == {"label": "写周报", "type": "report"}
|
||||
@@ -463,7 +468,7 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
|
||||
|
||||
rendered = pages()
|
||||
got = {name: json.loads(data_block(page)) for name, page in rendered.items()}
|
||||
assert set(got) == {"base", "two-reports", "metric-only", "legacy", "failed", "continued", "degraded",
|
||||
assert set(got) == {"base", "two-reports", "metric-only", "legacy", "failed", "rejected", "continued", "degraded",
|
||||
"missing-doc", "missing-card", "nan", "stray", "masked-card", "masked-old-doc", "masked-source",
|
||||
"formats", "formats-unit", "prose"}
|
||||
for name, bundle in got.items():
|
||||
@@ -504,6 +509,10 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
|
||||
assert [s["text"] for s in segments(only)] and not [s for s in segments(only) if s.get("cite", {}).get("kind") == "cell"]
|
||||
failed = got["failed"]
|
||||
assert failed["meta"]["status"] == "failed" and failed["meta"]["tier"] is None
|
||||
rejected = got["rejected"]
|
||||
assert (rejected["meta"]["status"], rejected["meta"]["end_reason"]) == ("cancelled", "rejected")
|
||||
assert max(rejected["events"], key=lambda e: e["seq"])["type"] == "run.cancelled"
|
||||
assert re.search(r'id="seal-failed"[^>]*\shidden', rendered["rejected"]), "被驳回不是失败:不提重新封存"
|
||||
assert max(failed["events"], key=lambda e: e["seq"])["type"] == "run.failed"
|
||||
# 先失败、经继续运行跑到成功的早期运行:范围里有那条运行失败,之后运行又继续了,最后一条不是结束事件
|
||||
continued = got["continued"]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""可验证报告里被驳回的运行写「被驳回」,不写成手动停止的「已取消」(05-已知遗留 (a)、总方案 X9)。
|
||||
|
||||
人的驳回让运行结束在 cancelled + end_reason=rejected,手动停止也是 cancelled(end_reason=stopped)。封存里二者是同一种
|
||||
结束事件(运行取消),分开它们的原因在那条事件的内容里,内容不随文件附带。所以:
|
||||
|
||||
- 运行结果照运行记录(元信息的 end_reason)写「被驳回」,核对的仍是「最后一条结束类事件是运行取消」这一半,旁边写明
|
||||
被驳回取自运行记录,不把它说成已核对出来的;
|
||||
- 手动停止、老运行(没有 end_reason)照旧「已取消」;
|
||||
- 文件里写着被驳回、最后一条事件却是运行结束的,照旧不通过,点名文件里写的「被驳回」。
|
||||
|
||||
用 node 直接载入 verifier.js(同 test_bundle_shown.py),只看运行信息(⑥)这一块。数据全是虚构的。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import app.bundle
|
||||
from tests.bundle_samples import RUN, Sample, WEEKLY, build, bundles
|
||||
|
||||
NODE = shutil.which("node")
|
||||
VERIFIER = Path(app.bundle.__file__).parent / "verifier.js"
|
||||
|
||||
pytestmark = pytest.mark.skipif(NODE is None, reason="没有 node,无法执行页面上的核对脚本")
|
||||
|
||||
#: 载入 verifier.js(node 里没有 document,它把核对用的纯函数交给 module.exports),交回运行信息每一项
|
||||
HARNESS = r"""
|
||||
const fs = require('fs')
|
||||
const { createHash } = require('crypto')
|
||||
const input = JSON.parse(fs.readFileSync(0, 'utf8'))
|
||||
const mod = { exports: {} }
|
||||
new Function('module', fs.readFileSync(process.argv[1], 'utf8'))(mod)
|
||||
const digest = async (bytes) => createHash('sha256').update(bytes).digest('hex')
|
||||
mod.exports.verify(input.data, digest, input.data.meta.manifest_hash).then((out) => {
|
||||
process.stdout.write(JSON.stringify(out.meta.map((m) => ({ field: m.field, shown: m.shown, claim: m.claim,
|
||||
result: m.result, reason: m.reason || '', mark: m.mark ?? null }))))
|
||||
}, (e) => { console.error((e && e.stack) || String(e)); process.exit(1) })
|
||||
"""
|
||||
|
||||
|
||||
def status_of(bundle: dict[str, Any]) -> dict[str, Any]:
|
||||
done = subprocess.run([NODE, "-e", HARNESS, str(VERIFIER)], input=json.dumps({"data": bundle}),
|
||||
capture_output=True, text=True, timeout=120)
|
||||
assert done.returncode == 0, done.stderr[-2000:]
|
||||
return next(m for m in json.loads(done.stdout) if m["field"] == "status")
|
||||
|
||||
|
||||
def ended(end_reason: str | None) -> dict[str, Any]:
|
||||
"""同一串事件(最后一条是运行取消),运行记录分别写被驳回、手动停止、没写(老运行)。"""
|
||||
s = Sample()
|
||||
events, bound = s.events({"write": s.doc(WEEKLY, "write")}, ending="rejected")
|
||||
return build(s, events, bound, run={**RUN, "status": "cancelled", "end_reason": end_reason})
|
||||
|
||||
|
||||
def test_a_rejected_run_reads_rejected_and_says_which_half_is_checked():
|
||||
got = status_of(bundles()["rejected"])
|
||||
assert got["result"] == "pass" and got["shown"] == "被驳回" and got["claim"] == "被驳回", got
|
||||
assert "运行取消" in got["reason"] and "取自运行记录" in got["reason"], got["reason"]
|
||||
# 旁边那一句不说成全核对过了
|
||||
assert got["mark"] == "已核对运行取消,被驳回取自运行记录", got
|
||||
|
||||
|
||||
@pytest.mark.parametrize("end_reason", ["stopped", None])
|
||||
def test_a_stopped_or_old_cancelled_run_still_reads_cancelled(end_reason):
|
||||
got = status_of(ended(end_reason))
|
||||
assert (got["result"], got["shown"], got["claim"], got["mark"]) == ("pass", "已取消", "已取消", None), got
|
||||
|
||||
|
||||
def test_a_file_claiming_rejected_over_a_finished_run_fails_and_names_the_claim():
|
||||
forged = copy.deepcopy(bundles()["base"])
|
||||
forged["meta"].update(status="cancelled", end_reason="rejected")
|
||||
got = status_of(forged)
|
||||
assert got["result"] == "fail" and got["shown"] == "已完成" and "被驳回" in got["reason"], got
|
||||
@@ -17,7 +17,7 @@
|
||||
// 口径卡、快照对出来(显示文字按显示规则重新渲染后逐字比对)不通过,原因点名正文显示的字和引用的值,报告标题旁的徽标
|
||||
// 跟着不通过;
|
||||
// - 深浅色、窄屏:390 宽不横向溢出,点开的数字面板在窄屏上从底部抽出,深色下底是深的、字是浅的;
|
||||
// - 样本页:tests/bundle_samples.py 用虚构数据真实生成的几份页面(遮罩快照、两份报告、早期运行、失败结束、先失败后
|
||||
// - 样本页:tests/bundle_samples.py 用虚构数据真实生成的几份页面(遮罩快照、两份报告、早期运行、失败结束、被驳回结束(写「被驳回」、写明取自运行记录)、先失败后
|
||||
// 经继续运行跑到成功的早期运行、两条出具
|
||||
// 判定、服务端缺文件、裸 NaN、别的运行的文档、口径卡有输入取自遮罩列(卡的节点完成事件只给哈希)、同一张卡配上证据
|
||||
// 目录里写着号码的老文档(文档只给哈希、页面写明原因)、设了遮罩的数据源上按地区汇总的查询、每种显示
|
||||
@@ -611,7 +611,7 @@ await section('样本页:各种形状', async () => {
|
||||
check('生成样本页(tests/bundle_samples.py)', false, String(e?.message ?? e))
|
||||
return
|
||||
}
|
||||
check('生成样本页(tests/bundle_samples.py)', Object.keys(samples).length === 17, Object.keys(samples).join('、'))
|
||||
check('生成样本页(tests/bundle_samples.py)', Object.keys(samples).length === 18, Object.keys(samples).join('、'))
|
||||
|
||||
await sample('base', async (s, page) => {
|
||||
check('base:有无法核对的项,总览是中性的「可核对的项全部通过,另有 N 项无法核对」,不是绿色的通过', neutral(s),
|
||||
@@ -734,6 +734,15 @@ await section('样本页:各种形状', async () => {
|
||||
await sample('base', async (_, page) => {
|
||||
check('成功结束的运行不提重新封存', await page.locator('#seal-failed').isHidden())
|
||||
})
|
||||
// 被审批人驳回结束(cancelled + end_reason=rejected):写「被驳回」,不写成手动停止的「已取消」;封存里只核对得到
|
||||
// 「运行取消」这一半,旁边写明被驳回取自运行记录。不是失败,不提重新封存
|
||||
await sample('rejected', async (s, page) => {
|
||||
check('rejected:运行结果写「被驳回」,与运行取消事件对上', s.meta.status?.result === 'pass'
|
||||
&& s.meta.status.text.includes('被驳回') && !s.meta.status.text.includes('已取消'), JSON.stringify(s.meta.status))
|
||||
check('rejected:写明被驳回取自运行记录', s.meta.status?.text.includes('取自运行记录')
|
||||
&& hasItem(s, 'meta', 'pass', '取自运行记录'), JSON.stringify(s.meta.status))
|
||||
check('rejected:不提重新封存', await page.locator('#seal-failed').isHidden())
|
||||
})
|
||||
// 早期运行先失败、经继续运行跑到成功:范围里那条运行失败之后运行又继续了,它不是运行的结束;成功结束的那条在范围外
|
||||
await sample('continued', async (s) => {
|
||||
check('continued:总览中性,运行结果无法核对(成功结束的那条不在封存范围内),不拿中途那条失败去对',
|
||||
|
||||
Reference in New Issue
Block a user