fix: scope timeoutOf by deadline code so nesting composes (codex round 2)

Round 2 P2: timeoutOf() accepted ANY TimeoutReason, so under nesting — when
the upstream handed to deadline() is itself a deadline (the RFC's named
tools/execute middleware follow-up) and its outer timer fires first —
AbortSignal.any preserves the outer reason and the inner bash/web would report
the outer timeout as their own (timedOut / WEB_FETCH_TIMEOUT) though their local
timer never expired. Add an optional code to timeoutOf; bash and web pass their
own code, so a foreign timeout falls through to the upstream-cancel path.
This commit is contained in:
Dudu-0223
2026-07-06 17:07:11 +08:00
parent b4ba84a1a9
commit 760bc9aa6a
6 changed files with 59 additions and 21 deletions

View File

@@ -157,4 +157,25 @@ describe('timeoutOf', () => {
expect(timeoutOf({ reason: 'user cancelled' })).toBeUndefined()
expect(timeoutOf({})).toBeUndefined()
})
it('matches only the requested code when one is given', () => {
const reason = new TimeoutReason('BASH_TIMEOUT', 100)
expect(timeoutOf({ reason }, 'BASH_TIMEOUT')).toBe(reason)
expect(timeoutOf({ reason }, 'WEB_FETCH_TIMEOUT')).toBeUndefined()
})
})
describe('deadline — nested deadlines', () => {
it("does not misclassify an outer deadline's timeout as the inner code", () => {
// The upstream handed to the inner deadline is ITSELF a deadline that has
// already timed out (outer). AbortSignal.any preserves the outer reason;
// scoping timeoutOf to the inner code keeps the inner capability from
// reporting the outer timeout as its own — it reads as an upstream cancel.
const outer = new AbortController()
outer.abort(new TimeoutReason('OUTER_TIMEOUT', 30))
using inner = deadline(outer.signal, 60_000, 'BASH_TIMEOUT')
expect(inner.signal.aborted).toBe(true)
expect(timeoutOf(inner.signal, 'BASH_TIMEOUT')).toBeUndefined() // not ours → upstream-cancel path
expect(timeoutOf(inner.signal)?.code).toBe('OUTER_TIMEOUT') // but IS a timeout, unscoped
})
})