fix(desktop): heal v1 SSH gateway routes into the v2 connections registry

reconcileRegistryDrift only healed remote/cloud v1 routes. A v1 global
mode:'ssh' route (host, no url) written by Settings after the one-shot
migration had no registry identity: resolvedConnectionId returned null,
primary stayed 'local', and every launch re-homed the window onto a
fresh local backend. Because the heal skipped SSH entirely, the two
config files re-drifted after every update relaunch instead of
converging once.

Normalize the v1 SSH descriptor into a v2 kind:'ssh' entry (via the
same validated normalizeConnectionInput path the editor uses) and align
primary/lastUsed, with the same narrow-heal rules as remote: already-
registered targets and deliberate primary picks are left alone, and
unusable hosts never touch the registry.

Diagnosis credit: mgallmur-glitch (root cause) and jakewvincent
(re-drift after update relaunch) on #93888.
This commit is contained in:
Teknium
2026-08-31 12:01:52 -07:00
parent 6e41ab3460
commit 2e9a39d28e
2 changed files with 147 additions and 2 deletions
@@ -1595,12 +1595,13 @@ test('drift heal respects a deliberate primary pick on a registered route', () =
assert.equal(drifted.registry.primary, LOCAL_CONNECTION_ID)
})
test('drift heal ignores local, ssh, and unparseable v1 routes', () => {
test('drift heal ignores local and unparseable v1 routes', () => {
const registry = emptyRegistry()
for (const v1 of [
{ mode: 'local', remote: {} },
{ mode: 'ssh', remote: { host: 'box' } },
{ mode: 'ssh', remote: {} },
{ mode: 'ssh', remote: { host: ' ' } },
{ mode: 'remote', remote: { url: 'not a url' } },
{ mode: 'remote', remote: {} },
null
@@ -1612,6 +1613,92 @@ test('drift heal ignores local, ssh, and unparseable v1 routes', () => {
}
})
test('drift heal registers a v1 SSH route the registry never learned about and makes it primary', () => {
// mgallmur-glitch's shape: registry migrated while local-only, then Settings
// pointed v1 at an SSH host (host, no url). The registry cannot name it, so
// primary stays 'local' and the files re-drift after every update relaunch.
const drifted = reconcileRegistryDrift(emptyRegistry(), {
mode: 'ssh',
remote: { host: 'devbox.example.com', user: 'omar', port: 2222 }
})
assert.equal(drifted.changed, true)
const ssh = drifted.registry.connections.find(connection => connection.kind === 'ssh')
assert.ok(ssh)
assert.equal(ssh.host, 'devbox.example.com')
assert.equal(ssh.user, 'omar')
assert.equal(ssh.port, 2222)
assert.equal(drifted.registry.primary, ssh.id)
assert.equal(drifted.registry.lastUsed, ssh.id)
// The whole point: the live v1 SSH descriptor can now be named.
assert.equal(
resolvedConnectionId(drifted.registry, {
mode: 'remote',
remoteKind: 'ssh',
ssh: { host: 'devbox.example.com', user: 'omar', port: 2222 }
}),
ssh.id
)
})
test('drift heal leaves a registry that already knows the v1 SSH route untouched', () => {
const first = reconcileRegistryDrift(emptyRegistry(), {
mode: 'ssh',
remote: { host: 'devbox.example.com', user: 'omar' }
})
assert.equal(first.changed, true)
const drifted = reconcileRegistryDrift(first.registry, {
mode: 'ssh',
remote: { host: 'DEVBOX.example.com', user: 'Omar' }
})
assert.equal(drifted.changed, false)
assert.equal(drifted.registry, first.registry)
})
test('drift heal respects a deliberate primary pick on a registered SSH route', () => {
let registry = reconcileRegistryDrift(emptyRegistry(), {
mode: 'ssh',
remote: { host: 'devbox.example.com' }
}).registry
registry = setPrimaryConnection(registry, LOCAL_CONNECTION_ID)
const drifted = reconcileRegistryDrift(registry, {
mode: 'ssh',
remote: { host: 'devbox.example.com' }
})
assert.equal(drifted.changed, false)
assert.equal(drifted.registry.primary, LOCAL_CONNECTION_ID)
})
test('drift heal adds the missing SSH source without disturbing other registered sources', () => {
let registry = emptyRegistry()
registry = upsertConnection(registry, {
id: 'homelab',
kind: 'remote',
label: 'Homelab',
url: 'https://homelab.example.com',
authMode: 'token',
token: { keep: true }
})
const drifted = reconcileRegistryDrift(registry, {
mode: 'ssh',
remote: { host: 'devbox.example.com' }
})
assert.equal(drifted.changed, true)
assert.ok(drifted.registry.connections.some(connection => connection.id === 'homelab'))
assert.ok(drifted.registry.connections.some(connection => connection.kind === 'ssh'))
})
test('drift heal adds the missing remote without disturbing other registered sources', () => {
let registry = emptyRegistry()
@@ -1496,6 +1496,13 @@ export function reconcileAppliedGlobalConnection(
* registry entry at all. That is the drift state and nothing else. If the
* route is already registered but `primary` names another source, the user
* chose that in the Connections panel and we leave it alone.
*
* SSH drifts the same way remote does: a v1 global `mode:'ssh'` route (host,
* no url) written by Settings after the one-shot migration has no registry
* identity, so `resolvedConnectionId` returns null, `primary` stays `local`,
* and every launch re-homes the window onto a local backend — and because the
* heal used to skip SSH entirely, the two files re-drifted after every update
* relaunch instead of converging once.
*/
export function reconcileRegistryDrift(
registry: ConnectionRegistry,
@@ -1504,6 +1511,57 @@ export function reconcileRegistryDrift(
const config = v1 && typeof v1 === 'object' ? (v1 as Record<string, any>) : {}
const unchanged = { changed: false, registry }
if (config.mode === 'ssh') {
const ssh = normalizeSshConfig({ ...(config.remote && typeof config.remote === 'object' ? config.remote : {}), mode: 'ssh' })
if (!ssh) {
// A v1 SSH route without a usable host is not a route we can register.
return unchanged
}
const target = normalizedSshTarget(ssh)
const alreadyRegistered = registry.connections.some(
connection =>
connection.kind === 'ssh' &&
normalizedSshTarget(connection) === target &&
(connection.port ?? 22) === (ssh.port ?? 22)
)
if (alreadyRegistered) {
// Route is known; if primary names another source, that is the user's
// Connections-panel choice, not drift.
return unchanged
}
const { mode: _mode, ...sshFields } = ssh
let entry: RegistryConnection
try {
entry = normalizeConnectionInput(
{
kind: 'ssh',
label: uniqueLabel(
ssh.host,
registry.connections.map(connection => connection.label)
),
...sshFields
},
registry
)
} catch {
// Validation failure (e.g. a crafted collision) must not corrupt the
// registry; the v1 path keeps failing the way it already does.
return unchanged
}
return {
changed: true,
registry: { ...upsertConnection(registry, entry), primary: entry.id, lastUsed: entry.id }
}
}
if (!modeIsRemoteLike(config.mode)) {
return unchanged
}