// src/services/compose-discovery.test.ts
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ComposeProjectCache } from "./compose-cache.js";
import { ComposeDiscovery } from "./compose-discovery.js";
import type { ComposeScanner } from "./compose-scanner.js";
import type { IComposeProjectLister } from "./interfaces.js";
describe("ComposeDiscovery", () => {
const mockProjectLister: IComposeProjectLister = {
listComposeProjects: vi.fn(),
};
const mockCache = {
load: vi.fn(),
save: vi.fn(),
getProject: vi.fn(),
updateProject: vi.fn(),
removeProject: vi.fn(),
} as ComposeProjectCache;
const mockScanner = {
findComposeFiles: vi.fn(),
extractProjectName: vi.fn(),
parseComposeName: vi.fn(),
batchParseComposeNames: vi.fn(),
} as unknown as ComposeScanner;
let discovery: ComposeDiscovery;
beforeEach(() => {
vi.clearAllMocks();
discovery = new ComposeDiscovery(mockProjectLister, mockCache, mockScanner);
});
it("should return cached path if found", async () => {
mockCache.getProject.mockResolvedValue({
path: "/compose/plex/docker-compose.yaml",
name: "plex",
discoveredFrom: "docker-ls",
lastSeen: new Date().toISOString(),
});
const host = { name: "test", host: "localhost", protocol: "ssh" as const };
const result = await discovery.resolveProjectPath(host, "plex");
expect(result).toBe("/compose/plex/docker-compose.yaml");
expect(mockProjectLister.listComposeProjects).not.toHaveBeenCalled();
});
it("should discover from docker compose ls if not cached", async () => {
mockCache.getProject.mockResolvedValue(undefined);
vi.mocked(mockProjectLister.listComposeProjects).mockResolvedValue([
{
name: "plex",
status: "running",
configFiles: ["/compose/plex/docker-compose.yaml"],
services: [],
},
]);
const host = { name: "test", host: "localhost", protocol: "ssh" as const };
const result = await discovery.resolveProjectPath(host, "plex");
expect(result).toBe("/compose/plex/docker-compose.yaml");
expect(mockCache.updateProject).toHaveBeenCalledWith(
"test",
"plex",
expect.objectContaining({
path: "/compose/plex/docker-compose.yaml",
discoveredFrom: "docker-ls",
})
);
});
it("should scan filesystem if not found in docker ls", async () => {
mockCache.getProject.mockResolvedValue(undefined);
mockCache.load.mockResolvedValue({
lastScan: new Date().toISOString(),
searchPaths: ["/compose"],
projects: {},
});
vi.mocked(mockProjectLister.listComposeProjects).mockResolvedValue([]);
mockScanner.findComposeFiles.mockResolvedValue(["/compose/plex/docker-compose.yaml"]);
mockScanner.extractProjectName.mockReturnValue("plex");
mockScanner.batchParseComposeNames.mockResolvedValue(
new Map([["/compose/plex/docker-compose.yaml", null]])
);
const host = { name: "test", host: "localhost", protocol: "ssh" as const };
const result = await discovery.resolveProjectPath(host, "plex");
expect(result).toBe("/compose/plex/docker-compose.yaml");
expect(mockScanner.findComposeFiles).toHaveBeenCalled();
expect(mockScanner.batchParseComposeNames).toHaveBeenCalledWith(
host,
["/compose/plex/docker-compose.yaml"],
10
);
});
it("should throw error if project not found", async () => {
mockCache.getProject.mockResolvedValue(undefined);
mockCache.load.mockResolvedValue({
lastScan: new Date().toISOString(),
searchPaths: ["/compose"],
projects: {},
});
vi.mocked(mockProjectLister.listComposeProjects).mockResolvedValue([]);
mockScanner.findComposeFiles.mockResolvedValue([]);
const host = { name: "test", host: "localhost", protocol: "ssh" as const };
await expect(discovery.resolveProjectPath(host, "missing")).rejects.toThrow(
"Project 'missing' not found on host 'test'"
);
});
});