Files
orca/config/scripts/check-host-list-render-ratchet.test.mjs
T
Neil 2bf77e605b fix(ssh): draw every host list through one bounded, searchable host picker, so a 13k-host ssh config can't crash any of them (#31426)
* fix(ssh): draw every host list through one bounded, searchable host picker

A generated ~/.ssh/config can import 13k+ SSH hosts, and several surfaces drew a
row per host: Settings > General workspace directory 'Apply to', Notifications >
Machines, the sidebar and Agents Hosts submenu, the composer run-target picker,
the Add Project host selector and remote step, the skill-install machine pickers,
Settings > Servers SSH host picker, repository 'Add to another host' and the
automation host field and filter.

They now share components/host-picker: boundHostList (pinned hosts plus a ranked,
capped match list), HostCombobox for single choice (a Command in a Popover that
mounts nothing until opened, replacing Selects that mount every item), and
useBoundedHostRows with HostListSearchField and HostListOverflowNote for
checklists. A host-list-render ratchet stops new JSX from mapping straight over a
host collection.

* fix(ssh): host lists keep room for search results, menu search is keyboard-reachable

- boundHostList gives pinned hosts and other matches separate limits, and narrows
  pinned hosts by the query, so a large selection never hides what the user types.
- In a dropdown, the host search is a menu item: arrow keys reach it, focus moves
  into the input, and arrows leave it again.
- The Add Project host picker hides its setup actions while searching, so Enter
  picks the matching host.

* fix(ssh): re-picking the current host in a host picker is not a change

The Selects these pickers replaced suppressed same-value changes; the skill
install form clears its workspace and target on a machine change.

* chore(ci): let the shared ratchet runner pick up the host-list render ratchet

main now discovers every check-*-ratchet.mjs through run-ratchets.mjs, so the
separate workflow step would run it twice.

* fix(ssh): host lists rank pinned matches and keep input order; label and scroll fixes

- boundHostList ranks pinned and other hosts by the same match order before
  cutting each to its limit, then keeps input order among equal ranks, so a
  muted or checked exact match is always reachable and clicking a row never
  moves it.
- The Servers SSH host picker is named by its form label again.
- The sidebar Hosts submenu scrolls its host rows instead of running off screen.
- The composer's overflow note sits outside the listbox.
2026-10-20 23:33:36 -07:00

38 lines
1.7 KiB
JavaScript

import { describe, expect, it } from 'vitest'
import { countHostCollectionJsxMaps, isScannedPath } from './check-host-list-render-ratchet.mjs'
describe('countHostCollectionJsxMaps', () => {
it('counts JSX drawn straight from a host collection', () => {
expect(
countHostCollectionJsxMaps(
'{hostOptions.map((host) => (\n <SelectItem key={host.id} value={host.id} />\n))}'
)
).toBe(1)
expect(
countHostCollectionJsxMaps('{props.sshConnections.map((c) => <Row key={c.id} />)}')
).toBe(1)
expect(countHostCollectionJsxMaps('{targets.map(t => <li key={t.id} />)}')).toBe(1)
})
it('ignores bounded rows, data maps and comments', () => {
expect(countHostCollectionJsxMaps('{rows.shown.map((host) => <Row key={host.id} />)}')).toBe(0)
expect(countHostCollectionJsxMaps('{targetRows.shown.map((t) => <Row key={t.id} />)}')).toBe(0)
expect(countHostCollectionJsxMaps('new Map(hostOptions.map((host) => [host.id, host]))')).toBe(
0
)
expect(countHostCollectionJsxMaps('hosts.map((host) => ({ id: host.id }))')).toBe(0)
expect(countHostCollectionJsxMaps('// hosts.map((h) => <Row />)')).toBe(0)
})
})
describe('isScannedPath', () => {
it('scans renderer components outside the host-picker primitive', () => {
expect(isScannedPath('src/renderer/src/components/settings/A.tsx')).toBe(true)
expect(isScannedPath('src/renderer/src/components/host-picker/HostCombobox.tsx')).toBe(false)
expect(isScannedPath('src/renderer/src/components/settings/A.test.tsx')).toBe(false)
expect(isScannedPath('src/renderer/src/components/settings/a.ts')).toBe(false)
expect(isScannedPath('src/main/a.tsx')).toBe(false)
})
})