710 lines
24 KiB
JavaScript
710 lines
24 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { jest } from "@jest/globals";
|
|
import { fileURLToPath } from "url";
|
|
|
|
// --- Read REAL supported-models.json data BEFORE mocks ---
|
|
const __filename = fileURLToPath(import.meta.url); // Get current file path
|
|
const __dirname = path.dirname(__filename); // Get current directory
|
|
const realSupportedModelsPath = path.resolve(
|
|
__dirname,
|
|
"../../scripts/modules/supported-models.json"
|
|
);
|
|
let REAL_SUPPORTED_MODELS_CONTENT;
|
|
let REAL_SUPPORTED_MODELS_DATA;
|
|
try {
|
|
REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync(
|
|
realSupportedModelsPath,
|
|
"utf-8"
|
|
);
|
|
REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT);
|
|
} catch (err) {
|
|
console.error(
|
|
"FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json",
|
|
err
|
|
);
|
|
REAL_SUPPORTED_MODELS_CONTENT = "{}"; // Default to empty object on error
|
|
REAL_SUPPORTED_MODELS_DATA = {};
|
|
process.exit(1); // Exit if essential test data can't be loaded
|
|
}
|
|
|
|
// --- Define Mock Function Instances ---
|
|
const mockFindProjectRoot = jest.fn();
|
|
const mockLog = jest.fn();
|
|
const mockIsSilentMode = jest.fn();
|
|
|
|
// --- Mock Dependencies BEFORE importing the module under test ---
|
|
|
|
// Mock the entire 'fs' module
|
|
jest.mock("fs");
|
|
|
|
// Mock the 'utils.js' module using a factory function
|
|
jest.mock("../../scripts/modules/utils.js", () => ({
|
|
__esModule: true, // Indicate it's an ES module mock
|
|
findProjectRoot: mockFindProjectRoot, // Use the mock function instance
|
|
log: mockLog, // Use the mock function instance
|
|
isSilentMode: mockIsSilentMode, // Use the mock function instance
|
|
// Include other necessary exports from utils if config-manager uses them directly
|
|
resolveEnvVariable: jest.fn(), // Example if needed
|
|
}));
|
|
|
|
// DO NOT MOCK 'chalk'
|
|
|
|
// --- Import the module under test AFTER mocks are defined ---
|
|
import * as configManager from "../../scripts/modules/config-manager.js";
|
|
// Import the mocked 'fs' module to allow spying on its functions
|
|
import fsMocked from "fs";
|
|
|
|
// --- Test Data (Keep as is, ensure DEFAULT_CONFIG is accurate) ---
|
|
const MOCK_PROJECT_ROOT = "/mock/project";
|
|
const MOCK_CONFIG_PATH = path.join(MOCK_PROJECT_ROOT, ".taskmasterconfig");
|
|
|
|
// Updated DEFAULT_CONFIG reflecting the implementation
|
|
const DEFAULT_CONFIG = {
|
|
global: {
|
|
logLevel: "info",
|
|
debug: false,
|
|
defaultSubtasks: 5,
|
|
defaultPriority: "medium",
|
|
projectName: "Taskmaster",
|
|
ollamaBaseURL: "http://localhost:11434/api",
|
|
azureBaseURL: "https://your-endpoint.azure.com/",
|
|
},
|
|
models: {
|
|
main: {
|
|
provider: "anthropic",
|
|
modelId: "claude-3-7-sonnet-20250219",
|
|
maxTokens: 64000,
|
|
temperature: 0.2,
|
|
},
|
|
research: {
|
|
provider: "perplexity",
|
|
modelId: "sonar-pro",
|
|
maxTokens: 8700,
|
|
temperature: 0.1,
|
|
},
|
|
fallback: {
|
|
provider: "anthropic",
|
|
modelId: "claude-3-5-sonnet",
|
|
maxTokens: 64000,
|
|
temperature: 0.2,
|
|
},
|
|
},
|
|
account: {
|
|
userId: "1234567890",
|
|
email: "",
|
|
mode: "byok",
|
|
telemetryEnabled: true,
|
|
},
|
|
};
|
|
|
|
// Other test data (VALID_CUSTOM_CONFIG, PARTIAL_CONFIG, INVALID_PROVIDER_CONFIG)
|
|
const VALID_CUSTOM_CONFIG = {
|
|
models: {
|
|
main: {
|
|
provider: "openai",
|
|
modelId: "gpt-4o",
|
|
maxTokens: 4096,
|
|
temperature: 0.5,
|
|
},
|
|
research: {
|
|
provider: "google",
|
|
modelId: "gemini-1.5-pro-latest",
|
|
maxTokens: 8192,
|
|
temperature: 0.3,
|
|
},
|
|
fallback: {
|
|
provider: "anthropic",
|
|
modelId: "claude-3-opus-20240229",
|
|
maxTokens: 100000,
|
|
temperature: 0.4,
|
|
},
|
|
},
|
|
global: {
|
|
logLevel: "debug",
|
|
defaultPriority: "high",
|
|
projectName: "My Custom Project",
|
|
},
|
|
};
|
|
|
|
const PARTIAL_CONFIG = {
|
|
models: {
|
|
main: { provider: "openai", modelId: "gpt-4-turbo" },
|
|
},
|
|
global: {
|
|
projectName: "Partial Project",
|
|
},
|
|
};
|
|
|
|
const INVALID_PROVIDER_CONFIG = {
|
|
models: {
|
|
main: { provider: "invalid-provider", modelId: "some-model" },
|
|
research: {
|
|
provider: "perplexity",
|
|
modelId: "llama-3-sonar-large-32k-online",
|
|
},
|
|
},
|
|
global: {
|
|
logLevel: "warn",
|
|
},
|
|
};
|
|
|
|
// Define spies globally to be restored in afterAll
|
|
let consoleErrorSpy;
|
|
let consoleWarnSpy;
|
|
let consoleLogSpy;
|
|
let fsReadFileSyncSpy;
|
|
let fsWriteFileSyncSpy;
|
|
let fsExistsSyncSpy;
|
|
|
|
beforeAll(() => {
|
|
// Set up console spies
|
|
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Restore all spies
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
// Reset mocks before each test for isolation
|
|
beforeEach(() => {
|
|
// Clear all mock calls and reset implementations between tests
|
|
jest.clearAllMocks();
|
|
// Reset the external mock instances for utils
|
|
mockFindProjectRoot.mockReset();
|
|
mockLog.mockReset();
|
|
mockIsSilentMode.mockReset();
|
|
|
|
// --- Set up spies ON the imported 'fs' mock ---
|
|
fsExistsSyncSpy = jest.spyOn(fsMocked, "existsSync");
|
|
fsReadFileSyncSpy = jest.spyOn(fsMocked, "readFileSync");
|
|
fsWriteFileSyncSpy = jest.spyOn(fsMocked, "writeFileSync");
|
|
|
|
// --- Default Mock Implementations ---
|
|
mockFindProjectRoot.mockReturnValue(MOCK_PROJECT_ROOT); // Default for utils.findProjectRoot
|
|
mockIsSilentMode.mockReturnValue(false); // Default for utils.isSilentMode
|
|
fsExistsSyncSpy.mockReturnValue(true); // Assume files exist by default
|
|
|
|
// Default readFileSync: Return REAL models content, mocked config, or throw error
|
|
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
|
const baseName = path.basename(filePath);
|
|
if (baseName === "supported-models.json") {
|
|
// Return the REAL file content stringified
|
|
return REAL_SUPPORTED_MODELS_CONTENT;
|
|
} else if (filePath === MOCK_CONFIG_PATH) {
|
|
// Still mock the .taskmasterconfig reads
|
|
return JSON.stringify(DEFAULT_CONFIG); // Default behavior
|
|
}
|
|
// Throw for unexpected reads - helps catch errors
|
|
throw new Error(`Unexpected fs.readFileSync call in test: ${filePath}`);
|
|
});
|
|
|
|
// Default writeFileSync: Do nothing, just allow calls
|
|
fsWriteFileSyncSpy.mockImplementation(() => {});
|
|
});
|
|
|
|
// --- Validation Functions ---
|
|
describe("Validation Functions", () => {
|
|
// Tests for validateProvider and validateProviderModelCombination
|
|
test("validateProvider should return true for valid providers", () => {
|
|
expect(configManager.validateProvider("openai")).toBe(true);
|
|
expect(configManager.validateProvider("anthropic")).toBe(true);
|
|
expect(configManager.validateProvider("google")).toBe(true);
|
|
expect(configManager.validateProvider("perplexity")).toBe(true);
|
|
expect(configManager.validateProvider("ollama")).toBe(true);
|
|
expect(configManager.validateProvider("openrouter")).toBe(true);
|
|
});
|
|
|
|
test("validateProvider should return false for invalid providers", () => {
|
|
expect(configManager.validateProvider("invalid-provider")).toBe(false);
|
|
expect(configManager.validateProvider("grok")).toBe(false); // Not in mock map
|
|
expect(configManager.validateProvider("")).toBe(false);
|
|
expect(configManager.validateProvider(null)).toBe(false);
|
|
});
|
|
|
|
test("validateProviderModelCombination should validate known good combinations", () => {
|
|
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
|
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
expect(
|
|
configManager.validateProviderModelCombination("openai", "gpt-4o")
|
|
).toBe(true);
|
|
expect(
|
|
configManager.validateProviderModelCombination(
|
|
"anthropic",
|
|
"claude-3-5-sonnet-20241022"
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
test("validateProviderModelCombination should return false for known bad combinations", () => {
|
|
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
|
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
expect(
|
|
configManager.validateProviderModelCombination(
|
|
"openai",
|
|
"claude-3-opus-20240229"
|
|
)
|
|
).toBe(false);
|
|
});
|
|
|
|
test("validateProviderModelCombination should return true for ollama/openrouter (empty lists in map)", () => {
|
|
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
|
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
expect(
|
|
configManager.validateProviderModelCombination("ollama", "any-model")
|
|
).toBe(false);
|
|
expect(
|
|
configManager.validateProviderModelCombination("openrouter", "any/model")
|
|
).toBe(false);
|
|
});
|
|
|
|
test("validateProviderModelCombination should return true for providers not in map", () => {
|
|
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
|
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
// The implementation returns true if the provider isn't in the map
|
|
expect(
|
|
configManager.validateProviderModelCombination(
|
|
"unknown-provider",
|
|
"some-model"
|
|
)
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
// --- getConfig Tests ---
|
|
describe("getConfig Tests", () => {
|
|
test("should return default config if .taskmasterconfig does not exist", () => {
|
|
// Arrange
|
|
fsExistsSyncSpy.mockReturnValue(false);
|
|
// findProjectRoot mock is set in beforeEach
|
|
|
|
// Act: Call getConfig with explicit root
|
|
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
|
|
|
|
// Assert
|
|
expect(config).toEqual(DEFAULT_CONFIG);
|
|
expect(mockFindProjectRoot).not.toHaveBeenCalled(); // Explicit root provided
|
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
|
expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); // No read if file doesn't exist
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("not found at provided project root")
|
|
);
|
|
});
|
|
|
|
test.skip("should use findProjectRoot and return defaults if file not found", () => {
|
|
// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
|
|
// Arrange
|
|
fsExistsSyncSpy.mockReturnValue(false);
|
|
// findProjectRoot mock is set in beforeEach
|
|
|
|
// Act: Call getConfig without explicit root
|
|
const config = configManager.getConfig(null, true); // Force reload
|
|
|
|
// Assert
|
|
expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
|
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
|
expect(config).toEqual(DEFAULT_CONFIG);
|
|
expect(fsReadFileSyncSpy).not.toHaveBeenCalled();
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("not found at derived root")
|
|
); // Adjusted expected warning
|
|
});
|
|
|
|
test("should read and merge valid config file with defaults", () => {
|
|
// Arrange
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
fsReadFileSyncSpy.mockReturnValue(JSON.stringify(VALID_CUSTOM_CONFIG));
|
|
|
|
// Act
|
|
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
|
|
// Assert
|
|
const expectedMergedConfig = {
|
|
models: {
|
|
main: {
|
|
...DEFAULT_CONFIG.models.main,
|
|
...VALID_CUSTOM_CONFIG.models.main,
|
|
},
|
|
research: {
|
|
...DEFAULT_CONFIG.models.research,
|
|
...VALID_CUSTOM_CONFIG.models.research,
|
|
},
|
|
fallback: {
|
|
...DEFAULT_CONFIG.models.fallback,
|
|
...VALID_CUSTOM_CONFIG.models.fallback,
|
|
},
|
|
},
|
|
global: { ...DEFAULT_CONFIG.global, ...VALID_CUSTOM_CONFIG.global },
|
|
account: { ...DEFAULT_CONFIG.account },
|
|
};
|
|
expect(config).toEqual(expectedMergedConfig);
|
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
|
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, "utf-8");
|
|
});
|
|
|
|
test("should merge defaults for partial config file", () => {
|
|
// Arrange
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
fsReadFileSyncSpy.mockReturnValue(JSON.stringify(PARTIAL_CONFIG));
|
|
|
|
// Act
|
|
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
|
|
// Assert
|
|
const expectedMergedConfig = {
|
|
models: {
|
|
main: { ...DEFAULT_CONFIG.models.main, ...PARTIAL_CONFIG.models.main },
|
|
research: { ...DEFAULT_CONFIG.models.research },
|
|
fallback: { ...DEFAULT_CONFIG.models.fallback },
|
|
},
|
|
global: { ...DEFAULT_CONFIG.global, ...PARTIAL_CONFIG.global },
|
|
account: { ...DEFAULT_CONFIG.account },
|
|
};
|
|
expect(config).toEqual(expectedMergedConfig);
|
|
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, "utf-8");
|
|
});
|
|
|
|
test("should handle JSON parsing error and return defaults", () => {
|
|
// Arrange
|
|
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
|
if (filePath === MOCK_CONFIG_PATH) return "invalid json";
|
|
// Mock models read needed for initial load before parse error
|
|
if (path.basename(filePath) === "supported-models.json") {
|
|
return JSON.stringify({
|
|
anthropic: [{ id: "claude-3-7-sonnet-20250219" }],
|
|
perplexity: [{ id: "sonar-pro" }],
|
|
fallback: [{ id: "claude-3-5-sonnet" }],
|
|
ollama: [],
|
|
openrouter: [],
|
|
});
|
|
}
|
|
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
|
});
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
|
|
// Act
|
|
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
|
|
// Assert
|
|
expect(config).toEqual(DEFAULT_CONFIG);
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Error reading or parsing")
|
|
);
|
|
});
|
|
|
|
test("should handle file read error and return defaults", () => {
|
|
// Arrange
|
|
const readError = new Error("Permission denied");
|
|
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
|
if (filePath === MOCK_CONFIG_PATH) throw readError;
|
|
// Mock models read needed for initial load before read error
|
|
if (path.basename(filePath) === "supported-models.json") {
|
|
return JSON.stringify({
|
|
anthropic: [{ id: "claude-3-7-sonnet-20250219" }],
|
|
perplexity: [{ id: "sonar-pro" }],
|
|
fallback: [{ id: "claude-3-5-sonnet" }],
|
|
ollama: [],
|
|
openrouter: [],
|
|
});
|
|
}
|
|
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
|
});
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
|
|
// Act
|
|
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
|
|
// Assert
|
|
expect(config).toEqual(DEFAULT_CONFIG);
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(`Permission denied. Using default configuration.`)
|
|
);
|
|
});
|
|
|
|
test("should validate provider and fallback to default if invalid", () => {
|
|
// Arrange
|
|
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
|
if (filePath === MOCK_CONFIG_PATH)
|
|
return JSON.stringify(INVALID_PROVIDER_CONFIG);
|
|
if (path.basename(filePath) === "supported-models.json") {
|
|
return JSON.stringify({
|
|
perplexity: [{ id: "llama-3-sonar-large-32k-online" }],
|
|
anthropic: [
|
|
{ id: "claude-3-7-sonnet-20250219" },
|
|
{ id: "claude-3-5-sonnet" },
|
|
],
|
|
ollama: [],
|
|
openrouter: [],
|
|
});
|
|
}
|
|
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
|
});
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
|
|
// Act
|
|
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
|
|
|
// Assert
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(
|
|
'Warning: Invalid main provider "invalid-provider"'
|
|
)
|
|
);
|
|
const expectedMergedConfig = {
|
|
models: {
|
|
main: { ...DEFAULT_CONFIG.models.main },
|
|
research: {
|
|
...DEFAULT_CONFIG.models.research,
|
|
...INVALID_PROVIDER_CONFIG.models.research,
|
|
},
|
|
fallback: { ...DEFAULT_CONFIG.models.fallback },
|
|
},
|
|
global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global },
|
|
account: { ...DEFAULT_CONFIG.account },
|
|
};
|
|
expect(config).toEqual(expectedMergedConfig);
|
|
});
|
|
});
|
|
|
|
// --- writeConfig Tests ---
|
|
describe("writeConfig", () => {
|
|
test("should write valid config to file", () => {
|
|
// Arrange (Default mocks are sufficient)
|
|
// findProjectRoot mock set in beforeEach
|
|
fsWriteFileSyncSpy.mockImplementation(() => {}); // Ensure it doesn't throw
|
|
|
|
// Act
|
|
const success = configManager.writeConfig(
|
|
VALID_CUSTOM_CONFIG,
|
|
MOCK_PROJECT_ROOT
|
|
);
|
|
|
|
// Assert
|
|
expect(success).toBe(true);
|
|
expect(fsWriteFileSyncSpy).toHaveBeenCalledWith(
|
|
MOCK_CONFIG_PATH,
|
|
JSON.stringify(VALID_CUSTOM_CONFIG, null, 2) // writeConfig stringifies
|
|
);
|
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should return false and log error if write fails", () => {
|
|
// Arrange
|
|
const mockWriteError = new Error("Disk full");
|
|
fsWriteFileSyncSpy.mockImplementation(() => {
|
|
throw mockWriteError;
|
|
});
|
|
// findProjectRoot mock set in beforeEach
|
|
|
|
// Act
|
|
const success = configManager.writeConfig(
|
|
VALID_CUSTOM_CONFIG,
|
|
MOCK_PROJECT_ROOT
|
|
);
|
|
|
|
// Assert
|
|
expect(success).toBe(false);
|
|
expect(fsWriteFileSyncSpy).toHaveBeenCalled();
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(`Disk full`)
|
|
);
|
|
});
|
|
|
|
test.skip("should return false if project root cannot be determined", () => {
|
|
// TODO: Fix mock interaction or function logic, returns true unexpectedly in test
|
|
// Arrange: Override mock for this specific test
|
|
mockFindProjectRoot.mockReturnValue(null);
|
|
|
|
// Act: Call without explicit root
|
|
const success = configManager.writeConfig(VALID_CUSTOM_CONFIG);
|
|
|
|
// Assert
|
|
expect(success).toBe(false); // Function should return false if root is null
|
|
expect(mockFindProjectRoot).toHaveBeenCalled();
|
|
expect(fsWriteFileSyncSpy).not.toHaveBeenCalled();
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Could not determine project root")
|
|
);
|
|
});
|
|
});
|
|
|
|
// --- Getter Functions ---
|
|
describe("Getter Functions", () => {
|
|
test("getMainProvider should return provider from config", () => {
|
|
// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
|
|
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
|
if (filePath === MOCK_CONFIG_PATH)
|
|
return JSON.stringify(VALID_CUSTOM_CONFIG);
|
|
if (path.basename(filePath) === "supported-models.json") {
|
|
return JSON.stringify({
|
|
openai: [{ id: "gpt-4o" }],
|
|
google: [{ id: "gemini-1.5-pro-latest" }],
|
|
anthropic: [
|
|
{ id: "claude-3-opus-20240229" },
|
|
{ id: "claude-3-7-sonnet-20250219" },
|
|
{ id: "claude-3-5-sonnet" },
|
|
],
|
|
perplexity: [{ id: "sonar-pro" }],
|
|
ollama: [],
|
|
openrouter: [],
|
|
}); // Added perplexity
|
|
}
|
|
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
|
});
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
|
|
// Act
|
|
const provider = configManager.getMainProvider(MOCK_PROJECT_ROOT);
|
|
|
|
// Assert
|
|
expect(provider).toBe(VALID_CUSTOM_CONFIG.models.main.provider);
|
|
});
|
|
|
|
test("getLogLevel should return logLevel from config", () => {
|
|
// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
|
|
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
|
if (filePath === MOCK_CONFIG_PATH)
|
|
return JSON.stringify(VALID_CUSTOM_CONFIG);
|
|
if (path.basename(filePath) === "supported-models.json") {
|
|
// Provide enough mock model data for validation within getConfig
|
|
return JSON.stringify({
|
|
openai: [{ id: "gpt-4o" }],
|
|
google: [{ id: "gemini-1.5-pro-latest" }],
|
|
anthropic: [
|
|
{ id: "claude-3-opus-20240229" },
|
|
{ id: "claude-3-7-sonnet-20250219" },
|
|
{ id: "claude-3-5-sonnet" },
|
|
],
|
|
perplexity: [{ id: "sonar-pro" }],
|
|
ollama: [],
|
|
openrouter: [],
|
|
});
|
|
}
|
|
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
|
});
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
|
|
// Act
|
|
const logLevel = configManager.getLogLevel(MOCK_PROJECT_ROOT);
|
|
|
|
// Assert
|
|
expect(logLevel).toBe(VALID_CUSTOM_CONFIG.global.logLevel);
|
|
});
|
|
|
|
// Add more tests for other getters (getResearchProvider, getProjectName, etc.)
|
|
});
|
|
|
|
// --- isConfigFilePresent Tests ---
|
|
describe("isConfigFilePresent", () => {
|
|
test("should return true if config file exists", () => {
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(true);
|
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
|
});
|
|
|
|
test("should return false if config file does not exist", () => {
|
|
fsExistsSyncSpy.mockReturnValue(false);
|
|
// findProjectRoot mock set in beforeEach
|
|
expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(false);
|
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
|
});
|
|
|
|
test.skip("should use findProjectRoot if explicitRoot is not provided", () => {
|
|
// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
// findProjectRoot mock set in beforeEach
|
|
expect(configManager.isConfigFilePresent()).toBe(true);
|
|
expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
|
|
});
|
|
});
|
|
|
|
// --- getAllProviders Tests ---
|
|
describe("getAllProviders", () => {
|
|
test("should return list of providers from supported-models.json", () => {
|
|
// Arrange: Ensure config is loaded with real data
|
|
configManager.getConfig(null, true); // Force load using the mock that returns real data
|
|
|
|
// Act
|
|
const providers = configManager.getAllProviders();
|
|
// Assert
|
|
// Assert against the actual keys in the REAL loaded data
|
|
const expectedProviders = Object.keys(REAL_SUPPORTED_MODELS_DATA);
|
|
expect(providers).toEqual(expect.arrayContaining(expectedProviders));
|
|
expect(providers.length).toBe(expectedProviders.length);
|
|
});
|
|
});
|
|
|
|
// Add tests for getParametersForRole if needed
|
|
|
|
// Note: Tests for setMainModel, setResearchModel were removed as the functions were removed in the implementation.
|
|
// If similar setter functions exist, add tests for them following the writeConfig pattern.
|
|
|
|
describe("ensureConfigFileExists", () => {
|
|
it("should create .taskmasterconfig file if it doesn't exist", () => {
|
|
// Override the default fs mocks for this test
|
|
fsExistsSyncSpy.mockReturnValue(false);
|
|
fsWriteFileSyncSpy.mockImplementation(() => {}); // Success, no throw
|
|
|
|
const result = configManager.ensureConfigFileExists(MOCK_PROJECT_ROOT);
|
|
|
|
expect(result).toBe(true);
|
|
expect(fsWriteFileSyncSpy).toHaveBeenCalledWith(
|
|
MOCK_CONFIG_PATH,
|
|
JSON.stringify(DEFAULT_CONFIG, null, 2)
|
|
);
|
|
});
|
|
|
|
it("should return true if .taskmasterconfig file already exists", () => {
|
|
// Mock file exists (this is the default, but let's be explicit)
|
|
fsExistsSyncSpy.mockReturnValue(true);
|
|
|
|
const result = configManager.ensureConfigFileExists(MOCK_PROJECT_ROOT);
|
|
|
|
expect(result).toBe(true);
|
|
expect(fsWriteFileSyncSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return false if project root cannot be determined", () => {
|
|
// Mock findProjectRoot to return null (no project root found)
|
|
mockFindProjectRoot.mockReturnValue(null);
|
|
|
|
// Mock file doesn't exist so function tries to create it (and needs project root)
|
|
fsExistsSyncSpy.mockReturnValue(false);
|
|
|
|
// Clear any previous calls to consoleWarnSpy to get clean test results
|
|
consoleWarnSpy.mockClear();
|
|
|
|
const result = configManager.ensureConfigFileExists(); // No explicitRoot provided
|
|
|
|
expect(result).toBe(false);
|
|
expect(fsWriteFileSyncSpy).not.toHaveBeenCalled();
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(
|
|
"Warning: Could not determine project root for config file creation."
|
|
)
|
|
);
|
|
});
|
|
|
|
it("should handle write errors gracefully", () => {
|
|
// Mock file doesn't exist
|
|
fsExistsSyncSpy.mockReturnValue(false);
|
|
// Mock write operation to throw error
|
|
fsWriteFileSyncSpy.mockImplementation(() => {
|
|
throw new Error("Permission denied");
|
|
});
|
|
|
|
const result = configManager.ensureConfigFileExists(MOCK_PROJECT_ROOT);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|