Context
Regression test PR: #1045
Hosted verification run: https://github.com/vibhor1102/Smart-AutoClicker/actions/runs/31606697584
Target branch: dev-4.0.0-beta08-fixes
The verify-only workflow completed all independent test tasks and reported four failures. The tests are intentionally kept in the PR; this issue tracks the production-side follow-up.
1. GestureExecutor: late callback completes the next gesture
Failing test: GestureExecutorTests.dispatchGesture_lateCallbackAfterTimeout_doesNotCompleteNextGesture
Test location: core/common/actions/src/test/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutorTests.kt:80
Production location: core/common/actions/src/main/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutor.kt:40-98
Technical path:
- GestureExecutor is a singleton and stores both resultCallback and currentContinuation as mutable fields.
- dispatchGesture uses withTimeoutOrNull. When the timeout fires, it clears currentContinuation but leaves resultCallback populated.
- The next dispatch reuses that callback through resultCallback ?: newGestureResultCallback().
- A delayed onCompleted/onCancelled callback from the timed-out Android gesture calls resumeExecution(). That method resumes whichever currentContinuation is stored at that moment, which can belong to the next gesture.
- The test dispatches gesture A, lets it time out, dispatches gesture B, invokes A's captured callback, and verifies B has not completed. With the current implementation, A's callback can resume B.
This is a real stale-callback/state-ownership race. A repair likely needs callback identity or a generation/token check so callbacks can only resume the continuation belonging to their own dispatch. Clearing the shared callback alone would not be sufficient if the callback still reaches the shared resumeExecution path.
2. Migration19to20: lowercase enum rows are filtered out before normalization
Failing test: Migration19to20Tests.migrate_counters_new_type_acceptsLowercasePersistedEnums
Test location: core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20Tests.kt:102
Production location: core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20.kt:84-121 and 202-219
Technical path:
- The migration collects old integer counter values, drops the old columns, creates REAL columns, and restores values from maps keyed by row ID.
- The row helpers use exact SQL predicates: WHERE type = ON_COUNTER_REACHED and WHERE type = CHANGE_COUNTER.
- The type columns are ordinary text columns without a NOCASE declaration, so the lowercase values on the test rows do not match those predicates.
- The Kotlin callbacks contain type.uppercase() normalization, but lowercase rows never reach those callbacks.
- Consequently, the rows are omitted from the restoration maps and their recreated counter-value columns remain unset/default rather than retaining the old values.
This is especially strong evidence of an incomplete production fix: upstream commit #955 changed valueOf(type) to valueOf(type.uppercase()), which indicates lowercase persisted values were intentionally meant to be accepted, but the SQL pre-filter was not updated. A repair could make the SQL predicate case-insensitive, use UPPER(type), or remove the narrow predicate and perform the type check in Kotlin.
3. Smart backup import: unknown nested fields are rejected
Failing test: ScenarioSerializerTests.deserialize_futureScenarioField_preservesKnownScenarioData
Test location: feature/backup/src/test/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/ScenarioSerializerTests.kt:40-61
Production locations:
- feature/backup/src/main/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/ScenarioSerializer.kt:41 and 63-67
- core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/serialization/KotlinDeserializer.kt:25-28
- core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/serialization/Deserializer.kt:44
Technical path:
- ScenarioSerializer creates Json with ignoreUnknownKeys enabled, but uses it only for parseToJsonElement. That step produces a JsonObject and does not deserialize the nested CompleteScenario.
- For a current-version backup, DeserializerFactory selects KotlinDeserializer.
- KotlinDeserializer calls the default Json.decodeFromJsonElement, whose configuration is strict and does not inherit ScenarioSerializer's ignoreUnknownKeys setting.
- Adding an otherwise harmless futureField to the nested scenario therefore throws JsonDecodingException before the known Scenario data can be imported.
ScenarioBackup's existing documentation explicitly describes backward/forward compatibility and says additional fields may be added. The test is checking unknown-field tolerance within a supported version; it is not claiming that a backup with a version greater than the installed DATABASE_VERSION should be accepted, since the factory intentionally rejects unsupported future versions.
A repair likely needs the Kotlin deserializer to use a Json instance configured with ignoreUnknownKeys, or for the configured Json instance to be passed through to the nested decoder.
4. Counter creation: direct command accepts whitespace-only names
Failing test: CounterCreationViewModelTests.createCounter_ignoresBlankName
Test location: feature/smart-config/src/test/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModelTests.kt:26
Production locations:
- feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModel.kt:54-75
- feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationDialog.kt:65 and 98
Technical path:
- toUiState correctly uses isNotBlank() and reports canBeSaved=false for whitespace-only input.
- The dialog normally disables the Save button from that state, so this is currently blocked through the ordinary UI path.
- createCounter itself checks only for a scenario, a nullable name, and duplicate exact names. It does not repeat the isNotBlank invariant.
- Calling the public ViewModel command directly with three spaces therefore adds a whitespace-only Counter to EditionState.
- The recent upstream validation change in the UI state made the intended invariant clearer, but the command path still does not enforce it.
The immediate user impact is limited because the UI disables Save, but the ViewModel command is an exposed mutation boundary and can be called by other code or future UI paths. The likely repair is a defensive isBlank/isNotBlank guard in createCounter, with the existing test retained as command-level coverage.
Suggested tracking approach
Fix the four production behaviors separately while retaining PR #1045 as regression coverage. The gesture and migration issues have the highest correctness/data-preservation risk; the backup issue affects import compatibility; the counter issue is a smaller defensive validation gap.
Context
Regression test PR: #1045
Hosted verification run: https://github.com/vibhor1102/Smart-AutoClicker/actions/runs/31606697584
Target branch: dev-4.0.0-beta08-fixes
The verify-only workflow completed all independent test tasks and reported four failures. The tests are intentionally kept in the PR; this issue tracks the production-side follow-up.
1. GestureExecutor: late callback completes the next gesture
Failing test: GestureExecutorTests.dispatchGesture_lateCallbackAfterTimeout_doesNotCompleteNextGesture
Test location: core/common/actions/src/test/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutorTests.kt:80
Production location: core/common/actions/src/main/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutor.kt:40-98
Technical path:
This is a real stale-callback/state-ownership race. A repair likely needs callback identity or a generation/token check so callbacks can only resume the continuation belonging to their own dispatch. Clearing the shared callback alone would not be sufficient if the callback still reaches the shared resumeExecution path.
2. Migration19to20: lowercase enum rows are filtered out before normalization
Failing test: Migration19to20Tests.migrate_counters_new_type_acceptsLowercasePersistedEnums
Test location: core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20Tests.kt:102
Production location: core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20.kt:84-121 and 202-219
Technical path:
This is especially strong evidence of an incomplete production fix: upstream commit #955 changed valueOf(type) to valueOf(type.uppercase()), which indicates lowercase persisted values were intentionally meant to be accepted, but the SQL pre-filter was not updated. A repair could make the SQL predicate case-insensitive, use UPPER(type), or remove the narrow predicate and perform the type check in Kotlin.
3. Smart backup import: unknown nested fields are rejected
Failing test: ScenarioSerializerTests.deserialize_futureScenarioField_preservesKnownScenarioData
Test location: feature/backup/src/test/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/ScenarioSerializerTests.kt:40-61
Production locations:
Technical path:
ScenarioBackup's existing documentation explicitly describes backward/forward compatibility and says additional fields may be added. The test is checking unknown-field tolerance within a supported version; it is not claiming that a backup with a version greater than the installed DATABASE_VERSION should be accepted, since the factory intentionally rejects unsupported future versions.
A repair likely needs the Kotlin deserializer to use a Json instance configured with ignoreUnknownKeys, or for the configured Json instance to be passed through to the nested decoder.
4. Counter creation: direct command accepts whitespace-only names
Failing test: CounterCreationViewModelTests.createCounter_ignoresBlankName
Test location: feature/smart-config/src/test/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModelTests.kt:26
Production locations:
Technical path:
The immediate user impact is limited because the UI disables Save, but the ViewModel command is an exposed mutation boundary and can be called by other code or future UI paths. The likely repair is a defensive isBlank/isNotBlank guard in createCounter, with the existing test retained as command-level coverage.
Suggested tracking approach
Fix the four production behaviors separately while retaining PR #1045 as regression coverage. The gesture and migration issues have the highest correctness/data-preservation risk; the backup issue affects import compatibility; the counter issue is a smaller defensive validation gap.