mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Add environment file support for CLI tools
- 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.
This commit is contained in:
258
ccw/tests/cli-env-file.test.ts
Normal file
258
ccw/tests/cli-env-file.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user