import { describe, it, expect, beforeEach, vi } from "vitest"; import { ConfigService } from "@nestjs/config"; import { ValkeyService } from "./valkey.service"; import type { TaskState, AgentState, OrchestratorEvent } from "./types"; // Create mock client methods that will be shared const mockClient = { getTaskState: vi.fn(), setTaskState: vi.fn(), deleteTaskState: vi.fn(), updateTaskStatus: vi.fn(), listTasks: vi.fn(), getAgentState: vi.fn(), setAgentState: vi.fn(), deleteAgentState: vi.fn(), updateAgentStatus: vi.fn(), listAgents: vi.fn(), publishEvent: vi.fn(), subscribeToEvents: vi.fn(), disconnect: vi.fn(), }; // Mock ValkeyClient before importing vi.mock("./valkey.client", () => { return { ValkeyClient: class { constructor() { return mockClient; } }, }; }); describe("ValkeyService", () => { let service: ValkeyService; let mockConfigService: ConfigService; beforeEach(() => { // Clear all mock calls vi.clearAllMocks(); // Create mock config service mockConfigService = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.valkey.host": "localhost", "orchestrator.valkey.port": 6379, }; return config[key] ?? defaultValue; }), } as unknown as ConfigService; // Create service directly service = new ValkeyService(mockConfigService); }); describe("Initialization", () => { it("should be defined", () => { expect(service).toBeDefined(); }); it("should create ValkeyClient with config from ConfigService", () => { expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.valkey.host", "localhost"); expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.valkey.port", 6379); }); it("should use password from config if provided", () => { const configWithPassword = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.valkey.host": "localhost", "orchestrator.valkey.port": 6379, "orchestrator.valkey.password": "secret", }; return config[key] ?? defaultValue; }), } as unknown as ConfigService; const serviceWithPassword = new ValkeyService(configWithPassword); expect(configWithPassword.get).toHaveBeenCalledWith("orchestrator.valkey.password"); }); }); describe("Security Warnings (SEC-ORCH-15)", () => { it("should check NODE_ENV when VALKEY_PASSWORD not set in production", () => { const configNoPassword = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.valkey.host": "localhost", "orchestrator.valkey.port": 6379, NODE_ENV: "production", }; return config[key] ?? defaultValue; }), } as unknown as ConfigService; // Create a service to trigger the warning const testService = new ValkeyService(configNoPassword); expect(testService).toBeDefined(); // Verify NODE_ENV was checked (warning path was taken) expect(configNoPassword.get).toHaveBeenCalledWith("NODE_ENV", "development"); }); it("should check NODE_ENV when VALKEY_PASSWORD not set in development", () => { const configNoPasswordDev = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.valkey.host": "localhost", "orchestrator.valkey.port": 6379, NODE_ENV: "development", }; return config[key] ?? defaultValue; }), } as unknown as ConfigService; const testService = new ValkeyService(configNoPasswordDev); expect(testService).toBeDefined(); // Verify NODE_ENV was checked (warning path was taken) expect(configNoPasswordDev.get).toHaveBeenCalledWith("NODE_ENV", "development"); }); it("should not check NODE_ENV when VALKEY_PASSWORD is configured", () => { const configWithPassword = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.valkey.host": "localhost", "orchestrator.valkey.port": 6379, "orchestrator.valkey.password": "secure-password", NODE_ENV: "production", }; return config[key] ?? defaultValue; }), } as unknown as ConfigService; const testService = new ValkeyService(configWithPassword); expect(testService).toBeDefined(); // NODE_ENV should NOT be checked when password is set (warning path not taken) expect(configWithPassword.get).not.toHaveBeenCalledWith("NODE_ENV", "development"); }); it("should default to development environment when NODE_ENV not set", () => { const configNoEnv = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.valkey.host": "localhost", "orchestrator.valkey.port": 6379, }; // Return default value for NODE_ENV (simulating undefined env var) if (key === "NODE_ENV") { return defaultValue; } return config[key] ?? defaultValue; }), } as unknown as ConfigService; const testService = new ValkeyService(configNoEnv); expect(testService).toBeDefined(); // Should have checked NODE_ENV with default "development" expect(configNoEnv.get).toHaveBeenCalledWith("NODE_ENV", "development"); }); }); describe("Lifecycle", () => { it("should disconnect on module destroy", async () => { mockClient.disconnect.mockResolvedValue(undefined); await service.onModuleDestroy(); expect(mockClient.disconnect).toHaveBeenCalled(); }); }); describe("Task State Management", () => { const mockTaskState: TaskState = { taskId: "task-123", status: "pending", context: { repository: "https://github.com/example/repo", branch: "main", workItems: ["item-1"], }, createdAt: "2026-02-02T10:00:00Z", updatedAt: "2026-02-02T10:00:00Z", }; it("should get task state", async () => { mockClient.getTaskState.mockResolvedValue(mockTaskState); const result = await service.getTaskState("task-123"); expect(mockClient.getTaskState).toHaveBeenCalledWith("task-123"); expect(result).toEqual(mockTaskState); }); it("should set task state", async () => { mockClient.setTaskState.mockResolvedValue(undefined); await service.setTaskState(mockTaskState); expect(mockClient.setTaskState).toHaveBeenCalledWith(mockTaskState); }); it("should delete task state", async () => { mockClient.deleteTaskState.mockResolvedValue(undefined); await service.deleteTaskState("task-123"); expect(mockClient.deleteTaskState).toHaveBeenCalledWith("task-123"); }); it("should update task status", async () => { const updatedTask = { ...mockTaskState, status: "assigned" as const }; mockClient.updateTaskStatus.mockResolvedValue(updatedTask); const result = await service.updateTaskStatus("task-123", "assigned", "agent-456"); expect(mockClient.updateTaskStatus).toHaveBeenCalledWith( "task-123", "assigned", "agent-456", undefined ); expect(result).toEqual(updatedTask); }); it("should list all tasks", async () => { const tasks = [mockTaskState]; mockClient.listTasks.mockResolvedValue(tasks); const result = await service.listTasks(); expect(mockClient.listTasks).toHaveBeenCalled(); expect(result).toEqual(tasks); }); }); describe("Agent State Management", () => { const mockAgentState: AgentState = { agentId: "agent-456", status: "spawning", taskId: "task-123", }; it("should get agent state", async () => { mockClient.getAgentState.mockResolvedValue(mockAgentState); const result = await service.getAgentState("agent-456"); expect(mockClient.getAgentState).toHaveBeenCalledWith("agent-456"); expect(result).toEqual(mockAgentState); }); it("should set agent state", async () => { mockClient.setAgentState.mockResolvedValue(undefined); await service.setAgentState(mockAgentState); expect(mockClient.setAgentState).toHaveBeenCalledWith(mockAgentState); }); it("should delete agent state", async () => { mockClient.deleteAgentState.mockResolvedValue(undefined); await service.deleteAgentState("agent-456"); expect(mockClient.deleteAgentState).toHaveBeenCalledWith("agent-456"); }); it("should update agent status", async () => { const updatedAgent = { ...mockAgentState, status: "running" as const }; mockClient.updateAgentStatus.mockResolvedValue(updatedAgent); const result = await service.updateAgentStatus("agent-456", "running"); expect(mockClient.updateAgentStatus).toHaveBeenCalledWith("agent-456", "running", undefined); expect(result).toEqual(updatedAgent); }); it("should list all agents", async () => { const agents = [mockAgentState]; mockClient.listAgents.mockResolvedValue(agents); const result = await service.listAgents(); expect(mockClient.listAgents).toHaveBeenCalled(); expect(result).toEqual(agents); }); }); describe("Event Pub/Sub", () => { const mockEvent: OrchestratorEvent = { type: "agent.spawned", agentId: "agent-456", taskId: "task-123", timestamp: "2026-02-02T10:00:00Z", }; it("should publish events", async () => { mockClient.publishEvent.mockResolvedValue(undefined); await service.publishEvent(mockEvent); expect(mockClient.publishEvent).toHaveBeenCalledWith(mockEvent); }); it("should subscribe to events", async () => { mockClient.subscribeToEvents.mockResolvedValue(undefined); const handler = vi.fn(); await service.subscribeToEvents(handler); expect(mockClient.subscribeToEvents).toHaveBeenCalledWith(handler, undefined); }); it("should subscribe to events with error handler", async () => { mockClient.subscribeToEvents.mockResolvedValue(undefined); const handler = vi.fn(); const errorHandler = vi.fn(); await service.subscribeToEvents(handler, errorHandler); expect(mockClient.subscribeToEvents).toHaveBeenCalledWith(handler, errorHandler); }); }); describe("Convenience Methods", () => { it("should create task state with timestamps", async () => { mockClient.setTaskState.mockResolvedValue(undefined); const context = { repository: "https://github.com/example/repo", branch: "main", workItems: ["item-1"], }; await service.createTask("task-123", context); expect(mockClient.setTaskState).toHaveBeenCalledWith({ taskId: "task-123", status: "pending", context, createdAt: expect.any(String), updatedAt: expect.any(String), }); }); it("should create agent state", async () => { mockClient.setAgentState.mockResolvedValue(undefined); await service.createAgent("agent-456", "task-123"); expect(mockClient.setAgentState).toHaveBeenCalledWith({ agentId: "agent-456", status: "spawning", taskId: "task-123", }); }); }); });