Skip to content

fix(versioning): wrong-version detection — strict equality for install CTA (E3 R2)#540

Merged
rainxchzed merged 3 commits into
mainfrom
fix/e3-r2-version-detection
May 7, 2026
Merged

fix(versioning): wrong-version detection — strict equality for install CTA (E3 R2)#540
rainxchzed merged 3 commits into
mainfrom
fix/e3-r2-version-detection

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 7, 2026

Fixes E3 R2 from roadmap/FREE_FEATURES.md. Survey #11: "部分软件识别错误,无法安装。提示已安装相同版本,但其实是不同版本" (Some apps recognized incorrectly, cannot install. Says same version installed, but actually different version.)

Root cause

VersionMath.isSameVersion is semver-correct: it strips +build metadata before comparing because per semver spec, build metadata is ignored for ordering. The CTA gating in SmartInstallButton and the "Installed: X" subtext in AppHeader consume this same comparator, so two releases that differ only in build metadata (v1.0.0+build.1 vs v1.0.0+build.2) — or maintainers who abuse +build to ship distinct artifacts under the same numeric core — render as "Open" / hide the version subtext, telling the user the app is already installed when it actually isn't.

The same false positive can fire on legitimately different releases when the maintainer's tag scheme uses the dotted-digit core for an unrelated grouping (e.g. flavor / arch suffixes that the comparator currently treats as semver pre-release strings).

Fix

Add VersionMath.isExactSameVersion(a, b) — strict literal equality after the conservative cleanup that normalizeVersion already applies before the semver normalization steps: trim, strip refs/tags/, strip leading v/V, trim again. Build metadata is preserved. Pre-release suffixes are preserved. Case-sensitive on the suffix.

Replace the three UI call sites that gate user-visible "open vs install" decisions:

  • SmartInstallButton.kt:105isSameVersionInstalled flag (drives "Open" vs "Install" CTA)
  • SmartInstallButton.kt:249 — "Install version X" button-label branch
  • AppHeader.kt:212 — "Installed: X" version-subtext display gate

isSameVersion is left in place for DetailsState.canSwitchToStable (best-effort tag-to-release lookup, where forgiving prefix-drift matching is desirable) and for the existing isVersionNewer / compareVersions ordering paths in InstalledAppsRepositoryImpl and ExternalInstallVerdict.

Acceptance criteria

  • v1.0.0+build.1 vs v1.0.0+build.2 — exact-same returns false → install CTA surfaces correctly
  • v1.0.0 vs 1.0.0 — exact-same returns true (prefix stripped on both sides) → still treated as same
  • v1.0.0 vs V1.0.0 — exact-same returns true (both V variants stripped)
  • v1.0.0-rc1 vs v1.0.0-rc1 — exact-same returns true
  • v1.0.0-rc1 vs v1.0.0 — exact-same returns false (suffix preserved)
  • v1.0.0.4 vs v1.0.0.4 — exact-same returns true (4-segment numeric core preserved)
  • 2024.10.15 vs 2024.10.15 — exact-same returns true (date-style version preserved)
  • Both blank/null → exact-same returns false (different from isSameVersion, which treats two empties as equal)
  • No regression on isVersionNewer / compareVersions semver semantics — unchanged
  • No regression on canSwitchToStable lookup — unchanged

Test plan

  • Manual on Android device:
    • Install an app with a build-metadata-bearing tag (e.g. v1.0.0+sha.abc)
    • Open Details for the same repo when a release with +sha.def is latest
    • Confirm the install CTA shows "Update / Install" (not "Open")
    • Confirm "Installed: X" subtext renders in the header
  • Compile clean: :composeApp:compileDebugKotlinAndroid

Notes

  • Unit tests deferred — repo has no *Test.kt files in any module, so adding the regression suite for compareVersions / isExactSameVersion requires bringing up kotlin-test + JVM source set wiring in :core:domain first. Filed for a follow-up sweep when test infra lands.

What's-new

  • Added a second FIXED bullet to whatsnew/17.json across 13 locales describing the version-detection improvement.

Summary by CodeRabbit

  • Bug Fixes

    • Improved version detection to prevent releases with identical version numbers but different build metadata from being incorrectly flagged as already installed.
  • Documentation

    • Updated release notes for version 1.9.0 across all supported languages to reflect the enhanced version detection improvements.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Walkthrough

This PR introduces isExactSameVersion, a stricter version comparison function that preserves build metadata differences, and applies it across the UI layer to prevent releases differing only in build artifacts from being misidentified as already installed. Release notes are updated in 15 locales.

Changes

Exact Version Detection Logic

