Tests: a timing assertion measures the property, not the machine it ran on - #3369
Merged
lahma merged 1 commit intoAug 25, 2026
Merged
Conversation
…an on Every CI failure across a day of v5 campaign work was environmental, and not one came from the twenty-two pull requests that merged. That is the cost this addresses: a genuine regression now arrives looking exactly like the noise. None of the assertions is deleted. Each one is either given a clock it can trust, a handshake that removes the clock, or a margin expressed as a ratio to the ceiling it discriminates against rather than as an absolute number a loaded runner walks past. WaitForScheduledWorkTests (sebastienros#3301), HostPumpWaitTests: the stopwatch is now started by the thread that releases the wait, immediately before it releases it, so what is measured is the wake latency and never the releasing thread's own scheduling, its settle sleep, or the script that got the waiter there. The cancellation row no longer releases through CancellationTokenSource.CancelAfter, whose callback a saturated thread pool delivers whenever it gets round to it, but from a dedicated thread the test owns. Ceiling moves to a minute with the margin derived from it as a half, so the ratio the assertion is about is unchanged and the midpoint is out of reach of a multi-second stall. HostPromiseTimeoutTests: the rejection message is a witness of which budget the wait actually ran under - UnwrapIfPromiseCore hands one local to the drain and formats the same local into the message - so it is asserted first and the elapsed check becomes the second half of the claim, bounded by half of whatever the wrong answer would have spent instead of by a fixed five seconds. PromiseTests.UnwrapIfPromiseAsync_WithIOBoundTask_DoesNotBlockCallerThread: the simulated IO ended on a 100 ms delay, so "still pending" was a race the test could lose. It now ends on a gate the test holds, which makes in-flight a fact. The rest are wedge ceilings on budgets nothing asserts: the off-thread stream bridge, the two await-current-stack rows on net472, the untrusted-code operation scope, the replaced execution-time interval, and the regular expression timeout under twenty-eight parallel SunSpider workloads. Fixes sebastienros#3358. Fixes sebastienros#3301. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
force-pushed
the
fix/3358-deterministic-timing-tests
branch
from
August 25, 2026 19:39
980fcb3 to
f9169ad
Compare
lahma
added a commit
that referenced
this pull request
Aug 26, 2026
…he machine (#3388) Follows #3369's pattern into Jint.Tests: a deterministic handshake or a fake clock where one reaches, an exact witness where the exception already carries one, and TestBudgets.WedgeCeiling for every budget nothing asserts. Fixes #3379 Fixes #3372 Fixes #3297 Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 26, 2026
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…ot because the machine was quick enough WebApiTimerTests.ATimerOutlivingTheUnwrapTimeoutTimesOutByDesign failed once on windows/net8.0 with "Expected: <PromiseRejectedException> But was: null" - a 200 ms unwrap returning without throwing against a five-second timer. A 25x margin does not close through runner load, so it looked like a race in the unwrap-timeout path. It is not. It is the same wall-clock assumption sebastienros#3369 and sebastienros#3388 spent a campaign removing, wearing a margin big enough to look safe. The five seconds do not bound the unwrap. They bound the gap between the setTimeout call inside Evaluate and the unwrap starting on the next line, both measured against the system clock. Spend five seconds in that gap and the timer is already due when the unwrap begins - and the first thing DrainEventLoopUntil does is run the continuations it finds, so the timer fires, the promise FULFILS, and nothing is thrown. Sleeping 5.1 s between those two statements reproduces the reported failure exactly, message for message. A runner whose test bodies all run at ThreadPriority.Lowest (sebastienros#3452) under three concurrent legs has spent longer than that on less; the same suite had a 200 ms budget measured at 47 s in sebastienros#3406. The fix is the one this file already uses four tests above: a ManualClock the test owns and never advances, so the timer can never be due and the unwrap's own bound is the only thing that can end the wait. No margin, no clock to lose a race against. With the 5.1 s widening still in place the test now passes; twenty consecutive runs of the fixture are green without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…an state the budget instead of racing it HostPromiseTimeoutTests.TheArgumentLessUnwrapTakesTheConfiguredPromiseTimeout was the one assertion sebastienros#3369 could not convert, and it said why: the wrong answer it discriminates against is the engine's own ten-second PromiseTimeout default, so the midpoint has to sit at five seconds and the test cannot move the thing it is discriminating against. Widening past ten seconds does not make it robust, it makes it assert nothing. It duly failed an unrelated pull request at 47 s (sebastienros#3406). The message half of that claim already worked - UnwrapIfPromiseCore resolves one local, hands it to the drain and formats the same local into the rejection. What it cannot state is the other half: that the drain ENDED on the budget it named. That is a property of the wait, and no message can witness it. So the wait gets a clock the host already owns. DrainEventLoopUntil armed its deadline from DateTime.UtcNow; it now arms it from a monotonic timestamp read through Options.Constraints.TimeProvider. That is not new public API - the property has existed for LimitExecutionTime since sebastienros#3232, and PromiseTimeout sits beside it on the same options group - and it fixes something on its own account: a wall-clock deadline is cut short by an NTP step forwards and stretched by one backwards, which is exactly why the pump's own ceiling has always been measured on Stopwatch instead (Engine.Pump.ElapsedSince). The drain was the one wait that was not. The assertion is now exact and positive rather than a margin: the wait survives one tick short of the configured budget and ends on the tick that reaches it, and no amount of real time on either side changes either answer. The wrong answer is excluded by construction - two hundred milliseconds of this clock never reach a ten-second deadline - and both were verified against builds carrying each regression: hard-coding the ten-second default fails the new row and the message row, and putting the drain back on DateTime.UtcNow fails the new row at its clock handshake. The rows the clock cannot reach keep the witness they had: the message, on every target framework. Also pinned, and its own line in the migration guide: a TimeProvider that reports a non-positive TimestampFrequency is now refused when the engine is built rather than only when a time limit happens to be registered, since the clock is resolved for every engine. docs/v5-migration.md 4.46 carries the three host-visible consequences, including the one a host has to act on - a frozen clock now keeps a blocking unwrap pending until the host advances it. Fixes sebastienros#3406. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…an state the budget instead of racing it HostPromiseTimeoutTests.TheArgumentLessUnwrapTakesTheConfiguredPromiseTimeout was the one assertion sebastienros#3369 could not convert, and it said why: the wrong answer it discriminates against is the engine's own ten-second PromiseTimeout default, so the midpoint has to sit at five seconds and the test cannot move the thing it is discriminating against. Widening past ten seconds does not make it robust, it makes it assert nothing. It duly failed an unrelated pull request at 47 s (sebastienros#3406). The message half of that claim already worked - UnwrapIfPromiseCore resolves one local, hands it to the drain and formats the same local into the rejection. What it cannot state is the other half: that the drain ENDED on the budget it named. That is a property of the wait, and no message can witness it. So the wait gets a clock the host already owns. DrainEventLoopUntil armed its deadline from DateTime.UtcNow; it now arms it from a monotonic timestamp read through Options.Constraints.TimeProvider. That is not new public API - the property has existed for LimitExecutionTime since sebastienros#3232, and PromiseTimeout sits beside it on the same options group - and it fixes something on its own account: a wall-clock deadline is cut short by an NTP step forwards and stretched by one backwards, which is exactly why the pump's own ceiling has always been measured on Stopwatch instead (Engine.Pump.ElapsedSince). The drain was the one wait that was not. The assertion is now exact and positive rather than a margin: the wait survives one tick short of the configured budget and ends on the tick that reaches it, and no amount of real time on either side changes either answer. The wrong answer is excluded by construction - two hundred milliseconds of this clock never reach a ten-second deadline - and both were verified against builds carrying each regression: hard-coding the ten-second default fails the new row and the message row, and putting the drain back on DateTime.UtcNow fails the new row at its clock handshake. The rows the clock cannot reach keep the witness they had: the message, on every target framework. Also pinned, and its own line in the migration guide: a TimeProvider that reports a non-positive TimestampFrequency is now refused when the engine is built rather than only when a time limit happens to be registered, since the clock is resolved for every engine. docs/v5-migration.md 4.46 carries the three host-visible consequences, including the one a host has to act on - a frozen clock now keeps a blocking unwrap pending until the host advances it. Fixes sebastienros#3406. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…an state the budget instead of racing it HostPromiseTimeoutTests.TheArgumentLessUnwrapTakesTheConfiguredPromiseTimeout was the one assertion sebastienros#3369 could not convert, and it said why: the wrong answer it discriminates against is the engine's own ten-second PromiseTimeout default, so the midpoint has to sit at five seconds and the test cannot move the thing it is discriminating against. Widening past ten seconds does not make it robust, it makes it assert nothing. It duly failed an unrelated pull request at 47 s (sebastienros#3406). The message half of that claim already worked - UnwrapIfPromiseCore resolves one local, hands it to the drain and formats the same local into the rejection. What it cannot state is the other half: that the drain ENDED on the budget it named. That is a property of the wait, and no message can witness it. So the wait gets a clock the host already owns. DrainEventLoopUntil armed its deadline from DateTime.UtcNow; it now arms it from a monotonic timestamp read through Options.Constraints.TimeProvider. That is not new public API - the property has existed for LimitExecutionTime since sebastienros#3232, and PromiseTimeout sits beside it on the same options group - and it fixes something on its own account: a wall-clock deadline is cut short by an NTP step forwards and stretched by one backwards, which is exactly why the pump's own ceiling has always been measured on Stopwatch instead (Engine.Pump.ElapsedSince). The drain was the one wait that was not. The assertion is now exact and positive rather than a margin: the wait survives one tick short of the configured budget and ends on the tick that reaches it, and no amount of real time on either side changes either answer. The wrong answer is excluded by construction - two hundred milliseconds of this clock never reach a ten-second deadline - and both were verified against builds carrying each regression: hard-coding the ten-second default fails the new row and the message row, and putting the drain back on DateTime.UtcNow fails the new row at its clock handshake. The rows the clock cannot reach keep the witness they had: the message, on every target framework. Also pinned, and its own line in the migration guide: a TimeProvider that reports a non-positive TimestampFrequency is now refused when the engine is built rather than only when a time limit happens to be registered, since the clock is resolved for every engine. docs/v5-migration.md 4.46 carries the three host-visible consequences, including the one a host has to act on - a frozen clock now keeps a blocking unwrap pending until the host advances it. Fixes sebastienros#3406. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
that referenced
this pull request
Aug 27, 2026
…an state the budget instead of racing it (#3476) HostPromiseTimeoutTests.TheArgumentLessUnwrapTakesTheConfiguredPromiseTimeout was the one assertion #3369 could not convert, and it said why: the wrong answer it discriminates against is the engine's own ten-second PromiseTimeout default, so the midpoint has to sit at five seconds and the test cannot move the thing it is discriminating against. Widening past ten seconds does not make it robust, it makes it assert nothing. It duly failed an unrelated pull request at 47 s (#3406). The message half of that claim already worked - UnwrapIfPromiseCore resolves one local, hands it to the drain and formats the same local into the rejection. What it cannot state is the other half: that the drain ENDED on the budget it named. That is a property of the wait, and no message can witness it. So the wait gets a clock the host already owns. DrainEventLoopUntil armed its deadline from DateTime.UtcNow; it now arms it from a monotonic timestamp read through Options.Constraints.TimeProvider. That is not new public API - the property has existed for LimitExecutionTime since #3232, and PromiseTimeout sits beside it on the same options group - and it fixes something on its own account: a wall-clock deadline is cut short by an NTP step forwards and stretched by one backwards, which is exactly why the pump's own ceiling has always been measured on Stopwatch instead (Engine.Pump.ElapsedSince). The drain was the one wait that was not. The assertion is now exact and positive rather than a margin: the wait survives one tick short of the configured budget and ends on the tick that reaches it, and no amount of real time on either side changes either answer. The wrong answer is excluded by construction - two hundred milliseconds of this clock never reach a ten-second deadline - and both were verified against builds carrying each regression: hard-coding the ten-second default fails the new row and the message row, and putting the drain back on DateTime.UtcNow fails the new row at its clock handshake. The rows the clock cannot reach keep the witness they had: the message, on every target framework. Also pinned, and its own line in the migration guide: a TimeProvider that reports a non-positive TimestampFrequency is now refused when the engine is built rather than only when a time limit happens to be registered, since the clock is resolved for every engine. docs/v5-migration.md 4.46 carries the three host-visible consequences, including the one a host has to act on - a frozen clock now keeps a blocking unwrap pending until the host advances it. Fixes #3406. Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…ot because the machine was quick enough WebApiTimerTests.ATimerOutlivingTheUnwrapTimeoutTimesOutByDesign failed once on windows/net8.0 with "Expected: <PromiseRejectedException> But was: null" - a 200 ms unwrap returning without throwing against a five-second timer. A 25x margin does not close through runner load, so it looked like a race in the unwrap-timeout path. It is not. It is the same wall-clock assumption sebastienros#3369 and sebastienros#3388 spent a campaign removing, wearing a margin big enough to look safe. The five seconds do not bound the unwrap. They bound the gap between the setTimeout call inside Evaluate and the unwrap starting on the next line, both measured against the system clock. Spend five seconds in that gap and the timer is already due when the unwrap begins - and the first thing DrainEventLoopUntil does is run the continuations it finds, so the timer fires, the promise FULFILS, and nothing is thrown. Sleeping 5.1 s between those two statements reproduces the reported failure exactly, message for message. A runner whose test bodies all run at ThreadPriority.Lowest (sebastienros#3452) under three concurrent legs has spent longer than that on less; the same suite had a 200 ms budget measured at 47 s in sebastienros#3406. The fix is the one this file already uses four tests above: a ManualClock the test owns and never advances, so the timer can never be due and the unwrap's own bound is the only thing that can end the wait. No margin, no clock to lose a race against. With the 5.1 s widening still in place the test now passes; twenty consecutive runs of the fixture are green without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
that referenced
this pull request
Aug 27, 2026
…ot because the machine was quick enough (#3477) WebApiTimerTests.ATimerOutlivingTheUnwrapTimeoutTimesOutByDesign failed once on windows/net8.0 with "Expected: <PromiseRejectedException> But was: null" - a 200 ms unwrap returning without throwing against a five-second timer. A 25x margin does not close through runner load, so it looked like a race in the unwrap-timeout path. It is not. It is the same wall-clock assumption #3369 and #3388 spent a campaign removing, wearing a margin big enough to look safe. The five seconds do not bound the unwrap. They bound the gap between the setTimeout call inside Evaluate and the unwrap starting on the next line, both measured against the system clock. Spend five seconds in that gap and the timer is already due when the unwrap begins - and the first thing DrainEventLoopUntil does is run the continuations it finds, so the timer fires, the promise FULFILS, and nothing is thrown. Sleeping 5.1 s between those two statements reproduces the reported failure exactly, message for message. A runner whose test bodies all run at ThreadPriority.Lowest (#3452) under three concurrent legs has spent longer than that on less; the same suite had a 200 ms budget measured at 47 s in #3406. The fix is the one this file already uses four tests above: a ManualClock the test owns and never advances, so the timer can never be due and the unwrap's own bound is the only thing that can end the wait. No margin, no clock to lose a race against. With the 5.1 s widening still in place the test now passes; twenty consecutive runs of the fixture are green without it. Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Across a full day of v5 campaign work, every single CI failure was environmental — not one was a real
regression from the twenty-two pull requests that merged. The cost is not the re-run. It is that a genuine
regression now arrives looking exactly like the noise, and gets waved through.
This is the mechanism behind the repeat offenders. Nothing here deletes an assertion, and nothing here raises
a number until it stops failing. #3301 states the trap explicitly and it applies to all of them: a wait that
ignored its token and ran out its whole ceiling would still throw
OperationCanceledException, so deletingthe elapsed check would let a real defect pass. Each row below is instead given a clock it can trust, a
handshake that removes the clock, or a margin expressed as a ratio to the ceiling it discriminates against.
What changed, per test
The stopwatch is started by the thread that releases the wait
Jint.Tests/Runtime/WaitForScheduledWorkTests.cs,Jint.Tests.PublicInterface/HostPumpWaitTests.csWaitForScheduledWorkTests.ThrowsOperationCanceledOnTheToken(#3301) missed its discriminator by 11 ms;HostPumpWaitTests.WaitForScheduledWorkWakesOnACrossThreadPostreported 12 s against a 10 s ceiling it neveractually spent. Both were measuring far more than the wake: the clock started before the releasing thread had
even been scheduled, and ran through its quarter-second settle sleep.
It now starts on that thread, immediately before the release, so the only interval measured is the wake
latency — which is the thing the assertion is about. The property survives exactly: a wait that noticed the
release only when its ceiling ran out still fails, because that interval is measured from the release.
The cancellation row also stops releasing through
CancellationTokenSource.CancelAfter. That callback isdelivered by the thread pool, and a saturated pool grows at roughly one worker per 500 ms, so it is the one
release whose latency the test cannot bound — a healthy 250 ms cancellation was measured at 5.011 s against a
5 s midpoint. It is now cancelled from a
DedicatedThreadthe test owns, the same shape the enqueue rowsalready used.
Ceilingmoves from ten seconds to a minute in both files, withEarlyReturnMarginderived from it as ahalf rather than written out again. #3301 asked for exactly this: the ratio is what the assertion is about,
so moving both together widens the margin without weakening it. A healthy run spends none of the ceiling; only
a genuine regression pays the extra fifty seconds before being reported.
The witness that needs no clock is asserted first
Jint.Tests.PublicInterface/HostPromiseTimeoutTests.csAnExplicitTimeoutStillWinsOverTheConfiguredOnefailed with "Expected elapsed to be less than 5s, but found1m 3s" — a 200 ms wait, stalled by a factor of three hundred.
UnwrapIfPromiseCoreresolves the effective timeout into one local, hands that local to the drain, and formatsthe same local into the rejection message. The message is therefore a witness of which budget the wait
actually ran under, not a restatement of the configuration: a wait that consulted the wrong budget cannot
name the right one. That is asserted first, by a named helper.
The elapsed check stays as the other half of the claim — that the wait ended on the budget it names rather
than sitting out the longer one it should have ignored — but its bound is now half of whatever the wrong
answer would have spent instead of a fixed five seconds. For the explicit row the wrong answer is the
configured five minutes, so the midpoint moves to 2 m 30 s and a three-hundred-fold stall no longer reaches it.
For the argument-less row the wrong answer is the ten-second default, which this test cannot change, so its
midpoint stays at five seconds — that is the one residual, and it is now a deliberate, documented consequence
of the rule rather than a magic constant.
The simulated IO ends when the test says so
Jint.Tests/Runtime/PromiseTests.csUnwrapIfPromiseAsync_WithIOBoundTask_DoesNotBlockCallerThreadasserted that the unwrap task is still pendingwhile IO is in flight — with the IO being
await Task.Delay(100). On a loaded runner the hundred millisecondscan be gone before the test thread is scheduled again, and a completed unwrap then reads as exactly the defect
the test exists to catch.
It now ends on a
TaskCompletionSourcethe test holds. "In flight" becomes a fact rather than a hope, and theassertion gets stronger: the unwrap task provably cannot have completed.
Wedge ceilings on budgets nothing asserts
TestBudgets.WedgeCeilingalready exists for this, and documents the distinction it turns on: a budget iseither part of what a test asserts, or it is a ceiling that exists only so a wedge is reported instead of a
hang. Five rows were sitting on budgets of the second kind, sized for an idle machine.
HostStreamBridgeTests.ReadsAStreamWhoseReadsCompleteOnAnotherThread— every chunk arrives as a thread-poolcontinuation, and the whole copy was failing as
PromiseRejectedException: Timeout of 00:00:10 reached. Theassertion is the text that came out of the stream.
AsyncTests.ShouldTaskAwaitCurrentStack(Three fixed 10-second budgets in tests blow on a contended Windows runner, the way the WPT fetch budget did #3358, seen on net472) and itsValueTasktwinShouldValueTaskAwaitCurrentStack— about 1.1 s of intended work inside the ten-second default. Theassertion is the order the three appends landed in.
UntrustedCodeProfileTests.OperationScopeCannotEndWhileAsyncWorkOwnsTheEngine— the profile's five-secondtimeout interval and ten-second operation deadline are budgets an embedder sizes for their own work. The
assertion is that a scope refuses to end while an evaluation is suspended, and ends once it resumes.
ConstraintReplacementTests.TheLaterTimeoutIsTheOneEnforced— the discriminator is the one-millisecondinterval that must no longer be registered, which two hundred thousand iterations pass by four orders of
magnitude on any machine. The widened interval was never a duration the test is about; thirty seconds was a
wedge ceiling sized for an idle box.
Jint.Tests.CommonScripts— the fixture is[Parallelizable(ParallelScope.All)], so twenty-eight CPU-boundreal-world workloads share whatever cores the runner has, and a Windows leg was observed taking 7 m 19 s for
the twenty-eight against ~20 s unloaded, with
RegexMatchTimeoutExceptionas the only symptom. Nothing hereasserts anything about
RegexTimeout. It stays finite deliberately —Regex.InfiniteMatchTimeoutwould turna genuinely catastrophic pattern into a hung run.
Evidence
A clean run on an idle box proves nothing here, so: a 32-core machine saturated with CPU-bound spinner
processes, the affected tests run in a loop on both
net10.0andnet472, before and after, same load andsame iteration count each time. "Before" is
upstream/main's copy of the nine files, built in the same tree.136/140 before, 140/140 after. All four baseline failures are named in this pull request, and both are
reproductions of the reported symptom rather than something new:
PromiseTests.UnwrapIfPromiseAsync_WithIOBoundTask_DoesNotBlockCallerThread— twice, at 128 spinners:"Expected unwrapTask.IsCompleted to be False … but found True". This is the
Task.Delay(100)race,reproduced exactly.
UntrustedCodeProfileTests.OperationScopeCannotEndWhileAsyncWorkOwnsTheEngine— once at each load:PromiseRejectedException: Timeout of 00:00:00.1000000 reached, the profile's 100 ms promise budget.The remaining rows did not fire in 140 attempts, which is expected — #3301's own failure was one occurrence
on a shared runner and it took six CI attempts on one pull request to establish the last one of these. Their
change is reasoned from the mechanism rather than from a reproduction, and the reasoning is stated per row
above.
#3358's third item is a different mechanism, and is not here
html/webappapis/timers/negative-settimeout.any.jswas listed in #3358 with the issue's own caveat that itshould not be assumed to be a budget until someone looked. It was looked at, and it is neither a budget nor a
timer-ordering defect: the engine's clamp and ordering are deterministic and correct, and the driver simply
has no completion boundary for uncaught-callback reports while draining the event loop twice more after the
file is over. Upstream
testharness.jshas an explicit guard for exactly this. It is a WPT-driver change withits own shim-symmetry tests and a worker-lane question to settle, so it is filed separately as #3366 — with
the full trace, upstream's guard quoted at the corpus pin, and the minimal fix — rather than folded in here.
One more flake found on the way, also filed rather than folded in
TypeConverterRegistrationTests.AnEngineWithACustomConverterSharesTheStockAccessorCachefails about one runin three on the
net472leg underJINT_HOST_CONTRACT_VERIFICATION=1, and never when its class is run alone.It is not a budget either:
JsAccessibleRegistry.Generationis a process-wide counter andTypeResolver.SyncGeneratedMembersdrops the whole accessor cache when it moves, so any of the fourRegisterAll()calls elsewhere in the same two assemblies landing between that test's two engines makes itscalls.Should().Be(0)see a re-resolution. Filed with the mechanism and a fix direction as #3368.Notes
Jint/.PublicApiTestandPublicApiDocumentationTestboth pass unchanged, andUndocumentedPublicApi.txtis untouched.Jint.Tests.Test262was not run: no engine code changed.dotnet build -c Releaseclean.Jint.Tests7316/10668/10668 (net472/net8.0/net10.0),Jint.Tests.PublicInterface2964/2339 (net10.0/net472),Jint.Tests.CommonScripts28/28 on both — and thesame two suites again with
JINT_HOST_CONTRACT_VERIFICATION=1, whose only failure was AnEngineWithACustomConverterSharesTheStockAccessorCache flakes because another test's JsAccessibleRegistry.Register drops the shared cache #3368 above.Fixes #3358.
Fixes #3301.
🤖 Generated with Claude Code
https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S