c19537fb03
A user who reopens Hermes while an update is running lands on the boot gate, which is what it is for. But the updater swaps the packaged bundle on disk after `hermes update` exits, and its `open` leg only focuses this already- running process, so nothing ever loads the new build. The parked instance then passes the gate and boots the new runtime under the old renderer — the "App build out of date" banner immediately after a fully successful update, over an Updates card that says "You're on the latest version" and so offers no remedy. Compare the install stamp this process loaded at boot with the one on disk when the gate clears. On positive proof of a swap — different commit, or a different builtAt at the same commit — relaunch instead of starting a backend. Detection fails quiet like bundle-skew, so a swap that never happened (the Windows locked-binary case) is unchanged. A one-shot argv flag makes a relaunch loop impossible and a 15s failsafe falls back to the old behavior. Co-authored-by: tk-pkm111 <133480534+tk-pkm111@users.noreply.github.com> Co-authored-by: aeonsong <aeonsong@users.noreply.github.com>
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { detectBundleSwap } from './bundle-swap'
|
|
|
|
const RUNNING = { builtAt: '2026-08-29T04:00:00.000Z', commit: 'a'.repeat(40), source: 'local' }
|
|
|
|
describe('detectBundleSwap', () => {
|
|
it('reports a swap when the on-disk stamp carries a different commit', () => {
|
|
const onDisk = { ...RUNNING, commit: 'b'.repeat(40) }
|
|
|
|
expect(detectBundleSwap(RUNNING, onDisk)).toBe(true)
|
|
})
|
|
|
|
it('reports a swap when the same commit was rebuilt (builtAt moved)', () => {
|
|
const onDisk = { ...RUNNING, builtAt: '2026-08-31T23:55:41.149Z' }
|
|
|
|
expect(detectBundleSwap(RUNNING, onDisk)).toBe(true)
|
|
})
|
|
|
|
// The Windows locked-binary case (#92233): the swap leg failed, so the
|
|
// bundle on disk is still the one we are running. A relaunch would repair
|
|
// nothing and cost the user their window.
|
|
it('is quiet when the on-disk stamp matches the running one', () => {
|
|
expect(detectBundleSwap(RUNNING, { ...RUNNING })).toBe(false)
|
|
})
|
|
|
|
it('is quiet without a running stamp (dev runs)', () => {
|
|
expect(detectBundleSwap(null, { ...RUNNING })).toBe(false)
|
|
})
|
|
|
|
it('is quiet without an on-disk stamp (unreadable resources)', () => {
|
|
expect(detectBundleSwap(RUNNING, null)).toBe(false)
|
|
})
|
|
|
|
it('is quiet on a fallback stamp on either side (non-git build)', () => {
|
|
const fallbackTagged = { ...RUNNING, source: 'fallback' }
|
|
const fallbackCommit = { ...RUNNING, commit: '0'.repeat(40) }
|
|
|
|
expect(detectBundleSwap(fallbackTagged, { ...RUNNING, commit: 'b'.repeat(40) })).toBe(false)
|
|
expect(detectBundleSwap(RUNNING, fallbackCommit)).toBe(false)
|
|
})
|
|
|
|
it('treats a missing builtAt on either side as unprovable at the same commit', () => {
|
|
const noBuiltAt = { commit: RUNNING.commit, source: 'local' }
|
|
|
|
expect(detectBundleSwap(noBuiltAt, { ...RUNNING })).toBe(false)
|
|
expect(detectBundleSwap(RUNNING, noBuiltAt)).toBe(false)
|
|
})
|
|
})
|