import { describe, it, expect, vi } from "vitest"; import { createChatProvider, openaiProvider } from "../../src/providers/factory.js"; import { manualExportProvider } from "../../src/providers/manual-export.js"; function mockConfig(provider) { const cfg = { chatgptMcpProvider: provider }; return cfg; } // --- Default provider --- describe("default provider", () => { it("returns openai provider when no provider specified", () => { const provider = createChatProvider({}); expect(provider).toBe(openaiProvider); }); it("returns openai provider when null config", () => { const provider = createChatProvider(null); expect(provider).toBe(openaiProvider); }); it("returns openai provider when undefined config", () => { const provider = createChatProvider(undefined); expect(provider).toBe(openaiProvider); }); it("returns openai provider when chatgptMcpProvider is empty string", () => { const provider = createChatProvider({ chatgptMcpProvider: "" }); expect(provider).toBe(openaiProvider); }); it("returns openai provider explicitly set", () => { const provider = createChatProvider({ chatgptMcpProvider: "openai" }); expect(provider).toBe(openaiProvider); }); }); // --- Supported providers --- describe("supported providers", () => { it("supports openai", () => { const provider = createChatProvider({ chatgptMcpProvider: "openai" }); expect(provider).toBe(openaiProvider); }); it("returns the same instance for repeated calls with same provider", () => { const p1 = createChatProvider({ chatgptMcpProvider: "openai" }); const p2 = createChatProvider({ chatgptMcpProvider: "openai" }); expect(p1).toBe(p2); }); }); // --- Unsupported providers --- describe("unsupported providers", () => { it("throws on ollama provider", () => { expect(() => createChatProvider({ chatgptMcpProvider: "ollama" })).toThrow( /Unsupported chat provider "ollama"/, ); }); it("throws on unknown provider name", () => { expect(() => createChatProvider({ chatgptMcpProvider: "unknown" })).toThrow( /Unsupported chat provider "unknown"/, ); }); it("defaults to openai for null provider name", () => { const provider = createChatProvider({ chatgptMcpProvider: null }); expect(provider.send).toBeDefined(); }); it("throws on numeric provider name (truthy but not supported)", () => { expect(() => createChatProvider({ chatgptMcpProvider: 123 })).toThrow('Unsupported chat provider "123"'); }); it("contains provider name in error message for unsupported providers", () => { try { createChatProvider({ chatgptMcpProvider: "ollama" }); } catch (err) { expect(err.message).toContain("ollama"); } }); it("mentions provider name in error message", () => { try { createChatProvider({ chatgptMcpProvider: "bedrock" }); } catch (err) { expect(err.message).toContain("bedrock"); } }); }); // --- Provider interface --- describe("provider interface", () => { it("returned provider has send method", () => { const provider = createChatProvider({ chatgptMcpProvider: "openai" }); expect(typeof provider.send).toBe("function"); }); it("send is a function, not undefined", () => { const provider = createChatProvider({ chatgptMcpProvider: "openai" }); expect(provider.send).not.toBeUndefined(); }); }); // --- Exported openaiProvider --- describe("exported openaiProvider", () => { it("openaiProvider is exported from factory", () => { expect(openaiProvider).toBeDefined(); }); it("openaiProvider has send method", () => { expect(typeof openaiProvider.send).toBe("function"); }); }); // --- Edge cases --- describe("edge cases", () => { it("handles case-sensitive provider name (Ollama != ollama)", () => { // This should also fail since only lowercase "openai" is supported expect(() => createChatProvider({ chatgptMcpProvider: "Ollama" })).toThrow(); }); it("handles whitespace provider name", () => { expect(() => createChatProvider({ chatgptMcpProvider: " openai " })).toThrow(); }); it("handles JSON string provider name", () => { expect(() => createChatProvider({ chatgptMcpProvider: '"openai"' })).toThrow(); }); it("creates provider even with minimal config object", () => { const provider = createChatProvider({ chatgptMcpProvider: "openai" }); expect(typeof provider.send).toBe("function"); }); it("works when config has extra unrelated fields", () => { const provider = createChatProvider({ chatgptMcpProvider: "openai", openaiApiKey: "sk-test", someOtherField: "ignored", }); expect(provider).toBe(openaiProvider); }); it("does not mutate the config object", () => { const cfg = { chatgptMcpProvider: "openai" }; createChatProvider(cfg); expect(cfg.chatgptMcpProvider).toBe("openai"); }); it("does not throw for all falsy values except explicit openai", () => { const falsyValues = [null, undefined, "", NaN]; // Only empty string and no-provider should default to openai // null/undefined → config check passes (defaults to "openai") // "" → defaults to "openai" }); it("defaults to openai for NaN provider name", () => { const provider = createChatProvider({ chatgptMcpProvider: NaN }); expect(provider.send).toBeDefined(); }); }); // --- Integration-like test --- describe("integration: send delegation", () => { it("provider.send delegates to the underlying provider implementation", async () => { const provider = createChatProvider({ chatgptMcpProvider: "openai" }); // The real openaiProvider.send calls OpenAI API — we just verify it exists and is callable expect(typeof provider.send).toBe("function"); // We don't call it here to avoid actual API calls in tests }); }); // --- Repeatability --- describe("repeatability", () => { it("creates identical providers for same config each time", () => { const results = []; for (let i = 0; i < 10; i++) { results.push(createChatProvider({ chatgptMcpProvider: "openai" })); } expect(results.every((p) => p === openaiProvider)).toBe(true); }); it("does not share mutable state between calls", () => { const p1 = createChatProvider({ chatgptMcpProvider: "openai" }); const p2 = createChatProvider({ chatgptMcpProvider: "openai" }); // Both should be the same singleton instance (by design) expect(p1).toBe(p2); }); }); // --- Manual provider --- describe("manual provider", () => { it("supports manual provider via createChatProvider", () => { const provider = createChatProvider({ chatgptMcpProvider: "manual" }); expect(provider.send).toBeDefined(); }); it("returns the same singleton instance for repeated calls with 'manual'", () => { const p1 = createChatProvider({ chatgptMcpProvider: "manual" }); const p2 = createChatProvider({ chatgptMcpProvider: "manual" }); expect(p1).toBe(p2); }); it("is different from openai provider", () => { const manualP = createChatProvider({ chatgptMcpProvider: "manual" }); expect(manualP).not.toBe(openaiProvider); }); it("can detect manual as a supported provider (not throw)", () => { const provider = createChatProvider({ chatgptMcpProvider: "manual" }); expect(provider.send).toBeDefined(); }); it("manual provider send returns { content: string } shape", async () => { const provider = createChatProvider({ chatgptMcpProvider: "manual" }); const result = await provider.send( { prompt: "test prompt from manual env", input: { question: "hi" } }, {}, ); expect(result).toHaveProperty("content"); expect(typeof result.content).toBe("string"); }); it("manual provider output contains MANUAL EXPORT marker", async () => { const provider = createChatProvider({ chatgptMcpProvider: "manual" }); const result = await provider.send({ prompt: "test", input: {} }, {}); expect(result.content).toContain("MANUAL EXPORT"); }); it("manual provider output contains COPY box delimiters", async () => { const provider = createChatProvider({ chatgptMcpProvider: "manual" }); const result = await provider.send({ prompt: "test", input: {} }, {}); expect(result.content).toContain("┌"); expect(result.content).toContain("┐"); expect(result.content).toContain("┘"); }); it("can be switched from openai to manual and back", () => { const p1 = createChatProvider({ chatgptMcpProvider: "openai" }); const p2 = createChatProvider({ chatgptMcpProvider: "manual" }); const p3 = createChatProvider({ chatgptMcpProvider: "openai" }); expect(p1).toBe(openaiProvider); expect(p2).not.toBe(openaiProvider); expect(p3).toBe(openaiProvider); }); it("manual provider is not the same as openaiProvider singleton", () => { const manualP = createChatProvider({ chatgptMcpProvider: "manual" }); expect(manualExportProvider).toBe(manualP); }); });