项目会不会延期,在测试阶段最容易看出端倪。以代号为 DS 的交付项目为例,如果团队引入了一套 harness 测试调度平台,那么每天发放多少任务、任务在队列里等待多久、失败之后有没有被及时退单重跑,这些信息比口头汇报更能说明版本是否真的接近可发布状态。本文把 DS 当成一个通用的数据服务或模型服务交付场景,介绍如何用 harness 的测试发放数据,对版本延期做第二次、第三次预测,并给出从建表、采集、计算到输出预测报告的最小实现。社区里经常讨论的 deepseek harness、codex harness 其实也可以套用同一套思路:只要任务是被统一发放、统一回传结果的,就能从队列和结果数据中读出风险信号。
软件团队常犯的一个错误,是把延期预测交给“感觉”。有人觉得功能还剩很多,有人觉得自动化脚本挺快,有人觉得多加班两天就能补上。这些判断没有统一数据口径,最后往往在测试阶段的倒数第二周才被发现来不及。真正靠谱的做法,是让每个任务都经过 harness 发放,把“工作量”变成“任务数”,把“处理速度”变成“每天完成数”,把“风险”变成“队列积压换算出的剩余天数”。
1. 为什么测试发放数据比测试通过率更早暴露延期
1.1 延期的本质是积压,而不是一个截止日期
延期不是某一天突然出现的。截止日期只是结果的表达,背后是“任务流入速度持续大于完成速度”。一个版本计划做 350 个测试用例,每天新发放 20 个回归任务,harness 每天只能稳定完成 15 个,那么每天就会新增大约 5 个积压任务。积压长时间不下降,截止日期就一定会往后推。
很多团队只看最终通过率,比如“当前通过率 80%”。这个数字有迷惑性。通过率只能说明已经跑过的任务里有多少通过,但无法回答两个更关键的问题:还剩多少任务没有跑?以当前速度跑完剩余任务需要几天?如果通过率是 80%,但还有 150 个任务没有进入执行队列,这个版本依然很危险。所以延期预测必须从“率”走向“量”,从“当前状态”走向“队列积压”。
用 harness 记录任务状态之后,可以每秒统计出三类数量:
- 已通过任务数:代表已完成且结果可依赖的工作量。
- 执行中/排队中任务数:代表已经被发放但还没有收口的工作量。
- 失败待重跑任务数:代表不是白做了,而是必须返工的工作量。
这三类数量放在一起,才能回答“还差多少工作量”。
判断基准不是“通过率 80%”,而是“按当前净完成速度,清理完剩余积压需要多少天”。通过率用于判断质量,积压量用于判断工期。
1.2 harness 的任务发放情况能反映什么
harness 在工程里通常指一套为被测对象提供输入、环境、执行和结果回收的驱动框架,也叫测试夹具或测试控制台。它不一定是一个具体软件,而是一种把手工验证变成可重复执行任务的方式。社区里看到的 deepseek harness、codex harness,本质上都是给模型或应用挂一个标准化的测评脚手架,让同一批样例可以被反复执行和对比。
从延期预测角度看,harness 最大的价值不是能跑多少用例,而是它会产生结构化的事件数据。一个用例从创建、发放、开始执行、通过、失败、重跑到最终关闭,每一步都有时间戳和状态位。这些数据经过聚合,就变成了延期预测的原料。
实际项目中,建议把每个 harness 任务看成一张“工作票”。工作票在队列里停留越久,说明测试资源越紧张;重跑次数越多,说明版本质量越不稳定;状态长期卡在 pending,说明环境影响或任务调度存在问题。
1.3 四个关键信号和对应的数据字段
下表可以先保存在项目文档里,用于日常观测:
| 信号 | 含义 | 数据字段 | 风险解读 |
|---|---|---|---|
| 任务发放量持续大于完成量 | 新任务进入速度比处理速度快 | created_at、finished_at | 积压会不断增长,延期概率上升 |
| 失败重跑率偏高 | 用例不是一次通过,而是反复试错 | retry_count、run_status | 环境不稳定或版本缺陷多,净工作量被放大 |
| 高优先级任务排队时间变长 | 重点验证没有被优先处理 | priority、started_at - created_at | 资源分配失序,阻断问题可能晚发现 |
| 阻塞任务数量不下降 | 问题依赖外部或前置修复 | status = BLOCKED | 即使有资源也无法推进,需要人工介入 |
这四个信号不是孤立存在的。任务发放量是输入,完成量是处理能力,失败重跑是损耗,阻塞是外部依赖。把它们放在同一张趋势图里看,延期风险往往能提前三到五个工作日显形。
1.4 第一次预测与再次预测的差异
第一次预测通常发生在提测前,依赖历史经验和排期计划,属于“先验预测”。真正有价值的是测试启动后的“再次预测”,也就是基于 harness 发放情况做修正。当测试任务实际进入执行通道后,团队开始知道真实速度、真实失败率、真实环境质量,先验预测的不确定性会快速下降。
本文要讲的正是这种“再次预测”:版本已经进入测试,harness 已经跑了一周左右,这时用真实任务数据重新估算剩余工作量。预测结果可能是“按当前速度可以按期完成”,也可能像标题里隐含的那样是“存在再次延期风险”。无论结论是哪一种,都必须有任务数据作为证据,而不是拍脑袋。
2. 搭建一个最小数据模型:采集任务发放、执行和退单状态
2.1 先明确要采集的范围
不要一开始就把所有测试数据都收集进来。数据字段越多,质量越难保证。针对延期预测,最少需要三类信息:任务基本信息、状态流转信息、失败原因信息。
任务基本信息回答“是什么”:哪个模块、哪个用例、什么优先级、属于哪个版本。状态流转信息回答“进展如何”:什么时候下发、什么时候开始跑、什么时候结束。失败原因信息回答“为什么慢”:是断言失败、环境故障、还是前置数据不存在。这些字段缺一不可,尤其是失败原因,否则后面只能看到失败率高,却不知道失败来自哪里。
2.2 用 SQLite 建表保存任务快照
先搭一个简单的 SQLite 模型。下面的表结构足够支撑中小型团队做延期预测:
CREATE TABLE harness_task ( task_id TEXT PRIMARY KEY, suite_name TEXT NOT NULL, case_name TEXT NOT NULL, priority TEXT NOT NULL DEFAULT 'P2', status TEXT NOT NULL, assignee TEXT, source_version TEXT, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, retry_count INTEGER NOT NULL DEFAULT 0, failure_stage TEXT, failure_reason TEXT ); CREATE TABLE harness_run_log ( log_id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, run_seq INTEGER NOT NULL DEFAULT 1, run_status TEXT NOT NULL, executor TEXT, started_at TEXT, finished_at TEXT, error_message TEXT, FOREIGN KEY (task_id) REFERENCES harness_task(task_id) ); CREATE INDEX idx_harness_task_status ON harness_task(status); CREATE INDEX idx_harness_task_created ON harness_task(created_at); CREATE INDEX idx_harness_task_finished ON harness_task(finished_at);harness_task保存当前任务的最新状态,harness_run_log保存每次运行历史。两者分开的好处是,重跑不会覆盖原始信息。分析时既能看到当前有多少任务失败,也能看到某个任务总失败了几次、每次失败原因是什么。
status 字段建议使用固定枚举:PENDING、RUNNING、PASSED、FAILED、BLOCKED、SKIPPED。不要使用“正在等待”“已跑完,失败”这类口语化状态,否则聚合 SQL 写起来非常痛苦。
建表时就把时间字段统一成 ISO 8601 格式,例如
2025-06-10 10:30:00。如果 harness 返回的是毫秒时间戳,优先在写入前转换,不要等分析时再调整。
2.3 用 Python 实现一个采集脚本
真实环境中,harness 通常提供 API。采集脚本只需要把自己的任务列表同步到上面两张表。下面脚本说明通用思路,实际项目需要按内部接口路径、鉴权方式和字段名调整:
import sqlite3 from datetime import datetime import requests def fetch_tasks(api_url, params): resp = requests.get(api_url, params=params, timeout=30) resp.raise_for_status() return resp.json().get("items", []) def upsert_task(conn, task): conn.execute( """ INSERT INTO harness_task ( task_id, suite_name, case_name, priority, status, assignee, source_version, created_at, started_at, finished_at, retry_count, failure_stage, failure_reason ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET status = excluded.status, started_at = coalesce(excluded.started_at, started_at), finished_at = coalesce(excluded.finished_at, finished_at), retry_count = excluded.retry_count, failure_stage = coalesce(excluded.failure_stage, failure_stage), failure_reason = coalesce(excluded.failure_reason, failure_reason) """, ( task["task_id"], task["suite_name"], task["case_name"], task.get("priority", "P2"), task["status"], task.get("assignee"), task.get("source_version", "v1.6"), task["created_at"], task.get("started_at"), task.get("finished_at"), task.get("retry_count", 0), task.get("failure_stage"), task.get("failure_reason"), ), ) def sync(conn, api_url, params): tasks = fetch_tasks(api_url, params) for task in tasks: upsert_task(conn, task) conn.commit() print(f"{datetime.now()} sync finished, count={len(tasks)}") if __name__ == "__main__": conn = sqlite3.connect("ds_harness_metrics.db") sync(conn, "https://harness.internal.example.com/api/v1/tasks", { "version": "v1.6.0", "limit": 500, }) conn.close()这段脚本解决了两个问题:新任务自动入库,旧任务状态自动更新。在 MySQL 或 PostgreSQL 中可以使用类似 upsert 语法,比如ON DUPLICATE KEY UPDATE或INSERT ... ON CONFLICT。学习环境中不需要一次接入真实 harness,可以先造一批模拟数据理解指标计算逻辑。
2.4 造一批模拟数据验证预测思路
没有真实 harness 时可以先生成演示数据。下面脚本按第 10 天快照造一批任务,覆盖排队、运行、失败、重跑、阻塞等情况。数据不代表任何真实版本,只为演示计算过程:
import sqlite3 from datetime import date, timedelta conn = sqlite3.connect("ds_harness_metrics.db") def days_ago(n, h=10, m=0): dt = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=n) return dt.replace(hour=h, minute=m).strftime("%Y-%m-%d %H:%M:%S") base = 20250601 for i in range(1, 350): task_id = f"DS-{i:04d}" ...为了省篇幅,这里不展开完整生成脚本。真实项目中,建议直接导出内部 harness 的数据,只需要保留核心字段,就能得到一致性远高于手工造数的训练样本。造数只用于学习理解和开发仪表盘。
3. 从原始数据计算风险指标,不要只看通过率
3.1 三个核心指标:发放量、完成量、积压量
把任务数据收进来之后,第一个要看的是趋势,而不是当前值。每天发放多少任务、每天完成多少任务,这两条曲线决定了积压的变化方向。
-- 每日任务发放量 SELECT substr(created_at, 1, 10) AS day, COUNT(*) AS issued_count FROM harness_task GROUP BY substr(created_at, 1, 10) ORDER BY day; -- 每日任务完成量,按最终状态统计 SELECT substr(finished_at, 1, 10) AS day, COUNT(*) AS finished_count, SUM(CASE WHEN status = 'PASSED' THEN 1 ELSE 0 END) AS passed_count FROM harness_task WHERE finished_at IS NOT NULL GROUP BY substr(finished_at, 1, 10) ORDER BY day;完成量比发放量更能反映团队真实推进速度。如果某天完成量为 0,即使发放量很高,也只是把更多任务堆积到了队列里。
积压量可以直接按状态聚合:
SELECT status, COUNT(*) AS task_count FROM harness_task GROUP BY status ORDER BY task_count DESC;一种更实用的口径是“净积压量”,等于所有尚未 PASSED 的任务数量,但不包括 SKIPPED。它同时包含排队、执行中、失败待重跑和阻塞。这个数字才是剩余工作量的近似值。
3.2 失败重跑率为什么比失败率更重要
失败率只反映用例执行结果,但失败之后是否重跑、重跑几次,直接影响时间成本。假设一个用例在 harness 上连续失败三次,最终重跑通过。从最终状态看,它是 PASSED,但从工作量看,它消耗了四次执行资源。如果这类情况大量存在,完成速度会被严重拖慢。
重跑率建议按周或天统计:
SELECT substr(created_at, 1, 10) AS day, COUNT(*) AS task_count, SUM(CASE WHEN retry_count >= 1 THEN 1 ELSE 0 END) AS retried_count FROM harness_task GROUP BY substr(created_at, 1, 10) ORDER BY day;当重跑率超过一定比例时,真正需要关注的不是重跑动作本身,而是重跑背后的原因。如果是因为断言不稳定,属于用例问题;如果是因为被测服务没起来,属于环境问题;如果是因为功能缺陷导致失败,属于版本问题。三种原因对应完全不同的处理方式。
不要把所有失败都归成“测试失败”。要在失败原因里区分环境、脚本、数据、功能,才能准确判断失败是否能通过单纯重跑消除,还是必须由开发修复代码。
3.3 回归窗口与修复收敛曲线
版本延期还有一个隐蔽信号:修复收敛速度变慢。测试前期发现缺陷是正常的,只要每天新增失败数在减少、修复通过率在上升,版本质量就在往可发布方向走。怕的是失败数高位震荡,甚至修复 A 缺陷后出现 B 缺陷。
观察修复曲线,可以把失败任务按“发现日期”和“关闭日期”透视:
-- 每日失败数量与已重跑通过数量的关系 SELECT substr(created_at, 1, 10) AS created_day, SUM(CASE WHEN status IN ('FAILED', 'BLOCKED') THEN 1 ELSE 0 END) AS open_failed, SUM(CASE WHEN status = 'PASSED' AND retry_count >= 1 THEN 1 ELSE 0 END) AS recovered_after_retry FROM harness_task GROUP BY substr(created_at, 1, 10) ORDER BY created_day;如果 open_failed 连续多日没有下降,而 recovered_after_retry 也在下降,说明开发修复速度跟不上缺陷暴露速度。此时即使测试队伍每天加班跑任务,也可能被新的失败任务拖住。
3.4 风险等级判定示例
把指标换算成风险等级,可以使用下表的思路。阈值需要根据项目历史调节,不能直接照抄:
| 指标 | 低风险 | 中风险 | 高风险 |
|---|---|---|---|
| 净积压趋势 | 连续 3 天下降 | 持平 | 连续 3 天上升 |
| 首次通过率 | 高于 85% | 70% 到 85% | 低于 70% |
| 失败重跑率 | 低于 10% | 10% 到 20% | 高于 20% |
| 高优先级任务平均等待时长 | 低于 2 小时 | 2 到 8 小时 | 超过 8 小时 |
| P0/P1 阻塞任务数 | 0 | 1 到 2 | 3 个以上 |
实际预测时不需要把所有指标混合成一个神秘分数。先找出“最差的那个指标”,它往往就是主要风险来源。比如积压趋势健康,但高优先级任务等待时间已经超过 12 小时,说明资源分配有问题,可能让关键路径验证延迟两三天。
4. 发布预测:从指标到结论
4.1 用净吞吐量估算剩余测试天数
数据层面的预测,最基础也最好解释的公式是:
剩余可执行天数 = 净积压任务数 / 每日净完成速度每日净完成速度不是简单取已完成任务数,要略低于原始完成数,因为失败重跑会占用额外产能。更保守的口径是:
每日净完成速度 = 最近 5 天平均完成数 - 最近 5 天平均新增任务数用 Python 实现一个最简估算函数:
def estimate_finish_days( snapshot_date, remaining_work, avg_daily_completed, avg_daily_new_inflow, blocked_work=0, ): # 阻塞任务在看板里会占位置,但在问题修复前无法推进,因此单独考虑 executable_work = max(0, remaining_work - blocked_work) net_speed = max(0.1, avg_daily_completed - avg_daily_new_inflow) base_days = executable_work / net_speed return { "snapshot_date": snapshot_date, "executable_work": executable_work, "blocked_work": blocked_work, "net_speed_per_day": round(net_speed, 2), "estimated_days_remaining": round(base_days, 1), "risk_note": "若阻塞任务长期不解决,实际完成日期会更晚", } # 演示参数 print( estimate_finish_days( snapshot_date="2025-06-10", remaining_work=152, avg_daily_completed=25, avg_daily_new_inflow=5, blocked_work=20, ) )这个函数输出的是“剩余工作量在当前净速度下需要多少天”,而不是最终承诺。预测模型的价值是把复杂环境压缩成一个可迭代更新的数字:今天跑出 6.1 天,明天再看如果变成 7.5 天,说明延期风险在扩大;如果变成 4.3 天,说明修复和产能都在好转。
4.2 输出一份包含证据链的预测摘要
预测结论要能给别人复核。不要只写一句话“存在延期风险”,要把当时的快照数据、计算口径和风险假设写清楚。下面是一份可以放进周报的摘要模板:
版本:DS v1.6.0 测试阶段第 10 天 快照日期:2025-06-10 总计划任务:350 个 已通过任务:198 个 净积压任务:152 个(其中阻塞 20 个,可执行 132 个) 近 5 天平均每日完成:25 个 近 5 天平均每日新增任务:5 个 每日净完成速度:20 个 估算结果: - 可执行任务清理时间:132 / 20 = 6.6 天 - 阻塞任务需另行处理,预计额外增加 1 到 2 天 - 预计测试完成日期区间:2025-06-17 到 2025-06-18 主要风险: 1. 高优先级路径的用例平均排队等待 9 小时,超过 8 小时阈值。 2. 失败重跑率 18%,处于中风险区间。 3. 阻塞任务连续 2 天未下降,需要产品、开发共同确认处理策略。 建议: - 优先安排 P0/P1 任务在每天上午第一批运行。 - 对连续失败 3 次以上的用例做失败原因聚类,定位是环境还是功能。这份摘要的好处是可以回溯。下周再开预测会时,可以直接对比“上次预测 6.6 天”和“实际消耗 5 天”之间的差距,找到是哪个假设失效了。
4.3 预测误差主要来自哪里
预测不准不代表方法不可用,而是要关注误差来源。常见有三种:
第一,任务范围没有冻结。测试期间仍在不断新增高优先级测试用例,那么剩余工作量会不断变大。这种情况不是计算错误,而是范围变化,需要把“新增任务速度”单独展示出来推动范围决策。
第二,环境因素导致完成量剧烈波动。某一天环境不可用,harness 跑不了任何任务,平均速度会被拉低。处理方式是剔除环境不可用的时间段,或者把环境可用时长作为单独字段记录。
第三,失败重跑没有完整写入历史表。如果只更新了任务当前状态,历史重跑次数丢失,就会低估返工工作量。这也是前面把运行日志单独存一张表的原因。
5. 常见问题排查:harness 数据为什么对不上
5.1 任务一直处于 PENDING 状态,队列积压被高估
现象是看板显示待执行任务很多,但测试机实际很空。可能原因是 harness 的调度器没有消费任务,或者 worker 进程挂了。
检查顺序:
# 查看 worker 是否在线 curl http://harness.internal.example.com/api/v1/workers # 查看调度队列长度 curl http://harness.internal.example.com/api/v1/queue/status处理建议:PENDING 状态要记录进入待执行队列的时间以及首次开始执行的时间,二者相减才是真实排队时间。如果 worker 不在线,PENDING 大量堆积会给出“产能不足”的假象。此时先恢复调度,再做预测。
5.2 同一用例被重复发放,通过率被稀释
现象是某个用例在任务表里有多条记录,统计通过率时出现重复计数。可能原因是采集脚本没有用自然 key 做幂等,或者 harness 为每次运行生成了新 task_id。
处理建议:任务表的主键不能是自增 ID,而应该使用source_version + suite_name + case_name拼成的稳定标识。如果每次运行本身就是独立任务,那就需要在采集时区分“同一任务重跑”和“新增任务”,否则通过率会被重跑记录稀释。
5.3 时间字段标准不一致,积压曲线变形
现象是图表出现整点跳变或每日完成量明显偏低。可能原因是 created_at 是本地时间,finished_at 是 UTC 时间,或者部分任务时间戳是毫秒。
处理建议:在写入 SQLite 前三方字段统一转成字符串格式。采集脚本里写一个公共方法处理时间,不在业务 SQL 中做大量时区转换。统计窗口建议统一使用东八区或 UTC,同时要在字段命名上标明时区,避免换人维护时产生歧义。
5.4 安装或启动 harness 控制台时卡在依赖安装阶段
现象是执行网页控制台相关安装命令时长时间没有输出,看起来像卡死。这类问题在依赖较多的测试框架中很常见。优先确认是卡在网络下载还是代码编译。
# 查看 Node 版本,某些模块对 Node 版本有要求 node -v # 查看包管理源配置 npm config get registry # 清理缓存后再安装 npm cache clean --force pnpm store prune pnpm install如果原始 harness 文档没有给出使用版本,落地前要主动确认依赖版本与当前 Node、pnpm 的兼容性。不要在大规模接入前忽略这类安装问题,否则测试任务下发放量的数据会从第一天就缺。
5.5 失败原因没有结构化,退单率失真
现象是失败任务全部存在 FAILED 状态,但无法区分环境问题、断言问题还是缺陷问题。这样团队看到失败率高,却不知道应该找运维、测试还是开发。
处理建议:在harness_run_log中增加failure_stage,至少标记为 ENV_SETUP、TEST_EXECUTION、DATA_PREP、APP_VERIFY。统计时先剔除环境类失败再评估版本质量,这样预测延期才不会因为环境故障误伤版本。环境稳定后,再根据功能失败密度判断修复工作量。
6. 最佳实践:让延期预测真正可复用
6.1 发布前三张表必看
推荐在每次发布预测会议前准备三张表:
| 表 | 内容 | 要回答的问题 |
|---|---|---|
| 任务队列透视表 | 按状态统计任务数、高优先级等待时长 | 还剩多少工作量,瓶颈在哪 |
| 每日完成趋势表 | 发放量、完成量、通过量、重跑量 | 当前速度能否覆盖剩余任务 |
| 失败原因聚类表 | 按 failure_stage 和错误信息聚类 | 失败是环境、脚本还是真实缺陷 |
这三张表能从数据上替代“我觉得还行”或“我觉得很悬”。发布评审时只要围绕这三张表讨论,结论就能落在工作量和工程质量上,而不是个人感观。
6.2 每次预测都要留下历史记录
预测不是一次性活动。建议把每次快照结果存成一张prediction_log表:
CREATE TABLE prediction_log ( prediction_id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_date TEXT NOT NULL, version TEXT NOT NULL, remaining_work INTEGER NOT NULL, blocked_work INTEGER NOT NULL, net_speed_per_day REAL NOT NULL, estimated_days REAL NOT NULL, actual_finish_date TEXT, risk_level TEXT NOT NULL, notes TEXT );积累两三次发布后,就能知道自己团队的预测偏差规律。例如某团队习惯性低估阻塞任务时间,那么下一次预测可以加入 1.5 的缓冲系数。没有历史记录,就无法做校准,预测一直会是拍脑袋。
6.3 从测试 harness 扩展到全链路交付观测
测试发放数据只是交付过程的其中一段。如果数据模型设计得足够通用,可以把相同思路扩展到手写需求、缺陷修复、联调环境、UI 验收等环节。核心都是记录“任务创建时间、开始时间、完成时间、状态、重试次数”。这些字段没变化,预测逻辑就能复用。
比如模型训练或评测场景中,deepseek harness 这类工具负责把评测样例发放到不同环境执行。同样会产生任务积压、执行失败、重跑延迟的问题。如果评测任务本身落后于训练版本迭代,就可以用这套指标判断模型迭代交付是否存在延期风险。
6.4 分离学习环境与生产环境
在学习环境中,可以先跑通 SQLite、造数脚本、Python 估算函数,理解积压和净吞吐之间的关系。生产环境则要额外考虑:
| 能力 | 学习环境 | 生产环境 |
|---|---|---|
| 数据存储 | SQLite | MySQL/PostgreSQL,支持并发写入 |
| 任务同步 | 手动运行 | 定时任务与失败告警 |
| 权限 | 本机访问 | 按角色隔离读写权限 |
| 历史归档 | 不保留 | 定期归档到数仓 |
| 异常处理 | 脚本崩溃可重跑 | 断点续传、幂等写入 |
| 监控 | 不需要 | 指标延迟超过阈值时告警 |
生产环境容易犯的错是只保留“最终通过状态”,不保留每次运行耗时。一段时间后想分析历史速度变化,发现数据全被覆盖了。因此在设计之初就划分harness_task和harness_run_log,不仅能支撑当下预测,也能支撑后续容量规划。
如果只保留一条实践建议,那就是:把“测试发放情况”看成一种交付探测信号。harness 每发放一个任务、每回传一次结果,都是在告诉你版本离可发布状态还有多远。把任务状态记全、时间戳统一、失败原因结构化,然后每天用净积压和净吞吐做一次估算。这样做几次之后,延期讨论就不再是冒险猜测,而是一份可以在评审会上展示的数据结论。