Layer / File(s) Summary
Version Comparison API
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
isExactSameVersion(a, b) added for strict equality checking. Performs conservative prefix cleanup (trim, remove refs/tags/, strip leading v/V, trim again) but preserves build metadata and case-sensitivity, unlike the broader isSameVersion function.
UI Component Integration
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt, feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt
AppHeader now suppresses "installed_version" badge text when versions are not exact matches. SmartInstallButton uses exact comparison to determine whether to show "Open" button (same version) vs "Install version X" label, treating build-metadata variants as distinct.
Release Notes
core/presentation/src/commonMain/composeResources/files/whatsnew/17.json, core/presentation/src/commonMain/composeResources/files/whatsnew/{ar,bn,es,fr,hi,it,ja,ko,pl,ru,tr,zh-CN}/17.json
Version 17 (1.9.0) release notes updated across English and 14 localized versions documenting improved version detection: releases with identical version numbers but different build metadata are no longer incorrectly marked as already installed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A version's dance, both near and far,
With tags and builds and v at the bar—
Exact comparison wins the day,
No metadata tricks lead us astray!
Badges and buttons now clearly know,
When versions match and when they don't show. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(versioning): wrong-version detection — strict equality for install CTA (E3 R2)' clearly and specifically describes the main change: fixing version detection by implementing strict equality for the install call-to-action.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/e3-r2-version-detection

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Microsoft Presidio Analyzer (2.2.362)
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt

Microsoft Presidio Analyzer failed to scan this file


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt (1)

137-147: ⚡ Quick win

Deduplicate prefix cleanup to avoid future drift with normalizeVersion.

Line 137 introduces cleanup logic that duplicates the normalization prefix handling already present in normalizeVersion (Line 53–58). Centralizing this reduces behavioral drift risk.

♻️ Proposed refactor
 fun normalizeVersion(version: String?): String {
-    if (version.isNullOrBlank()) return ""
-    val withoutRefs =
-        version
-            .trim()
-            .removePrefix("refs/tags/")
-            .removePrefix("v")
-            .removePrefix("V")
-            .trim()
+    val withoutRefs = stripCommonPrefixes(version) ?: return ""
     val withoutBuildMetadata = withoutRefs.substringBefore('+')
     if (parseSemanticVersion(withoutBuildMetadata) != null) {
         return withoutBuildMetadata
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt`
around lines 137 - 147, stripCommonPrefixes duplicates the prefix cleanup logic
found in normalizeVersion; extract the shared trimming/removal of "refs/tags/",
"v", and "V" (plus trim/empty check) into a single private helper (e.g.,
cleanVersionPrefix) and have both normalizeVersion and stripCommonPrefixes call
that helper (or have stripCommonPrefixes delegate to normalizeVersion if
semantics match) so prefix handling is centralized and behavior stays
consistent.
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt (1)

211-213: 💤 Low value

Stylistic inconsistency: AppHeader delegates version normalization to isExactSameVersion, while SmartInstallButton pre-normalizes explicitly.

Both approaches are safe. isExactSameVersion accepts nullable parameters and handles them robustly:

  • If either input is null or blank, stripCommonPrefixes() returns null, causing the function to return false
  • Both inputs are internally trimmed via stripCommonPrefixes() before comparison

However, SmartInstallButton (lines 96–105) explicitly pre-trims and null-guards both inputs before passing them, while AppHeader (line 212) passes installedApp.installedVersion raw and release?.tagName as-is. This inconsistency makes the null-handling implicit rather than explicit.

Consider aligning with SmartInstallButton's pattern for consistency and clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt`
around lines 211 - 213, The AppHeader.kt check currently passes
installedApp.installedVersion and release?.tagName directly into
VersionMath.isExactSameVersion; align this with SmartInstallButton by
pre-normalizing and null-guarding both inputs before calling isExactSameVersion
(e.g., apply the same trimming/strip-common-prefix behavior used in
SmartInstallButton to installedApp.installedVersion and release?.tagName), so
call isExactSameVersion only with the normalized values to make null/blank
handling explicit and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt`:
- Around line 137-147: stripCommonPrefixes duplicates the prefix cleanup logic
found in normalizeVersion; extract the shared trimming/removal of "refs/tags/",
"v", and "V" (plus trim/empty check) into a single private helper (e.g.,
cleanVersionPrefix) and have both normalizeVersion and stripCommonPrefixes call
that helper (or have stripCommonPrefixes delegate to normalizeVersion if
semantics match) so prefix handling is centralized and behavior stays
consistent.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt`:
- Around line 211-213: The AppHeader.kt check currently passes
installedApp.installedVersion and release?.tagName directly into
VersionMath.isExactSameVersion; align this with SmartInstallButton by
pre-normalizing and null-guarding both inputs before calling isExactSameVersion
(e.g., apply the same trimming/strip-common-prefix behavior used in
SmartInstallButton to installedApp.installedVersion and release?.tagName), so
call isExactSameVersion only with the normalized values to make null/blank
handling explicit and consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e82394ea-eff3-499b-8bd7-9c585cc5bdfc

📥 Commits

Reviewing files that changed from the base of the PR and between c3631f4 and 9699715.

📒 Files selected for processing (16)
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
  • core/presentation/src/commonMain/composeResources/files/whatsnew/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ar/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/bn/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/es/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/fr/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/hi/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/it/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ja/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ko/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/pl/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ru/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/tr/17.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/17.json
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt

@rainxchzed rainxchzed merged commit 9579325 into main May 7, 2026
1 check passed
@rainxchzed rainxchzed deleted the fix/e3-r2-version-detection branch May 7, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant