fix(desktop): update pre-probe source checkouts from main over git

A source checkout without hermes_cli/source_check.py predates release
channels, so the only line it can be on is git. The desktop treated the
missing probe as unsupported and parked the user on a manual
`hermes update --help` card ("This checkout predates desktop
source-channel checks"), so an older non-bundled install could never
update itself from the app.

Report such a checkout as tracking main with an update available and let
apply take the normal git handoff. That update brings in the probe, so
later checks resolve normally. A probe that exists but fails still throws.
This commit is contained in:
ethernet
2026-09-24 19:23:27 -04:00
parent 26b5494667
commit 473fd1663c
4 changed files with 16 additions and 30 deletions

View File

@@ -7,7 +7,7 @@ import { expect, it, vi } from 'vitest'
import { type CheckoutStrategyDeps, createCheckoutStrategy } from './checkout'
import { readSourceUpdate, type SourceUpdate } from './checkout-source'
it('offers manual recovery only for a missing source probe, never for a broken probe', async (): Promise<void> => {
it('moves a checkout without a source probe to main, but surfaces a broken probe', async (): Promise<void> => {
const root: string = fs.mkdtempSync(path.join(os.tmpdir(), 'legacy-channel-'))
const home: string = path.join(root, 'profile')
const modulePath: string = path.join(root, 'hermes_cli', 'source_check.py')
@@ -31,7 +31,9 @@ it('offers manual recovery only for a missing source probe, never for a broken p
defaultUpdateBranch: 'main',
updateHandoffDwellMs: 0,
resolveUpdateRoot: (): string => root,
resolveUpdaterBinary: vi.fn((): string => 'frozen-updater'),
// No staged updater and no repo hand-off script: apply ends on the manual
// command card, which exposes the target without spawning anything.
resolveUpdaterBinary: vi.fn((): null => null),
remoteGatewayActive: (): boolean => false,
emitUpdateProgress: vi.fn(),
rememberLog: vi.fn(),
@@ -54,14 +56,8 @@ it('offers manual recovery only for a missing source probe, never for a broken p
fs.rmSync(modulePath)
}
expect(await strategy.check()).toMatchObject({ supported: false, reason: 'source-probe-unavailable' })
const result: Awaited<ReturnType<typeof strategy.apply>> = await strategy.apply()
expect(result).toMatchObject({ manual: true, command: 'hermes update --help' })
expect(result.message).toContain('branch or channel')
expect(result.command).not.toContain('--branch')
expect(deps.stopBackendsForUpdate).not.toHaveBeenCalled()
expect(deps.resolveUpdaterBinary).not.toHaveBeenCalled()
expect(deps.quit).not.toHaveBeenCalled()
expect(await strategy.check()).toMatchObject({ supported: true, updateAvailable: true, branch: 'main' })
expect(await strategy.apply()).toMatchObject({ ok: true, manual: true, command: 'hermes update' })
}
fs.writeFileSync(modulePath, 'def main():\n raise RuntimeError("invalid channel configuration")\n')

View File

@@ -25,9 +25,6 @@ export interface SourceUpdateProbe {
const execute: typeof execFile.__promisify__ = promisify(execFile)
export const SOURCE_PROBE_RECOVERY: string =
'This checkout predates desktop source-channel checks. Run `hermes update --help` in this installation, then choose the intended branch or channel explicitly before updating.'
export function sourceUpdateEnvironment(updateRoot: string, hermesHome: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,

View File

@@ -18,7 +18,7 @@ import {
wrapHandoffForDetachedConsole
} from '../updater-process'
import { SOURCE_PROBE_RECOVERY, type SourceUpdate, sourceUpdateEnvironment } from './checkout-source'
import { type SourceUpdate, sourceUpdateEnvironment } from './checkout-source'
import type { UpdaterApplyResultWire, UpdaterMechanism, UpdaterStatusWire, UpdaterStrategy } from './index'
@@ -77,10 +77,13 @@ export function createCheckoutStrategy(deps: CheckoutStrategyDeps): UpdaterStrat
async function check(opts: { force?: boolean } = {}): Promise<UpdaterStatusWire> {
const root: string = deps.resolveUpdateRoot()
// A checkout without the source probe predates source channels, so it can
// only be on the git line: move it to main. Its update pulls the probe in.
const status: UpdaterStatusWire = (await deps.readSourceUpdate(root, opts)) ?? {
supported: false,
reason: 'source-probe-unavailable',
message: SOURCE_PROBE_RECOVERY,
supported: true,
updateAvailable: true,
behind: null,
branch: deps.defaultUpdateBranch,
hermesRoot: root
}
@@ -101,16 +104,6 @@ export function createCheckoutStrategy(deps: CheckoutStrategyDeps): UpdaterStrat
async function applyBody(): Promise<UpdaterApplyResultWire> {
const status: UpdaterStatusWire = await check({ force: true })
if (status.reason === 'source-probe-unavailable') {
return {
ok: true,
manual: true,
command: 'hermes update --help',
message: status.message,
hermesRoot: status.hermesRoot
}
}
if (!status.supported || status.error) {
return { ok: false, error: status.error ?? status.reason, message: status.message }
}

View File

@@ -106,9 +106,9 @@ replace a selected release channel with its default branch.
For branch-tracking source installs, the desktop keeps the current named branch
unless an explicit desktop branch override exists. A detached checkout uses the
default branch. Older checkouts without source-channel probing show manual
recovery instructions instead of guessing a target: inspect `hermes update --help`
in that installation and choose the intended branch or channel before updating.
default branch. Older checkouts without source-channel probing predate release
channels, so the desktop updates them from `main` over git; that update brings in
the probing.
Packaged desktop feed channels derive from their build tag and package owner.
Changing a source channel is not an MSIX or Store channel switch. Canary builds