317 lines
13 KiB
JavaScript
317 lines
13 KiB
JavaScript
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);
|
|
});
|
|
});
|