mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 18:31:59 +00:00
出具横幅把核查缺口分组,审批卡把运行类别交给反方审稿:IssuanceBanner(运行面板成果区、问数据回答、证据页同一个)把双盲复算、反方审稿报上来的缺口另起「核查未通过」「核查未完成」两组,排在「校验未全部完成」上面,先后同时间线和成果节点印章;证据页去掉另列一遍的 IssuanceWithChecks 包装,直接用横幅。RunPanel 的审批卡把 approval.run_class 传给 ChallengeSummary,运行的 GET 失败也照样写「出具会降档」、不另取。check-stream 补横幅分组的断言(新预览用例 issued-verify),check-verify 的 verify-gaps 改读横幅里的新组,check-challenge 补整张审批卡带运行类别的用例
This commit is contained in:
@@ -31,8 +31,9 @@ import fx from './challenge.json'
|
||||
* 卡上按「无法判断」的句数说,两个数不打架
|
||||
* - ?syn=challenge-approval-offpath:正式运行,按降档处置,但反方审稿不在带出具契约的成果节点上游(degrades 为假)
|
||||
* - ?syn=challenge-summary-given:反方审稿那一块单独画,审批自带的运行类别(正式运行)由调用方传进来;检查脚本让这次运行的
|
||||
* GET 失败,卡上照样写「出具会降档」,也不另发这次请求。审批卡(RunPanel 的 ApprovalCard)把 approval.run_class
|
||||
* 传给它之后,卡上走的就是这条路
|
||||
* GET 失败(500),卡上照样写「出具会降档」,也不另发这次请求
|
||||
* - ?syn=challenge-approval-given:同一次运行(GET 同样 500),整张审批卡(RunPanel 的 ApprovalCard):审批带着
|
||||
* run_class,卡把它传给反方审稿那一块,照它说出具、不另取
|
||||
* - ?syn=challenge-approval-dup:同 many,工件里补出来的一句和条目里的一句原文相同、unit 不同(工件由检查脚本合成)
|
||||
* - ?syn=challenge-approval-norecord:同 many,审稿记录没存上(artifact 为 null)
|
||||
*
|
||||
@@ -68,9 +69,8 @@ function Card({ approval, name, dense = false }: { approval: Approval; name: str
|
||||
}
|
||||
|
||||
const RUNS = fx.runs
|
||||
/** 期 4 起服务端另带的两项:unreviewed(没能反查的句数)、degrades(被推翻的会不会让出具降档) */
|
||||
type Challenge = ApprovalChallenge & { unreviewed?: number; degrades?: boolean }
|
||||
const withChallenge = (c: Partial<Challenge>): ApprovalReport => ({ ...report, challenge: { ...challenge, ...c } as ApprovalChallenge })
|
||||
/** 期 4 起服务端另带的两项(unreviewed 没能反查的句数、degrades 被推翻的会不会让出具降档)类型里都有,按需盖上 */
|
||||
const withChallenge = (c: Partial<ApprovalChallenge>): ApprovalReport => ({ ...report, challenge: { ...challenge, ...c } })
|
||||
const many = fx.many as unknown as ApprovalChallenge
|
||||
/** 没有被推翻的结论:同第一个用例去掉被推翻的那句,计数跟着改 */
|
||||
const clear: Partial<ApprovalChallenge> = {
|
||||
@@ -82,7 +82,7 @@ const clear: Partial<ApprovalChallenge> = {
|
||||
* 记为无法判断,unreviewed 不会多于 inconclusive)。原来无法判断的 u11 是查过了、数据不够,不算未能反查
|
||||
*/
|
||||
const UNREVIEWED_UNITS = ['u5', 'u8']
|
||||
const unreviewed: Partial<Challenge> = {
|
||||
const unreviewed: Partial<ApprovalChallenge> = {
|
||||
counts: { stands: 0, weakened: 0, refuted: 1, inconclusive: 3 },
|
||||
items: challenge.items.map((it) => (UNREVIEWED_UNITS.includes(it.unit) ? { ...it, verdict: 'inconclusive' } : it)),
|
||||
artifact: 'f'.repeat(64),
|
||||
@@ -245,6 +245,10 @@ export const cases: Record<string, () => JSX.Element> = {
|
||||
<ChallengeSummary reports={[withChallenge({ degrades: true })]} runId={RUNS.given} runClass="formal" />
|
||||
</div>
|
||||
),
|
||||
'challenge-approval-given': () => (
|
||||
<Card name="challenge-approval-given"
|
||||
approval={approvalOf('ap-chal-17', RUNS.given, 'formal', withChallenge({ degrades: true }))} />
|
||||
),
|
||||
'challenge-approval-offpath': () => (
|
||||
<Card name="challenge-approval-offpath"
|
||||
approval={approvalOf('ap-chal-15', RUNS.offpath, 'formal', withChallenge({ on_refuted: 'degrade', degrades: false, unreviewed: 0 }))} />
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AssistantStream, type StreamTurn } from '../../run/AssistantStream'
|
||||
import { decodeRun } from '../../run/decode'
|
||||
import type { RunEvent } from '../../types'
|
||||
|
||||
/**
|
||||
* 出具横幅里的核查缺口(运行面板成果区、问数据回答用的 run/AssistantStream 的 IssuanceBanner),scripts/check-stream.mjs
|
||||
* 按它检查:
|
||||
* - ?syn=issued-verify:正式运行降档出具。缺口里有一条没通过 SQL 检查的(横幅单列)、双盲复算和反方审稿报上来的几条
|
||||
* (两路结果不一致、结论被推翻、指标无法复算),和一条校验没做完的;先后照服务端(出具契约按上游节点的先后收),核查的和
|
||||
* 别的混在一起。横幅按「核查未通过」「核查未完成」「校验未全部完成」分组,先后同时间线上出具那一行
|
||||
*
|
||||
* 门店、指标、数字都是虚构的。缺口原话照服务端的写法(出具契约给上游节点的缺口加「节点类型「节点名」:」的前缀)
|
||||
*/
|
||||
export const ISSUED_VERIFY = {
|
||||
run: 'syn-issued-verify',
|
||||
sql: '查询「取门店流水」(Q1)未通过 SQL 检查(「门店」关联「流水明细」是一对多,对「门店」的「面积」求和会重复计算),结果不可靠;受影响的引用:指标「坪效」',
|
||||
failed: ['双盲复算「复核月报口径」:指标「客单价」两路结果不一致,审核人选择了盲算结果', '反方审稿:推翻了 1 句结论:「西区客流回升主要来自会员日」'],
|
||||
unfinished: ['双盲复算「复核月报口径」:指标「复购率」无法复算:输入来自合并查询,无法单独复算'],
|
||||
other: ['叙述模板渲染为空(路径可能有误)'],
|
||||
}
|
||||
|
||||
const V = ISSUED_VERIFY
|
||||
const ISSUANCE = {
|
||||
tier: 'degraded', matched_numbers: 4, metrics_checked: 3, missing_required: [], missing_expected: [], unmatched_numbers: [],
|
||||
calibers: [],
|
||||
gaps: [V.failed[0], V.sql, V.other[0], V.unfinished[0], V.failed[1]],
|
||||
sql_checks: [{ query: '查询「取门店流水」(Q1)', node_id: 'fetch', problems: ['「门店」关联「流水明细」是一对多,对「门店」的「面积」求和会重复计算'],
|
||||
refs: ['指标「坪效」'], gap: V.sql }],
|
||||
}
|
||||
const OUTPUT = { 月报: '上月各门店客单价 86.4 元,坪效 1,240 元。', _issuance: ISSUANCE }
|
||||
|
||||
function verifyTurn(): StreamTurn {
|
||||
const at = Date.now() / 1000 - 30
|
||||
const events: RunEvent[] = [
|
||||
{ seq: 1, type: 'run.started', node_id: null, ts: at, data: { nodes: 1 } },
|
||||
{ seq: 2, type: 'node.started', node_id: 'done', ts: at + 1, data: {} },
|
||||
{ seq: 3, type: 'issuance', node_id: 'done', ts: at + 1.2, data: { ...ISSUANCE, unmatched: 0 } },
|
||||
{ seq: 4, type: 'node.finished', node_id: 'done', ts: at + 1.3, data: { duration_ms: 300 } },
|
||||
{ seq: 5, type: 'run.finished', node_id: null, ts: at + 1.4, data: { output: OUTPUT, duration_ms: 1400 } },
|
||||
]
|
||||
return {
|
||||
id: 'issued-verify', question: '上月门店经营月报', phase: 'done', status: '已完成',
|
||||
steps: decodeRun(events, { status: 'succeeded' }), output: OUTPUT, runId: V.run, runClass: 'formal',
|
||||
}
|
||||
}
|
||||
|
||||
export const cases: Record<string, () => JSX.Element> = {
|
||||
'issued-verify': () => (
|
||||
<div className="h-screen" data-fixture="issued-verify"><AssistantStream turns={[verifyTurn()]} /></div>
|
||||
),
|
||||
}
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
issuanceMarks, segmentState, unitVerdict, verdictState, type AuditFilter, type AuditGroup, type AuditRow,
|
||||
} from '../../lib/evidence'
|
||||
import { formatNumber } from '../../lib/format'
|
||||
import { DUAL_CHECK_TEXT, EVIDENCE_AUDIT_TEXT as T, UPGRADE_TEXT } from '../../lib/terms'
|
||||
import { EVIDENCE_AUDIT_TEXT as T, UPGRADE_TEXT } from '../../lib/terms'
|
||||
import { IssuanceBanner } from '../../run/AssistantStream'
|
||||
import { splitIssuanceGaps } from '../../run/decode'
|
||||
import { EvidenceDoc, type EvidenceDocHandle } from '../../run/EvidenceDoc'
|
||||
import { EvidenceGuessView } from '../../run/EvidenceGuess'
|
||||
import { ReportInquiry, inquiryOpen, locateIn, useInquiryAccess } from '../../run/Inquiry'
|
||||
@@ -150,8 +149,13 @@ export function EvidencePane({ run, output, labelOf, refreshKey }: {
|
||||
reports={graph.reports ?? []} labelOf={labelOf} />
|
||||
<UpgradeBanner run={run} mode={mode} />
|
||||
{/* 出具档位和降档原因写在报告上方:降档时只在侧栏小标签里写「SQL 检查未通过」,报告上却是绿色的
|
||||
「10 个数字都有出处」,读的人会以为一切正常。SQL 检查导致的降档在横幅里单列一块 */}
|
||||
{!!output?._issuance?.tier && <IssuanceWithChecks issuance={output._issuance} runClass={run.run_class} />}
|
||||
「10 个数字都有出处」,读的人会以为一切正常。SQL 检查导致的降档在横幅里单列一块,双盲复算、反方审稿报上来的
|
||||
缺口分「核查未通过」「核查未完成」两组(和问数据、画布右栏同一个横幅) */}
|
||||
{!!output?._issuance?.tier && (
|
||||
<div data-evidence-issuance="">
|
||||
<IssuanceBanner issuance={output._issuance} runClass={run.run_class} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'cited' && reports.map((r) => {
|
||||
const doc = docOf(r.node_id)
|
||||
@@ -322,38 +326,6 @@ function useAudit(runId: string, key: string): AuditState {
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* 出具横幅,外加双盲复算、反方审稿报上来的缺口另起的两组:「核查未通过」(两路结果不一致、结论被推翻)、「核查未完成」
|
||||
* (指标无法复算)。横幅把缺口都列在「校验未全部完成」下,读起来像是校验没做完:这里从横幅里拿出来,紧跟在它下面单列,
|
||||
* 颜色和横幅里的缺口一样(分组见 run/decode.ts 的 splitIssuanceGaps)。导出给预览用例(dev/fixtures/dual_check.tsx)
|
||||
*/
|
||||
export function IssuanceWithChecks({ issuance, runClass }: { issuance: Record<string, any>; runClass?: string }) {
|
||||
const { failed, unfinished, other } = useMemo(
|
||||
() => splitIssuanceGaps(Array.isArray(issuance.gaps) ? issuance.gaps.map(String) : []), [issuance])
|
||||
const verify = failed.length + unfinished.length > 0
|
||||
const shown = useMemo(() => (verify ? { ...issuance, gaps: other } : issuance), [issuance, verify, other])
|
||||
const group = (kind: 'failed' | 'unfinished', gaps: string[]) => gaps.length > 0 && (
|
||||
<div data-evidence-verify-group={kind}>
|
||||
<div className="font-medium">{DUAL_CHECK_TEXT.verifyGaps[kind]}</div>
|
||||
<ul className="mt-0.5 space-y-0.5 pl-2 [overflow-wrap:anywhere]">
|
||||
{gaps.map((g) => <li key={g}>· {g}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="space-y-1.5" data-evidence-issuance="">
|
||||
<IssuanceBanner issuance={shown} runClass={runClass} />
|
||||
{verify && (
|
||||
<div className="space-y-1 rounded-lg border px-2 py-1.5 text-2xs leading-relaxed"
|
||||
style={{ borderColor: 'var(--st-waiting)', color: 'var(--st-waiting)' }} data-evidence-verify-gaps="">
|
||||
{group('failed', failed)}
|
||||
{group('unfinished', unfinished)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 顶上一行:这次运行按哪种方式展示证据、封存核对的结果、每份报告文档的哈希 */
|
||||
function SummaryBar({ mode, seal, message, reports, labelOf }: {
|
||||
mode?: string
|
||||
|
||||
@@ -16,11 +16,12 @@ import { explainRunError } from '../lib/explain'
|
||||
import { formatClock, formatDuration, formatNumber, NONE, shortId } from '../lib/format'
|
||||
import { statusLabel } from '../lib/status'
|
||||
import {
|
||||
COPILOT_CONTEXT_TEXT, EVIDENCE_TEXT, JUDGE_TEXT, MERGE_TEXT, RUN_SQL_CHECK_TEXT, evidenceTally, issuanceLabel, nodeTypeLabel,
|
||||
COPILOT_CONTEXT_TEXT, DUAL_CHECK_TEXT, EVIDENCE_TEXT, JUDGE_TEXT, MERGE_TEXT, RUN_SQL_CHECK_TEXT, evidenceTally, issuanceLabel,
|
||||
nodeTypeLabel,
|
||||
} from '../lib/terms'
|
||||
import { SqlCheckList } from './SqlChecks'
|
||||
import {
|
||||
ISSUANCE_HINT, childrenByExec, compactSteps, parseQueryResult, progressOf, spread, teamVerdictOf,
|
||||
ISSUANCE_HINT, childrenByExec, compactSteps, parseQueryResult, progressOf, splitIssuanceGaps, spread, teamVerdictOf,
|
||||
type Exec, type FixKind, type ResultTable as Table, type Step, type StepKind, type TeamMemberEx,
|
||||
type TeamRun,
|
||||
} from './decode'
|
||||
@@ -2296,6 +2297,22 @@ export function IssuanceSqlChecks({ tier, checks }: { tier: string; checks: Issu
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 出具横幅里双盲复算、反方审稿报上来的一组缺口:failed「核查未通过」、unfinished「核查未完成」。排在「校验未全部完成」
|
||||
* 上面,样子和它一样(同一种提醒色、小标题加圆点列表),一条都没有时不画
|
||||
*/
|
||||
function IssuanceGaps({ kind, title, gaps }: { kind: 'failed' | 'unfinished'; title: string; gaps: string[] }) {
|
||||
if (!gaps.length) return null
|
||||
return (
|
||||
<div className="mt-1.5 text-2xs leading-relaxed" style={{ color: 'var(--st-waiting)' }} data-issuance-verify-gaps={kind}>
|
||||
<div className="font-medium">{title}:</div>
|
||||
<ul className="mt-0.5 space-y-0.5 pl-2 [overflow-wrap:anywhere]">
|
||||
{gaps.map((g) => <li key={g}>· {g}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TIER_META: Record<string, { color: string; soft: string; hint: string }> = {
|
||||
formal: { color: 'var(--st-done)', soft: 'var(--st-done-soft)', hint: ISSUANCE_HINT.formal },
|
||||
degraded: { color: 'var(--st-waiting)', soft: 'var(--st-waiting-soft)', hint: ISSUANCE_HINT.degraded },
|
||||
@@ -2312,7 +2329,9 @@ const TIER_META: Record<string, { color: string; soft: string; hint: string }> =
|
||||
*
|
||||
* 降档的原因必须写在这里:以前只列缺指标和回指不上的数字,而「叙述模板渲染
|
||||
* 为空」「指标集为空」这类校验根本没跑起来的缺口(gaps)一条都不显示——横幅
|
||||
* 说「请对照下方声明」,下方什么都没有。
|
||||
* 说「请对照下方声明」,下方什么都没有。缺口分几块:没通过 SQL 检查的单列在最前,
|
||||
* 双盲复算、反方审稿报上来的分「核查未通过」「核查未完成」,其余的归「校验未全部完成」。
|
||||
* 运行详情的证据页、问数据的回答、画布右栏用的都是这一个横幅,三处说法一样。
|
||||
*/
|
||||
export function IssuanceBanner({ issuance, runClass, evidence }: {
|
||||
issuance: any; runClass?: string
|
||||
@@ -2331,7 +2350,11 @@ export function IssuanceBanner({ issuance, runClass, evidence }: {
|
||||
const sqlChecks: IssuanceSqlCheck[] = (Array.isArray(issuance?.sql_checks) ? issuance.sql_checks : [])
|
||||
.filter((x: unknown): x is IssuanceSqlCheck => !!x && typeof x === 'object')
|
||||
const sqlGaps = new Set(sqlChecks.map((x) => x.gap).filter(Boolean))
|
||||
const gaps: string[] = (Array.isArray(issuance?.gaps) ? issuance.gaps.map(String) : []).filter((g: string) => !sqlGaps.has(g))
|
||||
// 其余缺口里双盲复算、反方审稿报上来的另起两组:「核查未通过」(两路结果不一致、结论被推翻)、「核查未完成」(指标无法
|
||||
// 复算),排在「校验未全部完成」上面,先后同时间线、证据页、成果节点的印章(分组见 decode.ts 的 splitIssuanceGaps)。
|
||||
// 不分的话核查查出的问题读着像校验没做完,没核成的数又像被查出了错
|
||||
const { failed: verifyFailed, unfinished: verifyUnfinished, other: gaps } = splitIssuanceGaps(
|
||||
(Array.isArray(issuance?.gaps) ? issuance.gaps.map(String) : []).filter((g: string) => !sqlGaps.has(g)))
|
||||
const calibers: any[] = issuance?.calibers ?? []
|
||||
const matched = Number(issuance?.matched_numbers ?? 0)
|
||||
// 逐个数字的出处(matched[].caliber 形如「口径名 @ v2」):有它就能按卡数清楚,
|
||||
@@ -2404,6 +2427,8 @@ export function IssuanceBanner({ issuance, runClass, evidence }: {
|
||||
)}
|
||||
|
||||
{!!sqlChecks.length && <IssuanceSqlChecks tier={tier} checks={sqlChecks} />}
|
||||
<IssuanceGaps kind="failed" title={DUAL_CHECK_TEXT.verifyGaps.failed} gaps={verifyFailed} />
|
||||
<IssuanceGaps kind="unfinished" title={DUAL_CHECK_TEXT.verifyGaps.unfinished} gaps={verifyUnfinished} />
|
||||
{!!gaps.length && (
|
||||
<div className="mt-1.5 text-2xs leading-relaxed" style={{ color: 'var(--st-waiting)' }} data-issuance-gaps="">
|
||||
<div className="font-medium">{RUN_SQL_CHECK_TEXT.otherGaps}</div>
|
||||
|
||||
@@ -157,7 +157,7 @@ export function ApprovalCard({ approval, onResolved, showWorkflow = true }: {
|
||||
|
||||
<ApprovalReports reports={reports} runId={approval.run_id} runClass={approval.run_class}
|
||||
approvalId={approval.id} askable={approval.status === 'pending'} />
|
||||
<ChallengeSummary reports={reports} runId={approval.run_id} />
|
||||
<ChallengeSummary reports={reports} runId={approval.run_id} runClass={approval.run_class} />
|
||||
|
||||
{payload.tool && (
|
||||
<div className="mb-2 rounded border bg-bg p-2">
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//
|
||||
// 守的是这些「看着没坏、其实用不了」的地方:概况的句数和条目对不上、有被推翻的结论却不提醒降档(或者原因只藏在悬停里)、
|
||||
// 没有被推翻的结论也写降档、只标注时也写降档、探索运行里说出具、运行类别取不到时说起出具(取不到就不知道是不是正式
|
||||
// 运行,一句和出具有关的话都不说,窗口重新拿到焦点时再取)、审批带着运行类别时还另取一次(取失败了降档提示就没了)、
|
||||
// 运行,一句和出具有关的话都不说,窗口重新拿到焦点时再取)、审批带着运行类别时还另取一次(取失败了降档提示就没了;
|
||||
// 整张审批卡得把 approval.run_class 交给反方审稿那一块)、
|
||||
// 反方审稿不在出具路径上(服务端的 degrades 为假)还写降档、说明里用「本节点」(画在审批卡上,读着像审批节点)、
|
||||
// 反查没完成的几句混在「无法判断」里看不出来(或者比「无法判断」还多、插在降档标记和它的说明中间)、
|
||||
// 没经过反方审稿的报告也冒出一块空框(或者白发一次请求)、点开一句不取工件(或者每点一句取一次)、理由里的数字看不出引用了哪次查询、
|
||||
@@ -43,7 +44,7 @@ const DUP_CONTENT = {
|
||||
claims: fx.artifacts[MANY].claims.map((c) => (c.unit === 'u30' ? { ...c, text: fx.many.items.find((it) => it.unit === 'u20').text } : c)),
|
||||
}
|
||||
const RUNS = fx.runs
|
||||
// RUNS.given 不在这里:那次运行的 GET 一律答 404,审批带着的运行类别得照样管用
|
||||
// RUNS.given 不在这里:那次运行的 GET 一律答 500,审批带着的运行类别得照样管用
|
||||
const RUN_CLASS = {
|
||||
[RUNS.formal]: 'formal', [RUNS.flag]: 'formal', [RUNS.explore]: 'exploratory', [RUNS.none]: 'formal',
|
||||
[RUNS.many]: 'formal', [RUNS.broken]: 'formal', [RUNS.clear]: 'formal', [RUNS.clearFlag]: 'formal',
|
||||
@@ -178,6 +179,7 @@ async function open(syn, { width = 1100, height = 900, theme = 'dark' } = {}) {
|
||||
const id = decodeURIComponent(run[1])
|
||||
hits.runs.push(id)
|
||||
if (id === RUNS.runfail && state.runFail) return route.fulfill({ status: 500, json: { detail: '服务暂时不可用' } })
|
||||
if (id === RUNS.given) return route.fulfill({ status: 500, json: { detail: '服务暂时不可用' } })
|
||||
return RUN_CLASS[id] ? route.fulfill({ json: { id, run_class: RUN_CLASS[id], status: 'interrupted' } })
|
||||
: route.fulfill({ status: 404, json: { detail: '运行不存在' } })
|
||||
}
|
||||
@@ -193,9 +195,12 @@ const shot = async (page, name) => {
|
||||
mkdirSync(SHOTS, { recursive: true })
|
||||
await page.screenshot({ path: `${SHOTS}/${name}.png`, fullPage: true })
|
||||
}
|
||||
/** 等运行类别答完、界面画上:否定的断言(不写降档)得在这之后做 */
|
||||
async function runClassSettled(page, hits, runId) {
|
||||
for (let i = 0; i < 50 && !hits.runs.includes(runId); i++) await page.waitForTimeout(100)
|
||||
/**
|
||||
* 等界面画定:否定的断言(不写降档)得在这之后做。审批带着运行类别时卡上照它说、不另取,画上就是定了;老服务端的审批
|
||||
* 不带运行类别(fetches 为真),要等卡上按运行 id 取的那一次答完
|
||||
*/
|
||||
async function runClassSettled(page, hits, runId, { fetches = false } = {}) {
|
||||
if (fetches) for (let i = 0; i < 50 && !hits.runs.includes(runId); i++) await page.waitForTimeout(100)
|
||||
await page.waitForTimeout(400)
|
||||
}
|
||||
const item = (page, unit) => page.locator(`[data-challenge-item="${unit}"]`)
|
||||
@@ -232,7 +237,7 @@ await section('审批卡:概况与降档提示', async () => {
|
||||
check('每句是一个收起的按钮', await block.locator('[data-challenge-toggle][aria-expanded="false"]').count() === 4)
|
||||
check('没点开之前也说清判断出自反方模型', (await text(block.locator('[data-challenge-judged]'))) === '判断由反方模型给出,并非系统核对')
|
||||
check('没点开之前不取工件', hits.artifacts.length === 0, hits.artifacts.join('、'))
|
||||
check('运行类别按运行 id 取了一次', hits.runs.filter((r) => r === RUNS.formal).length === 1, hits.runs.join('、'))
|
||||
check('审批带着运行类别:照它说,不另取', !hits.runs.includes(RUNS.formal), hits.runs.join('、'))
|
||||
await shot(page, 'approval-dark')
|
||||
check('页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
@@ -377,7 +382,7 @@ await section('运行类别取不到:不说任何和出具有关的话,重
|
||||
const { ctx, page, hits, state, errors } = await open('challenge-approval-runfail')
|
||||
const block = page.locator('[data-challenge="write"]')
|
||||
await block.waitFor()
|
||||
await runClassSettled(page, hits, RUNS.runfail)
|
||||
await runClassSettled(page, hits, RUNS.runfail, { fetches: true })
|
||||
const quiet = async () => {
|
||||
const all = await text(block)
|
||||
return await page.locator('[data-challenge-degrade], [data-challenge-degrade-note], [data-challenge-degrade-off], '
|
||||
@@ -406,7 +411,7 @@ await section('运行类别取不到:不说任何和出具有关的话,重
|
||||
await section('审批带着运行类别:照它说出具,不另取(另取失败也不影响)', async () => {
|
||||
// 审批自带 approval.run_class(卡顶写的就是它)。以前反方审稿那一块只拿运行 id 另取一次,这一次失败了,正式运行里
|
||||
// 「出具会降档」整块消失,卡顶却照样写着正式运行。用例把反方审稿那一块单独画,运行类别由调用方传进来,这次运行的
|
||||
// GET 一律答 404
|
||||
// GET 一律答 500
|
||||
const { ctx, page, hits, errors } = await open('challenge-summary-given')
|
||||
const block = page.locator('[data-challenge="write"]')
|
||||
await block.waitFor()
|
||||
@@ -418,6 +423,22 @@ await section('审批带着运行类别:照它说出具,不另取(另取
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('整张审批卡:把审批带着的运行类别交给反方审稿那一块,运行的 GET 失败也照样写降档、不另取', async () => {
|
||||
// RunPanel 的 ApprovalCard 得把 approval.run_class 传给 ChallengeSummary(同 ApprovalReports)。不传的话那一块按运行 id
|
||||
// 另取一次:这里那次 GET 一律答 500,正式运行里「出具会降档」整块消失,卡顶却照样写着正式运行
|
||||
const { ctx, page, hits, errors } = await open('challenge-approval-given')
|
||||
const block = page.locator('[data-challenge="write"]')
|
||||
await block.waitFor()
|
||||
await page.waitForTimeout(500)
|
||||
check('卡顶照审批写正式运行', await page.locator('[data-approval-run-class="formal"]').count() === 1)
|
||||
const degrade = page.locator('[data-challenge-degrade]')
|
||||
check('……反方审稿那一块照样写「出具会降档」和原因', await degrade.count() === 1 && (await text(degrade)) === '出具会降档'
|
||||
&& (await text(page.locator('[data-challenge-degrade-note]'))) === DEGRADE_NOTE, await text(block))
|
||||
check('……不另取这次运行的类别(那次 GET 答 500)', !hits.runs.includes(RUNS.given), hits.runs.join('、') || '一次都没取')
|
||||
check('……页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('有句子反查没完成:单独说一行,不混在「无法判断」里,不插在降档标记和它的说明中间', async () => {
|
||||
// 服务端的 unreviewed:核查模型不可用、触顶没给出结论、数据源不在了……这几句记为无法判断,可反查没有完成。「无法判断
|
||||
// 3 句」分不出是查了、数据不够,还是没查成,审批卡单独说。未能反查的每一句都记为无法判断,unreviewed 不会多于它
|
||||
|
||||
@@ -1411,6 +1411,47 @@ await section('放弃、结构化的报错、恢复的轮次、出具横幅(RE
|
||||
await one.page.close()
|
||||
})
|
||||
|
||||
await section('出具横幅:核查报上来的缺口另起「核查未通过」「核查未完成」,不归在「校验未全部完成」下,先后同时间线', async () => {
|
||||
// 双盲复算、反方审稿报上来的缺口(出具契约加了「双盲复算「节点名」:」「反方审稿:」的前缀)以前全列在「校验未全部
|
||||
// 完成」下:两路结果不一致、结论被推翻读着像校验没做完。时间线、证据页、成果节点的印章早就分成「核查未通过」「核查未
|
||||
// 完成」「校验未全部完成」三组(印章的悬停说明最后一行还指向成果区),运行面板成果区、问数据回答的横幅得同样分、同样的
|
||||
// 先后。用例 issued-verify(dev/fixtures/issuance.tsx):缺口的先后照服务端,核查的和别的混在一起,另有一条没通过
|
||||
// SQL 检查的(横幅单列)
|
||||
for (const [w, label] of [[1100, '宽栏'], [390, '390px']]) {
|
||||
const { page, errors } = await open('syn=issued-verify', { w, h: 900, name: w === 1100 ? 'issued-verify' : undefined })
|
||||
const V = await page.evaluate(async () => (await import('/src/dev/fixtures/issuance.tsx')).ISSUED_VERIFY)
|
||||
const banner = page.locator('[data-issuance-banner=degraded]')
|
||||
await banner.waitFor()
|
||||
const blocks = await banner.evaluate((el) => [...el.querySelectorAll('[data-issuance-verify-gaps], [data-issuance-gaps]')]
|
||||
.map((g) => ({
|
||||
kind: g.getAttribute('data-issuance-verify-gaps') ?? 'other', head: g.firstElementChild?.textContent.trim(),
|
||||
items: [...g.querySelectorAll('li')].map((li) => li.textContent.replace(/^·\s*/, '').trim()),
|
||||
})))
|
||||
const want = [{ kind: 'failed', head: '核查未通过:', items: V.failed }, { kind: 'unfinished', head: '核查未完成:', items: V.unfinished },
|
||||
{ kind: 'other', head: '校验未全部完成:', items: V.other }]
|
||||
check(`${label}:横幅里先「核查未通过」、再「核查未完成」、最后「校验未全部完成」,各列各的`,
|
||||
JSON.stringify(blocks) === JSON.stringify(want), JSON.stringify(blocks))
|
||||
const rest = (await banner.locator('[data-issuance-gaps]').innerText().catch(() => '')).replace(/\s+/g, ' ')
|
||||
check(`${label}:……核查报上来的缺口一条都不在「校验未全部完成」下`,
|
||||
![...V.failed, ...V.unfinished].some((g) => rest.includes(g)) && !/双盲复算|反方审稿/.test(rest), rest)
|
||||
const sql = (await banner.locator('[data-issuance-sql-checks]').innerText().catch(() => '')).replace(/\s+/g, ' ')
|
||||
check(`${label}:……没通过 SQL 检查的那条照旧单列在最前,不在三组里重复`, sql.includes('查询「取门店流水」(Q1)')
|
||||
&& !blocks.some((b) => b.items.includes(V.sql)), sql)
|
||||
// 时间线上出具那一行:展开后按同样的先后列三组(时间线把「校验未全部完成」那一组叫「校验不完整」)
|
||||
const row = page.locator('[data-turn] [data-step-status]').filter({ hasText: '降档出具' }).first()
|
||||
await row.locator('button').first().click()
|
||||
await page.waitForTimeout(150)
|
||||
const line = (await row.innerText()).replace(/\s+/g, ' ')
|
||||
const at = (s) => line.indexOf(s)
|
||||
check(`${label}:……三组的先后和时间线上出具那一行一致`, at('核查未通过') >= 0 && at('核查未通过') < at('核查未完成')
|
||||
&& at('核查未完成') < at('校验不完整'), line)
|
||||
const over = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
|
||||
check(`${label}:页面不横向溢出`, over <= 0, `${over}px`)
|
||||
check(`${label}:没有运行时报错`, errors.length === 0, errors.join(' | '))
|
||||
await page.close()
|
||||
}
|
||||
})
|
||||
|
||||
await section('吸顶的轮次头贴着顶边(REQ-8)', async () => {
|
||||
for (const [q, w, h, label] of [['syn=mixed', 1024, 768, '宽栏 1024'], ['syn=mixed', 1180, 800, '宽栏 1180'],
|
||||
['syn=mixed&dense=1', 380, 700, '窄栏']]) {
|
||||
|
||||
+25
-17
@@ -20,8 +20,8 @@
|
||||
// 真写一遍的结果);没有合并预告时按其余指标当前的选择推,被目录挡下的同项指标也算进来(说法不同就说都不记,说不准的
|
||||
// 只说可能),结尾换成以时间线上的记录为准;盲算的理由按核查 agent 组装的理由文档画,有出处的数字画实线、裸数字醒目
|
||||
// 标出,老工件照旧纯文本;
|
||||
// - 出具缺口:证据页(真的 EvidencePane)和成果节点印章的悬停说明把双盲复算、反方审稿报上来的缺口另起「核查未通过」
|
||||
// 「核查未完成」两组,不放在「校验未全部完成」下;
|
||||
// - 出具缺口:证据页的出具横幅(真的 EvidencePane 里那一个 IssuanceBanner,和问数据、画布右栏同一个)和成果节点印章的
|
||||
// 悬停说明把双盲复算、反方审稿报上来的缺口另起「核查未通过」「核查未完成」两组,不放在「校验未全部完成」下;
|
||||
// - 记录页的工件列表:双盲复算对照、反方审稿记录两种工件有名字和说明,不露键名;
|
||||
// - 检查器:口径卡、指标、口径说明、盲算模型、相对误差、未采用原流程时、启用,每一项都真的写进节点配置;字段的
|
||||
// 叫法和服务端校验报错里的(engine/labels.py 的 DUAL_CHECK_FIELD_LABEL)一字不差;选口径卡、盲算模型、启用三态用的是
|
||||
@@ -1098,11 +1098,12 @@ await section('审批卡:390px 不横向溢出、窄栏里上下排、深浅
|
||||
await shot(rail.page, 'approval-rail-dark')
|
||||
})
|
||||
|
||||
await section('出具缺口:证据页、成果节点的印章把核查报上来的缺口另起两组,不放在「校验未全部完成」下', async () => {
|
||||
await section('出具缺口:证据页的出具横幅、成果节点的印章把核查报上来的缺口另起两组,不放在「校验未全部完成」下', async () => {
|
||||
// 双盲复算、反方审稿报上来的缺口(出具契约加了「双盲复算「节点名」:」「反方审稿:」的前缀)以前和「叙述模板渲染为空」
|
||||
// 一起列在「校验未全部完成」下。证据页(真的 EvidencePane)在出具横幅下面另起「核查未通过」(两路不一致、结论被推翻)
|
||||
// 「核查未完成」(无法复算)两组,横幅里只留校验没做完的;成果节点印章的悬停说明同样分组、同样的先后。用例
|
||||
// verify-gaps(dev/fixtures/dual_check.tsx):缺口的先后照服务端,核查的和别的混在一起;证据图、审计接口在这里伪造
|
||||
// 一起列在「校验未全部完成」下。证据页(真的 EvidencePane)的出具横幅里另起「核查未通过」(两路不一致、结论被推翻)
|
||||
// 「核查未完成」(无法复算)两组,排在「校验未全部完成」上面,那一组只留校验没做完的;横幅外面不再另列一遍。成果节点
|
||||
// 印章的悬停说明同样分组、同样的先后。用例 verify-gaps(dev/fixtures/dual_check.tsx):缺口的先后照服务端,核查的和
|
||||
// 别的混在一起;证据图、审计接口在这里伪造
|
||||
const RUN = 'run-verify-gaps'
|
||||
const FAILED = ['双盲复算「复核周报口径」:指标「销售额」两路结果不一致,审核人选择了盲算结果', '反方审稿:推翻了 1 句结论:「东区增长主要来自新客」']
|
||||
const UNFINISHED = ['双盲复算「复核周报口径」:指标「订单数」无法复算:输入来自合并查询,无法单独复算']
|
||||
@@ -1118,17 +1119,24 @@ await section('出具缺口:证据页、成果节点的印章把核查报上
|
||||
for (const [width, theme] of [[1280, 'dark'], [390, 'dark'], [390, 'light']]) {
|
||||
const what = `${width}px(${theme})`
|
||||
const { page, errors } = await preview('verify-gaps', { extra, width, height: 900, theme })
|
||||
await page.waitForSelector('[data-evidence-verify-gaps]')
|
||||
const groups = await page.evaluate(() => [...document.querySelectorAll('[data-evidence-verify-group]')].map((g) => ({
|
||||
kind: g.dataset.evidenceVerifyGroup, head: g.firstElementChild?.textContent.trim(),
|
||||
await page.waitForSelector('[data-evidence-issuance] [data-issuance-banner] [data-issuance-verify-gaps]')
|
||||
const groups = await page.evaluate(() => [...document.querySelectorAll('[data-evidence-issuance] [data-issuance-banner] '
|
||||
+ ':is([data-issuance-verify-gaps], [data-issuance-gaps])')].map((g) => ({
|
||||
kind: g.dataset.issuanceVerifyGaps ?? 'other', head: g.firstElementChild?.textContent.trim(),
|
||||
items: [...g.querySelectorAll('li')].map((li) => li.textContent.replace(/^·\s*/, '').trim()),
|
||||
})))
|
||||
check(`${what} 证据页:横幅下面另起两组,先「核查未通过」(两路不一致、被推翻),再「核查未完成」(无法复算)`,
|
||||
JSON.stringify(groups) === JSON.stringify([{ kind: 'failed', head: '核查未通过', items: FAILED },
|
||||
{ kind: 'unfinished', head: '核查未完成', items: UNFINISHED }]), JSON.stringify(groups))
|
||||
check(`${what} 证据页:出具横幅里先「核查未通过」(两路不一致、被推翻),再「核查未完成」(无法复算),最后「校验未全部完成」`,
|
||||
JSON.stringify(groups) === JSON.stringify([{ kind: 'failed', head: '核查未通过:', items: FAILED },
|
||||
{ kind: 'unfinished', head: '核查未完成:', items: UNFINISHED }, { kind: 'other', head: '校验未全部完成:', items: OTHER }]),
|
||||
JSON.stringify(groups))
|
||||
const banner = await page.locator('[data-evidence-issuance] [data-issuance-gaps] li').evaluateAll(items)
|
||||
check(`${what} ……横幅的「校验未全部完成」里只剩校验没做完的那条`, JSON.stringify(banner) === JSON.stringify(OTHER)
|
||||
&& (await text(page.locator('[data-evidence-issuance] [data-issuance-gaps]'))).startsWith('校验未全部完成'), JSON.stringify(banner))
|
||||
const outside = await page.evaluate((all) => [...document.querySelectorAll('[data-evidence-issuance] li')]
|
||||
.filter((li) => !li.closest('[data-issuance-banner]') && all.some((g) => li.textContent.includes(g))).length,
|
||||
[...FAILED, ...UNFINISHED])
|
||||
check(`${what} ……横幅外面不再另列一遍核查的缺口`, outside === 0 && await page.locator('[data-evidence-verify-gaps]').count() === 0,
|
||||
`${outside} 条`)
|
||||
// 成果节点的印章:悬停说明里核查的两组排在「校验未全部完成」前面,各组只列自己的
|
||||
const stamp = page.locator('.react-flow__node[data-id="done"] .nc-stamp')
|
||||
await stamp.waitFor()
|
||||
@@ -1140,8 +1148,7 @@ await section('出具缺口:证据页、成果节点的印章把核查报上
|
||||
&& at('核查未通过:') < at('核查未完成:') && at('核查未完成:') < at('校验未全部完成:'), lines.join(' / '))
|
||||
const w = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
|
||||
check(`${what}:证据页不横向溢出`, w <= 0, `${w}px`)
|
||||
const c = await page.evaluate(() => {
|
||||
const box = document.querySelector('[data-evidence-verify-gaps]')
|
||||
const cs = await page.evaluate(() => [...document.querySelectorAll('[data-evidence-issuance] [data-issuance-verify-gaps]')].map((box) => {
|
||||
const bgOf = (el) => {
|
||||
for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
|
||||
const v = getComputedStyle(n).backgroundColor
|
||||
@@ -1150,9 +1157,10 @@ await section('出具缺口:证据页、成果节点的印章把核查报上
|
||||
return 'rgba(0, 0, 0, 0)'
|
||||
}
|
||||
return { fg: getComputedStyle(box).color, bg: bgOf(box), base: getComputedStyle(document.body).backgroundColor }
|
||||
})
|
||||
const ratio = contrast(c.fg, c.bg, c.base)
|
||||
check(`${what}:核查那两组的字对比度 ≥ 4.5`, ratio >= 4.5, `${ratio.toFixed(2)}(${c.fg} on ${c.bg})`)
|
||||
}))
|
||||
const ratios = cs.map((c) => contrast(c.fg, c.bg, c.base))
|
||||
check(`${what}:核查那两组的字对比度 ≥ 4.5`, ratios.length === 2 && ratios.every((r) => r >= 4.5),
|
||||
cs.map((c, i) => `${ratios[i].toFixed(2)}(${c.fg} on ${c.bg})`).join(';'))
|
||||
await shot(page, `verify-gaps-${width}-${theme}`)
|
||||
check(`${what}:页面没有报错`, errors.length === 0, errors.join(';'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user