diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..65c3eba --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun Runtime + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + env: + NODE_ENV: test + run: bun test + + - name: Check formatting + run: bun run format --check \ No newline at end of file diff --git a/packages/mcp-dvm-bridge/src/config.ts b/packages/mcp-dvm-bridge/src/config.ts index a5b7338..09c10ae 100644 --- a/packages/mcp-dvm-bridge/src/config.ts +++ b/packages/mcp-dvm-bridge/src/config.ts @@ -1,41 +1,96 @@ import { parse } from 'yaml'; import { join } from 'path'; import { existsSync, readFileSync } from 'fs'; -import type { MCPServerConfig } from './types'; - -interface NostrConfig { - privateKey: string; - relayUrls: string[]; -} - -interface MCPConfig { - name: string; - about: string; - clientName: string; - clientVersion: string; - servers: MCPServerConfig[]; -} - -interface WhitelistConfig { - allowedPubkeys: Set | undefined; -} - -interface AppConfig { - nostr: NostrConfig; - mcp: MCPConfig; - whitelist: WhitelistConfig; -} +import type { AppConfig, MCPServerConfig } from './types'; const CONFIG_PATH = join(process.cwd(), 'config.yml'); const HEX_KEYS_REGEX = /^(?:[0-9a-fA-F]{64})$/; -if (!existsSync(CONFIG_PATH)) { - throw new Error( - 'No config.yml file found. Please create one based on config.example.yml' - ); +const TEST_CONFIG: AppConfig = { + nostr: { + privateKey: + 'd4d4d7aae7857054596c4c0976b22a73acac3a10d30bf56db35ee038bbf0dd44', + relayUrls: ['ws://localhost:3334'], + }, + mcp: { + name: 'Test DVM MCP Bridge', + about: 'Test MCP-enabled DVM', + clientName: 'Test Client', + clientVersion: '1.0.0', + servers: [], + }, + whitelist: { + allowedPubkeys: new Set(), + }, +}; + +function validateRequiredField(value: any, fieldName: string): string { + if (!value) { + throw new Error(`Missing required config field: ${fieldName}`); + } + return value; +} + +function getConfigValue( + value: string | undefined, + defaultValue: string +): string { + return value || defaultValue; +} + +function validateRelayUrls(urls: any): string[] { + if (!Array.isArray(urls) || urls.length === 0) { + throw new Error( + 'At least one relay URL must be provided in nostr.relayUrls' + ); + } + return urls.map((url: string) => { + try { + const trimmedUrl = url.trim(); + new URL(trimmedUrl); + if (!trimmedUrl.startsWith('ws://') && !trimmedUrl.startsWith('wss://')) { + throw new Error( + `Relay URL must start with ws:// or wss://: ${trimmedUrl}` + ); + } + return trimmedUrl; + } catch (error) { + throw new Error(`Invalid relay URL: ${url}`); + } + }); +} + +function validateMCPServers(servers: any): MCPServerConfig[] { + if (!Array.isArray(servers) || servers.length === 0) { + throw new Error( + 'At least one MCP server must be configured in mcp.servers' + ); + } + return servers.map((server: any, index: number) => { + if (!server.name || !server.command || !Array.isArray(server.args)) { + throw new Error( + `Invalid MCP server configuration at index ${index}. Required fields: name, command, args[]` + ); + } + return { + name: server.name, + command: server.command, + args: server.args, + }; + }); } function loadConfig(): AppConfig { + if (process.env.NODE_ENV === 'test') { + return TEST_CONFIG; + } + + if (!existsSync(CONFIG_PATH)) { + throw new Error( + 'No config.yml file found. Please create one based on config.example.yml' + ); + } + try { const configFile = readFileSync(CONFIG_PATH, 'utf8'); const rawConfig = parse(configFile); @@ -83,63 +138,4 @@ function loadConfig(): AppConfig { } } -function validateRequiredField(value: any, fieldName: string): string { - if (!value) { - throw new Error(`Missing required config field: ${fieldName}`); - } - return value; -} - -function getConfigValue( - value: string | undefined, - defaultValue: string -): string { - return value || defaultValue; -} - -function validateRelayUrls(urls: any): string[] { - if (!Array.isArray(urls) || urls.length === 0) { - throw new Error( - 'At least one relay URL must be provided in nostr.relayUrls' - ); - } - - return urls.map((url: string) => { - try { - const trimmedUrl = url.trim(); - new URL(trimmedUrl); - if (!trimmedUrl.startsWith('ws://') && !trimmedUrl.startsWith('wss://')) { - throw new Error( - `Relay URL must start with ws:// or wss://: ${trimmedUrl}` - ); - } - return trimmedUrl; - } catch (error) { - throw new Error(`Invalid relay URL: ${url}`); - } - }); -} - -function validateMCPServers(servers: any): MCPServerConfig[] { - if (!Array.isArray(servers) || servers.length === 0) { - throw new Error( - 'At least one MCP server must be configured in mcp.servers' - ); - } - - return servers.map((server: any, index: number) => { - if (!server.name || !server.command || !Array.isArray(server.args)) { - throw new Error( - `Invalid MCP server configuration at index ${index}. Required fields: name, command, args[]` - ); - } - - return { - name: server.name, - command: server.command, - args: server.args, - }; - }); -} - export const CONFIG = loadConfig(); diff --git a/packages/mcp-dvm-bridge/src/types.ts b/packages/mcp-dvm-bridge/src/types.ts index 79f37d3..4b11c33 100644 --- a/packages/mcp-dvm-bridge/src/types.ts +++ b/packages/mcp-dvm-bridge/src/types.ts @@ -1,3 +1,26 @@ +export interface NostrConfig { + privateKey: string; + relayUrls: string[]; +} + +export interface MCPConfig { + name: string; + about: string; + clientName: string; + clientVersion: string; + servers: MCPServerConfig[]; +} + +export interface WhitelistConfig { + allowedPubkeys: Set | undefined; +} + +export interface AppConfig { + nostr: NostrConfig; + mcp: MCPConfig; + whitelist: WhitelistConfig; +} + export interface MCPServerConfig { name: string; command: string;