Files
orca/src/main/localhost-worktree-label-proxy.test.ts
T
a56da57bb8 Add worktree labels for localhost ports (#6424)
* Add worktree labels for localhost ports

* Fix localhost label test fixtures

* Extend localhost worktree labels to agents

* Address localhost label review feedback

* Remove localhost label page injection; stream responses untouched

Drop the title/favicon HTML injection from the localhost label proxy.
The proxy now only relabels the hostname and pipes responses straight
through, so app CSP/cookies/bodies are preserved and large/streamed
responses are no longer buffered in memory.

- Normalize wildcard bind hosts (0.0.0.0 -> 127.0.0.1, :: -> ::1) before
  using them as a proxy connect target.
- Delete the favicon SVG generator and dead repoIcon/badgeColor plumbing.
- Remove orphaned LocalhostLabelMock i18n keys.

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

* Harden localhost label proxy from review

- IPC register: restrict proxy target to loopback or a matching scanned
  workspace port (close open-proxy/SSRF surface from untrusted renderer).
- Proxy: guard against ERR_HTTP_HEADERS_SENT on mid-stream upstream error;
  add error/cleanup listeners on client request/response and upgrade socket.
- labelLocalhostUrl: fall back to the raw URL when the proxy rejects a
  target (e.g. https) instead of throwing.
- Port label route: include worktreePath so button-open and terminal/CLI
  paths produce the same label.
- Terminal OSC link hover: discard stale async tooltip results via a hover
  token, matching the WebLinks path.

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

* Match default-port advertised hosts in localhost label target check

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

* Consolidate duplicated localhost label helpers

- Move the loopback host set, loopback-URL parser, and wildcard
  connect-host normalizer into shared/localhost-worktree-labels.ts; proxy,
  runtime, terminal link routing, and the IPC guard now share one copy.
- Extract the port -> repo -> worktree -> project label-route lookup into
  workspace-port-localhost-label-selector.ts (a hook plus an imperative
  resolver), replacing the block triplicated across the ports surfaces.

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

* Extract command-code prompt-status seed to its own module

Keeps launch-agent-in-new-tab.ts under the max-lines limit after the
localhost-hint additions, without a lint disable.

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

* Remove agent-facing localhost mechanism

Orca does not mutate user prompts or inject prompt snippets, so drop the
localhost-open agent hint entirely:

- Remove appendLocalhostOpeningHint / includeLocalhostOpeningHint and the
  hint constant from tui-agent-startup; agent prompts are no longer rewritten.
- Remove the ORCA_LOCALHOST_OPEN env var from local agent terminals (it was
  only discoverable via the now-removed hint).
- Remove the orca localhost label|open CLI commands, their workspacePorts RPC
  methods, and the runtime labelLocalhostUrl/openLocalhostUrl methods.

The feature is now purely structural: the loopback label proxy plus the
ports-panel and terminal-link 'Open in Browser' paths, which surface a
clickable labeled URL without touching agent prompts.

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

* Make localhost worktree labels opt-in (default off)

Serving a dev app under a different host than localhost:<port> can break
apps that bind cookies/sessions to localhost, so the feature should not
change existing users' Open-in-Browser behavior on upgrade.

- Default localhostWorktreeLabelsEnabled to false.
- Flip the gates to enable only when explicitly true (=== true / !== true)
  instead of treating undefined as enabled.
- Update the setting switch to checked only when explicitly enabled.
- Drop the now-unused runtime store settings field.

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

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 12:27:30 -07:00

128 lines
4.2 KiB
TypeScript

import http from 'node:http'
import type { AddressInfo } from 'node:net'
import { afterEach, describe, expect, it } from 'vitest'
import { LocalhostWorktreeLabelProxy } from './localhost-worktree-label-proxy'
const upstreamServers: http.Server[] = []
afterEach(async () => {
await Promise.all(
upstreamServers
.splice(0)
.map((server) => new Promise<void>((resolve) => server.close(() => resolve())))
)
})
async function startUpstream(
handler: (request: http.IncomingMessage, response: http.ServerResponse) => void
): Promise<number> {
const server = http.createServer(handler)
upstreamServers.push(server)
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
return (server.address() as AddressInfo).port
}
function fetchThroughProxy(
labeledUrl: string
): Promise<{ status: number; body: string; headers: http.IncomingHttpHeaders }> {
const url = new URL(labeledUrl)
return new Promise((resolve, reject) => {
// Why: *.orca.localhost is not resolvable DNS; connect to the proxy on
// loopback and carry the label through the Host header instead.
const request = http.request(
{
host: '127.0.0.1',
port: Number(url.port),
path: `${url.pathname}${url.search}`,
headers: { host: url.host }
},
(response) => {
const chunks: Buffer[] = []
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
response.on('end', () =>
resolve({
status: response.statusCode ?? 0,
body: Buffer.concat(chunks).toString('utf8'),
headers: response.headers
})
)
}
)
request.on('error', reject)
request.end()
})
}
describe('localhost worktree label proxy', () => {
it('rejects https targets because the label proxy serves plain http', async () => {
const proxy = new LocalhostWorktreeLabelProxy()
await expect(
proxy.registerRoute({
targetUrl: 'https://localhost:5173/',
projectName: 'Snap Studio',
worktreeName: 'main'
})
).rejects.toThrow('Only http workspace ports can be labeled.')
})
it('streams responses through untouched, preserving the app CSP and body', async () => {
const port = await startUpstream((_request, response) => {
response.writeHead(200, {
'content-type': 'text/html; charset=utf-8',
'content-security-policy': "default-src 'self'"
})
response.end('<html><head></head><body>hello</body></html>')
})
const proxy = new LocalhostWorktreeLabelProxy()
const { url } = await proxy.registerRoute({
targetUrl: `http://localhost:${port}/`,
projectName: 'Snap Studio',
worktreeName: 'analytics'
})
const result = await fetchThroughProxy(url)
expect(result.status).toBe(200)
// The proxy only relabels the hostname; the page is delivered verbatim
// with no injected favicon/title and the CSP header intact.
expect(result.body).toBe('<html><head></head><body>hello</body></html>')
expect(result.headers['content-security-policy']).toBe("default-src 'self'")
})
it('normalizes a 0.0.0.0 target to a connectable loopback host', async () => {
const port = await startUpstream((_request, response) => {
response.writeHead(200, { 'content-type': 'text/plain' })
response.end('ok')
})
const proxy = new LocalhostWorktreeLabelProxy()
const { url } = await proxy.registerRoute({
targetUrl: `http://0.0.0.0:${port}/`,
projectName: 'Snap Studio',
worktreeName: 'main'
})
const result = await fetchThroughProxy(url)
expect(result.status).toBe(200)
expect(result.body).toBe('ok')
})
it('returns 404 for unregistered orca.localhost labels', async () => {
const port = await startUpstream((_request, response) => {
response.end('ok')
})
const proxy = new LocalhostWorktreeLabelProxy()
const { url } = await proxy.registerRoute({
targetUrl: `http://localhost:${port}/`,
projectName: 'Snap Studio',
worktreeName: 'main'
})
const proxyPort = new URL(url).port
const result = await fetchThroughProxy(`http://unknown-label.orca.localhost:${proxyPort}/`)
expect(result.status).toBe(404)
})
})