mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:51:58 +00:00
驳回出口与子工作流证据(前端):画布多一个「驳回」出口、走了驳回出口的那一步画「被驳回」、子工作流里的步骤和证据有名字
接后端两个提交(docs/design/orchestration-round/00-总方案.md §2、§3): - canvas/nodeDefs:会被人驳回的节点多一个「驳回」出口(id rejected,和后端 REJECT_HANDLE 同一个,中性色):人工节点三种 模式、双盲复算一律给;沙箱代码只在审批策略是每次审批时给;调用工具选了工具、审批策略不是从不审批时给(数据源的查询 只读,只在明说每次审批时给)。出口跟着配置算:改成不会被驳回时,studio 原有的出口同步把连在驳回出口上的线收掉;补充输入 改成审批时普通出口的线跟到「批准」 - run/trace:node.finished 带 outcome=rejected 时这一步收成 rejected(exited,记下转到哪几步),下一次执行开始时清掉; 子工作流里的步骤不另算一路并行 - run/derive:接了驳回出口的关卡按出口路由(驳回出口 / 其余是普通出口,同 compiler._make_router);走了驳回出口的算 「送出了数据」,下游照常排队、跑完,普通出口那一侧未到达;退回修改时两条出口都算走过;withSubSteps 把子工作流里的 步骤排在子工作流那一步后面(泳道、航迹的节点列表) - run/decode:走了驳回出口的那一步状态 rejected、副标题写谁驳回、备注、转到哪一步;终态写「完成(途经驳回)」和途经的 那几步(run.finished 的 rejected_via),没有途经的一字不变;子工作流里的步骤名接上子工作流的名字 - run/useNodeView:回放时「走了驳回出口」只说被驳回的那一刻 - canvas/NodeCard、pages/runs/TracePane:走了驳回出口的节点卡写「被驳回,转到「X」」,航迹节点卡写出口和转到哪,关键时刻 多一条「「审批」被驳回,转到「X」」、结局写「运行完成(途经驳回)」 - run/EvidencePanel、types:输入来源写明经哪个子工作流的哪个出口字段;子工作流里的节点(「sub/…」,画布上没有)照证据接口 给的名字写(node_label、subgraph_label),悬停写 id;停在子工作流入口的写「子工作流的运行输入」 - lib/terms:REJECTED_TEXT 加出口、走出口那一步、途经驳回的说法;EVIDENCE_TEXT 加经子工作流的说法 检查脚本:check-decode(驳回出口的步骤、航迹、走过的边、跑到一半、批准走普通出口;子工作流步骤名)、check-run-states (reject-exit:卡片状态与剪影、槽里写转到哪、驳回边走过批准边没走、运行成功)、check-studio(各类节点的出口、能连能画、 改配置收掉驳回线、改模式线跟过去)、check-evidence(subgraph:输入来源写明经哪个子工作流、子图节点写名字)
This commit is contained in:
@@ -2959,5 +2959,78 @@ await section('被驳回的那一步:node.failed 带 reason=rejected 时画「
|
||||
&& old.nodes.out?.state === 'blocked', `${old.nodes.pay?.state} / ${old.failedNodeId} / ${old.nodes.out?.state}`)
|
||||
})
|
||||
|
||||
await section('驳回出口:node.finished 带 outcome=rejected 时这一步画「被驳回」、写转到哪一步,下游照常往下走;终态写「途经驳回」(编排完善 §3)', async () => {
|
||||
const trace = await import(await load('/src/run/trace.ts'))
|
||||
const derive = await import(await load('/src/run/derive.ts'))
|
||||
const E = (seq, type, node_id, data = {}) => ({ seq, type, node_id, ts: seq, data })
|
||||
const to = [{ node_id: 'tell', label: '通知申请人' }]
|
||||
const events = [E(1, 'run.started', null, { nodes: 4 }), E(2, 'node.started', 'pay', { label: '付款' }),
|
||||
E(3, 'human.requested', 'pay', { title: '确认发送:付款' }), E(4, 'run.interrupted', 'pay', { payload: { node_id: 'pay' } }),
|
||||
E(5, 'run.resumed', null, {}), E(6, 'node.started', 'pay', { label: '付款', resumed: true }),
|
||||
E(7, 'human.resolved', 'pay', { approved: false, note: '金额不对', actor: '张工' }),
|
||||
E(8, 'node.finished', 'pay', { duration_ms: 900, outcome: 'rejected', kind: 'action_confirm', decision: 'reject',
|
||||
actor: '张工', note: '金额不对', to,
|
||||
preview: { rejected: true, decision: 'reject', kind: 'action_confirm', __decision__: 'rejected',
|
||||
text: '「付款」未确认(被驳回),没有发送:金额不对' } }),
|
||||
E(9, 'node.started', 'tell', { label: '通知申请人' }), E(10, 'node.finished', 'tell', { duration_ms: 5 }),
|
||||
E(11, 'run.finished', null, { output: {}, rejected_via: [{ node_id: 'pay', label: '付款', kind: 'action_confirm',
|
||||
decision: 'reject', actor: '张工', note: '金额不对', to }] })]
|
||||
const steps = flatten(mod.decodeRun(events))
|
||||
const pay = steps.find((s) => s.kind === 'node' && s.nodeId === 'pay')
|
||||
check('这一步写「被驳回」:状态 rejected,不是 failed、不是 error', pay?.status === 'rejected' && pay?.level !== 'error',
|
||||
`${pay?.status} / ${pay?.level}`)
|
||||
check('副标题写谁驳回、备注、转到哪一步', pay?.sub === '张工 驳回:金额不对,转到「通知申请人」', pay?.sub)
|
||||
check('这一次执行也记成被驳回', pay?.execs?.at(-1)?.status === 'rejected', pay?.execs?.at(-1)?.status)
|
||||
const tell = steps.find((s) => s.kind === 'node' && s.nodeId === 'tell')
|
||||
check('驳回出口那一步照常跑完', tell?.status === 'done', tell?.status)
|
||||
const done = steps.find((s) => s.kind === 'lifecycle' && s.id === 'rd-11')
|
||||
check('终态写「完成(途经驳回)」,副标题写在哪一步被驳回、转到哪', done?.title === '完成(途经驳回)'
|
||||
&& done?.sub === '「付款」被驳回,转到「通知申请人」' && done?.status === 'done', `${done?.title} / ${done?.sub}`)
|
||||
const plain = flatten(mod.decodeRun([...events.slice(0, 10), E(11, 'run.finished', null, { output: {} })]))
|
||||
.find((s) => s.kind === 'lifecycle' && s.id === 'rd-11')
|
||||
check('没有途经驳回的终态和以前一字不差', plain?.title === '完成' && plain?.sub === undefined, `${plain?.title} / ${plain?.sub}`)
|
||||
|
||||
// 航迹与图:关卡状态 rejected、走了驳回出口;普通出口那一步没走到(未到达),驳回出口的边走过了
|
||||
const graph = { nodes: [{ id: 'start', type: 'input' }, { id: 'pay', type: 'tool' }, { id: 'out', type: 'output' },
|
||||
{ id: 'tell', type: 'output' }],
|
||||
edges: [{ source: 'start', target: 'pay' }, { source: 'pay', target: 'out', sourceHandle: 'out' },
|
||||
{ source: 'pay', target: 'tell', sourceHandle: 'rejected' }] }
|
||||
const t = derive.applyDerived(events.reduce(trace.foldEvent, trace.emptyTrace()), graph)
|
||||
check('航迹:关卡状态 rejected、走了驳回出口', t.nodes.pay?.state === 'rejected' && t.nodes.pay?.exited === true
|
||||
&& t.nodes.pay?.takenHandle === 'rejected', `${t.nodes.pay?.state} / ${t.nodes.pay?.exited} / ${t.nodes.pay?.takenHandle}`)
|
||||
check('航迹:转到哪一步记着(卡片照它写)', t.nodes.pay?.exitTo?.join() === '通知申请人', t.nodes.pay?.exitTo?.join())
|
||||
check('航迹:不记成失败的节点', t.failedNodeId === undefined, t.failedNodeId)
|
||||
check('航迹:普通出口那一步「未到达」,不是「被阻断」', t.nodes.out?.state === 'unreached', t.nodes.out?.state)
|
||||
const walked = derive.walkedEdges(t, graph)
|
||||
check('走过的边:驳回出口那条在,普通出口那条不在', walked.has('pay|tell|rejected') && !walked.has('pay|out|out'),
|
||||
[...walked].join(' , '))
|
||||
// 跑到一半(驳回出口那一步还没开始):它在排队,普通出口那一步没有
|
||||
const mid = derive.applyDerived(events.slice(0, 8).reduce(trace.foldEvent, trace.emptyTrace()), graph)
|
||||
// 没有事件、推导下来也还是 idle 的节点不进航迹(applyDerived 只补变了的)
|
||||
check('跑到一半:驳回出口那一步排队,普通出口那一步没动', mid.nodes.tell?.state === 'queued'
|
||||
&& (mid.nodes.out?.state ?? 'idle') === 'idle', `${mid.nodes.tell?.state} / ${mid.nodes.out?.state ?? 'idle'}`)
|
||||
// 批准了:正常结束(没有 outcome),走普通出口,驳回出口那一步未到达
|
||||
const ok = [...events.slice(0, 7), E(8, 'node.finished', 'pay', { duration_ms: 900, preview: { receipt: { id: 'R1' } } }),
|
||||
E(9, 'node.started', 'out', { label: '成果' }), E(10, 'node.finished', 'out', {}), E(11, 'run.finished', null, { output: {} })]
|
||||
const t2 = derive.applyDerived(ok.reduce(trace.foldEvent, trace.emptyTrace()), graph)
|
||||
check('批准了:关卡完成、驳回出口那一步未到达', t2.nodes.pay?.state === 'done' && !t2.nodes.pay?.exited
|
||||
&& t2.nodes.tell?.state === 'unreached', `${t2.nodes.pay?.state} / ${t2.nodes.tell?.state}`)
|
||||
const walked2 = derive.walkedEdges(t2, graph)
|
||||
check('批准了:走过的是普通出口那条', walked2.has('pay|out|out') && !walked2.has('pay|tell|rejected'), [...walked2].join(' , '))
|
||||
})
|
||||
|
||||
await section('子工作流里的每一步进了同一个运行(节点 id「子工作流节点 id/…」):时间线的步骤名接上子工作流的名字(编排完善 §2)', async () => {
|
||||
const E = (seq, type, node_id, data = {}) => ({ seq, type, node_id, ts: seq, data })
|
||||
const steps = flatten(mod.decodeRun([E(1, 'run.started', null, { nodes: 2 }),
|
||||
E(2, 'node.started', 'sub', { label: '取数子流程', node_type: 'subgraph' }),
|
||||
E(3, 'node.started', 'sub/fetch', { label: '查订单', node_type: 'tool' }), E(4, 'node.finished', 'sub/fetch', {}),
|
||||
E(5, 'node.started', 'sub/o', { label: 'o', node_type: 'output' }), E(6, 'node.finished', 'sub/o', {}),
|
||||
E(7, 'node.finished', 'sub', {}), E(8, 'run.finished', null, { output: {} })]))
|
||||
const title = (id) => steps.find((s) => s.kind === 'node' && s.nodeId === id)?.title
|
||||
check('子工作流里的一步:「取数子流程 › 查订单」', title('sub/fetch') === '取数子流程 › 查订单', title('sub/fetch'))
|
||||
check('没起名字的(名字就是子图里的 id)退到节点类型', title('sub/o') === '取数子流程 › 成果', title('sub/o'))
|
||||
check('子工作流节点自己照旧', title('sub') === '取数子流程', title('sub'))
|
||||
})
|
||||
|
||||
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 解码器全部通过')
|
||||
process.exit(failed ? 1 : 0)
|
||||
|
||||
@@ -974,6 +974,35 @@ await section('sources', '输入来源:与快照一致、模型报 X 快照是
|
||||
check('Esc 关掉面板,焦点回到原片段', await panel(page).count() === 0 && await active(page) === qseg('8.7%'), await active(page))
|
||||
})
|
||||
|
||||
await section('subgraph', '子工作流交回的证据:输入来源写明经哪个子工作流的哪个出口字段,子图里的节点写图上的名字(编排完善 §2)', async () => {
|
||||
// 父图口径卡读子工作流出口的字段、出处接上子图里那一格时,证据接口在输入步骤上另给 subgraph / field、
|
||||
// node_label / subgraph_label(api/evidence 的 _scoped_ids)。画布、记录页认不出「sub/fetch」这种 id:照接口给的名字写
|
||||
const target = qseg('37.0元')
|
||||
const { ctx, page: p, errors } = await probeQ((id, body) => {
|
||||
if (id !== target) return body
|
||||
for (const st of body.chain ?? []) {
|
||||
if (st.step === 'input' && st.path === 'vars.kpi.gmv') {
|
||||
Object.assign(st, { path: 'nodes.sub.output.gmv', node_id: 'sub/fetch', node_label: '取数子流程 › 查订单',
|
||||
subgraph: 'sub', subgraph_label: '取数子流程', field: 'gmv' })
|
||||
}
|
||||
}
|
||||
return body
|
||||
})
|
||||
try {
|
||||
await openQ(p, '37.0元', { wait: '[data-ev-sources]' })
|
||||
const row = p.locator('[data-evidence-panel] [data-ev-source="nodes.sub.output.gmv"]')
|
||||
const text = await row.innerText().catch(() => '')
|
||||
check('写明经哪个子工作流的哪个出口字段', text.includes('经子工作流「取数子流程」的出口字段「gmv」'), text)
|
||||
check('子图里的节点写图上的名字,不写裸 id', text.includes('取数子流程 › 查订单') && !text.includes('sub/fetch'), text)
|
||||
check('核对状态照常(与快照一致)', text.includes('与快照一致'), text)
|
||||
const chip = await row.locator('[data-ev-node]').first().getAttribute('title').catch(() => null)
|
||||
check('悬停照样看得到 id', chip === 'sub/fetch', String(chip))
|
||||
check('没有运行时报错', errors.length === 0, errors.slice(0, 3).join(' | '))
|
||||
} finally {
|
||||
await ctx.close()
|
||||
}
|
||||
})
|
||||
|
||||
await section('table', '整表:每格是可点的片段,方向键按行列走,值和数同一套线型', async () => {
|
||||
const cells = page.locator(`${Q} table td [data-seg]`)
|
||||
check('整表每个数据格都是可点的片段(3 行 × 3 列)', await cells.count() === 9, String(await cells.count()))
|
||||
|
||||
@@ -35,7 +35,7 @@ const SHOTS = process.env.RUN_STATES_SHOTS ?? '/tmp/agentlab-run-states'
|
||||
// 截图的像素密度:要看清卡片里 11px 的字时设成 2
|
||||
const DSF = Number(process.env.RUN_STATES_DSF ?? 1) || 1
|
||||
mkdirSync(SHOTS, { recursive: true })
|
||||
// 只跑其中几段(逗号分隔:themes, interact, team, endings, replay, history, issues, exits, reduced),调样式时省时间。
|
||||
// 只跑其中几段(逗号分隔:themes, interact, team, endings, replay, history, issues, exits, rejected, reject-exit, reduced),调样式时省时间。
|
||||
// RUN_STATES_SKIP 反过来,跳过这几段:check-all 把这个检查拆成几份分到不同的道上,最后一份用它
|
||||
// 兜住「其余全部」,以后新加的段不用改 check-all 也跑得到
|
||||
const ONLY = (process.env.RUN_STATES_ONLY ?? '').split(',').filter(Boolean)
|
||||
@@ -1562,6 +1562,61 @@ await section('rejected', async () => {
|
||||
await browser.close()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 驳回出口
|
||||
|
||||
await section('reject-exit', async () => {
|
||||
// 驳回出口(编排完善 §3):审批接了「驳回」出口,被驳回时这一步正常结束(node.finished 带 outcome=rejected),沿驳回
|
||||
// 那条边往下走。画布上它画「被驳回」(中性色、停止的剪影),槽里写转到哪一步;驳回出口的边走过了、下游照常跑完,
|
||||
// 普通出口那一侧未到达;运行收成成功(途经驳回),不是取消
|
||||
console.log('\n=== 驳回出口 ===')
|
||||
const EXIT_TAIL = [
|
||||
[3, 'human.resolved', 'human', { approved: false, note: '数字对不上', actor: '李工' }],
|
||||
[0.05, 'run.resumed', null, { actor: '李工' }],
|
||||
[0.05, 'node.started', 'human', { resumed: true }],
|
||||
[0.1, 'node.finished', 'human', { duration_ms: 40, outcome: 'rejected', kind: 'approve', decision: 'reject',
|
||||
actor: '李工', note: '数字对不上', to: [{ node_id: 'fix', label: '补数工单' }],
|
||||
preview: { rejected: true, approved: false, decision: 'reject', kind: 'approve', note: '数字对不上',
|
||||
text: '人工驳回:数字对不上', __decision__: 'rejected' } }],
|
||||
[0.1, 'node.started', 'fix', {}],
|
||||
[0.1, 'node.finished', 'fix', { duration_ms: 5, preview: { 缺口清单: '退回:数字对不上' } }],
|
||||
[0.05, 'run.finished', null, { output: { 缺口清单: '退回:数字对不上' },
|
||||
rejected_via: [{ node_id: 'human', label: '班长审批', kind: 'approve', decision: 'reject', actor: '李工',
|
||||
note: '数字对不上', to: [{ node_id: 'fix', label: '补数工单' }] }],
|
||||
timing: { wall_ms: 60000, active_ms: 40000, wait_ms: 20000 } }],
|
||||
]
|
||||
const EXITED = script([...HEAD, ...TEAM_ROUND0, ...TEAM_DONE, ...LOOP_DONE, ...TO_GATE, ...EXIT_TAIL])
|
||||
const { browser, page, errors } = await open({ theme: 'dark' })
|
||||
await setLod(page, 'full')
|
||||
await feed(page, EXITED(Date.now()))
|
||||
await page.waitForTimeout(400)
|
||||
const all = await cards(page)
|
||||
check('走驳回出口的那一步:卡片状态是 rejected', all.human?.state === 'rejected', all.human?.state)
|
||||
check('……剪影是停止,不是失败的三角', all.human?.shape === 'stop', all.human?.shape)
|
||||
check('……槽里写转到哪一步', /被驳回,转到「补数工单」/.test(all.human?.text ?? ''), JSON.stringify(all.human?.text))
|
||||
check('……不写「失败」', !/失败/.test(all.human?.text ?? ''), JSON.stringify(all.human?.text))
|
||||
check('驳回出口那一步照常跑完', all.fix?.state === 'done', all.fix?.state)
|
||||
check('普通出口那一步「未到达」', all.out?.state === 'unreached', all.out?.state)
|
||||
const edges = await page.evaluate(() => Object.fromEntries(window.__studio.getState().edges
|
||||
.filter((e) => e.source === 'human').map((e) => {
|
||||
const el = document.querySelector(`.react-flow__edge[data-id="${CSS.escape(e.id)}"]`)
|
||||
return [e.sourceHandle, el ? [...el.classList].filter((c) => c.startsWith('sf-e-')).join(' ') : '没画出来']
|
||||
})))
|
||||
check('驳回出口的边走过了,批准那条没走', /sf-e-walked/.test(edges.rejected ?? '') && /sf-e-unwalked/.test(edges.approved ?? ''),
|
||||
JSON.stringify(edges))
|
||||
const state = await page.evaluate(() => ({ phase: window.__studio.getState().runPhase,
|
||||
failed: window.__studio.getState().trace.failedNodeId ?? null }))
|
||||
check('运行收成成功(途经驳回),没有「失败的节点」', state.phase === 'succeeded' && state.failed === null, JSON.stringify(state))
|
||||
// 出口:驳回出口是中性色,写「驳回」
|
||||
const exit = await page.evaluate(() => {
|
||||
const labels = [...document.querySelectorAll('.react-flow__node[data-id="human"] .nc-exit')].map((el) => el.textContent)
|
||||
return labels
|
||||
})
|
||||
check('审批的两个出口:批准、驳回', exit.join() === '批准,驳回', exit.join())
|
||||
shots.push(await shot(page, 'dark-reject-exit'))
|
||||
check('没有运行时报错', errors.length === 0, errors.slice(0, 3).join(' | '))
|
||||
await browser.close()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 关掉动效
|
||||
|
||||
await section('reduced', async () => {
|
||||
|
||||
@@ -3025,6 +3025,77 @@ await section('应用平台:人工节点的流程配置、入口字段的类
|
||||
}
|
||||
})
|
||||
|
||||
await section('驳回出口:会被人驳回的节点多一个「驳回」出口,能连、能显示;改配置不再会被驳回时连线一并收掉(编排完善 §3)', async () => {
|
||||
FAKES['st-reject'] = wf('st-reject', { name: '__studio_check_reject__', version: 1, graph: {
|
||||
nodes: [
|
||||
node('in', 'input', 0, 200, '入口', { fields: [{ name: 'q' }] }),
|
||||
node('ask', 'human', 300, 60, '补充材料', { mode: 'input', title: '补一下' }),
|
||||
node('ok', 'human', 300, 260, '主管审批', { mode: 'approve', title: '可以吗' }),
|
||||
node('send', 'tool', 600, 60, '发通知', { tool: 'http_request', args: {}, approval: 'always' }),
|
||||
node('query', 'tool', 600, 260, '查订单', { tool: 'db_query__shop', args: { sql: 'SELECT 1' } }),
|
||||
node('run', 'code', 600, 460, '跑脚本', { code: 'print(1)', language: 'python', approval: 'always' }),
|
||||
node('calc', 'code', 600, 660, '算一下', { code: 'print(2)', language: 'python' }),
|
||||
node('out', 'output', 900, 200, '成果', { fields: [{ name: '结果', value: '{{ last_message }}' }] }),
|
||||
node('back', 'output', 900, 460, '退回', { fields: [{ name: '结果', value: '退回了' }] }),
|
||||
],
|
||||
edges: [edge('in', 'ask'), edge('in', 'ok'), edge('ask', 'send', 'out'), edge('ok', 'query', 'approved'),
|
||||
edge('send', 'out', 'out'), edge('query', 'out', 'out'), edge('ok', 'run', 'rejected'), edge('run', 'out', 'out'),
|
||||
edge('calc', 'out', 'out')],
|
||||
} })
|
||||
try {
|
||||
const { ctx, page, errors } = await open({ path: '/studio/st-reject' })
|
||||
await page.waitForTimeout(500)
|
||||
const exits = (id) => page.evaluate((x) => [...document.querySelectorAll(
|
||||
`.react-flow__node[data-id="${x}"] .react-flow__handle.source`)].map((h) => h.getAttribute('data-handleid')), id)
|
||||
const labels = (id) => page.evaluate((x) => [...document.querySelectorAll(
|
||||
`.react-flow__node[data-id="${x}"] .nc-exit`)].map((el) => el.textContent), id)
|
||||
check('补充输入:普通出口 + 驳回', (await exits('ask')).join() === 'out,rejected', (await exits('ask')).join())
|
||||
check('……出口上写「驳回」', (await labels('ask')).join() === '驳回', (await labels('ask')).join())
|
||||
check('审批:批准 + 驳回(同一个 id)', (await exits('ok')).join() === 'approved,rejected', (await exits('ok')).join())
|
||||
check('调用工具(每次审批):普通出口 + 驳回', (await exits('send')).join() === 'out,rejected', (await exits('send')).join())
|
||||
check('数据源查询(只读、不问人):只有普通出口', (await exits('query')).join() === 'out', (await exits('query')).join())
|
||||
check('沙箱代码(每次审批):普通出口 + 驳回', (await exits('run')).join() === 'out,rejected', (await exits('run')).join())
|
||||
check('沙箱代码(不审批):只有普通出口', (await exits('calc')).join() === 'out', (await exits('calc')).join())
|
||||
|
||||
// 连:从补充输入的驳回出口连到「退回」
|
||||
await S(page, () => window.__studio.getState().onConnect({ source: 'ask', target: 'back', sourceHandle: 'rejected', targetHandle: null }))
|
||||
await page.waitForTimeout(300)
|
||||
const wired = await S(page, () => window.__studio.getState().edges
|
||||
.filter((e) => e.source === 'ask').map((e) => `${e.sourceHandle}->${e.target}`).sort())
|
||||
check('驳回出口连上了(普通出口那条照旧)', wired.join() === 'out->send,rejected->back', wired.join())
|
||||
const drawn = await page.evaluate(() => {
|
||||
const e = window.__studio.getState().edges.find((x) => x.source === 'ask' && x.sourceHandle === 'rejected')
|
||||
return !!e && !!document.querySelector(`.react-flow__edge[data-id="${CSS.escape(e.id)}"] .react-flow__edge-path`)
|
||||
})
|
||||
check('……画得出来', drawn)
|
||||
// 改配置:审批策略改成从不审批,工具不再会被驳回,驳回出口和连在上面的线一起收掉
|
||||
await S(page, () => window.__studio.getState().onConnect({ source: 'send', target: 'back', sourceHandle: 'rejected', targetHandle: null }))
|
||||
await page.waitForTimeout(200)
|
||||
await S(page, () => {
|
||||
const n = window.__studio.getState().nodes.find((x) => x.id === 'send')
|
||||
window.__studio.getState().updateNode('send', { config: { ...n.data.config, approval: 'never' } })
|
||||
})
|
||||
await page.waitForTimeout(300)
|
||||
check('改成从不审批:驳回出口没了', (await exits('send')).join() === 'out', (await exits('send')).join())
|
||||
const left = await S(page, () => window.__studio.getState().edges
|
||||
.filter((e) => e.source === 'send').map((e) => `${e.sourceHandle}->${e.target}`))
|
||||
check('……连在驳回出口上的线一并收掉,普通出口的线还在', left.join() === 'out->out', left.join())
|
||||
// 补充输入改成审批:普通出口的线跟到「批准」,驳回出口的线还在
|
||||
await S(page, () => {
|
||||
const n = window.__studio.getState().nodes.find((x) => x.id === 'ask')
|
||||
window.__studio.getState().updateNode('ask', { config: { ...n.data.config, mode: 'approve' } })
|
||||
})
|
||||
await page.waitForTimeout(300)
|
||||
const moved = await S(page, () => window.__studio.getState().edges
|
||||
.filter((e) => e.source === 'ask').map((e) => `${e.sourceHandle}->${e.target}`).sort())
|
||||
check('补充输入改成审批:普通出口的线跟到批准,驳回的线还在', moved.join() === 'approved->send,rejected->back', moved.join())
|
||||
check('没有运行时报错', errors.length === 0, errors.slice(0, 3).join(' | '))
|
||||
await ctx.close()
|
||||
} finally {
|
||||
delete FAKES['st-reject']
|
||||
}
|
||||
})
|
||||
|
||||
await section('受管工作流在画布上改过之后:页头、版本历史、选择器照当前发布版的级别写「受管」;和发布版内容相同时不写领先', async () => {
|
||||
// 画布上改过之后 status 回到草稿,当前发布版仍是受管级(published_level)。st-govs:撤回改动后保存出来的 v8,
|
||||
// 服务端按图哈希比出和 v6 相同(same_as_published);st-gova:真的改过、领先 2 版
|
||||
|
||||
Reference in New Issue
Block a user