mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
- Introduced a new input group for environment file configuration in the dashboard CSS. - Updated hook manager to queue CLAUDE.md updates with configurable threshold and timeout. - Enhanced CLI manager view to include environment file input for built-in tools (gemini, qwen). - Implemented environment file loading mechanism in cli-executor-core, allowing custom environment variables. - Added unit tests for environment file parsing and loading functionalities. - Updated memory update queue to support dynamic configuration of threshold and timeout settings.
259 lines
8.3 KiB
TypeScript
259 lines
8.3 KiB
TypeScript
/**
|
|
* Unit tests for CLI env file loading mechanism
|
|
*
|
|
* Tests parseEnvFile and loadEnvFile functions without calling the actual CLI
|
|
*/
|
|
|
|
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir, homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
// Set test CCW home before importing module
|
|
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-env-file-test-'));
|
|
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
|
|
|
// Import from dist (built version)
|
|
const cliExecutorPath = new URL('../dist/tools/cli-executor-core.js', import.meta.url).href;
|
|
|
|
describe('Env File Loading Mechanism', async () => {
|
|
let parseEnvFile: (content: string) => Record<string, string>;
|
|
let loadEnvFile: (envFilePath: string) => Record<string, string>;
|
|
let testTempDir: string;
|
|
|
|
before(async () => {
|
|
const mod = await import(cliExecutorPath);
|
|
parseEnvFile = mod.parseEnvFile;
|
|
loadEnvFile = mod.loadEnvFile;
|
|
testTempDir = mkdtempSync(join(tmpdir(), 'env-test-'));
|
|
});
|
|
|
|
after(() => {
|
|
// Cleanup test directories
|
|
if (existsSync(testTempDir)) {
|
|
rmSync(testTempDir, { recursive: true, force: true });
|
|
}
|
|
if (existsSync(TEST_CCW_HOME)) {
|
|
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('parseEnvFile', () => {
|
|
it('should parse simple KEY=value pairs', () => {
|
|
const content = `API_KEY=abc123
|
|
SECRET=mysecret`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['API_KEY'], 'abc123');
|
|
assert.equal(result['SECRET'], 'mysecret');
|
|
});
|
|
|
|
it('should handle double-quoted values', () => {
|
|
const content = `API_KEY="value with spaces"
|
|
PATH="/usr/local/bin"`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['API_KEY'], 'value with spaces');
|
|
assert.equal(result['PATH'], '/usr/local/bin');
|
|
});
|
|
|
|
it('should handle single-quoted values', () => {
|
|
const content = `API_KEY='value with spaces'
|
|
NAME='John Doe'`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['API_KEY'], 'value with spaces');
|
|
assert.equal(result['NAME'], 'John Doe');
|
|
});
|
|
|
|
it('should skip comments', () => {
|
|
const content = `# This is a comment
|
|
API_KEY=value
|
|
# Another comment
|
|
SECRET=test`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(Object.keys(result).length, 2);
|
|
assert.equal(result['API_KEY'], 'value');
|
|
assert.equal(result['SECRET'], 'test');
|
|
});
|
|
|
|
it('should skip empty lines', () => {
|
|
const content = `
|
|
API_KEY=value
|
|
|
|
SECRET=test
|
|
|
|
`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(Object.keys(result).length, 2);
|
|
});
|
|
|
|
it('should handle values with equals signs', () => {
|
|
const content = `DATABASE_URL=postgresql://user:pass@host/db?sslmode=require`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['DATABASE_URL'], 'postgresql://user:pass@host/db?sslmode=require');
|
|
});
|
|
|
|
it('should handle Windows-style line endings (CRLF)', () => {
|
|
const content = `API_KEY=value\r\nSECRET=test\r\n`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['API_KEY'], 'value');
|
|
assert.equal(result['SECRET'], 'test');
|
|
});
|
|
|
|
it('should trim whitespace around keys and values', () => {
|
|
const content = ` API_KEY = value
|
|
SECRET = test `;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['API_KEY'], 'value');
|
|
assert.equal(result['SECRET'], 'test');
|
|
});
|
|
|
|
it('should skip lines without equals sign', () => {
|
|
const content = `API_KEY=value
|
|
INVALID_LINE
|
|
SECRET=test`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(Object.keys(result).length, 2);
|
|
assert.equal(result['INVALID_LINE'], undefined);
|
|
});
|
|
|
|
it('should handle empty values', () => {
|
|
const content = `EMPTY_VALUE=
|
|
ANOTHER=test`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['EMPTY_VALUE'], '');
|
|
assert.equal(result['ANOTHER'], 'test');
|
|
});
|
|
|
|
it('should handle mixed format content', () => {
|
|
const content = `# Gemini API Configuration
|
|
GEMINI_API_KEY="sk-gemini-xxx"
|
|
|
|
# OpenAI compatible settings
|
|
OPENAI_API_BASE='https://api.example.com/v1'
|
|
OPENAI_API_KEY=abc123
|
|
|
|
# Feature flags
|
|
ENABLE_DEBUG=true`;
|
|
const result = parseEnvFile(content);
|
|
assert.equal(result['GEMINI_API_KEY'], 'sk-gemini-xxx');
|
|
assert.equal(result['OPENAI_API_BASE'], 'https://api.example.com/v1');
|
|
assert.equal(result['OPENAI_API_KEY'], 'abc123');
|
|
assert.equal(result['ENABLE_DEBUG'], 'true');
|
|
assert.equal(Object.keys(result).length, 4);
|
|
});
|
|
});
|
|
|
|
describe('loadEnvFile', () => {
|
|
it('should load env file from absolute path', () => {
|
|
const envPath = join(testTempDir, 'test.env');
|
|
writeFileSync(envPath, 'API_KEY=test_value\nSECRET=123');
|
|
|
|
const result = loadEnvFile(envPath);
|
|
assert.equal(result['API_KEY'], 'test_value');
|
|
assert.equal(result['SECRET'], '123');
|
|
});
|
|
|
|
it('should return empty object for non-existent file', () => {
|
|
const result = loadEnvFile('/non/existent/path/.env');
|
|
assert.deepEqual(result, {});
|
|
});
|
|
|
|
it('should expand ~ to home directory', () => {
|
|
// Create .env-test in home directory for testing
|
|
const homeEnvPath = join(homedir(), '.ccw-env-test');
|
|
writeFileSync(homeEnvPath, 'HOME_TEST_KEY=home_value');
|
|
|
|
try {
|
|
const result = loadEnvFile('~/.ccw-env-test');
|
|
assert.equal(result['HOME_TEST_KEY'], 'home_value');
|
|
} finally {
|
|
// Cleanup
|
|
if (existsSync(homeEnvPath)) {
|
|
rmSync(homeEnvPath);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle relative paths', () => {
|
|
const envPath = join(testTempDir, 'relative.env');
|
|
writeFileSync(envPath, 'RELATIVE_KEY=rel_value');
|
|
|
|
// Save and change cwd
|
|
const originalCwd = process.cwd();
|
|
try {
|
|
process.chdir(testTempDir);
|
|
const result = loadEnvFile('./relative.env');
|
|
assert.equal(result['RELATIVE_KEY'], 'rel_value');
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should handle empty file', () => {
|
|
const envPath = join(testTempDir, 'empty.env');
|
|
writeFileSync(envPath, '');
|
|
|
|
const result = loadEnvFile(envPath);
|
|
assert.deepEqual(result, {});
|
|
});
|
|
|
|
it('should handle file with only comments', () => {
|
|
const envPath = join(testTempDir, 'comments.env');
|
|
writeFileSync(envPath, '# Just a comment\n# Another comment\n');
|
|
|
|
const result = loadEnvFile(envPath);
|
|
assert.deepEqual(result, {});
|
|
});
|
|
});
|
|
|
|
describe('Integration scenario: Gemini CLI env file', () => {
|
|
it('should correctly parse typical Gemini .env file', () => {
|
|
const geminiEnvContent = `# Gemini CLI Environment Configuration
|
|
# Created by CCW Dashboard
|
|
|
|
# Google AI API Key
|
|
GOOGLE_API_KEY="AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
|
|
|
# Optional: Custom API endpoint
|
|
# GOOGLE_API_BASE=https://generativelanguage.googleapis.com/v1beta
|
|
|
|
# Model configuration
|
|
GEMINI_MODEL=gemini-2.5-pro
|
|
|
|
# Rate limiting
|
|
GEMINI_RATE_LIMIT=60
|
|
`;
|
|
const envPath = join(testTempDir, '.gemini-env');
|
|
writeFileSync(envPath, geminiEnvContent);
|
|
|
|
const result = loadEnvFile(envPath);
|
|
assert.equal(result['GOOGLE_API_KEY'], 'AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
|
assert.equal(result['GEMINI_MODEL'], 'gemini-2.5-pro');
|
|
assert.equal(result['GEMINI_RATE_LIMIT'], '60');
|
|
assert.equal(result['GOOGLE_API_BASE'], undefined); // Commented out
|
|
});
|
|
|
|
it('should correctly parse typical Qwen .env file', () => {
|
|
const qwenEnvContent = `# Qwen CLI Environment Configuration
|
|
|
|
# DashScope API Key (Alibaba Cloud)
|
|
DASHSCOPE_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
|
|
# OpenAI-compatible endpoint settings
|
|
OPENAI_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
|
|
# Model selection
|
|
QWEN_MODEL=qwen2.5-coder-32b
|
|
`;
|
|
const envPath = join(testTempDir, '.qwen-env');
|
|
writeFileSync(envPath, qwenEnvContent);
|
|
|
|
const result = loadEnvFile(envPath);
|
|
assert.equal(result['DASHSCOPE_API_KEY'], 'sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
|
assert.equal(result['OPENAI_API_BASE'], 'https://dashscope.aliyuncs.com/compatible-mode/v1');
|
|
assert.equal(result['QWEN_MODEL'], 'qwen2.5-coder-32b');
|
|
});
|
|
});
|
|
});
|