mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
审批质询收尾:工件页签认得质询回答;几处问答串要对的审批不同、审批列表取不到时各自照样列出
- 工件名:质询的回答文档以 inquiry_answer 登记在运行名下,工件页签原先直接显示英文原值。ARTIFACT_KIND_LABEL 加「质询 回答」,说明在 meta.question 有值时写「质询「…」的回答」(服务端只记前 40 字,满 40 字的补省略号),没有时写「审批 质询的回答:逐段的片段和每个数字的出处」;文案放在 INQUIRY_TEXT - 问答串的竞态:loadApprovalDocs 发现别处正在取审批列表就直接返回,没把这一处要对的审批记进 asked。请求回来后这一处的 unknown 不变、effect 不再执行,settled 一直为假,头上写着「· N 次」,别的审批里问的那几条一直不显示(一张卡审两份 报告;运行详情的待审批卡和证据页签同时挂着、审批列表失败时)。现在提前返回前也把这一处的 id 并进 asked:列表按运行取, 在路上的那一份本来就带着全部审批,不另发请求 - 预览用例:inquiry-approval-two(一张卡审两份报告)、inquiry-run-waiting(待审批卡和证据页签同时挂着)、 inquiry-run-artifacts(工件页签);夹具补探索运行的工件列表 run_artifacts - check-inquiry:审批列表压住、再答 404,两处的条目都列出(查不到审批的单独成组),头上的次数和列出的条数一致、审批 列表只取一次;工件页签里有「质询回答」的名字和说明、不出现英文的种类名、打开的弹窗标题也是中文;390px 深浅色下不溢出、 看得清
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"//": "审批质询预览用例与 check-inquiry 共用的夹具(frontend/src/dev/fixtures/inquiry.tsx)。报告文档和回答文档由后端 evidence.build_catalog + compose_doc 真实组装(虚构的超市、片区、金额),查询快照照质询工件接口的样子给:遮罩列的值写「已遮罩」,另带 masked。生成脚本不在仓库里:改了文档的形状,照这个思路按后端的输出重新生成",
|
||||
"//": "审批质询预览用例与 check-inquiry 共用的夹具(frontend/src/dev/fixtures/inquiry.tsx)。报告文档和回答文档由后端 evidence.build_catalog + compose_doc 真实组装(虚构的超市、片区、金额),查询快照照质询工件接口的样子给:遮罩列的值写「已遮罩」,另带 masked。生成脚本不在仓库里:改了文档的形状,照这个思路按后端的输出重新生成。run_artifacts 是探索运行的工件列表(照 GET /api/runs/{id}/artifacts 的样子手写,质询回答的 meta 同 engine/inquiry.py 写的 {report, unit, question: 问题的前 40 字}),给运行详情「工件」页签的用例",
|
||||
"runs": {
|
||||
"approve": "run-inq-approve",
|
||||
"formal": "run-inq-formal",
|
||||
@@ -4278,5 +4278,58 @@
|
||||
"masked": []
|
||||
}
|
||||
},
|
||||
"run_artifacts": [
|
||||
{
|
||||
"id": "8cb677489bbaafcafd3b0f0df1939c1ca57cccaa3c065e14c4efe2991a3dd9a9",
|
||||
"kind": "report_doc",
|
||||
"node_id": "write",
|
||||
"size": 18234,
|
||||
"meta": {},
|
||||
"created_at": "2026-10-05T06:30:00"
|
||||
},
|
||||
{
|
||||
"id": "02ca8aeabca59d785d982b321d22ff889cb2da5a037cd4a080407e9044356434",
|
||||
"kind": "query_snapshot",
|
||||
"node_id": null,
|
||||
"size": 1460,
|
||||
"meta": {},
|
||||
"created_at": "2026-10-05T06:44:30"
|
||||
},
|
||||
{
|
||||
"id": "911780ed990217a2a2e7e3424a9408d23b53a524ba5d1812ec677b7d6b5b80aa",
|
||||
"kind": "inquiry_answer",
|
||||
"node_id": null,
|
||||
"size": 2140,
|
||||
"meta": {
|
||||
"report": "write",
|
||||
"unit": "u2",
|
||||
"question": "退款率怎么算的?"
|
||||
},
|
||||
"created_at": "2026-10-05T06:45:00"
|
||||
},
|
||||
{
|
||||
"id": "4ed3d62d2af13bbea40195283f3caf63462ca76bb5d4f7ddf1216d36dd5e0463",
|
||||
"kind": "inquiry_answer",
|
||||
"node_id": null,
|
||||
"size": 2510,
|
||||
"meta": {
|
||||
"report": "write",
|
||||
"unit": null,
|
||||
"question": "枫岭区的销售额是否包含线上订单?如果包含,线上订单按下单门店还是按收货地址归到片"
|
||||
},
|
||||
"created_at": "2026-10-05T06:52:00"
|
||||
},
|
||||
{
|
||||
"id": "1c66d590eac155dec35e2215e20c6e7497e96d0b2d73894eff1dea3fb641bb51",
|
||||
"kind": "inquiry_answer",
|
||||
"node_id": null,
|
||||
"size": 1980,
|
||||
"meta": {
|
||||
"report": "write",
|
||||
"unit": "u2"
|
||||
},
|
||||
"created_at": "2026-10-05T06:58:00"
|
||||
}
|
||||
],
|
||||
"card_artifact": "757e03414694c3b3480236718d32082ba449e7712d9ce99f4aee39ffdc5c3f4e"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, type JSX } from 'react'
|
||||
import { useMemo, useState, type JSX } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ArtifactsPane, useRunArtifacts } from '../../pages/runs/ArtifactsPane'
|
||||
import { EvidencePane } from '../../pages/runs/EvidencePane'
|
||||
import { ApprovalCard } from '../../run/RunPanel'
|
||||
import { emptyTrace } from '../../run/trace'
|
||||
import type { Approval, ApprovalReport, Run } from '../../types'
|
||||
import fx from './inquiry.json'
|
||||
|
||||
@@ -20,12 +22,17 @@ import fx from './inquiry.json'
|
||||
* React 树里排在前面)和右栏;「收起浮层」按钮开关浮层
|
||||
* - ?syn=inquiry-approval-steps:同一份报告先后两张审批(主管审批 → 总监审批),「处理掉这张」换下一张;卡按审批 id
|
||||
* 重新挂(同右栏)。?syn=inquiry-approval-steps-same:同一个卡片实例换审批(同画布的审批浮层,不按 id 重新挂)
|
||||
* - ?syn=inquiry-approval-two:一张卡审两份报告(写月报、写周报),两份报告的问答串各自挂着、共用这次运行的审批列表
|
||||
*
|
||||
* 运行详情的「证据」页签(pages/runs/EvidencePane,是否可问由它按运行状态判断):
|
||||
* - ?syn=inquiry-run-explore:封存了的探索运行,报告上给入口,问答追加在封存之后
|
||||
* - ?syn=inquiry-run-formal:封存了的正式运行,不给入口,审批时问过的照列
|
||||
* - ?syn=inquiry-run-waiting:停在审批上的运行,同运行详情直接打开「证据」页签:时间线里的待审批卡和证据页签同时挂着
|
||||
* 同一份报告的问答串(卡上这张审批里问的直接认,证据页签哪张审批都要对一下)
|
||||
* - ?syn=inquiry-run-artifacts:运行详情的「工件」页签(pages/runs/ArtifactsPane),工件列表里有质询的回答
|
||||
*
|
||||
* 列表、提问、质询工件、报告文档、证据图、审批列表、设置都由检查脚本用 page.route 伪造;直接打开时问答串照实说无法读取。
|
||||
* 列表、提问、质询工件、报告文档、证据图、审批列表、工件列表、设置都由检查脚本用 page.route 伪造;直接打开时问答串照实说
|
||||
* 无法读取。
|
||||
*/
|
||||
|
||||
const RUNS = fx.runs
|
||||
@@ -115,6 +122,36 @@ function RunEvidence({ run, name }: { run: Run; name: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停在审批上的运行,同运行详情直接打开「证据」页签:时间线(一直挂着)里的待审批卡在上,证据页签在下,两处挂着同一份
|
||||
* 报告的问答串。卡上的问答串认得这张审批,证据页签不认任何审批(cardId 为空),两处要为不同的几张审批取审批列表
|
||||
*/
|
||||
function Waiting({ approval, run }: { approval: Approval; run: Run }) {
|
||||
return (
|
||||
<div className="flex min-h-screen min-w-0 flex-col" data-fixture="inquiry-run-waiting">
|
||||
<div className="mx-auto w-full max-w-3xl p-4" data-fixture-card="">
|
||||
<div className="overflow-hidden rounded-lg border" style={{ borderColor: 'var(--warn)' }}>
|
||||
<ApprovalCard approval={approval} onResolved={() => undefined} showWorkflow={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[80vh] min-w-0 border-t" data-fixture-evidence="">
|
||||
<EvidencePane run={run} output={null} labelOf={(id) => (id ? LABELS[id] : undefined)} refreshKey="0" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 运行详情的「工件」页签:工件列表由检查脚本按 inquiry.json 的 run_artifacts 答 */
|
||||
function RunArtifacts({ run }: { run: Run }) {
|
||||
const list = useRunArtifacts(run.id, '0')
|
||||
const trace = useMemo(() => emptyTrace(), [])
|
||||
return (
|
||||
<div className="h-screen min-w-0" data-fixture="inquiry-run-artifacts">
|
||||
<ArtifactsPane list={list} graph={null} trace={trace} labelOf={(id) => (id ? LABELS[id] : undefined)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const cases: Record<string, () => JSX.Element> = {
|
||||
'inquiry-approval': () => (
|
||||
<Card name="inquiry-approval" approval={approvalOf(fx.approval_id, RUNS.approve, 'exploratory', reports.approve)} />
|
||||
@@ -138,6 +175,18 @@ export const cases: Record<string, () => JSX.Element> = {
|
||||
),
|
||||
'inquiry-approval-steps': () => <Steps name="inquiry-approval-steps" keyed />,
|
||||
'inquiry-approval-steps-same': () => <Steps name="inquiry-approval-steps-same" keyed={false} />,
|
||||
'inquiry-approval-two': () => (
|
||||
<Card name="inquiry-approval-two" approval={{
|
||||
...approvalOf(fx.approval_id, RUNS.approve, 'exploratory', reports.approve),
|
||||
payload: { kind: 'human_node', message: '请审核 9 月月报和第 39 周周报。',
|
||||
reports: [reports.approve, { ...reports.approve, node_id: 'write2', label: '写周报' }] },
|
||||
}} />
|
||||
),
|
||||
'inquiry-run-explore': () => <RunEvidence name="inquiry-run-explore" run={runOf(RUNS.explore, 'exploratory')} />,
|
||||
'inquiry-run-formal': () => <RunEvidence name="inquiry-run-formal" run={runOf(RUNS.sealed, 'formal')} />,
|
||||
'inquiry-run-waiting': () => (
|
||||
<Waiting approval={approvalOf(fx.approval_id, RUNS.approve, 'exploratory', reports.approve)}
|
||||
run={{ ...runOf(RUNS.approve, 'exploratory'), status: 'interrupted', manifest_hash: null, manifest_seq: null, finished_at: null }} />
|
||||
),
|
||||
'inquiry-run-artifacts': () => <RunArtifacts run={runOf(RUNS.explore, 'exploratory')} />,
|
||||
}
|
||||
|
||||
@@ -3500,6 +3500,19 @@ export const INQUIRY_TEXT = {
|
||||
snapshotLoading: '正在读取查询结果…',
|
||||
snapshotFailed: '无法读取这次查询的结果',
|
||||
masked: (columns: string) => `${columns} 在数据源中设置了遮罩,不显示原值`,
|
||||
|
||||
// ---- 运行详情「工件」页签:回答文档按工件种类 inquiry_answer 记在运行名下(pages/runs/model.ts 的工件名和说明) ----
|
||||
artifactKind: '质询回答',
|
||||
/**
|
||||
* 服务端在工件的 meta 里只记问题的前 40 字(engine/inquiry.py 的 QUESTION_BRIEF):满 40 字的多半截断过,末尾补省略号,
|
||||
* 免得半句话读成整个问题
|
||||
*/
|
||||
artifactAbout: (question: string) => {
|
||||
const chars = [...question.trim()]
|
||||
return `质询「${chars.join('')}${chars.length >= 40 ? '…' : ''}」的回答`
|
||||
},
|
||||
/** 没记问题摘要的(这一版之前存的):照报告文档的说法写装的是什么 */
|
||||
artifactAboutNone: '审批质询的回答:逐段的片段和每个数字的出处',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
RUN_STATUS_ORDER, STATUS, resolveStatus, serverStatusOf, type StatusCode,
|
||||
} from '../../lib/status'
|
||||
import { formatSpan, parseServerTime } from '../../lib/format'
|
||||
import { INQUIRY_TEXT } from '../../lib/terms'
|
||||
import { hasPendingApproval } from '../../store/catalog'
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -334,6 +335,7 @@ export const ARTIFACT_KIND_LABEL: Record<string, string> = {
|
||||
schema_snapshot: '表结构快照',
|
||||
dual_check: '双盲复算对照',
|
||||
challenge: '反方审稿记录',
|
||||
inquiry_answer: INQUIRY_TEXT.artifactKind,
|
||||
}
|
||||
export const artifactKindLabel = (kind: string): string => ARTIFACT_KIND_LABEL[kind] ?? kind
|
||||
|
||||
@@ -356,6 +358,9 @@ export function artifactDescription(kind: string, meta?: Record<string, any> | n
|
||||
? `双盲复算:${meta.metrics} 个指标两边的 SQL、取值与差异说明` : '双盲复算:两边的 SQL、取值与差异说明'
|
||||
// 反方审稿的完整记录(工件行的 meta 里有 {report: 审的报告节点})
|
||||
case 'challenge': return '反方审稿:每句结论的判断、理由和反方执行的查询'
|
||||
// 审批质询的回答文档(工件行的 meta 里有 {report, unit, question: 问题的前 40 字}):不挂在节点上,靠问题认出是哪一条
|
||||
case 'inquiry_answer': return typeof meta?.question === 'string' && meta.question.trim()
|
||||
? INQUIRY_TEXT.artifactAbout(meta.question) : INQUIRY_TEXT.artifactAboutNone
|
||||
default: return '工件'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,17 @@ export const useInquiry = create<InquiryState>((set, get) => ({
|
||||
},
|
||||
|
||||
loadApprovalDocs: async (runId, ids) => {
|
||||
if (get().approvalDocs[runId]?.status === 'loading') return
|
||||
const inflight = get().approvalDocs[runId]
|
||||
if (inflight?.status === 'loading') {
|
||||
// 别处已经在取(同一张卡审两份报告;运行详情的待审批卡和证据页签同时挂着,两处要对的审批不同):不再另取,但这一处
|
||||
// 要对的也记成取过——列表按运行取、不按审批,在路上的那一份本来就带着全部审批。只是提前返回的话,这一处的 unknown
|
||||
// 在请求回来之后不变、effect 不再执行,settled 一直为假:头上写着「· N 次」,别的审批里问的那几条一直不显示
|
||||
const asked = [...new Set([...inflight.asked, ...ids])]
|
||||
if (asked.length > inflight.asked.length) {
|
||||
set((s) => ({ approvalDocs: { ...s.approvalDocs, [runId]: { ...s.approvalDocs[runId]!, asked } } }))
|
||||
}
|
||||
return
|
||||
}
|
||||
set((s) => {
|
||||
const cur = s.approvalDocs[runId]
|
||||
return { approvalDocs: { ...s.approvalDocs, [runId]: {
|
||||
|
||||
+126
-5
@@ -1,6 +1,6 @@
|
||||
// 审批质询的回归检查:报告上的入口(run/EvidenceDoc.tsx 的句末「质询」、划选原文浮出的「质询」)、问答串和提问框
|
||||
// (run/Inquiry.tsx)、审批卡上的挂法(run/ApprovalReport.tsx、run/RunPanel.tsx)、运行详情「证据」页签的挂法
|
||||
// (pages/runs/EvidencePane.tsx)。
|
||||
// (pages/runs/EvidencePane.tsx),以及运行详情「工件」页签里质询回答的名字(pages/runs/model.ts)。
|
||||
//
|
||||
// 守的是这些「看着没坏、其实用不了」的地方:悬停、键盘聚焦一句都不出「质询」(或者出了 Tab 却到不了、整份报告多出一串
|
||||
// Tab 位)、没有可点内容的句子键盘走不到、划选原文不浮出入口(或者带上的原文混进了徽标和「质询」本身)、提交的请求体
|
||||
@@ -8,11 +8,14 @@
|
||||
// 进入审批卡时问答串没有恢复、问答串写不出问的是哪一句、回答里的数字点了不去取质询工件(或者每点一次取一次、标不出
|
||||
// 那一格)、引用报告已有出处的数字点了没反应、查询的 SQL / 行数 / 截断 / 遮罩不显示、触顶和没答成不说、recorded 为假
|
||||
// 不醒目、409 / 422 不照服务端的中文说明显示(或者问题文字丢了)、Esc 关不掉提问框(或者连带收起了别的、焦点掉到页面上)、
|
||||
// 已处理的审批和封存了的正式运行还给入口、封存了的探索运行反而没有;窄屏(390px)被撑出横向滚动、浅色主题下看不清。
|
||||
// 已处理的审批和封存了的正式运行还给入口、封存了的探索运行反而没有;同一次运行的几处问答串要对的审批不同、审批列表
|
||||
// 取不到时后一处一直不列别的审批里问的(头上写着次数,下面一条没有);工件页签里质询的回答只写英文的种类名;
|
||||
// 窄屏(390px)被撑出横向滚动、浅色主题下看不清。
|
||||
//
|
||||
// 跑在预览页上:/preview.html?syn=inquiry-*(用例见 frontend/src/dev/fixtures/inquiry.tsx)。夹具在
|
||||
// frontend/src/dev/fixtures/inquiry.json,报告文档和回答文档是后端 build_catalog + compose_doc 真实组装的(虚构的数据)。
|
||||
// 质询的三个接口、工件、证据图、片段接口都在这里伪造;别的写请求一律拦掉并记下来,别的 GET 答 404,不碰真实服务。
|
||||
// 质询的三个接口、工件、证据图、片段接口、审批列表、运行的工件列表都在这里伪造;别的写请求一律拦掉并记下来,别的 GET
|
||||
// 答 404,不碰真实服务。
|
||||
//
|
||||
// node scripts/check-inquiry.mjs
|
||||
// 对别的实例跑时带上地址:AGENTLAB_WEB=http://localhost:<前端端口> node scripts/check-inquiry.mjs
|
||||
@@ -66,8 +69,9 @@ const answerFor = (body, extra = {}) => ({
|
||||
/**
|
||||
* 开一个预览用例。列表按运行答夹具里的问答(state.lists 可改),提问按 state.reply 答({status, json} 或按请求体算的函数;
|
||||
* state.gate 是个没兑现的 Promise 时先挂着,测「回答期间」;state.listGate 同样挂着列表)。审批列表按 state.approvals
|
||||
* (运行 → 审批数组)答,设置按 state.settings 答;state.docs、state.snaps 盖过夹具里的文档、快照。setup 在打开页面之前
|
||||
* 改 state(进入时就要取的列表、设置得先备好);ws 接管运行的事件流。记下列表、提问、取快照、取工件、取审批列表的请求和拦下的写请求
|
||||
* (运行 → 审批数组)答,没有的答 404,state.approvalsGate 同样先挂着;设置按 state.settings 答;state.docs、state.snaps
|
||||
* 盖过夹具里的文档、快照;运行的工件列表答夹具里的 run_artifacts(探索运行)。setup 在打开页面之前改 state(进入时就要
|
||||
* 取的列表、设置得先备好);ws 接管运行的事件流。记下列表、提问、取快照、取工件、取审批列表的请求和拦下的写请求
|
||||
*/
|
||||
async function open(syn, { width = 1100, height = 900, theme = 'dark', setup, ws, waitUntil = 'networkidle' } = {}) {
|
||||
const ctx = await browser.newContext({ viewport: { width, height }, colorScheme: theme, timezoneId: 'Asia/Shanghai' })
|
||||
@@ -79,6 +83,7 @@ async function open(syn, { width = 1100, height = 900, theme = 'dark', setup, ws
|
||||
reply: (body) => ({ status: 200, json: answerFor(body) }),
|
||||
gate: null,
|
||||
listGate: null,
|
||||
approvalsGate: null,
|
||||
approvals: {},
|
||||
settings: {},
|
||||
docs: {},
|
||||
@@ -114,6 +119,7 @@ async function open(syn, { width = 1100, height = 900, theme = 'dark', setup, ws
|
||||
if (path === '/api/approvals') {
|
||||
const run = url.searchParams.get('run_id')
|
||||
hits.approvals.push({ run, status: url.searchParams.get('status') })
|
||||
if (state.approvalsGate) await state.approvalsGate
|
||||
const list = state.approvals[run]
|
||||
return list ? route.fulfill({ json: list }) : route.fulfill({ status: 404, json: { detail: '检查脚本没有伪造这个接口' } })
|
||||
}
|
||||
@@ -141,6 +147,8 @@ async function open(syn, { width = 1100, height = 900, theme = 'dark', setup, ws
|
||||
hits.segments.push(decodeURIComponent(seg[2]))
|
||||
return route.fulfill({ json: {} })
|
||||
}
|
||||
const arts = path.match(/^\/api\/runs\/([^/]+)\/artifacts$/)
|
||||
if (arts) return route.fulfill({ json: decodeURIComponent(arts[1]) === RUNS.explore ? fx.run_artifacts : [] })
|
||||
const graph = path.match(/^\/api\/runs\/([^/]+)\/evidence$/)
|
||||
if (graph) {
|
||||
const run = decodeURIComponent(graph[1])
|
||||
@@ -822,6 +830,37 @@ await section('运行详情「证据」页签:封存了的探索运行有入
|
||||
}
|
||||
})
|
||||
|
||||
await section('运行详情「工件」页签:质询的回答有名字和说明,不出现英文的工件种类名', async () => {
|
||||
const { ctx, page, errors } = await open('inquiry-run-artifacts')
|
||||
await page.locator('[data-run-artifacts] [data-artifact-id]').first().waitFor()
|
||||
// 每一行:种类(行首的标签)、说明(同一种类有几件时末尾带「#n」,去掉再比)、悬停
|
||||
const rows = await page.locator('[data-artifact-id]').evaluateAll((els) => els.map((el) => {
|
||||
const open = el.querySelector('[data-action="artifact-open"]')
|
||||
const spans = open ? [...open.children] : []
|
||||
return { id: el.getAttribute('data-artifact-id'), kind: spans[0]?.textContent?.trim() ?? '',
|
||||
about: (spans[1]?.textContent ?? '').replace(/\s*#\d+$/, '').trim(), title: open?.getAttribute('title') ?? '' }
|
||||
}))
|
||||
const answers = fx.run_artifacts.filter((a) => a.kind === 'inquiry_answer').map((a) => rows.find((r) => r.id === a.id))
|
||||
check('质询的回答:种类写「质询回答」', answers.length === 3 && answers.every((r) => r?.kind === '质询回答'), JSON.stringify(answers))
|
||||
check('有问题摘要的:说明写「质询「问题」的回答」', answers[0]?.about === '质询「退款率怎么算的?」的回答', answers[0]?.about)
|
||||
check('问题摘要满 40 字(服务端只记前 40 字):末尾补省略号',
|
||||
answers[1]?.about === '质询「枫岭区的销售额是否包含线上订单?如果包含,线上订单按下单门店还是按收货地址归到片…」的回答', answers[1]?.about)
|
||||
check('没有问题摘要的:也有一句说明', answers[2]?.about === '审批质询的回答:逐段的片段和每个数字的出处', answers[2]?.about)
|
||||
check('悬停写「打开质询回答」', answers.every((r) => r?.title.startsWith('打开质询回答')), answers.map((r) => r?.title).join(' / '))
|
||||
const pane = await text(page.locator('[data-run-artifacts]'))
|
||||
check('工件页签里不出现英文的工件种类名', !/inquiry_answer|report_doc|query_snapshot/.test(pane) && rows.every((r) => !/[a-z_]{4,}/.test(r.kind)),
|
||||
rows.map((r) => r.kind).join('、'))
|
||||
check('不挂在节点上的写「不属于某个节点」', (await text(page.locator('[data-artifact-group=""] h3'))).startsWith('不属于某个节点'))
|
||||
// 打开一件:弹窗标题也是中文的种类名
|
||||
await page.locator(`[data-artifact-id="${answers[0]?.id}"] [data-action="artifact-open"]`).click()
|
||||
const dialog = page.getByRole('dialog')
|
||||
await dialog.waitFor()
|
||||
const head = await text(dialog.locator('h2').first())
|
||||
check('打开的弹窗标题写「质询回答」,不写英文', head.includes('质询回答') && !head.includes('inquiry_answer'), head)
|
||||
check('页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('有表格的报告:悬停句子、表格的格子都不挪动排版;表格的格子不挂「质询」', async () => {
|
||||
// 报告文档换成带表格的那份(run/__tests__/evidence-doc.json):表里带出处的格子也是结论句
|
||||
const ev = JSON.parse(readFileSync(`${root}frontend/src/run/__tests__/evidence-doc.json`, 'utf8'))
|
||||
@@ -1054,6 +1093,73 @@ await section('之前几轮审批里问的:报告改写过的不按句子编
|
||||
}
|
||||
})
|
||||
|
||||
/** 一处问答串:抬头写的次数、实际列出的条数 */
|
||||
const tally = async (page, scope) => ({
|
||||
head: (await page.locator(`${scope} [data-inquiry-count]`).getAttribute('data-inquiry-count').catch(() => null)) ?? '',
|
||||
shown: await page.locator(`${scope} [data-inquiry-entry]`).count(),
|
||||
})
|
||||
|
||||
await section('两处要对的审批不同、审批列表取不到:两处的问答都列出来,次数和条数一致', async () => {
|
||||
// 一张卡审两份报告:写月报上有 ap-x 里问的,写周报上有 ap-y 里问的,两份报告的问答串共用这次运行的审批列表。先发的那一处
|
||||
// 请求还在路上时,后一处要对的审批也得记上:不然请求回来(这里是 404)之后后一处一直等着,头上写「· 1 次」,下面一条没有
|
||||
const x = { ...clone(fx.items[0]), seq: 41, approval_id: 'ap-x', question: '写月报上一轮问的:退款率怎么算?', at: '2026-10-05T05:30:00Z' }
|
||||
const y = { ...clone(fx.items[1]), seq: 42, report: 'write2', approval_id: 'ap-y', question: '写周报上一轮问的:新客怎么界定?',
|
||||
at: '2026-10-05T05:35:00Z' }
|
||||
let release
|
||||
// 审批列表压着,页面等不到网络空闲:打开到 load 就往下走
|
||||
const { ctx, page, hits, errors } = await open('inquiry-approval-two', { waitUntil: 'load', setup: (s) => {
|
||||
s.lists[RUNS.approve] = [x, y]
|
||||
s.approvalsGate = new Promise((r) => { release = r })
|
||||
} })
|
||||
// 两份报告的问答串都取回来了(头上写着次数),审批列表的请求还压着
|
||||
await page.locator('[data-inquiry="write"] [data-inquiry-count]').waitFor()
|
||||
await page.locator('[data-inquiry="write2"] [data-inquiry-count]').waitFor()
|
||||
await until(() => hits.approvals.length >= 1)
|
||||
await page.waitForTimeout(200)
|
||||
release()
|
||||
const both = await until(async () => (await tally(page, '[data-inquiry="write"]')).shown === 1
|
||||
&& (await tally(page, '[data-inquiry="write2"]')).shown === 1)
|
||||
const [gw, gw2] = [await groupsOf(page, '[data-inquiry="write"]'), await groupsOf(page, '[data-inquiry="write2"]')]
|
||||
check('写月报:查不到那张审批,问答单独一组', JSON.stringify(gw.map((g) => [g.unit, g.entries])) === JSON.stringify([['other', ['41']]]),
|
||||
JSON.stringify(gw.map((g) => [g.unit, g.entries])))
|
||||
check('写周报:请求是另一处先发的,回来之后照样列出、单独一组', both
|
||||
&& JSON.stringify(gw2.map((g) => [g.unit, g.entries])) === JSON.stringify([['other', ['42']]]), JSON.stringify(gw2.map((g) => [g.unit, g.entries])))
|
||||
const [tw, tw2] = [await tally(page, '[data-inquiry="write"]'), await tally(page, '[data-inquiry="write2"]')]
|
||||
check('两份报告头上的次数都和列出的条数一致', tw.head === '1' && tw.shown === 1 && tw2.head === '1' && tw2.shown === 1,
|
||||
JSON.stringify({ tw, tw2 }))
|
||||
check('按运行只取一次审批列表(一份列表里各张审批都有)', hits.approvals.length === 1, JSON.stringify(hits.approvals))
|
||||
check('页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
{
|
||||
// 运行详情直接打开「证据」页签、运行停在审批上:时间线里的待审批卡只为更早那一轮(ap-inq-0)取审批列表,证据页签不认
|
||||
// 任何审批,ap-inq-0 和这张卡的 ap-inq-1 都要对。卡那一处先发了请求,证据页签那一处的条目在请求回来之后也得列出来
|
||||
const early = { ...clone(fx.items[0]), seq: 12, approval_id: 'ap-inq-0', question: '上一轮:退款率 3.5% 是怎么算的?', at: '2026-10-05T05:40:00Z' }
|
||||
const { ctx, page, hits, errors } = await open('inquiry-run-waiting', { waitUntil: 'load', setup: (s) => {
|
||||
s.lists[RUNS.approve] = [early, clone(fx.items[1])]
|
||||
s.approvalsGate = new Promise((r) => { release = r })
|
||||
} })
|
||||
const card = '[data-fixture-card] [data-inquiry="write"]'
|
||||
const pane = '[data-fixture-evidence] [data-inquiry="write"]'
|
||||
await page.locator(`${card} [data-inquiry-count]`).waitFor()
|
||||
await page.locator(`${pane} [data-inquiry-count]`).waitFor()
|
||||
await until(() => hits.approvals.length >= 1)
|
||||
await page.waitForTimeout(200)
|
||||
release()
|
||||
const listed = await until(async () => (await tally(page, card)).shown === 2 && (await tally(page, pane)).shown === 2)
|
||||
const [gc, gp] = [await groupsOf(page, card), await groupsOf(page, pane)]
|
||||
check('待审批卡:这张审批里问的挂句子下面,查不到的那一轮单独一组',
|
||||
JSON.stringify(gc.map((g) => [g.unit, g.entries])) === JSON.stringify([['u3', ['34']], ['other', ['12']]]),
|
||||
JSON.stringify(gc.map((g) => [g.unit, g.entries])))
|
||||
check('证据页签:两条都列出来,查不到审批的单独一组(不再一直等着)', listed
|
||||
&& JSON.stringify(gp.map((g) => [g.unit, g.entries])) === JSON.stringify([['other', ['12', '34']]]), JSON.stringify(gp.map((g) => [g.unit, g.entries])))
|
||||
const [tc, tp] = [await tally(page, card), await tally(page, pane)]
|
||||
check('两处头上的次数都和列出的条数一致', tc.head === '2' && tc.shown === 2 && tp.head === '2' && tp.shown === 2, JSON.stringify({ tc, tp }))
|
||||
check('运行详情:按运行只取一次审批列表', hits.approvals.length === 1, JSON.stringify(hits.approvals))
|
||||
check('运行详情:页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
}
|
||||
})
|
||||
|
||||
await section('回答期间另开了提问框,正在回答的那一条没问成:新写的不被覆盖', async () => {
|
||||
const { ctx, page, state, errors } = await open('inquiry-approval')
|
||||
await expand(page)
|
||||
@@ -1366,6 +1472,21 @@ await section('窄屏与深浅色', async () => {
|
||||
check('390px 运行详情:页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
}
|
||||
for (const theme of ['dark', 'light']) {
|
||||
// 运行详情「工件」页签:质询回答的说明可能很长(问题的前 40 字),窄屏上截断、不撑宽;种类和说明深浅色都看得清
|
||||
const { ctx, page, errors } = await open('inquiry-run-artifacts', { width: 390, height: 844, theme })
|
||||
const long = fx.run_artifacts.find((a) => a.kind === 'inquiry_answer' && [...(a.meta.question ?? '')].length >= 40)
|
||||
const row = `[data-artifact-id="${long.id}"] [data-action="artifact-open"]`
|
||||
await page.locator(row).waitFor()
|
||||
const o = await overflow(page, '[data-run-artifacts]')
|
||||
const ratios = { kind: await ratioOf(page, `${row} > span:nth-child(1)`), about: await ratioOf(page, `${row} > span:nth-child(2)`) }
|
||||
check(`390px 工件页签(${theme}):质询回答的长说明截断、不横向溢出,种类和说明对比度 ≥ 4.5`,
|
||||
o.page <= 0 && o.box <= 0 && ratios.kind >= 4.5 && ratios.about >= 4.5,
|
||||
JSON.stringify({ o, kind: Number(ratios.kind.toFixed(2)), about: Number(ratios.about.toFixed(2)) }))
|
||||
await shot(page, `run-artifacts-narrow-${theme}`)
|
||||
check(`390px 工件页签(${theme}):页面没有报错`, errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
}
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
Reference in New Issue
Block a user