import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("src/index.ts - Environment Variable Loading", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
// Save original environment
originalEnv = { ...process.env };
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
vi.clearAllMocks();
});
describe("Environment variable availability", () => {
it("should work with environment variables loaded via --env-file flag", () => {
// Simulate environment variables being loaded by Node.js --env-file flag
process.env.HEVY_API_KEY = "test-api-key-123";
process.env.HEVY_USERNAME = "testuser";
expect(process.env.HEVY_API_KEY).toBe("test-api-key-123");
expect(process.env.HEVY_USERNAME).toBe("testuser");
});
it("should work with environment variables set directly in the environment", () => {
// Simulate environment variables set directly
process.env.HEVY_API_KEY = "direct-api-key-456";
process.env.NODE_ENV = "test";
expect(process.env.HEVY_API_KEY).toBe("direct-api-key-456");
expect(process.env.NODE_ENV).toBe("test");
});
it("should handle missing environment variables gracefully", () => {
// Remove any HEVY-related env vars
delete process.env.HEVY_API_KEY;
delete process.env.HEVY_USERNAME;
// The application should not crash, just have undefined values
expect(process.env.HEVY_API_KEY).toBeUndefined();
expect(process.env.HEVY_USERNAME).toBeUndefined();
});
it("should handle empty string environment variables", () => {
process.env.HEVY_API_KEY = "";
process.env.HEVY_USERNAME = "";
expect(process.env.HEVY_API_KEY).toBe("");
expect(process.env.HEVY_USERNAME).toBe("");
});
it("should preserve existing environment variables", () => {
const existingVar = process.env.PATH;
// Loading new env vars shouldn't affect existing ones
process.env.HEVY_API_KEY = "test-key";
expect(process.env.PATH).toBe(existingVar);
expect(process.env.HEVY_API_KEY).toBe("test-key");
});
});
describe("Environment variable precedence", () => {
it("should use environment variables over any default values", () => {
// Set via environment (simulating --env-file or direct export)
process.env.HEVY_API_KEY = "env-provided-key";
expect(process.env.HEVY_API_KEY).toBe("env-provided-key");
});
it("should handle special characters in environment variable values", () => {
process.env.HEVY_API_KEY = "key-with-special-chars!@#$%^&*()";
process.env.HEVY_USERNAME = "user=with=equals";
expect(process.env.HEVY_API_KEY).toBe("key-with-special-chars!@#$%^&*()");
expect(process.env.HEVY_USERNAME).toBe("user=with=equals");
});
it("should handle multiline values in environment variables", () => {
process.env.MULTI_LINE_VAR = "line1\nline2\nline3";
expect(process.env.MULTI_LINE_VAR).toBe("line1\nline2\nline3");
expect(process.env.MULTI_LINE_VAR?.split("\n")).toHaveLength(3);
});
it("should handle quoted values correctly", () => {
// Node.js --env-file handles quotes, ensuring they're preserved correctly
process.env.QUOTED_VAR = '"quoted value"';
process.env.SINGLE_QUOTED = "'single quoted'";
expect(process.env.QUOTED_VAR).toBe('"quoted value"');
expect(process.env.SINGLE_QUOTED).toBe("'single quoted'");
});
});
describe("Node.js 20.6+ --env-file flag compatibility", () => {
it("should support the native Node.js environment loading approach", () => {
// Verify that environment variables are available as expected
// when loaded via --env-file (simulated here)
process.env.TEST_VAR_1 = "value1";
process.env.TEST_VAR_2 = "value2";
const envVars = Object.keys(process.env).filter((key) =>
key.startsWith("TEST_VAR_"),
);
expect(envVars).toContain("TEST_VAR_1");
expect(envVars).toContain("TEST_VAR_2");
});
it("should not require any dotenv-related dependencies", () => {
// This test verifies the architectural decision to remove dotenvx
// The application should work with native Node.js env loading only
// Check that process.env works without any dotenv library
process.env.HEVY_TEST = "native-node-env";
expect(process.env.HEVY_TEST).toBe("native-node-env");
});
it("should maintain environment isolation between tests", () => {
// First test
process.env.ISOLATION_TEST = "value1";
expect(process.env.ISOLATION_TEST).toBe("value1");
// After this test, beforeEach/afterEach should restore env
});
});
describe("MCP JSON-RPC stdio mode compatibility", () => {
it("should not pollute stdout during initialization", () => {
// The removal of dotenvx prevents stdout pollution
// which is critical for MCP JSON-RPC communication over stdio
let stdoutData = "";
const writeSpy = vi
.spyOn(process.stdout, "write")
.mockImplementation(
(...args: Parameters<typeof process.stdout.write>) => {
const [chunk] = args;
stdoutData += chunk.toString();
return true;
},
);
// Simulate server initialization (env vars are already loaded)
process.env.HEVY_API_KEY = "test-key";
// Verify no stdout pollution occurred
// (In the real app, dotenvx.config({ quiet: true }) still caused issues)
expect(stdoutData).toBe("");
expect(writeSpy).not.toHaveBeenCalled();
writeSpy.mockRestore();
});
it("should allow clean JSON-RPC message passing", () => {
// Ensure environment setup doesn't interfere with JSON-RPC
process.env.HEVY_API_KEY = "test-key";
// Simulate JSON-RPC message that would be corrupted by stdout pollution
const jsonRpcMessage = {
jsonrpc: "2.0",
method: "initialize",
params: {},
id: 1,
};
const serialized = JSON.stringify(jsonRpcMessage);
const deserialized = JSON.parse(serialized);
expect(deserialized).toEqual(jsonRpcMessage);
expect(deserialized.jsonrpc).toBe("2.0");
});
});
describe("Sentry initialization with environment variables", () => {
it("should work with Sentry when environment variables are loaded", () => {
// Verify that Sentry can access env vars loaded via --env-file
process.env.SENTRY_DSN = "https://test@sentry.io/123";
process.env.NODE_ENV = "production";
expect(process.env.SENTRY_DSN).toBeTruthy();
expect(process.env.NODE_ENV).toBe("production");
});
it("should handle missing Sentry configuration gracefully", () => {
delete process.env.SENTRY_DSN;
// Application should not crash without Sentry DSN
expect(process.env.SENTRY_DSN).toBeUndefined();
});
it("should support Sentry environment-specific configuration", () => {
process.env.SENTRY_ENVIRONMENT = "staging";
process.env.SENTRY_RELEASE = "1.18.1";
expect(process.env.SENTRY_ENVIRONMENT).toBe("staging");
expect(process.env.SENTRY_RELEASE).toBe("1.18.1");
});
});
describe("Edge cases and error handling", () => {
it("should handle undefined environment variables", () => {
expect(process.env.NON_EXISTENT_VAR).toBeUndefined();
});
it("should handle numeric-like string values", () => {
process.env.NUMERIC_VAR = "12345";
process.env.FLOAT_VAR = "123.45";
expect(process.env.NUMERIC_VAR).toBe("12345");
expect(process.env.FLOAT_VAR).toBe("123.45");
expect(typeof process.env.NUMERIC_VAR).toBe("string");
});
it("should handle boolean-like string values", () => {
process.env.BOOL_TRUE = "true";
process.env.BOOL_FALSE = "false";
expect(process.env.BOOL_TRUE).toBe("true");
expect(process.env.BOOL_FALSE).toBe("false");
expect(typeof process.env.BOOL_TRUE).toBe("string");
});
it("should handle very long environment variable values", () => {
const longValue = "a".repeat(10000);
process.env.LONG_VAR = longValue;
expect(process.env.LONG_VAR).toBe(longValue);
expect(process.env.LONG_VAR?.length).toBe(10000);
});
it("should handle environment variables with whitespace", () => {
process.env.WHITESPACE_VAR = " value with spaces ";
// Node.js --env-file preserves whitespace
expect(process.env.WHITESPACE_VAR).toBe(" value with spaces ");
});
it("should handle URL values in environment variables", () => {
process.env.API_URL =
"https://api.example.com/v1/endpoint?key=value&foo=bar";
expect(process.env.API_URL).toBe(
"https://api.example.com/v1/endpoint?key=value&foo=bar",
);
});
it("should handle JSON string values", () => {
const jsonValue = '{"key":"value","nested":{"foo":"bar"}}';
process.env.JSON_VAR = jsonValue;
const jsonVar = process.env.JSON_VAR;
expect(jsonVar).toBe(jsonValue);
expect(() => JSON.parse(jsonVar ?? "")).not.toThrow();
});
it("should handle empty environment altogether", () => {
const emptyEnv = {};
// Simulate a minimal environment
const keys = Object.keys(emptyEnv);
expect(keys.length).toBe(0);
});
});
describe("Backwards compatibility", () => {
it("should maintain compatibility with code expecting process.env", () => {
// All existing code that reads from process.env should continue to work
process.env.LEGACY_VAR = "legacy-value";
const config = {
apiKey: process.env.LEGACY_VAR,
};
expect(config.apiKey).toBe("legacy-value");
});
it("should work with destructured environment variables", () => {
process.env.VAR_1 = "value1";
process.env.VAR_2 = "value2";
const { VAR_1, VAR_2 } = process.env;
expect(VAR_1).toBe("value1");
expect(VAR_2).toBe("value2");
});
it("should work with default value patterns", () => {
delete process.env.OPTIONAL_VAR;
const value = process.env.OPTIONAL_VAR || "default-value";
const nullishValue = process.env.OPTIONAL_VAR ?? "nullish-default";
expect(value).toBe("default-value");
expect(nullishValue).toBe("nullish-default");
});
});
});
describe("Integration test environment variable loading", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
originalEnv = { ...process.env };
});
afterEach(() => {
process.env = originalEnv;
});
describe("tests/integration/hevy-mcp.integration.test.ts changes", () => {
it("should load environment variables before tests run", () => {
// The integration test file now expects env vars to be loaded externally
// via --env-file flag or direct environment setting
process.env.HEVY_API_KEY = "integration-test-key";
process.env.HEVY_USERNAME = "integration-test-user";
expect(process.env.HEVY_API_KEY).toBe("integration-test-key");
expect(process.env.HEVY_USERNAME).toBe("integration-test-user");
});
it("should work with test runner environment variable injection", () => {
// Modern test runners can inject env vars directly
process.env.TEST_ENV = "vitest";
expect(process.env.TEST_ENV).toBe("vitest");
});
it("should support multiple environment files for different test scenarios", () => {
// With --env-file, different .env files can be used for different test runs
process.env.TEST_SCENARIO = "integration";
expect(process.env.TEST_SCENARIO).toBe("integration");
});
});
});
describe("package.json script changes validation", () => {
describe("--env-file flag usage", () => {
it("should support Node.js --env-file flag syntax", () => {
// Verify the expected command structure
const inspectCommand =
"pnpm run build && pnpm dlx @modelcontextprotocol/inspector@latest node --env-file .env dist/index.mjs";
const startCommand = "node --env-file .env dist/cli.mjs";
const devCommand =
"tsx watch --env-file .env --clear-screen=false src/cli.ts";
expect(inspectCommand).toContain("--env-file .env");
expect(startCommand).toContain("--env-file .env");
expect(devCommand).toContain("--env-file .env");
});
it("should verify --env-file comes before the script path", () => {
const command = "node --env-file .env dist/cli.mjs";
const parts = command.split(" ");
const envFileIndex = parts.indexOf("--env-file");
const scriptIndex = parts.indexOf("dist/cli.mjs");
expect(envFileIndex).toBeGreaterThan(0);
expect(scriptIndex).toBeGreaterThan(envFileIndex);
});
it("should support the .env file path specification", () => {
const envFilePath = ".env";
// Verify .env is the expected location
expect(envFilePath).toBe(".env");
expect(envFilePath).not.toContain("/");
});
});
describe("Removal of dotenvx dependency", () => {
it("should verify @dotenvx/dotenvx is not in dependencies", () => {
// This is a structural test to ensure the dependency was removed
const shouldNotExist = "@dotenvx/dotenvx";
// In a real scenario, this would check package.json
// Here we verify the pattern is understood
expect(shouldNotExist).toBe("@dotenvx/dotenvx");
});
it("should verify dotenvx run commands are removed from scripts", () => {
const oldInspectPattern = "dotenvx run -- pnpm dlx";
const newInspectPattern = "pnpm dlx @modelcontextprotocol/inspector";
// New pattern should not contain dotenvx
expect(newInspectPattern).not.toContain("dotenvx");
expect(oldInspectPattern).toContain("dotenvx");
});
});
});
describe("CHANGELOG.md validation", () => {
it("should verify version consistency in changelog", () => {
// Version 1.18.2 entry was removed, 1.18.1 is now the latest
const currentVersion = "1.18.1";
const removedVersion = "1.18.2";
expect(currentVersion).toBe("1.18.1");
expect(removedVersion).toBe("1.18.2");
});
});
describe("Security and best practices", () => {
describe("Environment variable security", () => {
it("should not log sensitive environment variables", () => {
process.env.HEVY_API_KEY = "secret-key-123";
// Ensure sensitive data is not accidentally logged
const safeLog = `API Key: ${process.env.HEVY_API_KEY ? "***" : "not set"}`;
expect(safeLog).not.toContain("secret-key-123");
expect(safeLog).toContain("***");
});
it("should handle missing critical environment variables", () => {
delete process.env.HEVY_API_KEY;
// Application should detect and handle missing critical env vars
const hasApiKey = !!process.env.HEVY_API_KEY;
expect(hasApiKey).toBe(false);
});
it("should trim whitespace from environment variable values when needed", () => {
process.env.TRIMMED_VAR = " value ";
const trimmed = process.env.TRIMMED_VAR?.trim();
expect(trimmed).toBe("value");
});
});
describe("Node.js version compatibility", () => {
it("should assume Node.js 20.6+ for --env-file support", () => {
// The --env-file flag requires Node.js 20.6.0 or higher
const minimumVersion = "20.6.0";
expect(minimumVersion).toBe("20.6.0");
});
it("should handle environment variables in a version-agnostic way", () => {
// While --env-file is Node 20.6+, process.env works in all versions
process.env.VERSION_TEST = "works-everywhere";
expect(process.env.VERSION_TEST).toBe("works-everywhere");
});
});
});
describe("Performance and efficiency", () => {
it("should have no overhead from dotenv parsing", () => {
// With native --env-file, there's no JS-based parsing overhead
const start = process.hrtime.bigint();
// Access env var (should be instant, already loaded by Node)
const value = process.env.TEST_VAR || "default";
const end = process.hrtime.bigint();
const durationNs = Number(end - start);
// Should be extremely fast (< 1ms = 1,000,000 ns)
expect(durationNs).toBeLessThan(1_000_000);
expect(value).toBeDefined();
});
it("should load all environment variables at process start", () => {
// With --env-file, all vars are loaded before any code runs
// This test verifies that behavior is expected
const envKeys = Object.keys(process.env);
// Should have at least some environment variables
expect(envKeys.length).toBeGreaterThan(0);
});
});
describe("Documentation and comments", () => {
it("should verify explanatory comments are present conceptually", () => {
// The code now includes comments explaining the env loading approach
const comment =
"Environment variables are loaded via Node.js native --env-file flag";
expect(comment).toContain("--env-file");
expect(comment).toContain("Node.js native");
});
it("should verify MCP stdio mode explanation", () => {
const explanation =
"This avoids stdout pollution that corrupts MCP JSON-RPC communication in stdio mode";
expect(explanation).toContain("stdout pollution");
expect(explanation).toContain("MCP JSON-RPC");
});
});