test(scripts): align the translation-pairing-merge budget with the coverage lane

Every case in the suite drives real git invocations against a scratch
repository, so it is bound by process creation rather than by its assertions.
The describe-level 15 s capped all 23 cases below the 90 s the Windows coverage
lane passes as --testTimeout, and the suite has been observed timing out at
15000ms on a branch that did not touch the file.

Refs #2677.
This commit is contained in:
Chinesezjc 2026-08-28 04:44:22 +08:00
parent cd5ef81481
commit 00f2a701bd
4 changed files with 77 additions and 1 deletions

View file

@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-27-translation-pairing-merge-budget.md
2026-08-27-translation-pairing-merge-budget.md: 296e71fbbebf136602e02d2d8d64dbbfd8a336a3
2026-08-27-translation-pairing-merge-budget.zh.md: 8bdac5902301d7105b7851e3c446a9a327b69819

View file

@ -0,0 +1,31 @@
# Agent Note: Coverage-lane budget for the translation-pairing-merge suite
Status: implemented
English | [中文](2026-08-27-translation-pairing-merge-budget.zh.md)
## Problem
[`scripts/translation-pairing-merge.spec.ts`](../../../../scripts/translation-pairing-merge.spec.ts) took a `describe`-level `{ timeout: 15_000 }`. All 23 of its cases inherit that value; none carries an allowance of its own.
Every case builds a scratch repository and drives it through spawned `git` invocations, so the suite is bound by process creation rather than by its assertions. On the self-hosted Windows runners all instances share one volume, and process creation there shows occasional multi-second spikes rather than a uniform slowdown. Under that contention this suite has been observed reporting `Test timed out in 15000ms` on a branch that did not touch the file, so the budget rather than the change under test decided the outcome.
## Decision
The suite takes `{ timeout: 90_000 }`, matching `DSH_COVERAGE_TEST_TIMEOUT_MS` in [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml), which the Windows coverage lane passes as `--testTimeout`.
A `describe` value takes precedence over that flag rather than deferring to it. A smaller one therefore lowers what the lane already grants, and because no case here carries its own allowance, every one of the 23 was capped at 15 s while the lane offered 90 s.
## Consequences
The suite tolerates a multi-second `git` spawn spike on the shared-volume runners and defers to the budget the coverage lane provides. The value is not a measurement of how long these cases need: the slowest three complete in roughly 0.7-1.2 s depending on the host, and raising the ceiling does not slow a passing run.
A raised ceiling does not weaken the assertions: with the budget raised sixfold a suite still fails through its own assertions rather than through a timeout, because the ceiling only decides when waiting stops. It does widen what counts as acceptable duration, so a real slowdown from a few hundred milliseconds to tens of seconds now passes where the previous 15 s would have caught it. That detection is traded away deliberately: the 15 s ceiling was firing on contention rather than on regressions, so what it caught was the shared volume, not the code.
## Alternatives considered
**Raise `testTimeout` for the whole unit lane.** That would change every suite in the repository to fix one whose cost is specific to spawning `git`.
**Give each case its own allowance.** Twenty-three separate values restate one property of the machine, and a later case added without one would silently inherit the lower ceiling again.
**Leave the value and retry on failure.** A retry moves the failure to another case or another run and leaves a red gate that carries no information about the code under test.

View file

@ -0,0 +1,31 @@
# Agent Note: translation-pairing-merge 套件的 coverage lane 预算
Status: implemented
[English](2026-08-27-translation-pairing-merge-budget.md) | 中文
## 问题
[`scripts/translation-pairing-merge.spec.ts`](../../../../scripts/translation-pairing-merge.spec.ts) 在 `describe` 层加了 `{ timeout: 15_000 }`。它的 23 个用例全部继承这个值,没有任何一个自带余量。
每个用例都会建一个临时仓库并通过 spawn 的 `git` 驱动它,因此这个套件受进程创建约束,而不是受它的断言约束。在自托管 Windows runner 上所有实例共用一个卷,而那里的进程创建表现为偶发的数秒尖峰,不是均匀变慢。在那种争抢下,这个套件曾在一个没有改动该文件的分支上报出 `Test timed out in 15000ms`,也就是说决定结果的是预算而不是被测改动。
## 决定
套件取 `{ timeout: 90_000 }`,与 [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml) 里的 `DSH_COVERAGE_TEST_TIMEOUT_MS` 一致,Windows 覆盖率 lane 把它作为 `--testTimeout` 传入。
`describe` 层的取值优先于那个 flag,而不是让位于它。所以更小的值会压低 lane 已经给出的预算;又因为这里没有任何用例自带余量,23 个用例全部被限制在 15 秒,而 lane 提供的是 90 秒。
## 后果
套件能容忍共享卷 runner 上一次数秒的 `git` spawn 尖峰,并让位于 coverage lane 提供的预算。这个值不是对「这些用例需要多久」的测量:最慢的三个用例视主机而定约为 0.7–1.2 秒,而抬高上限不会让一次通过的运行变慢。
抬高上限不会削弱断言:把预算抬到六倍之后,套件仍然通过它自己的断言失败而不是通过超时失败,因为上限只决定何时停止等待。但它确实放宽了「多长算可接受」——一个从几百毫秒退化到几十秒的真实变慢现在会通过,而此前的 15 秒会拦住它。这项检测能力是有意换掉的:15 秒上限触发的是争抢而不是回归,所以它拦住的是共享卷,不是代码。
## 备选方案
**给整个 unit lane 抬高 `testTimeout`。** 那会为了修一个成本特定于 spawn `git` 的套件而改变仓库里的每一个套件。
**给每个用例各自加余量。** 23 个分散的取值重复表达同一个机器属性,而后续新增的用例若没写,又会静默继承较低的上限。
**保留取值、失败时重跑。** 重跑只是把失败挪到另一个用例或另一次运行,同时留下一个不携带被测代码信息的红灯。

View file

@ -261,7 +261,15 @@ function expectMergedPair(fixture: Fixture): void {
) )
} }
describe('translation pairing merge composition', { timeout: 15_000 }, () => { // Every case in this suite drives real `git` invocations against a scratch
// repository, so it is bound by process creation rather than by its assertions.
// The value matches DSH_COVERAGE_TEST_TIMEOUT_MS, which the Windows coverage
// lane passes as --testTimeout: a describe value overrides that flag rather than
// yielding to it, so a smaller one here lowers what the lane grants every case
// in this file, none of which carries an allowance of its own. Measurements and
// the rejected alternatives are in
// .agents/notes/implemented/testing/2026-08-27-translation-pairing-merge-budget.md.
describe('translation pairing merge composition', { timeout: 90_000 }, () => {
it('rejects a pairing-record path outside the repository', () => { it('rejects a pairing-record path outside the repository', () => {
const fixture = createFixture(false) const fixture = createFixture(false)