Coordination on x.ruv.io was one flat topic: every member read every message,
and there was no way to keep a stream separate — or confidential — without
standing up another relay tenant.
Channels are one tag on the existing events, in two visibilities:
public c=pub:<name> plaintext, any relay member reads it
private c=prv:<16 hex> NIP-44 v2 ciphertext, k=enc hides the message type
The private channel key is generated client-side and cached at
~/.ruflo/channels.json (0600). Access is granted by sealing that key to a
member's pubkey over ECDH and publishing it as a ChannelGrant only they can
open. The gateway holds no channel keys and cannot decrypt — a hosted gateway
that encrypted on your behalf would be a custodian of every private channel on
the service, which is the thing worth not building.
Not hidden, and said so in the ADR: the relay sees that a channel exists, its
opaque id, who published and when. There is no revocation either; removing
someone means rotating the channel.
The channel tag is 'c', not the obvious 'h'. buzz-relay already implements
NIP-29-style server-side group membership on : an h-tagged event publishes
fine and the matching REQ comes back CLOSED "restricted: not a channel member",
so the author cannot read back their own message. Measured every other
single-letter tag against the live relay — c/d/g/l/m/r/x/y/z are all indexed and
unrestricted.
Surfaces: gateway channel_list / channel_sync / channel_publish (public only,
admin-gated) + ruv://swarm/channels; client x_federation_channel_{create,grant,
accept,publish,read,list} and 'ruflo federation channel --action ...'.
Verified: gateway 12/12, CLI 11/11, and a live two-key E2E on wss://relay.ruv.io
— grantee reads the plaintext, an outsider key cannot decrypt, the ciphertext
does not contain the secret, and the gateway's own channel_sync returns
encrypted:true. Deployed as ruflo-x-gateway-00008-s6n.
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
CLI Module Tests
This directory contains comprehensive tests for the V3 CLI module using Vitest.
Test Files
1. cli.test.ts (~15+ tests)
Tests for the main CLI class covering:
- Version command (
--version,-V) - Help output (
--help,-h) - Command parsing (long flags, short flags, equals syntax)
- Positional arguments
- Boolean flags and negation
- Global flags (
--quiet,--format,--config,--no-color) - Error handling (unknown commands, missing options)
- Subcommand execution and aliases
- Exit codes
2. mcp-client.test.ts (~10+ tests)
Tests for MCP tool invocation:
callMCPTool()- Tool execution with various inputsgetToolMetadata()- Metadata retrievallistMCPTools()- Tool listing and filtering by categoryhasTool()- Tool existence checksgetToolCategories()- Category enumerationvalidateToolInput()- Input validation against schemasMCPClientError- Custom error handling
3. commands.test.ts (~48+ tests)
Tests for all CLI commands:
Agent Commands (spawn, list, status, stop, metrics):
- Spawning agents with various configurations
- Listing agents with filters
- Getting agent status and metrics
- Stopping agents (graceful/force)
- Agent performance metrics
Swarm Commands (init, start, status, stop, scale, coordinate):
- Initializing swarms with different topologies
- Starting swarm execution with objectives
- Checking swarm status
- Stopping and scaling swarms
- V3 15-agent coordination structure
Memory Commands (store, retrieve, search, list, delete, stats, configure):
- Storing data in memory (with/without vectors)
- Retrieving data by key
- Semantic/vector search
- Listing memory entries
- Deleting entries
- Viewing statistics
- Backend configuration
Config Commands (init, get, set, providers, reset, export, import):
- Initializing configuration
- Getting/setting config values
- Managing AI providers
- Resetting to defaults
- Exporting/importing configuration
Mocking Strategy
MCP Tools
All MCP tools are mocked at the module level to prevent actual tool execution:
agent-tools.js- Mocked agent operationsswarm-tools.js- Mocked swarm coordinationmemory-tools.js- Mocked memory operationsconfig-tools.js- Mocked configuration
Output
The output module is fully mocked to capture formatted output without console pollution.
Prompts
Interactive prompts (select, confirm, input, multiSelect) are mocked to return default values for non-interactive testing.
Process
process.exit() is mocked to throw errors instead of terminating the test process.
Running Tests
# Run all CLI tests
npm test -- v3/@claude-flow/cli/__tests__/
# Run specific test file
npm test -- v3/@claude-flow/cli/__tests__/cli.test.ts
# Run with coverage
npm test -- v3/@claude-flow/cli/__tests__/ --coverage
# Run in watch mode
npm test -- v3/@claude-flow/cli/__tests__/ --watch
Test Coverage Goals
- Statements: >80%
- Branches: >75%
- Functions: >80%
- Lines: >80%
Key Testing Patterns
1. Command Execution
const result = await command.action!(ctx);
expect(result.success).toBe(true);
expect(result.data).toHaveProperty('expectedField');
2. Flag Parsing
ctx.flags = { myFlag: 'value', _: [] };
await command.action!(ctx);
// Assertions in action callback
3. Error Handling
try {
await cli.run(['invalid-command']);
} catch (e) {
expect((e as Error).message).toContain('process.exit');
}
4. Output Validation
const output = consoleOutput.join('');
expect(output).toContain('Expected text');
Notes
- All tests are non-interactive (
interactive: false) - Console output is captured for verification
- Process exits are converted to exceptions
- MCP client is fully isolated from actual MCP server
- Tests use unique command names to avoid conflicts