BMAD-METHOD/bmad-mcp/tests/providers/resourceProvider.test.ts

186 lines
5.9 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { promises as fs } from "fs";
import { join } from "path";
import { ResourceProvider } from "../../src/providers/resourceProvider.js";
import type { BaseResource, ResourceConfig } from "../../src/types/index.js";
// Mock fs module
vi.mock("fs", () => ({
promises: {
readdir: vi.fn(),
readFile: vi.fn(),
},
}));
const mockFs = vi.mocked(fs);
interface TestResource extends BaseResource {
testProperty?: string;
}
class TestResourceProvider extends ResourceProvider<TestResource> {
constructor(config: ResourceConfig, baseDir?: string) {
super(config, baseDir);
}
protected enhanceResource(baseResource: BaseResource): TestResource {
return {
...baseResource,
testProperty: "enhanced",
};
}
}
describe("ResourceProvider", () => {
let provider: TestResourceProvider;
const mockBaseDir = "/test/project";
const resourcesDir = join(mockBaseDir, "bmad-core/test-resources");
const testConfig: ResourceConfig = {
resourceType: "TestResource",
uriScheme: "test://resources",
directory: "test-resources",
nameSuffix: "-test",
description: "Test resources",
supportedExtensions: [".md", ".txt"],
};
beforeEach(() => {
provider = new TestResourceProvider(testConfig, mockBaseDir);
vi.clearAllMocks();
});
afterEach(() => {
vi.resetAllMocks();
});
describe("discoverResources", () => {
it("should return empty array when resources directory does not exist", async () => {
mockFs.readdir.mockRejectedValue({ code: "ENOENT" });
const resources = await provider.discoverResources();
expect(resources).toEqual([]);
expect(mockFs.readdir).toHaveBeenCalledWith(resourcesDir, { withFileTypes: true });
});
it("should discover resource files with supported extensions", async () => {
const mockDirents = [
{ name: "resource1.md", isDirectory: () => false, isFile: () => true },
{ name: "resource2.txt", isDirectory: () => false, isFile: () => true },
{ name: "ignored.json", isDirectory: () => false, isFile: () => true }, // Should be ignored
{ name: "resource3.md", isDirectory: () => false, isFile: () => true },
];
mockFs.readdir.mockResolvedValue(mockDirents as any);
const resources = await provider.discoverResources();
expect(resources).toHaveLength(3);
expect(resources[0]).toEqual({
id: "resource1",
name: "resource1",
path: join(resourcesDir, "resource1.md"),
extension: "md",
testProperty: "enhanced",
});
expect(resources[1]).toEqual({
id: "resource2",
name: "resource2",
path: join(resourcesDir, "resource2.txt"),
extension: "txt",
testProperty: "enhanced",
});
});
});
describe("getResource", () => {
beforeEach(() => {
const mockDirents = [
{ name: "test-resource.md", isDirectory: () => false, isFile: () => true },
];
mockFs.readdir.mockResolvedValue(mockDirents as any);
});
it("should load resource content", async () => {
const mockContent = "# Test Resource\n\nThis is test content.";
mockFs.readFile.mockResolvedValue(mockContent);
const resource = await provider.getResource("test-resource");
expect(resource.name).toBe("test-resource");
expect(resource.content).toBe(mockContent);
expect(resource.testProperty).toBe("enhanced");
});
it("should cache loaded resources", async () => {
mockFs.readFile.mockResolvedValue("Content");
// First call
await provider.getResource("test-resource");
// Second call
await provider.getResource("test-resource");
expect(mockFs.readFile).toHaveBeenCalledTimes(1);
});
});
describe("getAllResources", () => {
beforeEach(() => {
const mockDirents = [
{ name: "resource1.md", isDirectory: () => false, isFile: () => true },
{ name: "resource2.md", isDirectory: () => false, isFile: () => true },
];
mockFs.readdir.mockResolvedValue(mockDirents as any);
});
it("should return all resources with content loaded", async () => {
mockFs.readFile.mockResolvedValueOnce("Content 1").mockResolvedValueOnce("Content 2");
const resources = await provider.getAllResources();
expect(resources).toHaveLength(2);
expect(resources[0].content).toBe("Content 1");
expect(resources[1].content).toBe("Content 2");
});
it("should include resources without content when file read fails", async () => {
mockFs.readFile
.mockResolvedValueOnce("Content 1")
.mockRejectedValueOnce(new Error("File read error"));
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const resources = await provider.getAllResources();
expect(resources).toHaveLength(2);
expect(resources[0].content).toBe("Content 1");
expect(resources[1].content).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
"Failed to load resource resource2:",
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
describe("supported extensions", () => {
it("should only discover files with configured extensions", async () => {
const mockDirents = [
{ name: "file.md", isDirectory: () => false, isFile: () => true },
{ name: "file.txt", isDirectory: () => false, isFile: () => true },
{ name: "file.json", isDirectory: () => false, isFile: () => true }, // Not in configured extensions
{ name: "file.doc", isDirectory: () => false, isFile: () => true }, // Not supported
];
mockFs.readdir.mockResolvedValue(mockDirents as any);
const resources = await provider.discoverResources();
expect(resources).toHaveLength(2);
expect(resources.map((r) => r.extension)).toEqual(["md", "txt"]);
});
});
});