import { describe, expect, it, vi } from 'vitest' import { decodeMuxFrame, encodeGoaway, encodeMuxFrame, encodeOpen, type MuxOpen } from 'relay-contracts' import type { AgentConfig } from '../src/config/agentConfig.js' import type { Keystore } from '../src/keys/keystore.js' import { createBackoff } from '../src/transport/backoff.js' import { runTunnel, type RunTunnelDeps } from '../src/transport/runTunnel.js' import { FakeTimer, FakeWs } from './fixtures/fakes.js' /** * C2 supervisor: proves runTunnel ports the cafeDemo assembly into a supervised loop — * bytes splice both ways, a dead session reconnects, and a `revoked` GOAWAY stops for good (INV12). */ const CFG: AgentConfig = { relayUrl: 'wss://relay/agent', enrollUrl: 'https://x/enroll', stateDir: '/tmp/x', localTargetUrl: 'ws://127.0.0.1:3000', subdomain: 'host-42', hostId: 'h-1', } const OPEN: MuxOpen = { streamId: 5, subdomain: 'host-42', requestPath: '/term?join=abc', originHeader: 'https://host-42.term.example.com', remoteAddrHash: 'x', capabilityTokenRef: 'jti', } const KS = {} as unknown as Keystore // unused when connectRelay/dialLoopback are injected const flush = (): Promise => new Promise((r) => setImmediate(r)) function emitOpen(upstream: FakeWs, open: MuxOpen): void { const payload = encodeOpen(open) upstream.emitMessage( encodeMuxFrame( { version: 1, type: 'open', fin: false, rst: false, streamId: open.streamId, payloadLen: payload.length }, payload, ), ) } function emitGoAwayRevoked(upstream: FakeWs): void { const payload = encodeGoaway(0, 'revoked') upstream.emitMessage( encodeMuxFrame( { version: 1, type: 'goaway', fin: false, rst: false, streamId: 0, payloadLen: payload.length }, payload, ), ) } function baseDeps(over: Partial): Partial { return { timer: new FakeTimer(), // never auto-fires ⇒ heartbeat is inert during the test sleep: async () => {}, backoff: createBackoff(), ...over, } } describe('runTunnel supervisor (C2)', () => { it('splices bytes both ways through the loopback', async () => { const upstream = new FakeWs() const loopback = new FakeWs() const connectRelay = vi.fn(async () => upstream) const dialLoopback = vi.fn(async () => loopback) const handle = await runTunnel(CFG, KS, baseDeps({ connectRelay, dialLoopback: dialLoopback as never })) await flush() emitOpen(upstream, OPEN) await flush() expect(dialLoopback).toHaveBeenCalledWith('/term?join=abc', 'https://host-42.term.example.com') upstream.emitMessage(encodeMuxFrame({ version: 1, type: 'data', fin: false, rst: false, streamId: 5, payloadLen: 3 }, new Uint8Array([104, 105, 10]))) expect(loopback.sent.at(-1)).toEqual(new Uint8Array([104, 105, 10])) loopback.emit('message', new Uint8Array([79, 75])) // "OK" echoes back upstream as DATA const last = decodeMuxFrame(upstream.sent.at(-1)!) expect(last.header.type).toBe('data') expect([...last.payload]).toEqual([79, 75]) await handle.stop() expect(await handle.done).toBe(0) }) it('reconnects after the tunnel dies', async () => { const sockets = [new FakeWs(), new FakeWs()] let i = 0 const connectRelay = vi.fn(async () => sockets[i++]!) const handle = await runTunnel(CFG, KS, baseDeps({ connectRelay, dialLoopback: async () => new FakeWs() })) await flush() expect(connectRelay).toHaveBeenCalledTimes(1) sockets[0]!.emit('close') // first session dies ⇒ supervisor redials await flush() expect(connectRelay).toHaveBeenCalledTimes(2) await handle.stop() }) it('a revoked GOAWAY tears down and NEVER reconnects (INV12)', async () => { const connectRelay = vi.fn(async () => new FakeWs()) let socket: FakeWs | undefined const wrapped = vi.fn(async () => { socket = new FakeWs() return socket }) const handle = await runTunnel(CFG, KS, baseDeps({ connectRelay: wrapped, dialLoopback: async () => new FakeWs() })) await flush() expect(wrapped).toHaveBeenCalledTimes(1) emitGoAwayRevoked(socket!) await flush() expect(await handle.done).toBe(0) expect(wrapped).toHaveBeenCalledTimes(1) // no reconnect after revocation expect(connectRelay).not.toHaveBeenCalled() }) it('stop() ends the loop with exit code 0 and does not reconnect', async () => { const connectRelay = vi.fn(async () => new FakeWs()) const handle = await runTunnel(CFG, KS, baseDeps({ connectRelay, dialLoopback: async () => new FakeWs() })) await flush() await handle.stop() expect(await handle.done).toBe(0) expect(connectRelay).toHaveBeenCalledTimes(1) }) })