检查脚本跟上:驳回、使用者选人、自由形「谁来做」

为什么:前几个提交改了驳回的终态、选人控件的候选、自由形蓝图的「谁来做」,浏览器检查里的伪造数据和断言要守住新说法
(05-已知遗留 (a)(b)(d))。check-decode 的一节已在驳回前端那个提交里。

做了什么:
- check-apps:伪造接口加 /api/directory/candidates(按段里给的池答)和 /api/directory(使用者 403);新段「入口:使用者选人」
  ——边输入边按入口和字段取候选、列表里是池里的人、不去取整份名录、不在池里当场说清、选中的写显示名悬停给用户名;新段「实例页:
  被驳回」——状态照服务端写「「付款」未确认(被驳回)」、不写失败、那一步写未确认(被驳回)。
- check-inbox:新待办「指定复核人」(补充输入里有选人控件);按 task_id 和字段问候选、不取整份名录;服务端 422 code=principal
  挂回选人字段。
- check-personal:提交审核选人按 purpose=review 取候选,不是搭建者的名字当场说清。
- check-blueprint:自由形模板的 cards 加 people;新段「自由形:谁来做」——按 derived_people 摆三行(岗位、核实人、只读的发起人)、
  没绑的标缺、问题归在这张卡片上、只读行没有修改、不能加减步骤、选人发 bp_set 到那一行的 path。

