Skip to content

Commit 67885e0

Browse files
fryanpanclaude
andcommitted
ADFA-4128: apply the mtime margin when the rebuild holds Unknown
absorbEchoesLocked treated a held ChangedFiles.Unknown as "every file is already held" and skipped the granularity margin for all of them. After an external Gradle build untrusted the baseline (pending = Unknown) and a rebuild started, a .kt saved 0.5 s into the rebuild on a 1 s-mtime filesystem stamped a whole second before the start, was absorbed, and onBaselineReset dropped it - the edit was never built. An Unknown held set names no file, so nothing proves an arrival is an echo; only an enumerated held set keeps the exact cutoff. New test pins the Unknown case; a second new test pins the margin's upper bound (an 8_000 stamp against a 10_000 start absorbs), which with the existing 9_000 case fixes the constant at 2 s. Adversarial review 2026-09-04, finding #9 on 60d5a27ca. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2
1 parent 36718e8 commit 67885e0

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎quickbuild/core/src/main/java/org/appdevforall/cotg/quickbuild/domain/reload/LiveReloadOrchestrator.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,9 @@ class LiveReloadOrchestrator(
507507
* arrival is the echo of the save the rebuild is absorbing, and stranding that echo is
508508
* the spurious invalidation this split exists to prevent. The residual is a re-edit of a
509509
* held file within the margin after the start on a coarse filesystem, which absorbs.
510+
* Only an enumerated held set earns that: a held [ChangedFiles.Unknown] (an untrusted
511+
* baseline handed to the rebuild) names no file, so nothing proves an arrival is an echo,
512+
* and every truncated mtime gets the margin - one spare build rather than a dropped edit.
510513
*
511514
* The absorbed part joins [awaitingAbsorption] via [ChangedFiles.plus], NOT
512515
* [unionPendingLocked]: this rebuild IS the Gradle build these files would demand, so
@@ -522,7 +525,7 @@ class LiveReloadOrchestrator(
522525
val absorbed =
523526
changes.files.filterTo(mutableSetOf()) { file ->
524527
val mtime = fileLastModified(file)
525-
val heldAlready = held !is ChangedFiles.Known || file in held.files
528+
val heldAlready = held is ChangedFiles.Known && file in held.files
526529
val truncated = mtime % 1_000L == 0L
527530
val cutoff =
528531
if (heldAlready || !truncated) {

‎quickbuild/core/src/test/java/org/appdevforall/cotg/quickbuild/domain/reload/LiveReloadOrchestratorTest.kt‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,62 @@ class LiveReloadOrchestratorTest {
755755
assertThat(executor.requests[0].changes).isEqualTo(known(srcA))
756756
}
757757

758+
@Test
759+
fun `a truncated mtime a full margin before the start is absorbed - the margin is two seconds, not more`() =
760+
runTest {
761+
// The test above pins the margin at more than 1 s (a 9_000 stamp stays pending).
762+
// This one pins the other side: a whole-second stamp exactly 2 s before the start
763+
// is inside the cutoff and absorbs, so the constant cannot silently grow past 2 s
764+
// and strand a genuinely older save. Together they fix MTIME_GRANULARITY_MARGIN_MILLIS
765+
// at 2_000 to within the whole-second grain the filesystems involved use.
766+
val executor = GatedExecutor()
767+
val orchestrator =
768+
LiveReloadOrchestrator(
769+
executor,
770+
ChangeClassifier(),
771+
backgroundScope,
772+
wallClock = { 10_000L },
773+
fileLastModified = { file -> if (file.path == srcA) 8_000L else 0L },
774+
) {}
775+
776+
orchestrator.onFilesChanged(known("app/src/main/AndroidManifest.xml"))
777+
runCurrent()
778+
orchestrator.onProxyAppRebuildStarted()
779+
orchestrator.onFilesChanged(known(srcA))
780+
orchestrator.onBaselineReset()
781+
runCurrent()
782+
783+
assertThat(executor.requests).isEmpty()
784+
}
785+
786+
@Test
787+
fun `a mid-rebuild edit gets the margin when the rebuild holds Unknown - nothing there proves it is an echo`() =
788+
runTest {
789+
// An external Gradle build untrusts the baseline (pending becomes Unknown), a rebuild
790+
// then starts and takes that Unknown as its held set. A .kt saved 0.5 s into the
791+
// rebuild on a 1 s-mtime filesystem reports a whole-second stamp before the start.
792+
// Treating "held is Unknown" as "every file is held" skipped the margin, absorbed
793+
// the save, and onBaselineReset dropped it - the edit was never built.
794+
val executor = GatedExecutor()
795+
val orchestrator =
796+
LiveReloadOrchestrator(
797+
executor,
798+
ChangeClassifier(),
799+
backgroundScope,
800+
wallClock = { 10_000L },
801+
fileLastModified = { file -> if (file.path == srcA) 9_000L else 0L },
802+
) {}
803+
804+
orchestrator.onBaselineUntrusted()
805+
orchestrator.onProxyAppRebuildStarted()
806+
orchestrator.onFilesChanged(known(srcA))
807+
orchestrator.onBaselineReset()
808+
runCurrent()
809+
810+
assertThat(executor.requests).hasSize(1)
811+
assertThat(executor.requests[0].changes).isEqualTo(known(srcA))
812+
}
813+
758814
@Test
759815
fun `a mid-rebuild file with no readable mtime stays pending - nothing proves it predates the read`() =
760816
runTest {

0 commit comments

Comments
 (0)