mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
双盲复算审批卡的预览夹具跟上收尾版差异说明:变体换成仍有代表性的一组,预告按带日期类型的表结构重新生成
- 收尾版差异说明对同一列上的时间范围只给兜底,旧的 time_range 变体失去意义:改名为 fallback(说不清的,兜底加 逐行对照,审批卡默认展开对照),另加 time_column(按哪一列筛时间不同,两列都声明为日期类型,预告写业务日期); 有效记录条件(filter_extra)、去重规则(dedup)和其余仍说得出的变体照留。夹具由服务端重新生成:filter_extra、 two_filters、peers.filter 的筛选说法也跟上了现在的输出(「……的行和 status 为空的行」),原先排在 time_range 后面没被比到。 - test_dual_check_fixture 的表结构把 paid_at、created_at 声明为 TIMESTAMP(业务日期的类型门槛),预告、合并预告、 被挡下的那一份照旧写业务日期;新增一条守住几种变体的方面和预告会写的项,免得规则一改变体悄悄变了形状。 - check-verify 跟着改:没有预告的那一节换上 fallback、time_column,兜底的那种对照默认已展开、不再点一下收起;有预告 的那一节兜底变体按兜底说明核对,换了时间列两边都写同一项时后一条按省略写法核对,并看选盲算时加重的那一条;几个 输入都差在时间列的合并用例改取 time_column 的说法。check-verify、check-decode 浏览器检查通过。
This commit is contained in:
@@ -9,11 +9,18 @@ check-verify 在夹具上能过,真实数据上却一处都不高亮(2026-10
|
||||
差异都没有」,delta_pct 是 (盲算值 − 原流程值) ÷ |原流程值| × 100 保留两位。夹具的数据源当 SQLite 比(方言 sqlite),
|
||||
两边的叫法是「原流程」「盲算」,同节点调用时一样。
|
||||
|
||||
**变体。** variants 换掉「销售额」的那一对 SQL,挑的是收尾版差异说明(只说时间列、多出来的筛选、聚合与去重、关联
|
||||
四类,别的只给兜底)下各有代表性的几种:按哪一列筛时间不同(time_column,两列都声明为日期类型,预告写业务日期)、
|
||||
只有一边按时间筛、一边多出来的筛选(排除无效记录的 filter_extra 预告写有效记录条件,指标自己范围的 filter_scope 不写)、
|
||||
差两个筛选条件、关联方式、聚合只差去重(dedup,预告写去重规则)、说不清的(fallback:同一列上的时间范围不同,只给兜底,
|
||||
审批卡默认展开逐行对照)、一边解析失败。
|
||||
|
||||
**写目录的预告。** 服务端在摘要里另带每个指标的 catalog(采用原流程、采用盲算各会往数据目录里写什么,
|
||||
dual_check_facts.preview,约定之外另加的),审批卡照它说;老服务端没有时卡上按差异的形状有条件地说,两种说法都按
|
||||
「服务端到底写不写」核对(2026-10 复核:卡上说会记入、服务端却不写的有好几种)。夹具里的预告(catalog_preview 按指标、
|
||||
variants 和 peers 各自的 catalog)也是服务端的真实输出:按下面的 SCHEMA、空目录、指标自己的范围留空算。双盲复算的
|
||||
服务端合入之前没有这个模块,逐项比对那几项跳过,形状和门槛照样查。
|
||||
variants 和 peers 各自的 catalog)也是服务端的真实输出:按下面的 SCHEMA、空目录、指标自己的范围留空算。SCHEMA 里
|
||||
paid_at、created_at 声明为 TIMESTAMP:业务日期只在两边的时间列都声明为日期 / 时间类型时才写。双盲复算的服务端合入之前
|
||||
没有这个模块,逐项比对那几项跳过,形状和门槛照样查。
|
||||
|
||||
**同一次裁决里几个指标落到同一项上。** 预告是每个指标单独算的,服务端写目录前却要把同一次裁决里落到同一张表同一项上的
|
||||
说法归拢(业务日期、去重规则说法不同的一项都不写,有效记录条件合成一句写,驳回过的拿出来不写)。服务端把每种组合按
|
||||
@@ -54,16 +61,17 @@ DIALECT = "sqlite"
|
||||
LABELS = ("原流程", "盲算")
|
||||
#: 摘要里每个指标最多列几条差异(同双盲复算节点的 SUMMARY_DIFFS)
|
||||
SUMMARY_DIFFS = 3
|
||||
#: 预告用的数据源名和表结构(虚构):服务端要认得出表、认得出聚合的列属于哪张表,才会预告写哪一项
|
||||
#: 预告用的数据源名和表结构(虚构):服务端要认得出表、认得出聚合的列属于哪张表,才会预告写哪一项;时间列声明为
|
||||
#: TIMESTAMP,业务日期才会预告写(文本、整数的时间列不记)
|
||||
SOURCE = "shop"
|
||||
SCHEMA: dict[str, Any] = {"tables": {
|
||||
"orders": {"name": "orders", "columns": [
|
||||
{"name": "id", "type": "INTEGER"}, {"name": "user_id", "type": "INTEGER"}, {"name": "amount", "type": "REAL"},
|
||||
{"name": "status", "type": "TEXT"}, {"name": "channel", "type": "TEXT"}, {"name": "paid_at", "type": "TEXT"},
|
||||
{"name": "created_at", "type": "TEXT"}], "primary_key": ["id"]},
|
||||
{"name": "status", "type": "TEXT"}, {"name": "channel", "type": "TEXT"}, {"name": "paid_at", "type": "TIMESTAMP"},
|
||||
{"name": "created_at", "type": "TIMESTAMP"}], "primary_key": ["id"]},
|
||||
"refunds": {"name": "refunds", "columns": [
|
||||
{"name": "id", "type": "INTEGER"}, {"name": "order_id", "type": "INTEGER"}, {"name": "amount", "type": "REAL"},
|
||||
{"name": "created_at", "type": "TEXT"}], "primary_key": ["id"]},
|
||||
{"name": "created_at", "type": "TIMESTAMP"}], "primary_key": ["id"]},
|
||||
}}
|
||||
#: 写目录时认得出的三项(dual_check_facts.FACT_ITEMS)
|
||||
FACT_ITEMS = ("business_date", "valid_filter", "dedup")
|
||||
@@ -183,6 +191,28 @@ def test_variants_carry_the_real_diff():
|
||||
assert swap["diff"] == _diff(original, blind), f"{group}.{name}"
|
||||
|
||||
|
||||
#: 变体里各有代表性的几种(见模块说明「变体」):名字 → (差异条目的方面, 预告会写的边和项)
|
||||
REPRESENTATIVE = {
|
||||
"time_column": (["time"], {"a": "business_date", "b": "business_date"}),
|
||||
"filter_extra": (["filter"], {"a": "valid_filter"}),
|
||||
"dedup": (["aggregate"], {"a": "dedup"}),
|
||||
"fallback": (["other"], {}),
|
||||
}
|
||||
|
||||
|
||||
def test_variants_keep_their_representative_shapes():
|
||||
"""检查脚本拿这几种变体看卡上怎么说。差异说明或写目录的规则一改,变体可能悄悄变成别的形状(换了时间列却只给兜底、
|
||||
预告不再写业务日期),那几项检查就什么也没测:这里守住每种的方面和预告会写的项,说不清的那种还得有逐行对照。不依赖
|
||||
服务端的模块,一直查。"""
|
||||
fx = _load()
|
||||
for name, (aspects, writes) in REPRESENTATIVE.items():
|
||||
swap = fx["variants"][name]
|
||||
assert [i["aspect"] for i in swap["diff"]["items"]] == aspects, name
|
||||
assert {side: said["item"] for side, said in swap["catalog"].items() if said["write"]} == writes, name
|
||||
fallback = fx["variants"]["fallback"]["diff"]
|
||||
assert fallback["fallback"] is True and fallback["text_diff"], fallback
|
||||
|
||||
|
||||
def test_text_diff_is_not_the_raw_text():
|
||||
"""守住这次的教训:至少有一处规范化之后的行和原文的任何一行都不一样(真实输出就是这样),夹具不再照原文手写。"""
|
||||
fx = _load()
|
||||
|
||||
@@ -565,33 +565,43 @@
|
||||
"daily_max_usd": 2.0
|
||||
},
|
||||
"variants": {
|
||||
"time_range": {
|
||||
"time_column": {
|
||||
"sql": [
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders\nWHERE paid_at >= '2026-09-01'\n AND paid_at < '2026-09-08'",
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders\nWHERE paid_at >= '2026-09-01'\n AND paid_at < '2026-09-09'"
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders\nWHERE created_at >= '2026-09-01'\n AND created_at < '2026-09-08'",
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders\nWHERE paid_at >= '2026-09-01'\n AND paid_at < '2026-09-08'"
|
||||
],
|
||||
"diff": {
|
||||
"equivalent": false,
|
||||
"items": [
|
||||
{
|
||||
"aspect": "time_range",
|
||||
"text": "原流程的时间范围是 paid_at >= '2026-09-01' AND paid_at < '2026-09-08',盲算是 paid_at >= '2026-09-01' AND paid_at < '2026-09-09'"
|
||||
"aspect": "time",
|
||||
"text": "原流程按 created_at 筛时间,盲算按 paid_at"
|
||||
}
|
||||
],
|
||||
"fallback": false,
|
||||
"text_diff": [
|
||||
"- orders.paid_at >= '2026-09-01' AND orders.paid_at < '2026-09-08'",
|
||||
"+ orders.paid_at >= '2026-09-01' AND orders.paid_at < '2026-09-09'"
|
||||
"- orders.created_at >= '2026-09-01' AND orders.created_at < '2026-09-08'",
|
||||
"+ orders.paid_at >= '2026-09-01' AND orders.paid_at < '2026-09-08'"
|
||||
]
|
||||
},
|
||||
"catalog": {
|
||||
"a": {
|
||||
"write": false,
|
||||
"reason": "指标「销售额」两边差在时间范围,对不上数据目录里的业务日期、有效记录条件或去重规则,不写入数据目录"
|
||||
"write": true,
|
||||
"source": "shop",
|
||||
"table": "orders",
|
||||
"item": "business_date",
|
||||
"value": {
|
||||
"column": "created_at"
|
||||
}
|
||||
},
|
||||
"b": {
|
||||
"write": false,
|
||||
"reason": "指标「销售额」两边差在时间范围,对不上数据目录里的业务日期、有效记录条件或去重规则,不写入数据目录"
|
||||
"write": true,
|
||||
"source": "shop",
|
||||
"table": "orders",
|
||||
"item": "business_date",
|
||||
"value": {
|
||||
"column": "paid_at"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -635,7 +645,7 @@
|
||||
"items": [
|
||||
{
|
||||
"aspect": "filter",
|
||||
"text": "原流程排除了 status = 'test' 的行,盲算没有"
|
||||
"text": "原流程排除了 status = 'test' 的行和 status 为空的行,盲算没有"
|
||||
}
|
||||
],
|
||||
"fallback": false,
|
||||
@@ -702,7 +712,7 @@
|
||||
},
|
||||
{
|
||||
"aspect": "filter",
|
||||
"text": "原流程排除了 status = 'refunded' 的行,盲算没有"
|
||||
"text": "原流程排除了 status = 'refunded' 的行和 status 为空的行,盲算没有"
|
||||
}
|
||||
],
|
||||
"fallback": false,
|
||||
@@ -789,6 +799,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"sql": [
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders\nWHERE paid_at >= '2026-09-01'\n AND paid_at < '2026-09-08'",
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders\nWHERE paid_at >= '2026-09-01'\n AND paid_at < '2026-09-09'"
|
||||
],
|
||||
"diff": {
|
||||
"equivalent": false,
|
||||
"items": [
|
||||
{
|
||||
"aspect": "other",
|
||||
"text": "两条 SQL 的写法不同,未能归纳成具体差异,请对照原文"
|
||||
}
|
||||
],
|
||||
"fallback": true,
|
||||
"text_diff": [
|
||||
"- orders.paid_at >= '2026-09-01' AND orders.paid_at < '2026-09-08'",
|
||||
"+ orders.paid_at >= '2026-09-01' AND orders.paid_at < '2026-09-09'"
|
||||
]
|
||||
},
|
||||
"catalog": {
|
||||
"a": {
|
||||
"write": false,
|
||||
"reason": "指标「销售额」两边的 SQL 不止差一个方面,或有未能归纳的差异,无法确定审核人认可的是哪一处,不写入数据目录"
|
||||
},
|
||||
"b": {
|
||||
"write": false,
|
||||
"reason": "指标「销售额」两边的 SQL 不止差一个方面,或有未能归纳的差异,无法确定审核人认可的是哪一处,不写入数据目录"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parse": {
|
||||
"sql": [
|
||||
"SELECT SUM(amount) AS gmv\nFROM orders",
|
||||
@@ -903,7 +943,7 @@
|
||||
"items": [
|
||||
{
|
||||
"aspect": "filter",
|
||||
"text": "原流程排除了 orders.status = 'cancelled' 的行,盲算没有"
|
||||
"text": "原流程排除了 orders.status = 'cancelled' 的行和 orders.status 为空的行,盲算没有"
|
||||
}
|
||||
],
|
||||
"fallback": false,
|
||||
|
||||
+41
-10
@@ -382,12 +382,14 @@ await section('审批卡:没有预告时,「记入数据目录」只说必
|
||||
// 不写。卡上不能说得比服务端宽——每句只说必要条件(「仅当」开头),这几种不写的照实说,免得审核人为了不让目录被写错
|
||||
// 而避开本来对的一边。变体的 catalog 是服务端对这一对 SQL 的真实预告,按它核对
|
||||
const { CATALOG_TABLE_FIELD_LABEL: LABEL } = await import(await load('/src/lib/terms.ts'))
|
||||
// [变体, 说明, 服务端不写的那一种卡上照实说的那句(只在卡上提数据目录时有)]
|
||||
// [变体, 说明, 服务端不写的那一种卡上照实说的那句(只在卡上提数据目录时有)]。变体挑的是收尾版差异说明下各有代表性
|
||||
// 的几种(backend/tests/test_dual_check_fixture.py 的模块说明):同一列上的时间范围不同只给兜底(fallback)
|
||||
const cases = [
|
||||
['time_range', '只差时间范围(不是换了时间列)'],
|
||||
['fallback', '说不清的(同一列上的时间范围不同,只给兜底)'],
|
||||
['two_filters', '差两个筛选条件'],
|
||||
['join', '差在关联方式'],
|
||||
['parse', '一边解析失败'],
|
||||
['time_column', '换了时间列(两列都声明为日期类型)', '只有一边按时间筛时不记'],
|
||||
['time_one_side', '只有一边按时间筛', '只有一边按时间筛时不记'],
|
||||
['filter_extra', '只多一个排除测试单的条件', '只看某个地区、某个渠道这类限定指标范围的条件不记'],
|
||||
['filter_scope', '只多一个渠道条件', '只看某个地区、某个渠道这类限定指标范围的条件不记'],
|
||||
@@ -412,11 +414,22 @@ await section('审批卡:没有预告时,「记入数据目录」只说必
|
||||
// 「不记」后面紧跟一句「记为人工确认的事实」,读起来像在说不记的那一项
|
||||
check('……「人工确认」说的是写入的项', note.includes('写入的项记为人工确认的事实') && !note.includes('不记。记为'), note)
|
||||
}
|
||||
if (name === 'time_one_side' || name === 'dedup') {
|
||||
if (name === 'time_column' || name === 'time_one_side' || name === 'dedup') {
|
||||
// 同一次裁决里几个指标给同一张表记的业务日期、去重规则不同时,服务端一项都不写(有效记录条件是合成一句写)。
|
||||
// 没有预告时卡上看不出别的指标落到哪一项上,照实说这一条
|
||||
check('……同一次裁决里几个指标给同一张表记的说法不同时都不记', note.includes('同一次裁决里') && note.includes('不同时都不记'), note)
|
||||
}
|
||||
if (name === 'time_column') {
|
||||
// 服务端两边都会写(各记被采纳那一边的列):卡上的必要条件说的就是按不同的列筛时间
|
||||
check('……两边按不同的列筛时间,采用的一边所用的时间列才记为业务日期', writes.length === 2
|
||||
&& note.includes('按不同的列筛时间') && note.includes('采用的一边所用的时间列'), note)
|
||||
}
|
||||
if (name === 'fallback') {
|
||||
// 收尾版差异说明归纳不了同一列上的时间范围:卡上写那句固定的兜底说明,默认展开对照,审核人逐行对照两条 SQL
|
||||
check('……写那句固定的兜底说明,不当成一条具体差异', (await text(gmv.locator('[data-dual-fallback]'))) === '两条 SQL 的写法不同,未能归纳成具体差异'
|
||||
&& await gmv.locator('[data-dual-diff-item]').count() === 0, await text(gmv.locator('[data-dual-fallback]')))
|
||||
check('……默认展开对照', (await gmv.locator('[data-dual-compare-toggle]').getAttribute('aria-expanded')) === 'true')
|
||||
}
|
||||
if (name === 'time_one_side') {
|
||||
// 正上方那条差异说「盲算没有按时间筛选」:卡上不能再说两边只差在按哪一列筛时间
|
||||
check('……不和正上方那条差异自相矛盾', (await text(gmv.locator('[data-dual-diff-item]'))).includes('没有按时间筛选')
|
||||
@@ -436,7 +449,9 @@ await section('审批卡:没有预告时,「记入数据目录」只说必
|
||||
}
|
||||
const diff = fx.variants[name].diff
|
||||
if (diff.text_diff.length) {
|
||||
await gmv.locator('[data-dual-compare-toggle]').click()
|
||||
// 只有兜底说明时对照默认就展开着:再点一下就收起来了
|
||||
const toggle = gmv.locator('[data-dual-compare-toggle]')
|
||||
if ((await toggle.getAttribute('aria-expanded')) !== 'true') await toggle.click()
|
||||
const m = await marksIn(gmv)
|
||||
const real = sidesOf(diff)
|
||||
check(`……规范化后的行按真实输出分两栏高亮(${real.del.length} + ${real.add.length} 行)`,
|
||||
@@ -449,7 +464,7 @@ await section('审批卡:没有预告时,「记入数据目录」只说必
|
||||
}
|
||||
|
||||
// 几个输入都差在时间列:节点把两处的差异合成一份(每条前面写明是哪个输入),不止一条,不写目录
|
||||
const t = fx.variants.time_range.diff.items[0]
|
||||
const t = fx.variants.time_column.diff.items[0]
|
||||
const merged = {
|
||||
equivalent: false, fallback: false, text_diff: fx.card.metrics[0].diff.text_diff,
|
||||
items: [{ aspect: 'time', text: `输入「gmv_web」:${fx.card.metrics[0].diff.items[0].text}` }, { aspect: 'time', text: `输入「gmv_app」:${t.text}` }],
|
||||
@@ -520,23 +535,39 @@ await section('审批卡:服务端预告了写目录时照它说,当前选
|
||||
await shot(base.page, 'approval-preview-dark')
|
||||
|
||||
// 几个变体:预告说两边都不写的,卡上就不提(没有预告时这几种会有条件地提);写的那一边说清写到哪、写成什么
|
||||
for (const [name, what] of [['time_one_side', '只有一边按时间筛'], ['filter_scope', '只多一个渠道条件'], ['filter_extra', '只多一个排除测试单的条件'],
|
||||
['dedup', '只差去不去重'], ['time_range', '只差时间范围']]) {
|
||||
for (const [name, what] of [['time_column', '换了时间列'], ['time_one_side', '只有一边按时间筛'], ['filter_scope', '只多一个渠道条件'],
|
||||
['filter_extra', '只多一个排除测试单的条件'], ['dedup', '只差去不去重'], ['fallback', '说不清的(只给兜底)']]) {
|
||||
const { page, errors } = await preview('dual-check-approval', { query: `variant=${name}&catalog=1`, artifact: cardWith(name) })
|
||||
await page.waitForSelector('[data-dual-check][data-state="ready"]')
|
||||
const m = metric(page, 'gmv')
|
||||
const expect = fx.variants[name].catalog
|
||||
const writes = ['a', 'b'].filter((s) => expect[s].write)
|
||||
check(`${what}(变体):摘要换成这一对 SQL 的差异`, (await text(m.locator('[data-dual-diff-item]'))) === fx.variants[name].diff.items[0].text,
|
||||
await text(m.locator('[data-dual-diff-item]')))
|
||||
if (fx.variants[name].diff.items.some((i) => i.aspect !== 'other' && i.aspect !== 'parse')) {
|
||||
check(`${what}(变体):摘要换成这一对 SQL 的差异`, (await text(m.locator('[data-dual-diff-item]'))) === fx.variants[name].diff.items[0].text,
|
||||
await text(m.locator('[data-dual-diff-item]')))
|
||||
} else {
|
||||
check(`${what}(变体):摘要换成兜底说明,不当成一条具体差异`, (await text(m.locator('[data-dual-fallback]'))) === '两条 SQL 的写法不同,未能归纳成具体差异'
|
||||
&& await m.locator('[data-dual-diff-item]').count() === 0, await text(m.locator('[data-dual-fallback]')))
|
||||
}
|
||||
if (!writes.length) {
|
||||
check(`${what}:服务端两边都不写,卡上不提数据目录`, await m.locator('[data-dual-catalog]').count() === 0, await text(m.locator('[data-dual-catalog]')))
|
||||
} else {
|
||||
const got = await previewClauses(m)
|
||||
// 两边都写、写的是同一张表的同一项(换了时间列:同一张表的业务日期)时,后一条省掉哪张表的哪一项
|
||||
const same = writes.length === 2 && ['source', 'table', 'item'].every((k) => expect.a[k] === expect.b[k])
|
||||
const ok = ['a', 'b'].every((s) => (expect[s].write
|
||||
? got[s]?.write && got[s].text.includes(LABEL[expect[s].item]) && got[s].text.includes(`会记为「${previewValue(expect[s].value)}」`)
|
||||
? got[s]?.write && got[s].text.includes(`会记为「${previewValue(expect[s].value)}」`)
|
||||
&& (s === 'b' && same ? got[s].text === `${CHOICE_LABEL.b}时会记为「${previewValue(expect.b.value)}」` : got[s].text.includes(LABEL[expect[s].item]))
|
||||
: !got[s]?.write && got[s]?.reason && got[s].text === blockedText(CHOICE_LABEL[s], expect[s].reason)))
|
||||
check(`${what}:写的一边说清写到哪、会记为什么,不写的一边照服务端的原话说不会记入的原因`, ok, `${got.a?.text} ‖ ${got.b?.text}`)
|
||||
if (name === 'time_column') {
|
||||
// 两列都声明为日期类型:选哪一边都记被采纳那一边的列,加重的那一条跟着选择走
|
||||
await m.locator('label', { hasText: '采用盲算的结果' }).click()
|
||||
const cur = await previewClauses(m)
|
||||
check('换了时间列,选盲算:加重的那一条说业务日期会记为盲算那一列', cur.b?.current && cur.b.write && !cur.a.current
|
||||
&& cur.b.text === `采用盲算的结果时会记为「${previewValue(expect.b.value)}」`
|
||||
&& cur.a.text === `采用原流程的结果时,「orders」(shop)的业务日期会记为「${previewValue(expect.a.value)}」`, `${cur.a?.text} ‖ ${cur.b?.text}`)
|
||||
}
|
||||
if (name === 'dedup') {
|
||||
// 采用去重的原流程记去重规则,采用不去重的盲算不记:两种选择分别看加重的那一句
|
||||
await m.locator('label', { hasText: '采用原流程的结果' }).click()
|
||||
|
||||
Reference in New Issue
Block a user