import { describe, expect, it, vi } from 'vitest' import type { AgentConfig } from '../src/config/agentConfig.js' import type { Keystore } from '../src/keys/keystore.js' import type { AgentIdentity } from '../src/keys/identity.js' import type { EnrollResult } from 'relay-contracts' import { CliUsageError, parseArgs, runCli, type CliDeps } from '../src/cli.js' import type { InstallOptions } from '../src/service/install.js' 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', } function fakeIdentity(alg: AgentIdentity['alg'] = 'ed25519'): AgentIdentity { return { alg, publicKey: new Uint8Array(32), enrollFpr: 'fpr', sign: () => new Uint8Array(64), exportPrivatePkcs8Pem: () => 'PEM', privateKeyObject: () => ({}) as never, } } function fakeKeystore(enrolled: boolean, alg: AgentIdentity['alg'] = 'ed25519'): Keystore { return { saveIdentity: vi.fn(), loadIdentity: () => (enrolled ? fakeIdentity(alg) : null), saveCert: vi.fn(), loadCert: () => (enrolled ? { certPem: 'C', caChainPem: 'CA' } : null), saveContentSecret: vi.fn(), loadContentSecret: () => null, } } const NATIVE_OPTIONS: InstallOptions = { env: { BIND_HOST: '127.0.0.1', PORT: '3000' }, domain: 'yaojia.wang', zone: 'terminal', } function deps(overrides: Partial = {}, enrolled = false): { d: CliDeps; out: string[] } { const out: string[] = [] const d: CliDeps = { loadConfig: () => CFG, openKeystore: () => fakeKeystore(enrolled), generateIdentity: () => fakeIdentity('ed25519'), generateP256Identity: () => fakeIdentity('p256'), redeem: async (): Promise => ({ hostId: 'h-1', subdomain: 'host-42', cert: 'C', caChain: 'CA', hostContentSecret: new Uint8Array([1]), }), enrollNative: async () => ({ hostId: 'h-1', subdomain: 'host-42' }), provisionFrpc: async () => '/opt/frpc', writeFrpcConfig: vi.fn(), nativeConfigExists: () => false, runTunnel: async () => 0, superviseFrpc: async () => 0, resolveInstallOptions: () => NATIVE_OPTIONS, installService: vi.fn(async () => {}), uninstallService: vi.fn(async () => {}), print: (l) => out.push(l), ...overrides, } return { d, out } } describe('parseArgs (T5)', () => { it('parses `pair ABCD-1234`', () => { expect(parseArgs(['pair', 'ABCD-1234'])).toEqual({ command: 'pair', code: 'ABCD-1234', flags: {} }) }) it('rejects an unknown command', () => { expect(() => parseArgs(['frobnicate'])).toThrow(CliUsageError) }) it('requires a code on pair', () => { expect(() => parseArgs(['pair'])).toThrow(CliUsageError) }) it('collects flags', () => { expect(parseArgs(['pair', 'X', '--install'])).toEqual({ command: 'pair', code: 'X', flags: { install: true }, }) }) }) describe('runCli — legacy relay pair (no --install)', () => { it('pair happy path calls redeem and prints no secrets', async () => { const { d, out } = deps() const code = await runCli(parseArgs(['pair', 'ABCD']), d) expect(code).toBe(0) expect(out.join('\n')).toContain('host-42') expect(out.join('\n')).not.toContain('PEM') }) }) describe('runCli — native pair --install onboard (B5)', () => { it('wires keygen(P-256)→enroll→provisionFrpc→write toml+env→install both→print URL, in order', async () => { const calls: string[] = [] const enrolledIds: AgentIdentity[] = [] const generateP256Identity = vi.fn(() => { calls.push('keygen') return fakeIdentity('p256') }) const enrollNative = vi.fn(async (_cfg: AgentConfig, _code: string, id: AgentIdentity) => { calls.push('enroll') enrolledIds.push(id) return { hostId: 'h-1', subdomain: 'host-42' } }) const provisionFrpc = vi.fn(async () => { calls.push('provision') return '/opt/frpc' }) const writeFrpcConfig = vi.fn(() => { calls.push('writeConfig') }) const installService = vi.fn(async () => { calls.push('install') }) const { d, out } = deps({ generateP256Identity, enrollNative, provisionFrpc, writeFrpcConfig, installService, }) const code = await runCli(parseArgs(['pair', 'ABCD-1234', '--install']), d) expect(code).toBe(0) expect(calls).toEqual(['keygen', 'enroll', 'provision', 'writeConfig', 'install']) // CSR is built from a P-256 identity (FIX H-host-2) expect(enrolledIds[0]!.alg).toBe('p256') // enroll seam received the pairing code expect(enrollNative).toHaveBeenCalledWith(CFG, 'ABCD-1234', expect.anything(), expect.anything()) // both units installed with the resolved options (env routed to base-app by installService) expect(installService).toHaveBeenCalledWith(CFG, NATIVE_OPTIONS) // frpc.toml written for the returned subdomain expect(writeFrpcConfig).toHaveBeenCalledWith(CFG, 'host-42') // prints the final tunnel URL expect(out.join('\n')).toContain('https://host-42.terminal.yaojia.wang') }) it('does NOT use the legacy relay redeem path on --install', async () => { const redeem = vi.fn() const { d } = deps({ redeem: redeem as unknown as CliDeps['redeem'] }) await runCli(parseArgs(['pair', 'ABCD', '--install']), d) expect(redeem).not.toHaveBeenCalled() }) it('saves the freshly generated P-256 identity before enrolling', async () => { const ks = fakeKeystore(false) const { d } = deps({ openKeystore: () => ks }) await runCli(parseArgs(['pair', 'ABCD', '--install']), d) expect(ks.saveIdentity).toHaveBeenCalled() }) it('rejects a native install whose zone is not `terminal` (FIX L-host-zone)', async () => { const { d } = deps({ resolveInstallOptions: () => ({ env: { BIND_HOST: '127.0.0.1' }, domain: 'yaojia.wang', zone: 'term' }), }) await expect(runCli(parseArgs(['pair', 'ABCD', '--install']), d)).rejects.toThrow(/terminal/) }) it('rejects a native install with no TUNNEL_DOMAIN (cannot form the origin)', async () => { const { d } = deps({ resolveInstallOptions: () => ({ env: { BIND_HOST: '127.0.0.1' } }) }) await expect(runCli(parseArgs(['pair', 'ABCD', '--install']), d)).rejects.toBeInstanceOf(CliUsageError) }) it('prints no key/cert material during --install (INV9)', async () => { const { d, out } = deps() await runCli(parseArgs(['pair', 'ABCD', '--install']), d) const joined = out.join('\n') expect(joined).not.toContain('PEM') expect(joined).not.toContain('CA') }) }) describe('runCli — install / run / status', () => { it('install threads the resolved InstallOptions into installService (S2 env injection)', async () => { const install = vi.fn(async () => {}) const { d } = deps({ resolveInstallOptions: () => NATIVE_OPTIONS, installService: install }) const code = await runCli(parseArgs(['install']), d) expect(code).toBe(0) expect(install).toHaveBeenCalledWith(CFG, NATIVE_OPTIONS) }) it('run before pairing fails fast', async () => { const { d } = deps({}, false) await expect(runCli({ command: 'run', flags: {} }, d)).rejects.toBeInstanceOf(CliUsageError) }) it('legacy run (Ed25519 identity, no frpc.toml) drives runTunnel — not frpc supervision', async () => { const runTunnel = vi.fn(async () => 0) const superviseFrpc = vi.fn(async () => 0) const { d } = deps( { runTunnel, superviseFrpc, nativeConfigExists: () => false }, true, // enrolled with the default Ed25519 identity ) const code = await runCli({ command: 'run', flags: {} }, d) expect(code).toBe(0) expect(runTunnel).toHaveBeenCalledWith(CFG, expect.anything()) expect(superviseFrpc).not.toHaveBeenCalled() }) it('native run (P-256 identity + frpc.toml) supervises frpc — not the legacy relay', async () => { const runTunnel = vi.fn(async () => 0) const superviseFrpc = vi.fn(async () => 0) const { d } = deps({ openKeystore: () => fakeKeystore(true, 'p256'), nativeConfigExists: () => true, runTunnel, superviseFrpc, }) const code = await runCli({ command: 'run', flags: {} }, d) expect(code).toBe(0) expect(superviseFrpc).toHaveBeenCalledWith(CFG, expect.anything()) expect(runTunnel).not.toHaveBeenCalled() }) it('native identity but missing frpc.toml falls back to the legacy relay path', async () => { const runTunnel = vi.fn(async () => 0) const superviseFrpc = vi.fn(async () => 0) const { d } = deps({ openKeystore: () => fakeKeystore(true, 'p256'), nativeConfigExists: () => false, runTunnel, superviseFrpc, }) await runCli({ command: 'run', flags: {} }, d) expect(runTunnel).toHaveBeenCalled() expect(superviseFrpc).not.toHaveBeenCalled() }) it('status prints no key/cert material (INV9)', async () => { const { d, out } = deps({}, true) await runCli({ command: 'status', flags: {} }, d) const joined = out.join('\n') expect(joined).toContain('subdomain: host-42') expect(joined).not.toContain('PEM') expect(joined).not.toContain('CA') }) })