触发历史不再给基准已过时的自动对比;对比页找不到默认基准时标题不再和「可在上方选择」打架,换基准的下拉和基准卡写明失败、取消的运行

- 中间失败的那次事后被接着运行成功之后,第 3 周存着的那份对比(比的是第 1 周,previous 为假)和运行详情「与上一次
  对比」的默认基准(现在是第 2 周)又对不上,两处却都写「上一次成功的运行」。auto_compare.outdated 对基准当时就不是
  紧挨着的上一次的那几份按 pick_base 现挑一遍,基准变了的算过时;api/schedules._fire_outs 对过时的给 null(不给链接、
  不给旧计数),两个同名入口比的始终是同一对运行。基准没变的照旧给:中间那次还没结束、后来失败、被删掉都一样。紧挨着
  的上一次不会过时(两次之间插不进新的运行),不核
- test_base_follows_launch_order_not_finish_time 补上:接着运行完之后第 3 周的触发记录不再带对比、任务列表的 last_fire
  同样不带、运行详情的默认基准是第 2 周;另加一条基准仍是挑出来的那一次时照旧给出
- 对比页找不到默认基准:标题由「未找到可作为基准的运行」改为「没有默认基准」——失败、取消但封存了的运行不当默认基准,
  却照样能在上方手动选;服务端的原因改成「没有找到上一次成功的运行:需要同一工作流、同一运行类别里比这次先发起、成功
  结束并已封存的运行」,运行详情弹出的提示单独读也说得清
- 换基准的下拉给失败、已取消的运行写上状态,基准卡也标出来;下拉说明补一句默认基准只取成功结束的运行
- 运行详情「更多」的菜单项点之前不知道基准是哪一次,基准不是紧挨着的上一次时仍叫「与上一次对比」,和触发历史的叫法
  不同:注释照实写明,留作后续
