docs: prepare v1 milestone release

This commit is contained in:
2026-06-16 10:58:31 +01:00
parent 5a078bb108
commit f4fa1cdbf6
9 changed files with 1089 additions and 166 deletions
+316
View File
@@ -0,0 +1,316 @@
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);
});
});