验证:check-all --stacks 1 跑 decode、apps、inbox、personal、blueprint(1030 项)和 ui-kit、chat、runs、stream(1982 项),全部通过。
This commit is contained in:
ThearchyHelios
2026-10-16 11:04:28 +08:00
parent 7e4c3c58a0
commit 335dc99c3e
4 changed files with 194 additions and 7 deletions
+76 -3
View File
@@ -9,10 +9,11 @@
// 不认识的组件写「此组件需要更新前端」,不报错、不空白;服务端解析出错的组件(problems)列在页面顶上;
// 有视图在生成时自动重取;
// - 入口页:必填没填本地拦下、错误挂到字段下面;服务端 422 的 {errors:[{path, code, message}]} 按 path 挂回字段,
// 对不上字段的列在表单顶上;⌘/Ctrl+Enter 发起;成功后进实例页;需升级时不让发起;
// 对不上字段的列在表单顶上;⌘/Ctrl+Enter 发起;成功后进实例页;需升级时不让发起;使用者在选人控件里边输入边按
// 候选池取候选(GET /api/directory/candidates,只有名字),不在池里的当场说清,不去取整份名录(05-已知遗留 (b));
// - 实例页:状态(服务端给的说法:结果未知写「等待核实写入结果」)、印章、进度(结果未知的那一步不写「完成」)、
// 本实例的待办卡(就地处理);不是处理人的待办表单只画这一步的状态、不去取待办;动作状态(哈希前 8 位、环境;
// 结果未知的经过记一笔,不当失败原因);
// 结果未知的经过记一笔,不当失败原因);被驳回的实例照服务端的说法写「「付款」未确认(被驳回)」,不写失败(05-已知遗留 (a));
// - 窄屏(390px)不出横向滚动;
// - 页面上直接改(normal-user-apps N6):有 manage 的人点「编辑」进编辑状态(地址记 edit=1),按草稿解析;点组件是选中、
// 不打开出处;小工具条改文字只给目录标了 x-text 的属性;隐藏、上移、删除都自动存草稿(PATCH 带整份草稿)后按草稿
@@ -340,6 +341,15 @@ async function open({ width = 1280, height = 900, state }) {
return route.fulfill({ status: 503, json: { detail: '检查脚本拦下了写请求' } })
}
if (p === '/api/auth/me') return route.fulfill({ json: state.me ?? { mode: 'off', username: null, display_name: null, role: 'admin', groups: [], verified: false } })
// 使用者选人的候选池(05-已知遗留 (b)):按段里给的 pool 答;整份名录只给搭建者,使用者取了就是 403
if (p === '/api/directory/candidates') {
(state.candidateCalls ??= []).push(Object.fromEntries(url.searchParams))
return route.fulfill({ json: state.pool ? state.pool(url.searchParams.get('q') ?? '') : { users: [], groups: [], truncated: false } })
}
if (p === '/api/directory') {
state.directoryHits = (state.directoryHits ?? 0) + 1
return route.fulfill({ status: 403, json: { detail: '需要搭建者权限' } })
}
if (p === '/api/apps') return route.fulfill({ json: { items: APPS } })
// 应用详情:默认不给草稿(不进编辑状态);页面上直接改的段落给 manage 的详情
if (p === '/api/apps/app_weekly') return route.fulfill({ json: state.detail ?? { ...APPS[0], published: DETAIL.published } })
@@ -360,7 +370,7 @@ async function open({ width = 1280, height = 900, state }) {
if (p === '/api/apps/app_weekly/pages/detail') return route.fulfill({ json: { ...HOME, page: { id: 'detail', title: '门店明细' }, tree: [] } })
if (p === '/api/apps/app_claim/pages/home') return route.fulfill({ json: state.claimHome ?? CLAIM_HOME })
if (/^\/api\/apps\/[^/]+\/instances$/.test(p)) return route.fulfill({ json: { items: p.includes('app_claim') ? [INSTANCE] : [] } })
if (p === '/api/apps/app_claim/instances/run_claim_1') return route.fulfill({ json: INSTANCE })
if (p === '/api/apps/app_claim/instances/run_claim_1') return route.fulfill({ json: state.instance ?? INSTANCE })
if (p === `/api/runs/${RUN}/evidence/metrics/kpi/gmv`) {
state.chainHits = (state.chainHits ?? 0) + 1
return route.fulfill({ json: state.traced ? METRIC_CHAIN_TRACED : METRIC_CHAIN })
@@ -555,6 +565,46 @@ await section('入口:本地校验、422 挂回字段、发起后进实例页'
check('没有未捕获的运行时错误', page.errors.length === 0, page.errors[0] ?? '')
})
await section('入口:使用者选人只出候选池里的人,不在池里当场说清(05-已知遗留 (b))', async () => {
const people = [{ username: 'fay', display_name: '费伊' }, { username: 'fin', display_name: '樊宁' }]
const apply = CLAIM_HOME.forms.apply
const state = {
writes: [],
me: { mode: 'local', username: 'zhang', display_name: '张三', role: 'user', groups: [], verified: true },
claimHome: { ...CLAIM_HOME, forms: { apply: { ...apply, 'x-order': [...apply['x-order'], 'approver'],
properties: { ...apply.properties, approver: { type: 'string', title: '审批人', 'x-widget': 'principal' } } } } },
pool: (q) => ({ users: people.filter((u) => !q || u.username.startsWith(q) || u.display_name.startsWith(q)),
groups: [], truncated: false }),
}
const { page } = await open({ state })
await page.goto(`${WEB}/apps/app_claim/new/apply`)
const field = page.locator('[data-schema-form] [data-field="approver"]')
await field.waitFor({ timeout: 15000 })
const input = field.locator('input.field')
await input.fill('费')
await page.waitForFunction(() => !!document.querySelector('[data-field="approver"] datalist option'), null, { timeout: 5000 })
.catch(() => {})
const options = await field.locator('datalist option').evaluateAll((els) => els.map((e) => `${e.getAttribute('value')}|${e.textContent}`))
check('边输入边按开头取候选:列表里是候选池给的人,显示名作说明', options.join(',') === 'fay|费伊', options.join(','))
const asked = (state.candidateCalls ?? []).at(-1) ?? {}
check('……按这个入口、这个字段问(purpose=form)', asked.purpose === 'form' && asked.app_id === 'app_claim'
&& asked.entry === 'apply' && asked.field === 'approver' && asked.q === '费', JSON.stringify(asked))
check('……不去取整份名录(使用者读不到)', !state.directoryHits, String(state.directoryHits ?? 0))
await input.fill('wangwu')
await input.press('Enter')
const refused = field.locator('[data-principal-refused="hard"]')
await refused.waitFor({ timeout: 5000 }).catch(() => {})
check('不在池里的名字当场说清,不收', (await refused.count()) === 1
&& (await refused.innerText()).includes('「wangwu」不在可选的人里') && (await field.locator('[data-principal]').count()) === 0)
await input.fill('fay')
await input.press('Enter')
const chip = field.locator('[data-principal="user"] span[title]')
await chip.waitFor({ timeout: 5000 }).catch(() => {})
check('选中的人写显示名,悬停给用户名', (await chip.count()) === 1 && (await chip.innerText()) === '费伊'
&& (await chip.getAttribute('title')) === 'fay')
check('没有未捕获的运行时错误', page.errors.length === 0, page.errors[0] ?? '')
})
await section('入口:需升级时不让发起', async () => {
const state = { writes: [], claimHome: { ...CLAIM_HOME, app: { ...CLAIM_HOME.app, needs_upgrade: true } } }
const { page } = await open({ state })
@@ -566,6 +616,29 @@ await section('入口:需升级时不让发起', async () => {
// ---------------------------------------------------------------- 实例
await section('实例页:被驳回写人的决定,不写失败(05-已知遗留 (a))', async () => {
// 服务端:动作确认被驳回,运行收成 cancelled + end_reason=rejected;实例状态、进度的说法都是服务端给的(instance_state)
const said = { status: 'cancelled', status_code: 'cancelled', status_text: '「付款」未确认(被驳回)' }
const view = { ...INSTANCE.view, sources: {}, tree: [
{ id: 'inst_header', type: 'instance_header', props: {}, data: { ...INSTANCE.view.tree[0].data, ...said, pending_tasks: 0,
finished_at: ago(60_000) } },
{ id: 'inst_timeline', type: 'instance_timeline', props: {}, data: { state: 'ok', ...said, steps: [
{ node: 'input', title: '报销单', state: 'done', text: '完成', at: ago(1790_000) },
{ node: 'pay', title: '付款', state: 'rejected', text: '未确认(被驳回)', at: ago(60_000) },
] } },
] }
const state = { writes: [], instance: { ...INSTANCE, ...said, pending_tasks: 0, finished_at: ago(60_000), view } }
const { page } = await open({ state })
await page.goto(`${WEB}/apps/app_claim/i/run_claim_1`)
await page.locator('[data-view-renderer]').waitFor({ timeout: 15000 })
const header = page.locator('[data-component="instance_header"]')
check('实例状态写「「付款」未确认(被驳回)」', (await header.innerText()).includes('「付款」未确认(被驳回)'),
(await header.innerText()).replace(/\n/g, ' '))
check('……不写「失败」', !(await page.locator('[data-instance-page]').innerText()).includes('失败'))
check('被驳回的那一步写未确认(被驳回)', (await page.locator('[data-timeline-step="rejected"]').innerText()).includes('未确认(被驳回)'))
check('没有未捕获的运行时错误', page.errors.length === 0, page.errors[0] ?? '')
})
await section('实例页:状态、进度、待办卡、动作状态', async () => {
const state = { writes: [] }
const { page } = await open({ state })
+53 -1
View File
@@ -9,6 +9,8 @@
// - 卡片:卡头标状态(缺按服务端检查、待确认、已确认),对到数据的项写表、中文名、粒度、业务日期、为什么;「修改」发 bp_set
// (纯值),确认只改状态,「确认全部」一次带上整张卡片待确认的几项,加一项发 bp_add(纯值);缺项时「用真数试一次」置灰并写清缺什么;
// - 写回卡片:目标能力没登记查询时摆「结果不明时谁去核实」(服务端 reconciler_needed),缺时只挡发布,选人发 bp_set;
// - 自由形的「谁来做」(05-已知遗留 (d)):按服务端的 derived_people 一行一行摆(岗位、核实人能选人,发起人、表达式的
// 只读),不能加减步骤、不改会签;没绑的照服务端的 bp.people_unbound 标「缺」;选人发 bp_set 到那一行的 path;
// - 反问:助手的 bp_ask 画成可点的选项,「这一轮在等你回答」常显,输入框提示回答;点选项就是把它作为下一轮发出去(回答记在
// 问题上、这一轮落进蓝图对话都是服务端做的,前端不另记);
// - 关节:只列生成关节,按卡片填的汇总一句;「让助手填」走 fill-joints-stream,逐组写进度,组之间并行;
@@ -77,7 +79,8 @@ const TEMPLATES = [
cards: [{ card: 'purpose', note: '必填' }, { card: 'fields', note: '必填:发起时填什么' }, { card: 'people', note: '必填:谁审批' },
{ card: 'writeback', note: '可选:通过后写回哪个系统' }, { card: 'access', note: '' }] },
{ id: 'freeform', version: 1, label: '自由形', description: '从已有的运行或工作流导入', applies_when: '', tags: [], personal: '只读部分',
cards: [{ card: 'purpose', note: '' }, { card: 'cadence', note: '' }, { card: 'access', note: '' }] },
cards: [{ card: 'purpose', note: '' }, { card: 'cadence', note: '' },
{ card: 'people', note: '可选:只定每一步由谁做;会签、时限、超时在画布上改' }, { card: 'access', note: '' }] },
]
const item = (value, source = 'assistant', status, why = null) =>
@@ -550,6 +553,55 @@ await section('写回:结果不明时谁去核实(能力没登记查询才
&& await third.page.locator('[data-bp-reconciler-hint="unused"]').count() === 1)
})
// ---------------------------------------------------------------- 自由形:谁来做
/** 从已发布的「财务审批(岗位)+ 写回(没登记查询)」工作流导入的自由形蓝图:服务端按图列出的「谁来做」(freeform.people_rows) */
const FREE_ROWS = [
{ kind: 'slot', slot: 'finance', label: '「财务审批」', detail: '「财务审批」处理', path: '/people/0/who', who: null,
uses: [{ node_id: 'review', title: '财务审批', role: 'assignee' }] },
{ kind: 'reconciler', slot: 'reconciler', label: '写入结果不明时谁去核实', detail: '「写回报销单」写入结果不明时去核实',
path: '/writeback/reconciler', who: null, uses: [{ node_id: 'pay', title: '写回报销单', role: 'reconcile' }] },
{ kind: 'initiator', slot: null, label: '「本人确认」', detail: '由发起人自己处理', path: null, who: null,
uses: [{ node_id: 'me', title: '本人确认', role: 'assignee' }] },
]
const unboundAt = (path, message) => ({ code: 'bp.people_unbound', level: 'error', path, message, slot: 'x',
plain: { title: '有一步还没定谁来处理', why: '流程走到这一步就没人接手', who: 'you', action: '在「谁来做」里选人、组或岗位。' } })
const FREE = () => ({ ...clone(BP), template_id: 'freeform',
spec: { ...clone(SPEC), template: { id: 'freeform', version: 1 }, data: [], people: [] },
joints: {}, derived_people: clone(FREE_ROWS),
issues: [unboundAt('/people/0/who', '「财务审批」还没定谁来处理(「财务审批」处理)'),
unboundAt('/writeback/reconciler', '写入结果不明时谁去核实还没定')] })
await section('自由形:「谁来做」按导入的图列出,只定由谁做(05-已知遗留 (d))', async () => {
const state = { bp: FREE() }
const { page } = await open({ state })
await page.goto(`${WEB}/blueprints/bp_week`)
const card = page.locator('[data-card="people"]')
await card.waitFor({ timeout: 15000 })
check('按服务端给的行摆:岗位、核实人、只读的发起人', (await card.locator('[data-bp-derived]').evaluateAll(
(els) => els.map((e) => e.getAttribute('data-bp-derived')))).join(',') === 'slot,reconciler,initiator')
check('行名、用途照服务端写(节点在图上的名字)', (await card.locator('[data-bp-derived="slot"]').innerText()).includes('「财务审批」处理'))
check('没绑的标「缺」,卡头也是缺', await card.locator('[data-bp-item="/people/0/who"]').getAttribute('data-item-status') === 'missing'
&& await card.locator('[data-bp-item="/writeback/reconciler"]').getAttribute('data-item-status') === 'missing'
&& await card.getAttribute('data-card-status') === 'missing')
check('卡片上照白话写缺什么、怎么改(核实人那一条也归在这张卡片上)',
(await card.locator('[data-card-issues]').getAttribute('data-card-issues')) === '2')
check('发起人的那一步只读:没有「修改」', await card.locator('[data-bp-derived="initiator"] [data-bp-edit]').count() === 0)
check('不能加减步骤:没有「加一步」、没有删除', await card.locator('[data-bp-add="/people"]').count() === 0
&& await card.locator('[data-bp-remove]').count() === 0)
const row = card.locator('[data-bp-item="/people/0/who"]')
await row.locator('[data-bp-edit]').click()
const editor = row.locator('[data-bp-editor]')
await editor.locator('[data-principal-picker] input').fill('zhang')
await editor.locator('[data-principal-picker] input').press('Enter')
await row.locator('[data-bp-save]').click()
check('选人:bp_set 到那一行的 path,值是一组人或组', await until(() => (state.patches ?? []).length === 1)
&& JSON.stringify(state.patches[0].ops) === JSON.stringify([
{ op: 'bp_set', path: '/people/0/who', value: [{ kind: 'user', name: 'zhang' }] }]),
JSON.stringify(state.patches?.[0]))
check('没有未捕获的运行时错误', page.errors.length === 0, page.errors[0] ?? '')
})
// ---------------------------------------------------------------- 反问
await section('反问:选项可点、「这一轮在等你回答」常显、回答记在问题上', async () => {
+49 -2
View File
@@ -11,7 +11,8 @@
// - 不是派给我的(can_respond 为假):只读,写明原因;同意过、还在等别人的写「你已处理:同意」,不写「没有派给你」;
// - 升级之后:升级加派的那一份不算进会签人数,另写升级后的规则(升级给谁、其中一人说了算),名单里标「升级」;
// 待办箱一行写所属实例,同标题的分得开;
// - 补充输入带表单的:本地校验,服务端 422 的错误挂回字段;
// - 补充输入带表单的:本地校验,服务端 422 的错误挂回字段;表单里的选人控件按这条待办的候选池取候选(使用者读不到
// 整份名录),不在池里的服务端 422(code=principal)挂回那个字段(05-已知遗留 (b));
// - 关登录时签批人一行写本机署名;会签只记下一票(resolved 为假)时提示「等待其他人处理」;
// - 审核个人应用(kind=review,payload 同服务端 blueprints/personal.review_payload):写提交人(显示名)、目的、说明;没有
// 「同意」——「取副本,去发布」调 POST /api/blueprints/{id}/copy 后进副本蓝图;取过的给「打开我的副本」(只认自己取的那
@@ -133,6 +134,13 @@ const TASKS = {
{ id: 'bp_daily_mine', owner: '审批人甲', status: 'trialed', app_id: null }] },
people: { zhangsan: '张三' },
},
// 补充输入里选复核人(选人控件):使用者按这条待办的候选池选
pick: {
...base, id: 'pick', approval_id: 'ap8', kind: 'input', title: '指定复核人',
form: { format: 'agentlab.form/1', type: 'object', required: ['reviewer'], properties: {
reviewer: { type: 'string', title: '复核人', 'x-widget': 'principal' } } },
payload: { kind: 'human_node', message: '请指定复核人' },
},
input: {
...base, id: 'input', approval_id: 'ap5', kind: 'input', title: '补填发票信息',
form: { format: 'agentlab.form/1', type: 'object', required: ['invoice'], properties: {
@@ -162,6 +170,10 @@ async function open(state) {
return route.fulfill({ status: 422, json: { detail: '表单没有通过校验', code: 'form_invalid', errors: [
{ path: 'invoice', code: 'max_length', message: '最多 20 个字' }] } })
}
if (m[1] === 'pick' && body?.values?.reviewer !== 'fin') {
return route.fulfill({ status: 422, json: { detail: '表单没有通过校验', code: 'form_invalid', errors: [
{ path: 'reviewer', code: 'principal', message: `「${body?.values?.reviewer}」不在可选的人里,请从候选中选择` }] } })
}
const t = TASKS[m[1]]
// 会签:这一票记下了,还没凑齐
const resolved = m[1] !== 'signoff'
@@ -171,7 +183,17 @@ async function open(state) {
if (p === '/api/blueprints/bp_mine/copy') return route.fulfill({ json: { id: 'bp_mine_copy', scope: 'shared', owner: '审批人甲' } })
return route.fulfill({ status: 503, json: { detail: '检查脚本拦下了写请求' } })
}
if (p === '/api/auth/me') return route.fulfill({ json: { mode: 'off', username: null, display_name: null, role: 'admin', groups: [], verified: false } })
if (p === '/api/auth/me') return route.fulfill({ json: state.me ?? { mode: 'off', username: null, display_name: null, role: 'admin', groups: [], verified: false } })
if (p === '/api/directory/candidates') {
const q = new URL(r.url()).searchParams
;(state.candidateCalls ??= []).push(Object.fromEntries(q))
const people = [{ username: 'fin', display_name: '樊宁' }, { username: 'fay', display_name: '费伊' }]
return route.fulfill({ json: { users: people.filter((u) => u.username.startsWith(q.get('q') ?? '')), groups: [], truncated: false } })
}
if (p === '/api/directory') {
state.directoryHits = (state.directoryHits ?? 0) + 1
return route.fulfill({ status: 403, json: { detail: '需要搭建者权限' } })
}
// 进了副本蓝图之后的页面不在这里查:蓝图一律答「不存在」,不打到真实服务端
if (p.startsWith('/api/blueprints/')) return route.fulfill({ status: 404, json: { detail: '蓝图不存在或已被删除' } })
if (p === '/api/tasks/count') return route.fulfill({ json: { mine: Object.keys(TASKS).length, apps: Object.keys(TASKS).length, other: 0 } })
@@ -306,6 +328,31 @@ await section('补充输入:表单校验、422 挂回字段', async () => {
check('提交的是表单的值,decision 是 submit', sent?.body?.decision === 'submit' && sent?.body?.values?.invoice === 'FP-001', JSON.stringify(sent?.body))
})
await section('补充输入里选人:按待办的候选池取候选,不在池里 422 挂回字段(05-已知遗留 (b))', async () => {
const state = { writes: [], me: { mode: 'local', username: 'zhangsan', display_name: '张三', role: 'user', groups: [], verified: true } }
const page = await open(state)
await page.goto(`${WEB}/apps/inbox/pick`)
const field = page.locator('[data-schema-form] [data-field="reviewer"]')
await field.waitFor({ timeout: 15000 })
const input = field.locator('input.field')
await input.fill('f')
await page.waitForFunction(() => document.querySelectorAll('[data-field="reviewer"] datalist option').length > 1, null,
{ timeout: 5000 }).catch(() => {})
const asked = (state.candidateCalls ?? []).at(-1) ?? {}
check('按这条待办、这个字段问候选(purpose=form、task_id)', asked.purpose === 'form' && asked.task_id === 'pick'
&& asked.field === 'reviewer' && !asked.app_id, JSON.stringify(asked))
check('……列出池里的人(只有名字),不去取整份名录', (await field.locator('datalist option').count()) === 2 && !state.directoryHits)
// 候选接口一时取不到时先收下(提交时服务端判):这里绕过前端的提前核对,直接看服务端 422 怎么挂回来
await page.route('**/api/directory/candidates**', (route) => route.abort())
await input.fill('wangwu')
await input.press('Enter')
await page.locator('[data-task-approve]').click()
await field.locator('[data-field-error="principal"]').waitFor({ timeout: 5000 }).catch(() => {})
check('服务端 422(code=principal)挂回选人字段', (await field.locator('[data-field-error="principal"]').count()) === 1
&& (await field.locator('[data-field-error="principal"]').innerText()).includes('不在可选的人里'))
check('没有未捕获的运行时错误', page.errors.length === 0, page.errors[0] ?? '')
})
await section('审核个人应用:取副本去发布、打开我的副本、退回', async () => {
const state = { writes: [] }
const page = await open(state)
+16 -1
View File
@@ -7,7 +7,8 @@
// - 「刷新」写明今天还剩几次(服务端在详情和刷新回包里给):点一次发 POST /refresh,剩余跟着回包变;用完了按钮置灰、
// 写「今天的刷新次数已用完」;服务端回 429 时照实说服务端的原话,不当成网络错误;
// - 「提交审核」:没选人不让提交;选一位搭建者或一个组、附一句说明,POST /submit-review 带 {to, note};提交后写
// 「审核中」和交给了谁,不再给提交按钮;
// 「审核中」和交给了谁,不再给提交按钮;选审核的人按候选池取(purpose=review:启用的搭建者和管理员,只有名字),
// 不在池里的当场说清(05-已知遗留 (b));
// - 审核状态以服务端为准(详情的 personal,形状同服务端 blueprints/personal.personal_state):审核待办还在等写「审核中」;
// 结论是已发布写「已有正式版本:去看看」并链到那个应用;
// 详情没给时看蓝图的状态(in_review 写「审核中」);
@@ -134,6 +135,13 @@ async function open({ state, width = 1280, height = 900 }) {
return route.fulfill({ status: 503, json: { detail: '检查脚本拦下了写请求' } })
}
if (p === '/api/auth/me') return route.fulfill({ json: state.me ?? USER })
// 提交审核选人的候选池(purpose=review):启用的搭建者和管理员,只有名字
if (p === '/api/directory/candidates') {
(state.candidateCalls ??= []).push(Object.fromEntries(url.searchParams))
const builders = [{ username: 'lisi', display_name: '李四' }, { username: 'root', display_name: '管理员甲' }]
return route.fulfill({ json: { users: builders.filter((u) => u.username.startsWith(url.searchParams.get('q') ?? '')),
groups: [], truncated: false } })
}
if (p === '/api/tasks/count') return route.fulfill({ json: { mine: 0, apps: 0, other: 0 } })
if (p === '/api/tasks') return route.fulfill({ json: { items: [] } })
if (p === '/api/apps') {
@@ -240,8 +248,15 @@ await section('提交审核:选人才能交、带说明;提交后写「审
await dlg.locator('[data-personal-review-dialog]').waitFor({ timeout: 5000 })
check('写明搭建者在副本上发布、个人应用不会被改动', /副本/.test(await dlg.innerText()) && /不会被改动/.test(await dlg.innerText()))
check('没选人不让提交', await dlg.locator('[data-personal-review-submit]').isDisabled())
await dlg.getByRole('textbox', { name: '交给谁审核' }).fill('wangwu')
await dlg.getByRole('button', { name: '添加' }).click()
await dlg.locator('[data-principal-refused="hard"]').waitFor({ timeout: 5000 }).catch(() => {})
check('不是搭建者的名字当场说清,不收', /「wangwu」不在可选的人里/.test(await dlg.locator('[data-principal-refused]').innerText().catch(() => '')))
check('……候选按提交审核的池问(purpose=review)', (state.candidateCalls ?? []).some((c) => c.purpose === 'review'),
JSON.stringify(state.candidateCalls ?? []))
await dlg.getByRole('textbox', { name: '交给谁审核' }).fill('lisi')
await dlg.getByRole('button', { name: '添加' }).click()
await dlg.locator('[data-principal="user"]').waitFor({ timeout: 5000 }).catch(() => {})
await dlg.locator('#personal-review-note').fill('门店店长都想看')
await dlg.locator('[data-personal-review-submit]').click()
await page.locator('[data-personal-review="in_review"]').waitFor({ timeout: 5000 }).catch(() => {})