diff --git a/apps/desktop/src/store/updates-backend-check.test.ts b/apps/desktop/src/store/updates-backend-check.test.ts index 69484e2dff..a4175c7dd5 100644 --- a/apps/desktop/src/store/updates-backend-check.test.ts +++ b/apps/desktop/src/store/updates-backend-check.test.ts @@ -47,12 +47,25 @@ describe('mapBackendCheck', () => { expect(status.targetSha).toBe('backend:9.9.9') }) + it('maps the "behind but uncountable" sentinel to behind: null, not 0', () => { + // The producer answers behind: -1 (source_check.UPDATE_AVAILABLE_NO_COUNT) + // when the checkout is behind but the count can't be computed (shallow + // clone without a merge-base, unusable compare API). DesktopUpdateStatus + // types that state as null — "never render it as a literal number" — so + // clamping -1 to 0 made it byte-identical to the up-to-date answer while + // updateAvailable still pitched the install. + const status = mapBackendCheck(response({ behind: -1, update_available: true })) + + expect(status.error).toBeUndefined() + expect(status.behind).toBeNull() + expect(status.updateAvailable).toBe(true) + expect(status.targetSha).toBe('backend:9.9.9') + }) + it('leaves a backend that cannot self-update to the unsupported branch', () => { // pip/nix and managed runtimes also answer `behind: null`, but `can_apply: false` renders // the "not available" copy first — those must not be turned into failures. - const status = mapBackendCheck( - response({ can_apply: false, behind: null, message: 'pip install -U hermes-agent' }) - ) + const status = mapBackendCheck(response({ can_apply: false, behind: null, message: 'pip install -U hermes-agent' })) expect(status.supported).toBe(false) expect(status.error).toBeUndefined() diff --git a/apps/desktop/src/store/updates.test.ts b/apps/desktop/src/store/updates.test.ts index 3a53a4c25b..173ff4c36a 100644 --- a/apps/desktop/src/store/updates.test.ts +++ b/apps/desktop/src/store/updates.test.ts @@ -438,7 +438,9 @@ describe('checkBackendUpdates', () => { const result = await checkBackendUpdates() - expect(result?.behind).toBe(0) + // behind: -1 is the producer's "update available, count unknown" sentinel; + // DesktopUpdateStatus carries it as null, never a literal number. + expect(result?.behind).toBeNull() expect(result?.updateAvailable).toBe(true) expect(result?.targetSha).toBe('backend:0.16.0') }) diff --git a/apps/desktop/src/store/updates.ts b/apps/desktop/src/store/updates.ts index c82f9fa700..bdcd35b8cb 100644 --- a/apps/desktop/src/store/updates.ts +++ b/apps/desktop/src/store/updates.ts @@ -489,7 +489,13 @@ function isRemoteMode(): boolean { } export function mapBackendCheck(res: BackendUpdateCheckResponse): DesktopUpdateStatus { - const behind = res.behind ?? 0 + // The producer's contract (web_routers/actions.py): behind is 0 = up to + // date, -1 = update available but the count is unknown (shallow clone + // without a merge-base, unusable GitHub compare), null = the check could + // not run at all. DesktopUpdateStatus types "unknown count" as null — + // "never render it as a literal number" — so pass that through instead of + // clamping the sentinel to a byte-identical copy of "up to date". + const behind = res.behind === undefined ? 0 : res.behind // `behind: null` from a supported (git) backend is the endpoint's "the check // could not run" answer (GitHub unreachable, rate limited, offline) and @@ -507,7 +513,7 @@ export function mapBackendCheck(res: BackendUpdateCheckResponse): DesktopUpdateS error: checkFailed ? 'check-failed' : undefined, message: res.message ?? undefined, updateAvailable: res.update_available, - behind: behind > 0 ? behind : 0, + behind: behind === null || behind < 0 ? null : behind, currentVersion: res.current_version, targetSha: res.update_available ? `backend:${res.current_version}` : undefined, commits: res.commits,