mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
* fix(ssh): connect to Linux hosts that cannot compile node-pty node-pty ships no Linux prebuilt at any architecture, so it is compiled on the remote. On a host without a C/C++ toolchain that build fails, and because both native deps install in one npm command it also took down @parcel/watcher — which does have a working Linux prebuilt — and failed the whole connection. Every Linux image without build tools was unusable. node-pty only backs remote terminals; files, git, and the editor do not need it, and a missing native dep is already non-fatal further down the deploy. So when the existing toolchain probe confirms the compiler is missing, reinstall without node-pty instead of aborting. The manifest has to drop it too — npm reconciles every dependency in package.json, not just the ones named on the command line, so naming only @parcel/watcher still rebuilds node-pty. If that reinstall also fails the actionable build-tools error is rethrown, so a host broken for some other reason still reports the toolchain gap. The relay's PTY error now names the fix rather than saying only that node-pty is unavailable. Verified on a stock Rocky Linux 10.2 aarch64 container (openssh-server, git, nodejs, npm, no compiler): connect succeeds, /etc lists over SSH, node-pty is absent while @parcel/watcher installs its linux-arm64-glibc prebuilt, and spawning a terminal reports the install hint. * fix(ssh): keep the node-pty skip path honest about platform and watcher The PTY unavailable message named build tools unconditionally, but only Linux compiles node-pty — the deploy-side skip is gated on linux and the toolchain probe returns null on Windows. A Windows or macOS remote, where node-pty ships prebuilds, was told to install make/g++/python3. Pick the remedy by the relay's own platform. The skip path returned before the install probe, so a @parcel/watcher that installs but cannot require() (glibc below the floor) connected with dead file watching and nothing logged. Probe before returning and warn; no rebuild, since node-pty provably cannot compile on that host, and never fatal. Also log the pty-less reinstall's own failure and attach it as cause — the rethrown toolchain message is built from the original npm error, so an unrelated retry failure (registry, ENOSPC, EACCES) was lost. The reinstall now keeps the caller's resetDeps as well, so a repair reconnect still clears every dep the probe found broken. Tests: the skip-success fixture queued a chmod/probe/rebuild sequence production never runs, and the surplus slots were absorbed by launchRelay's readiness poll (1817ms vs 3-9ms for its peers). It now emits exactly the 12 execs production performs, and pins that no rebuild is issued. Adds the missing negative case: a gyp-shaped failure on a host whose probe reports a complete toolchain must still hard-fail rather than silently degrade. * fix(ssh): hedge the node-pty remedy and keep repair resets on the skip path
128 lines
5.0 KiB
TypeScript
128 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildToolchainProbeCommand,
|
|
parseBuildToolchainProbe,
|
|
formatMissingToolchainError,
|
|
formatSkippedNodePtyWarning,
|
|
shouldProbeBuildToolchainAfterNativeDepsFailure
|
|
} from './ssh-relay-build-toolchain'
|
|
|
|
describe('buildToolchainProbeCommand', () => {
|
|
it('probes make and a C++ compiler and detects the package manager', () => {
|
|
const cmd = buildToolchainProbeCommand()
|
|
expect(cmd).toContain('command -v "$t"')
|
|
expect(cmd).toMatch(/\bmake\b/)
|
|
expect(cmd).toMatch(/g\+\+/)
|
|
expect(cmd).toContain('apt-get')
|
|
// Single POSIX-sh line (runs under `/bin/sh -c`), no embedded single quotes
|
|
// that shellEscape would have to wrap.
|
|
expect(cmd).not.toContain('\n')
|
|
expect(cmd).not.toContain("'")
|
|
})
|
|
})
|
|
|
|
describe('parseBuildToolchainProbe', () => {
|
|
it('flags a missing toolchain when make and the C++ compiler are absent', () => {
|
|
const status = parseBuildToolchainProbe('PKG apt-get\n')
|
|
expect(status.toolchainMissing).toBe(true)
|
|
expect(status.present).toEqual([])
|
|
expect(status.packageManager).toBe('apt-get')
|
|
})
|
|
|
|
it('treats a full toolchain as present', () => {
|
|
const status = parseBuildToolchainProbe('HAVE make\nHAVE gcc\nHAVE g++\nHAVE python3\nPKG dnf')
|
|
expect(status.toolchainMissing).toBe(false)
|
|
expect(status.present).toContain('make')
|
|
expect(status.present).toContain('g++')
|
|
expect(status.packageManager).toBe('dnf')
|
|
})
|
|
|
|
it('still flags missing when make is present but no C++ compiler is', () => {
|
|
const status = parseBuildToolchainProbe('HAVE make\nHAVE gcc\nHAVE python3')
|
|
expect(status.toolchainMissing).toBe(true)
|
|
expect(status.packageManager).toBeNull()
|
|
})
|
|
|
|
it('accepts clang++ as the C++ compiler', () => {
|
|
const status = parseBuildToolchainProbe('HAVE make\nHAVE clang\nHAVE clang++\nHAVE python3')
|
|
expect(status.toolchainMissing).toBe(false)
|
|
})
|
|
|
|
it('ignores shell noise around the markers', () => {
|
|
const status = parseBuildToolchainProbe(
|
|
'Welcome to Acme\nHAVE make\nHAVE g++\nHAVE python3\nMOTD line\nPKG apk'
|
|
)
|
|
expect(status.toolchainMissing).toBe(false)
|
|
expect(status.packageManager).toBe('apk')
|
|
})
|
|
})
|
|
|
|
describe('shouldProbeBuildToolchainAfterNativeDepsFailure', () => {
|
|
it('matches node-gyp missing build-tool output', () => {
|
|
expect(
|
|
shouldProbeBuildToolchainAfterNativeDepsFailure('gyp ERR! stack Error: not found: make')
|
|
).toBe(true)
|
|
expect(
|
|
shouldProbeBuildToolchainAfterNativeDepsFailure(
|
|
'node-gyp ERR! Could not find any Python installation'
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('does not match unrelated npm failures', () => {
|
|
expect(shouldProbeBuildToolchainAfterNativeDepsFailure('npm ERR! network ETIMEDOUT')).toBe(
|
|
false
|
|
)
|
|
expect(
|
|
shouldProbeBuildToolchainAfterNativeDepsFailure('npm ERR! E404 Not Found node-pty')
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('formatMissingToolchainError', () => {
|
|
it('lists the missing tools and a package-manager-specific install command', () => {
|
|
const status = parseBuildToolchainProbe('PKG apt-get')
|
|
const msg = formatMissingToolchainError(status, 'gyp ERR! not found: make')
|
|
expect(msg).toContain('make')
|
|
expect(msg).toContain('a C++ compiler (g++ or clang++)')
|
|
expect(msg).toContain('python3')
|
|
expect(msg).toContain('sudo apt-get install -y build-essential python3')
|
|
// Tailored hint replaces the generic distro list.
|
|
expect(msg).not.toContain('Fedora/RHEL:')
|
|
// Original error retained for triage.
|
|
expect(msg).toContain('gyp ERR! not found: make')
|
|
})
|
|
|
|
it('falls back to a multi-distro list when no package manager was detected', () => {
|
|
const status = parseBuildToolchainProbe('')
|
|
const msg = formatMissingToolchainError(status, 'exit 1')
|
|
expect(msg).toContain('Debian/Ubuntu:')
|
|
expect(msg).toContain('Fedora/RHEL:')
|
|
expect(msg).toContain('Arch:')
|
|
expect(msg).toContain('Alpine:')
|
|
})
|
|
|
|
it('only lists the genuinely missing pieces', () => {
|
|
const status = parseBuildToolchainProbe('HAVE make\nHAVE python3\nPKG pacman')
|
|
const msg = formatMissingToolchainError(status, 'err')
|
|
expect(msg).toContain('a C++ compiler (g++ or clang++)')
|
|
expect(msg).not.toMatch(/\(make,/)
|
|
expect(msg).toContain('sudo pacman -S --needed base-devel python')
|
|
})
|
|
})
|
|
|
|
describe('formatSkippedNodePtyWarning', () => {
|
|
it('quotes the tailored install command when a package manager was detected', () => {
|
|
const warning = formatSkippedNodePtyWarning(parseBuildToolchainProbe('PKG dnf'))
|
|
expect(warning).toContain('skipping node-pty')
|
|
expect(warning).toContain('sudo dnf install -y make gcc gcc-c++ python3')
|
|
})
|
|
|
|
it('stays distro-neutral when no package manager was detected', () => {
|
|
// The hint list is the cross-distro menu here; quoting its first line would name Debian on any host.
|
|
const warning = formatSkippedNodePtyWarning(parseBuildToolchainProbe(''))
|
|
expect(warning).not.toContain('apt-get')
|
|
expect(warning).toContain('install a C/C++ toolchain')
|
|
})
|
|
})
|