Files
orca/src/shared/remote-runtime-tailscale-hint.test.ts
T
3f39d7548b Recommend Tailscale when the remote Orca runtime is unreachable (#6637)
* Recommend Tailscale when the remote Orca runtime is unreachable

When a remote-runtime connection fails (RemoteRuntimeClientError "Could not
connect to the remote Orca runtime."), append an actionable Tailscale hint to
the user-facing error, branched on whether the endpoint is already on a tailnet:

- Non-Tailscale endpoint: recommend connecting both devices over Tailscale and
  pairing with its Tailscale address, with a download link.
- Tailscale endpoint (*.ts.net or 100.64.0.0/10): point at the real causes —
  server offline on the tailnet, or Funnel reverted to tailnet-only — and note
  that already-paired devices reconnect without re-pairing.

Applied at the desktop transport chokepoint (status probe, in-use calls, and
subscriptions — connection failures reject, so the hint is applied to the thrown
error, not just ok:false responses) and at the web client's connect/timeout
sites. New pure shared helper mirrors withMacTailscaleDnsHint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address review: scope CGNAT hint to IPv4 literals, track re-paired endpoint

- isTailscaleEndpoint: gate the 100.64.0.0/10 check on a full IPv4 literal so
  DNS names like 100.64.0.1.example.com no longer get tailnet-specific advice.
- callRuntimeEnvironment: capture the endpoint the queued closure actually used,
  so a re-pair between enqueue and dispatch can't append the wrong hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Recognize Tailscale IPv6 endpoints and trailing-dot FQDNs in hint

The remote-runtime Tailscale hint classified IPv6 Tailscale nodes
(fd7a:115c:a1e0::/48) and trailing-dot FQDNs as non-Tailscale, so a
user already reaching their server over Tailscale by IPv6 literal was
wrongly told to 'connect both devices to Tailscale'. Pairing endpoints
can carry bracketed IPv6 literals (resolvePairingEndpoint), so this is
a reachable path. Normalize the extracted host (strip brackets and the
trailing FQDN dot) and add an IPv6 ULA-range check.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: s546126 <268420947+s546126@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 13:41:13 -07:00

89 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isTailscaleEndpoint,
withRemoteRuntimeTailscaleHint
} from './remote-runtime-tailscale-hint'
const UNREACHABLE = 'Could not connect to the remote Orca runtime.'
describe('isTailscaleEndpoint', () => {
it('matches MagicDNS hostnames', () => {
expect(isTailscaleEndpoint('wss://example-host.tailnet.ts.net')).toBe(true)
expect(isTailscaleEndpoint('ws://host.ts.net:6768')).toBe(true)
})
it('matches the 100.64.0.0/10 CGNAT range', () => {
expect(isTailscaleEndpoint('ws://100.64.0.5:6768')).toBe(true)
expect(isTailscaleEndpoint('ws://100.127.255.255:6768')).toBe(true)
})
it('matches Tailscale IPv6 (fd7a:115c:a1e0::/48) literals', () => {
// Pairing endpoints can carry a bracketed IPv6 literal (resolvePairingEndpoint).
expect(isTailscaleEndpoint('wss://[fd7a:115c:a1e0::1]:443')).toBe(true)
expect(isTailscaleEndpoint('ws://[fd7a:115c:a1e0:ab12:4843:cd96:626b:1]:6768')).toBe(true)
expect(isTailscaleEndpoint('ws://[2001:db8::1]:6768')).toBe(false)
expect(isTailscaleEndpoint('ws://[::1]:6768')).toBe(false)
})
it('rejects non-Tailscale hosts and the surrounding 100.x space', () => {
expect(isTailscaleEndpoint('ws://192.168.1.10:6768')).toBe(false)
expect(isTailscaleEndpoint('wss://orca.example.com')).toBe(false)
expect(isTailscaleEndpoint('ws://100.63.0.1:6768')).toBe(false)
expect(isTailscaleEndpoint('ws://100.128.0.1:6768')).toBe(false)
expect(isTailscaleEndpoint('ws://notts.net.evil.com')).toBe(false)
// A DNS name that merely starts with a CGNAT-shaped label is not a TS IP.
expect(isTailscaleEndpoint('ws://100.64.0.1.example.com:6768')).toBe(false)
})
it('handles bare hosts without a scheme and empty input', () => {
expect(isTailscaleEndpoint('host.ts.net')).toBe(true)
// A trailing-dot FQDN is still the same tailnet host.
expect(isTailscaleEndpoint('wss://host.ts.net.')).toBe(true)
expect(isTailscaleEndpoint('')).toBe(false)
expect(isTailscaleEndpoint(null)).toBe(false)
expect(isTailscaleEndpoint(undefined)).toBe(false)
})
})
describe('withRemoteRuntimeTailscaleHint', () => {
it('recommends switching to Tailscale when the endpoint is not on a tailnet', () => {
const result = withRemoteRuntimeTailscaleHint(UNREACHABLE, 'ws://192.168.1.10:6768')
expect(result).toContain(UNREACHABLE)
expect(result).toContain('connect both devices to Tailscale')
expect(result).toContain('https://tailscale.com/download')
})
it('points at tailnet-specific causes when the endpoint is already Tailscale', () => {
const result = withRemoteRuntimeTailscaleHint(UNREACHABLE, 'wss://example-host.tailnet.ts.net')
expect(result).toContain('Funnel reverted to tailnet-only')
expect(result).toContain('already-paired devices reconnect with their saved token')
expect(result).not.toContain('https://tailscale.com/download')
})
it('covers the close and timeout failure variants', () => {
expect(
withRemoteRuntimeTailscaleHint(
'Remote Orca runtime closed the connection.',
'ws://192.168.1.10:6768'
)
).toContain('connect both devices to Tailscale')
expect(
withRemoteRuntimeTailscaleHint(
'Timed out while connecting to the remote Orca runtime.',
'wss://host.ts.net'
)
).toContain('Funnel reverted to tailnet-only')
})
it('leaves non-connectivity errors untouched', () => {
const auth = 'Remote Orca runtime rejected the pairing token.'
expect(withRemoteRuntimeTailscaleHint(auth, 'ws://192.168.1.10:6768')).toBe(auth)
})
it('is idempotent — does not append the hint twice', () => {
const once = withRemoteRuntimeTailscaleHint(UNREACHABLE, 'ws://192.168.1.10:6768')
const twice = withRemoteRuntimeTailscaleHint(once, 'ws://192.168.1.10:6768')
expect(twice).toBe(once)
})
})