import { vi, expect, type Mock } from "vitest"; import type { BaseCommandDependencies, CommandDependencies, CommandIO, CommandPaths, } from "../../../src/cli/commands/types"; import type { AppService } from "../../../src/services/app"; /** * Captured CLI IO buffers used by tests. */ export type MockIOCapture = { out: string[]; err: string[]; verbose: string[]; }; /** * Spy functions for each IO channel. */ export type MockIOSpies = { out: Mock; err: Mock; verbose: Mock; }; /** * Complete mock IO result including the IO adapter, capture buffers, and spies. */ export type MockIO = { io: CommandIO; capture: MockIOCapture; spies: MockIOSpies; }; /** * Defines an expected log message for assertion. * At least one of out, err, or verbose should be specified. */ export type LogExpectation = { /** Expected substring (or exact match if exact=true) in io.out */ out?: string; /** Expected substring (or exact match if exact=true) in io.err */ err?: string; /** Expected substring (or exact match if exact=true) in io.verbose */ verbose?: string; /** If true, match the string exactly instead of using contains (default: false) */ exact?: boolean; }; /** * Creates a command IO adapter that records every message using vi.fn() spies. * This enables vitest's built-in matchers like toHaveBeenCalledWith. */ export const createMockIO = (): MockIO => { const capture: MockIOCapture = { out: [], err: [], verbose: [], }; const outSpy = vi.fn((message: string) => { capture.out.push(message); }); const errSpy = vi.fn((message: string) => { capture.err.push(message); }); const verboseSpy = vi.fn((message: string) => { capture.verbose.push(message); }); const io: CommandIO = { out: outSpy, err: errSpy, verbose: verboseSpy, }; return { io, capture, spies: { out: outSpy, err: errSpy, verbose: verboseSpy, }, }; }; /** * Asserts that the expected log messages were printed to the appropriate IO channels. * @param spies - The mock IO spies from createMockIO * @param logs - Array of log expectations to validate */ export const expectLogs = (spies: MockIOSpies, logs: LogExpectation[]): void => { for (const log of logs) { if (log.out !== undefined) { if (log.exact) { expect(spies.out).toHaveBeenCalledWith(log.out); } else { expect(spies.out).toHaveBeenCalledWith(expect.stringContaining(log.out)); } } if (log.err !== undefined) { if (log.exact) { expect(spies.err).toHaveBeenCalledWith(log.err); } else { expect(spies.err).toHaveBeenCalledWith(expect.stringContaining(log.err)); } } if (log.verbose !== undefined) { if (log.exact) { expect(spies.verbose).toHaveBeenCalledWith(log.verbose); } else { expect(spies.verbose).toHaveBeenCalledWith(expect.stringContaining(log.verbose)); } } } }; /** * Creates mock paths for testing. * @param tempDir - Optional temp directory to use as base for all paths */ export const createMockPaths = (tempDir?: string): CommandPaths => { const base = tempDir ?? "/tmp/xo-cli-test"; return { mnemonicsDir: base, dataDir: base, walletConfigPath: `${base}/.wallet`, workingDir: base, }; }; /** * Creates base command dependencies for commands that do not require the app. * @param io - Command IO adapter * @param paths - Optional custom paths (defaults to mock paths) */ export const createBaseCommandDeps = ( io: CommandIO, paths?: CommandPaths, ): BaseCommandDependencies => ({ io, paths: paths ?? createMockPaths(), }); /** * Creates command dependencies for app-backed command handlers. * @param app - App service instance * @param io - Command IO adapter * @param paths - Optional custom paths (defaults to mock paths) */ export const createCommandDeps = ( app: AppService, io: CommandIO, paths?: CommandPaths, ): CommandDependencies => ({ app, io, paths: paths ?? createMockPaths(), });