This commit is contained in:
ThearchyHelios
2026-10-06 13:33:40 +08:00
parent 238fe94fee
commit 1bee9b5100
8 changed files with 176 additions and 35 deletions
+50 -9
View File
@@ -11,7 +11,8 @@
// 都不能出现对比页)。下一步的提示要和页面上真能做的事对得上:下拉里没有能换的运行就不叫人去上方换,本次的记录
// 不存在时不给注定失败的「重试」。默认基准的说法照服务端的挑法(api/compare.py 的 pick_base)写「上一次成功的运行」
// ——上一次失败了,基准是再往前成功的那一次;定时任务触发历史里的「与上一次对比」和运行详情里的同名入口打开同一对
// 运行、写同一句来历,基准不是紧挨着的上一次时触发历史上看得见的字写明「上一次成功的运行」。
// 运行、写同一句来历,基准不是紧挨着的上一次时触发历史上看得见的字写明「上一次成功的运行」;找不到默认基准时标题不说
// 「没有可作为基准的运行」(失败、取消的照样能手动选),换基准的下拉和基准卡写明哪次失败、取消了。
//
// 全部用 page.route 假接口:虚构的「区域销售周报」三次已封存的正式运行和一次没封存的,运行详情、运行列表、
// compare/base、compare、定时任务列表、发起它们的定时任务和触发历史都在浏览器层答掉,别的 GET 放行(外壳要的工作流、
@@ -190,6 +191,8 @@ const REASON_WORKFLOW = '默认基准:同一工作流、同一运行类别上
const FINDING = '正在查找上一次成功的运行'
/** 409、422、缺基准的 404 时下拉里没有能换的运行:不叫人去上方换,照实说 */
const NO_OTHER = '同一工作流没有其他可换作基准的已封存运行'
/** 服务端(api/compare.py 的 NO_BASE_TEXT)找不到默认基准时的原话 */
const NO_BASE_SERVER = '没有找到上一次成功的运行:需要同一工作流、同一运行类别里比这次先发起、成功结束并已封存的运行'
/**
* 发起本次和基准的定时任务(RUNS 里的 schedule_id)和它的触发历史:本次那一行带服务端自动对比的计数,基准是 B、不是
@@ -249,7 +252,7 @@ function defaults(opts) {
if (m) {
if (opts.base) return opts.base(m[1])
return m[1] === T ? { json: { base_run_id: B, reason: 'same_schedule', base: refOf(B) } }
: { status: 404, json: { detail: '没有找到可作为基准的运行' } }
: { status: 404, json: { detail: NO_BASE_SERVER, code: 'compare_no_base' } }
}
m = p.match(/^\/api\/runs\/([^/]+)\/compare$/)
if (m) {
@@ -531,7 +534,8 @@ await section('默认基准的说法:上一次成功的运行', async () => {
})
await section('运行详情与触发历史的「与上一次对比」:同一个基准、同一句来历', async () => {
// 两处的基准由服务端用同一个函数挑(api/compare.py 的 pick_base):上一次失败了,两处都是再往前成功的 B
// 两处的基准由服务端用同一个函数挑(api/compare.py 的 pick_base):上一次失败了,两处都是再往前成功的 B。两处真的
// 一致由 backend/tests/test_scheduler_compare.py 守,这里守前端两个入口都原样用服务端给的基准
const menu = await open(`/runs/${T}`)
await viaMenu(menu.page)
const hist = await open(`/schedules/${SCHEDULE_ID}?tab=history`)
@@ -871,6 +875,41 @@ await section('换基准、互换', async () => {
`${listsBefore} → ${lists()}`)
})
await section('换基准:失败、取消的运行写明状态,默认基准只取成功结束的', async () => {
// 默认基准跳过失败、取消的运行(服务端 pick_base),手动选不限:下拉和基准卡上得看得出哪次没成功,人读懂「上一次成功的
// 运行」之后才挑得出成功的那一次。两次都封存过,都能选
const F = 'c0ffee0failed0000000000000000005'
const C = 'c0ffee0cancel0000000000000000006'
RUNS[F] = runOf(F, { status: 'failed', version: 3, created_at: day('10-01'), started_at: day('10-01'), finished_at: '2026-10-01T01:01:00Z' })
RUNS[C] = runOf(C, { status: 'cancelled', version: 3, created_at: day('10-02'), started_at: day('10-02'), finished_at: '2026-10-02T01:01:00Z' })
try {
const { page, errors } = await open(comparePath(T, F))
await settled(page)
const picker = page.locator('select[data-compare-base-picker]')
await until(async () => (await picker.locator('option').count()) > 2)
const options = Object.fromEntries(await picker.locator('option').evaluateAll((els) =>
els.map((o) => [o.value, { text: o.textContent ?? '', disabled: o.disabled }])))
check('下拉:失败的那次写「失败」,照样能选', !!options[F]?.text.includes('· 失败') && !options[F].disabled, JSON.stringify(options[F]))
check('下拉:取消的那次写「已取消」,照样能选', !!options[C]?.text.includes('· 已取消') && !options[C].disabled, JSON.stringify(options[C]))
check('下拉:成功结束的不另写状态', !!options[B] && !/失败|已取消|已完成/.test(options[B].text), options[B]?.text)
const card = page.locator('[data-compare-run="base"]')
check('下拉下面的说明写明默认基准只取成功结束的运行', (await card.innerText()).includes('默认基准只取成功结束的运行'),
(await card.innerText()).replace(/\s+/g, ' '))
const status = card.locator('[data-compare-run-status]')
check('基准卡:失败的那次标出「失败」', await status.count() === 1 && (await status.innerText()).trim() === '失败',
await status.innerText().catch(() => '没有'))
check('本次卡:成功结束的不标状态', await page.locator('[data-compare-run="target"] [data-compare-run-status]').count() === 0)
await picker.selectOption(B)
await until(async () => new URL(page.url()).searchParams.get('base') === B)
await settled(page)
check('换成成功结束的那次:基准卡不再标状态', await until(async () => await card.locator('[data-compare-run-status]').count() === 0))
check('页面没有报错', errors.length === 0, errors.join(';'))
} finally {
delete RUNS[F]
delete RUNS[C]
}
})
await section('过时的答复不盖掉新的', async () => {
let release
const gate = new Promise((r) => { release = r })
@@ -958,15 +997,18 @@ await section('出错与找不到基准', async () => {
await settled(flaky.page)
check('其他错误:给「重试」,重试后正常显示', await flaky.page.locator('tr[data-compare-metric]').count() === METRICS.length)
// 只带本次:页面自己找基准,找不到时写原因,下拉照样能选
const lone = await open(comparePath(T), { base: () => ({ status: 404, json: { detail: '同一工作流没有其他已封存的正式运行' } }) })
// 只带本次:页面自己找基准,找不到时写原因,下拉照样能选。标题只说没有默认基准:失败、取消但封存了的运行不当默认
// 基准,却照样能手动选,写成「没有可作为基准的运行」就和下面「可在上方选择」打架
const lone = await open(comparePath(T), { base: () => ({ status: 404, json: { detail: NO_BASE_SERVER, code: 'compare_no_base' } }) })
await settled(lone.page, 'error')
const noBase = lone.page.locator('[data-compare-error="no-base"]')
check('找不到基准:写明原因', (await noBase.locator('[data-compare-error-title]').innerText()).includes('未找到可作为基准的运行')
&& (await noBase.innerText()).includes('同一工作流没有其他已封存的正式运行'))
check('找不到基准:标题写「没有默认基准」,后面跟服务端的原因', (await noBase.locator('[data-compare-error-title]').innerText()).trim() === '没有默认基准'
&& (await noBase.innerText()).includes(NO_BASE_SERVER), (await noBase.innerText()).replace(/\s+/g, ' '))
check('找不到基准:仍能手动选', await lone.page.locator('select[data-compare-base-picker]').isEnabled())
check('找不到基准、下拉里有能选的:说可在上方选', await until(async () => (await noBase.innerText()).includes('可在上方选择')),
(await noBase.innerText()).replace(/\s+/g, ' '))
check('找不到基准:不说「没有可作为基准的运行」这类和「可在上方选择」矛盾的话', !/可作为基准的运行|没有可以对比/.test(await noBase.innerText()),
(await noBase.innerText()).replace(/\s+/g, ' '))
// 同一工作流只有一次没封存的:下拉里没有能选的,不能再叫人去上方选
const bare = await open(comparePath(T), { base: () => ({ status: 404, json: { detail: '同一工作流没有其他已封存的正式运行', code: 'compare_no_base' } }),
list: () => ({ json: [RUNS[T], RUNS[U]] }) })
@@ -1071,8 +1113,7 @@ await section('不属于已保存工作流的运行:对比页的说明前后
try {
// 只带本次:服务端找不到基准(这种运行没有可比的)
const lone = await open(comparePath(T), { base: () => ({ status: 404, json: {
detail: '没有找到可以对比的上一次成功的运行:需要同一工作流、同一运行类别(定时任务发起的优先找同一定时任务)、比这次先发起、成功结束并已封存的运行',
code: 'compare_no_base' } }) })
detail: NO_BASE_SERVER, code: 'compare_no_base' } }) })
await settled(lone.page, 'error')
await until(async () => (await lone.page.locator('[data-compare-run="base"]').innerText()).includes('未保存或已删除'), 3000)
const card = (await lone.page.locator('[data-compare-run="base"]').innerText()).replace(/\s+/g, ' ')
+2 -1
View File
@@ -1509,7 +1509,8 @@ await section('详情:触发历史里的「与上一次对比」', async () =>
await section('详情:触发历史和运行详情的「与上一次对比」拿同一个基准', async () => {
// f10 的上一次(f11)失败了,服务端两处都挑再往前成功的 f12:自动对比和运行详情的默认基准用的是同一个函数
// (api/compare.py 的 pick_base)。这里守前端:两个入口都原样用服务端给的基准,打开同一对运行、写同一句来历
// (api/compare.py 的 pick_base),基准事后变了的旧对比触发历史不再给,这些由 backend/tests/test_scheduler_compare.py
// 守。这里守前端:两个入口都原样用服务端给的基准,打开同一对运行、写同一句来历
const runOf = (id, at) => ({ ...RUNS[0], id, started_by: '定时任务:门店周报 · 每周', created_at: iso(at), started_at: iso(at),
finished_at: iso(at + 4200), manifest_hash: 'ab'.repeat(32), manifest_seq: 2 })
const f10 = runOf('run-f10', nextMonday9(-11))