import { describe, it, expect, vi } from "vitest"; import path from "node:path"; import fs from "node:fs"; import { SUPPORTED_PROVIDERS, buildEnvContent, buildClaudeConfig } from "../../scripts/setup.js"; const TEST_TMP = path.join(process.cwd(), "test", "setup", ".tmp"); function cleanup() { if (fs.existsSync(TEST_TMP)) { fs.rmSync(TEST_TMP, { recursive: true }); } } describe("SUPPORTED_PROVIDERS constant", () => { it('contains exactly openai, manual, ollama', () => { expect(SUPPORTED_PROVIDERS).toEqual(["openai", "manual", "ollama"]); }); it("is immutable (frozen Set or array)", () => { expect(Array.isArray(SUPPORTED_PROVIDERS)).toBe(true); }); }); // ── .env content generation ─────────────────────────────── describe("buildEnvContent — OpenAI provider", () => { it("includes CHATGPT_MCP_PROVIDER=openai", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null }); expect(result).toContain("CHATGPT_MCP_PROVIDER=openai"); }); it("includes OPENAI_API_KEY when provided", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-mykey123", openaiModel: null }); expect(result).toContain("OPENAI_API_KEY=sk-mykey123"); }); it("includes OPENAI_MODEL when provided", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: "gpt-4o" }); expect(result).toContain("OPENAI_MODEL=gpt-4o"); }); it("omits OPENAI_MODEL key line when null/undefined", () => { const resultA = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null }); const lines = resultA.split("\n").map((l) => l.trim()); expect(lines.filter((l) => l.startsWith("OPENAI_MODEL="))).toHaveLength(0); const resultB = buildEnvContent("openai", { openaiApiKey: "sk-test" }); const linesB = resultB.split("\n").map((l) => l.trim()); expect(linesB.filter((l) => l.startsWith("OPENAI_MODEL="))).toHaveLength(0); }); it("includes empty OPENAI_API_KEY line when key is empty string", () => { const result = buildEnvContent("openai", { openaiApiKey: "", openaiModel: null }); expect(result).toContain("OPENAI_API_KEY="); }); it("ends with a trailing newline", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null }); expect(result.endsWith("\n")).toBe(true); }); it("preserves existing non-conflicting .env content", () => { // Create a fake .env for this test const originalEnvPath = path.join(process.cwd(), ".env"); fs.writeFileSync(originalEnvPath, "SOME_OTHER_VAR=hello\nLLM_TEMP=0.5\n", "utf-8"); try { const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null }); expect(result).toContain("# ── Other settings (preserved from existing .env) ──"); expect(result).toContain("SOME_OTHER_VAR=hello"); expect(result).toContain("LLM_TEMP=0.5"); } finally { fs.unlinkSync(originalEnvPath); } }); it("filters out provider keys from existing .env content", () => { const originalEnvPath = path.join(process.cwd(), ".env"); fs.writeFileSync( originalEnvPath, "CHATGPT_MCP_PROVIDER=ollama\nOPENAI_API_KEY=oldkey\nSOME_OTHER_VAR=hello\n", "utf-8" ); try { const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null }); // CHATGPT_MCP_PROVIDER should NOT appear in the preserved section (only in the new header) const lines = result.split("\n"); // The provider key should only be in the generated header, not in the preserved section const preservedSection = result.split("# ── Other settings")[1] || ""; expect(preservedSection).not.toContain("CHATGPT_MCP_PROVIDER=ollama"); expect(preservedSection).not.toContain("OPENAI_API_KEY=oldkey"); expect(preservedSection).toContain("SOME_OTHER_VAR=hello"); } finally { fs.unlinkSync(originalEnvPath); } }); it("includes comment header for provider section", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null }); expect(result).toContain("# Chat provider:"); }); }); describe("buildEnvContent — Manual provider", () => { it("includes CHATGPT_MCP_PROVIDER=manual", () => { const result = buildEnvContent("manual", {}); expect(result).toContain("CHATGPT_MCP_PROVIDER=manual"); }); it("does not include any API key lines for manual provider", () => { const result = buildEnvContent("manual", {}); expect(result).not.toContain("OPENAI_API_KEY="); expect(result).not.toContain("OLLAMA_BASE_URL="); expect(result).not.toContain("OLLAMA_MODEL="); }); it("ends with a trailing newline", () => { const result = buildEnvContent("manual", {}); expect(result.endsWith("\n")).toBe(true); }); }); describe("buildEnvContent — Ollama provider", () => { it("includes CHATGPT_MCP_PROVIDER=ollama", () => { const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b" }); expect(result).toContain("CHATGPT_MCP_PROVIDER=ollama"); }); it("includes OLLAMA_BASE_URL when provided", () => { const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://custom:11434", ollamaModel: "qwen3.6:35b-a3b" }); expect(result).toContain("OLLAMA_BASE_URL=http://custom:11434"); }); it("includes OLLAMA_MODEL when provided", () => { const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b" }); expect(result).toContain("OLLAMA_MODEL=qwen3.6:35b-a3b"); }); it("includes OLLAMA_TEMPERATURE when provided", () => { const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b", ollamaTemperature: 0.7 }); expect(result).toContain("OLLAMA_TEMPERATURE=0.7"); }); it("includes OLLAMA_TIMEOUT when provided", () => { const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b", ollamaTimeout: 120 }); expect(result).toContain("OLLAMA_TIMEOUT=120"); }); it("excludes Ollama keys from OpenAI config output", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-test" }); expect(result).not.toContain("OLLAMA_BASE_URL="); expect(result).not.toContain("OLLAMA_MODEL="); }); it("ends with a trailing newline", () => { const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b" }); expect(result.endsWith("\n")).toBe(true); }); }); // ── Claude config generation ────────────────────────────── describe("buildClaudeConfig", () => { it("returns valid JSON string", () => { const result = buildClaudeConfig(); const parsed = JSON.parse(result); expect(parsed).toHaveProperty("mcpServers"); }); it("contains chatgpt-mcp server entry", () => { const result = buildClaudeConfig(); const parsed = JSON.parse(result); expect(parsed.mcpServers).toHaveProperty("chatgpt-mcp"); }); it("sets command to npm", () => { const result = buildClaudeConfig(); const parsed = JSON.parse(result); expect(parsed.mcpServers["chatgpt-mcp"].command).toBe("npm"); }); it('sets args to ["start"]', () => { const result = buildClaudeConfig(); const parsed = JSON.parse(result); expect(parsed.mcpServers["chatgpt-mcp"].args).toEqual(["start"]); }); it("is pretty-printed (not minified)", () => { const result = buildClaudeConfig(); // Pretty-printed JSON contains newlines and indentation expect(result).toContain("\n"); expect(result).toContain(" "); }); }); // ── Provider selection validation (via supported providers set) ── describe("Provider selection validation", () => { it("rejects invalid provider values (not in SUPPORTED_PROVIDERS)", () => { const invalid = ["anthropic", "claude", "", "OPENAI", "OpenAI", "ollama ", "1", "true"]; invalid.forEach((val) => { expect(SUPPORTED_PROVIDERS).not.toContain(val); }); }); it("valid provider is in the supported list", () => { SUPPORTED_PROVIDERS.forEach((provider) => { expect(["openai", "manual", "ollama"]).toContain(provider); }); }); }); // ── .env overwrite behavior (integration test via mock fs) ── describe(".env file overwrite confirmation", () => { it("generated content does not include secret key values in a masked form (key is written raw to file)", () => { // The key IS written to the file raw — this test confirms no masking happens at write time. // Masking only applies to display/console output. const result = buildEnvContent("openai", { openaiApiKey: "sk-secret-key-12345", openaiModel: null }); expect(result).toContain("OPENAI_API_KEY=sk-secret-key-12345"); // written raw, masked only in display }); it("does not include any Ollama setting keys in OpenAI config block", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-test" }); expect(result).not.toMatch(/^OLLAMA_BASE_URL=/m); expect(result).not.toMatch(/^OLLAMA_MODEL=/m); expect(result).not.toMatch(/^OLLAMA_TEMPERATURE=/m); expect(result).not.toMatch(/^OLLAMA_TIMEOUT=/m); }); it("generated content is deterministic (same input → same output)", () => { const config = { openaiApiKey: "sk-deterministic-test", openaiModel: "gpt-5.1" }; const a = buildEnvContent("openai", config); const b = buildEnvContent("openai", config); expect(a).toBe(b); }); it("rejecting write does not produce any file side effects (integration)", () => { // buildEnvContent never calls fs.writeFileSync — confirm .env is untouched. const envPath = path.join(process.cwd(), ".env"); const existsBefore = fs.existsSync(envPath); const content = buildEnvContent("manual", {}); expect(content).not.toBe(""); expect(fs.existsSync(envPath)).toBe(existsBefore); // unchanged }); it("accepting write would overwrite .env with the generated content (integration)", () => { const envPath = path.join(process.cwd(), ".env"); const beforeExists = fs.existsSync(envPath); const originalContent = beforeExists ? fs.readFileSync(envPath, "utf-8") : null; try { // Simulate acceptance: write the generated content to .env const content = buildEnvContent("manual", {}); fs.writeFileSync(envPath, content, "utf-8"); expect(fs.readFileSync(envPath, "utf-8")).toBe(content); expect(fs.readFileSync(envPath, "utf-8")).toContain("CHATGPT_MCP_PROVIDER=manual"); } finally { // Restore original state if (beforeExists && originalContent !== null) { fs.writeFileSync(envPath, originalContent, "utf-8"); } else if (!beforeExists) { fs.unlinkSync(envPath); } } }); }); describe("Non-interactive terminal handling", () => { it("supports reading provider default from existing .env via getProviderDefault", () => { const envPath = path.join(process.cwd(), ".env"); // Write a test .env with ollama provider fs.writeFileSync(envPath, "CHATGPT_MCP_PROVIDER=ollama\nOPENAI_API_KEY=\n", "utf-8"); try { const content = buildEnvContent("openai", { openaiApiKey: "sk-test" }); // The generated file should NOT contain the old ollama provider value expect(content).toContain("CHATGPT_MCP_PROVIDER=openai"); expect(content).not.toContain("CHATGPT_MCP_PROVIDER=ollama"); } finally { fs.unlinkSync(envPath); } }); it("non-TTY detection works (process.stdin is readable)", () => { // The non-TTY check in setup.js is: // process.stdin.isTTY && typeof process.stdin.setRawMode === "function" // In tests, stdin may not be a TTY — verify we don't crash on setRawMode calls. expect(process.stdin).toBeDefined(); expect(typeof process.stdin.setRawMode).not.toBe("function"); // non-TTY in vitest ✅ }); }); describe("Secret masking and output safety", () => { it("generated .env content contains raw key (masking only applies to display)", () => { const result = buildEnvContent("openai", { openaiApiKey: "sk-secret-key-12345", openaiModel: null }); expect(result).toContain("OPENAI_API_KEY=sk-secret-key-12345"); }); it("Claude config generation produces valid JSON", () => { const result = buildClaudeConfig(); expect(() => JSON.parse(result)).not.toThrow(); const parsed = JSON.parse(result); expect(parsed.mcpServers["chatgpt-mcp"].command).toBe("npm"); expect(parsed.mcpServers["chatgpt-mcp"].args).toEqual(["start"]); }); it("Claude config is idempotent (same output each call)", () => { const a = buildClaudeConfig(); const b = buildClaudeConfig(); expect(a).toBe(b); }); });