Issue/90 fix windows (#91)
* Window対応および Codexが.gitを必要とする問題があるので.gitがみつからない場合はエラーとする fix #90 * 文字化け修正
This commit is contained in:
parent
54ade15dcb
commit
8e509e13c6
@ -476,9 +476,6 @@ model: sonnet # Default model (optional)
|
|||||||
anthropic_api_key: sk-ant-... # For Claude (Anthropic)
|
anthropic_api_key: sk-ant-... # For Claude (Anthropic)
|
||||||
# openai_api_key: sk-... # For Codex (OpenAI)
|
# openai_api_key: sk-... # For Codex (OpenAI)
|
||||||
|
|
||||||
trusted_directories:
|
|
||||||
- /path/to/trusted/dir
|
|
||||||
|
|
||||||
# Pipeline execution configuration (optional)
|
# Pipeline execution configuration (optional)
|
||||||
# Customize branch names, commit messages, and PR body.
|
# Customize branch names, commit messages, and PR body.
|
||||||
# pipeline:
|
# pipeline:
|
||||||
@ -490,6 +487,8 @@ trusted_directories:
|
|||||||
# Closes #{issue}
|
# Closes #{issue}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** The Codex SDK requires running inside a Git repository. `--skip-git-repo-check` is only available in the Codex CLI.
|
||||||
|
|
||||||
**API Key Configuration Methods:**
|
**API Key Configuration Methods:**
|
||||||
|
|
||||||
1. **Set via environment variables**:
|
1. **Set via environment variables**:
|
||||||
|
|||||||
5
bin/takt
5
bin/takt
@ -9,7 +9,7 @@
|
|||||||
* - npm exec takt
|
* - npm exec takt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@ -19,7 +19,8 @@ const __dirname = dirname(__filename);
|
|||||||
const cliPath = join(__dirname, '..', 'dist', 'app', 'cli', 'index.js');
|
const cliPath = join(__dirname, '..', 'dist', 'app', 'cli', 'index.js');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await import(cliPath);
|
const cliUrl = pathToFileURL(cliPath).href;
|
||||||
|
await import(cliUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load TAKT CLI. Have you run "npm run build"?');
|
console.error('Failed to load TAKT CLI. Have you run "npm run build"?');
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
|
|||||||
@ -472,9 +472,6 @@ model: sonnet # デフォルトモデル(オプション)
|
|||||||
anthropic_api_key: sk-ant-... # Claude (Anthropic) を使う場合
|
anthropic_api_key: sk-ant-... # Claude (Anthropic) を使う場合
|
||||||
# openai_api_key: sk-... # Codex (OpenAI) を使う場合
|
# openai_api_key: sk-... # Codex (OpenAI) を使う場合
|
||||||
|
|
||||||
trusted_directories:
|
|
||||||
- /path/to/trusted/dir
|
|
||||||
|
|
||||||
# パイプライン実行設定(オプション)
|
# パイプライン実行設定(オプション)
|
||||||
# ブランチ名、コミットメッセージ、PRの本文をカスタマイズできます。
|
# ブランチ名、コミットメッセージ、PRの本文をカスタマイズできます。
|
||||||
# pipeline:
|
# pipeline:
|
||||||
@ -486,6 +483,8 @@ trusted_directories:
|
|||||||
# Closes #{issue}
|
# Closes #{issue}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**注意:** Codex SDK は Git 管理下のディレクトリでのみ動作します。`--skip-git-repo-check` は Codex CLI 専用です。
|
||||||
|
|
||||||
**API Key の設定方法:**
|
**API Key の設定方法:**
|
||||||
|
|
||||||
1. **環境変数で設定**:
|
1. **環境変数で設定**:
|
||||||
|
|||||||
734
package-lock.json
generated
734
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,6 @@
|
|||||||
# Language setting (en or ja)
|
# Language setting (en or ja)
|
||||||
language: en
|
language: en
|
||||||
|
|
||||||
# Trusted directories - projects in these directories skip confirmation prompts
|
|
||||||
trusted_directories: []
|
|
||||||
|
|
||||||
# Default piece to use when no piece is specified
|
# Default piece to use when no piece is specified
|
||||||
default_piece: default
|
default_piece: default
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,7 @@
|
|||||||
# 言語設定 (en または ja)
|
# 言語設定 (en または ja)
|
||||||
language: ja
|
language: ja
|
||||||
|
|
||||||
# 信頼済みディレクトリ - これらのディレクトリ内のプロジェクトは確認プロンプトをスキップします
|
# デフォルトのピース - 指定がない場合に使用します
|
||||||
trusted_directories: []
|
|
||||||
|
|
||||||
# デフォルトピース - ピースが指定されていない場合に使用します
|
|
||||||
default_piece: default
|
default_piece: default
|
||||||
|
|
||||||
# ログレベル: debug, info, warn, error
|
# ログレベル: debug, info, warn, error
|
||||||
|
|||||||
@ -1,292 +1,282 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for API key authentication feature
|
* Tests for API key authentication feature
|
||||||
*
|
*
|
||||||
* Tests the resolution logic for Anthropic and OpenAI API keys:
|
* Tests the resolution logic for Anthropic and OpenAI API keys:
|
||||||
* - Environment variable priority over config.yaml
|
* - Environment variable priority over config.yaml
|
||||||
* - Config.yaml fallback when env var is not set
|
* - Config.yaml fallback when env var is not set
|
||||||
* - Undefined when neither is set
|
* - Undefined when neither is set
|
||||||
* - Schema validation for API key fields
|
* - Schema validation for API key fields
|
||||||
* - GlobalConfig load/save round-trip with API keys
|
* - GlobalConfig load/save round-trip with API keys
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { GlobalConfigSchema } from '../core/models/index.js';
|
import { GlobalConfigSchema } from '../core/models/index.js';
|
||||||
|
|
||||||
// Mock paths module to redirect config to temp directory
|
// Mock paths module to redirect config to temp directory
|
||||||
const testId = randomUUID();
|
const testId = randomUUID();
|
||||||
const testDir = join(tmpdir(), `takt-api-key-test-${testId}`);
|
const testDir = join(tmpdir(), `takt-api-key-test-${testId}`);
|
||||||
const taktDir = join(testDir, '.takt');
|
const taktDir = join(testDir, '.takt');
|
||||||
const configPath = join(taktDir, 'config.yaml');
|
const configPath = join(taktDir, 'config.yaml');
|
||||||
|
|
||||||
vi.mock('../infra/config/paths.js', async (importOriginal) => {
|
vi.mock('../infra/config/paths.js', async (importOriginal) => {
|
||||||
const original = await importOriginal() as Record<string, unknown>;
|
const original = await importOriginal() as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
getGlobalConfigPath: () => configPath,
|
getGlobalConfigPath: () => configPath,
|
||||||
getTaktDir: () => taktDir,
|
getTaktDir: () => taktDir,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Import after mocking
|
// Import after mocking
|
||||||
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
|
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
|
||||||
|
|
||||||
describe('GlobalConfigSchema API key fields', () => {
|
describe('GlobalConfigSchema API key fields', () => {
|
||||||
it('should accept config without API keys', () => {
|
it('should accept config without API keys', () => {
|
||||||
const result = GlobalConfigSchema.parse({
|
const result = GlobalConfigSchema.parse({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
});
|
});
|
||||||
expect(result.anthropic_api_key).toBeUndefined();
|
expect(result.anthropic_api_key).toBeUndefined();
|
||||||
expect(result.openai_api_key).toBeUndefined();
|
expect(result.openai_api_key).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept config with anthropic_api_key', () => {
|
it('should accept config with anthropic_api_key', () => {
|
||||||
const result = GlobalConfigSchema.parse({
|
const result = GlobalConfigSchema.parse({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
anthropic_api_key: 'sk-ant-test-key',
|
anthropic_api_key: 'sk-ant-test-key',
|
||||||
});
|
});
|
||||||
expect(result.anthropic_api_key).toBe('sk-ant-test-key');
|
expect(result.anthropic_api_key).toBe('sk-ant-test-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept config with openai_api_key', () => {
|
it('should accept config with openai_api_key', () => {
|
||||||
const result = GlobalConfigSchema.parse({
|
const result = GlobalConfigSchema.parse({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
openai_api_key: 'sk-openai-test-key',
|
openai_api_key: 'sk-openai-test-key',
|
||||||
});
|
});
|
||||||
expect(result.openai_api_key).toBe('sk-openai-test-key');
|
expect(result.openai_api_key).toBe('sk-openai-test-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept config with both API keys', () => {
|
it('should accept config with both API keys', () => {
|
||||||
const result = GlobalConfigSchema.parse({
|
const result = GlobalConfigSchema.parse({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
anthropic_api_key: 'sk-ant-key',
|
anthropic_api_key: 'sk-ant-key',
|
||||||
openai_api_key: 'sk-openai-key',
|
openai_api_key: 'sk-openai-key',
|
||||||
});
|
});
|
||||||
expect(result.anthropic_api_key).toBe('sk-ant-key');
|
expect(result.anthropic_api_key).toBe('sk-ant-key');
|
||||||
expect(result.openai_api_key).toBe('sk-openai-key');
|
expect(result.openai_api_key).toBe('sk-openai-key');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GlobalConfig load/save with API keys', () => {
|
describe('GlobalConfig load/save with API keys', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
invalidateGlobalConfigCache();
|
invalidateGlobalConfigCache();
|
||||||
mkdirSync(taktDir, { recursive: true });
|
mkdirSync(taktDir, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load config with API keys from YAML', () => {
|
it('should load config with API keys from YAML', () => {
|
||||||
const yaml = [
|
const yaml = [
|
||||||
'language: en',
|
'language: en',
|
||||||
'trusted_directories: []',
|
'default_piece: default',
|
||||||
'default_piece: default',
|
'log_level: info',
|
||||||
'log_level: info',
|
'provider: claude',
|
||||||
'provider: claude',
|
'anthropic_api_key: sk-ant-from-yaml',
|
||||||
'anthropic_api_key: sk-ant-from-yaml',
|
'openai_api_key: sk-openai-from-yaml',
|
||||||
'openai_api_key: sk-openai-from-yaml',
|
].join('\n');
|
||||||
].join('\n');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
const config = loadGlobalConfig();
|
||||||
const config = loadGlobalConfig();
|
expect(config.anthropicApiKey).toBe('sk-ant-from-yaml');
|
||||||
expect(config.anthropicApiKey).toBe('sk-ant-from-yaml');
|
expect(config.openaiApiKey).toBe('sk-openai-from-yaml');
|
||||||
expect(config.openaiApiKey).toBe('sk-openai-from-yaml');
|
});
|
||||||
});
|
|
||||||
|
it('should load config without API keys', () => {
|
||||||
it('should load config without API keys', () => {
|
const yaml = [
|
||||||
const yaml = [
|
'language: en',
|
||||||
'language: en',
|
'default_piece: default',
|
||||||
'trusted_directories: []',
|
'log_level: info',
|
||||||
'default_piece: default',
|
'provider: claude',
|
||||||
'log_level: info',
|
].join('\n');
|
||||||
'provider: claude',
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
].join('\n');
|
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
const config = loadGlobalConfig();
|
||||||
|
expect(config.anthropicApiKey).toBeUndefined();
|
||||||
const config = loadGlobalConfig();
|
expect(config.openaiApiKey).toBeUndefined();
|
||||||
expect(config.anthropicApiKey).toBeUndefined();
|
});
|
||||||
expect(config.openaiApiKey).toBeUndefined();
|
|
||||||
});
|
it('should save and reload config with API keys', () => {
|
||||||
|
// Write initial config
|
||||||
it('should save and reload config with API keys', () => {
|
const yaml = [
|
||||||
// Write initial config
|
'language: en',
|
||||||
const yaml = [
|
'default_piece: default',
|
||||||
'language: en',
|
'log_level: info',
|
||||||
'trusted_directories: []',
|
'provider: claude',
|
||||||
'default_piece: default',
|
].join('\n');
|
||||||
'log_level: info',
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'provider: claude',
|
|
||||||
].join('\n');
|
const config = loadGlobalConfig();
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
config.anthropicApiKey = 'sk-ant-saved';
|
||||||
|
config.openaiApiKey = 'sk-openai-saved';
|
||||||
const config = loadGlobalConfig();
|
saveGlobalConfig(config);
|
||||||
config.anthropicApiKey = 'sk-ant-saved';
|
|
||||||
config.openaiApiKey = 'sk-openai-saved';
|
const reloaded = loadGlobalConfig();
|
||||||
saveGlobalConfig(config);
|
expect(reloaded.anthropicApiKey).toBe('sk-ant-saved');
|
||||||
|
expect(reloaded.openaiApiKey).toBe('sk-openai-saved');
|
||||||
const reloaded = loadGlobalConfig();
|
});
|
||||||
expect(reloaded.anthropicApiKey).toBe('sk-ant-saved');
|
|
||||||
expect(reloaded.openaiApiKey).toBe('sk-openai-saved');
|
it('should not persist API keys when not set', () => {
|
||||||
});
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
it('should not persist API keys when not set', () => {
|
'default_piece: default',
|
||||||
const yaml = [
|
'log_level: info',
|
||||||
'language: en',
|
'provider: claude',
|
||||||
'trusted_directories: []',
|
].join('\n');
|
||||||
'default_piece: default',
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'log_level: info',
|
|
||||||
'provider: claude',
|
const config = loadGlobalConfig();
|
||||||
].join('\n');
|
saveGlobalConfig(config);
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
const content = readFileSync(configPath, 'utf-8');
|
||||||
const config = loadGlobalConfig();
|
expect(content).not.toContain('anthropic_api_key');
|
||||||
saveGlobalConfig(config);
|
expect(content).not.toContain('openai_api_key');
|
||||||
|
});
|
||||||
const content = readFileSync(configPath, 'utf-8');
|
});
|
||||||
expect(content).not.toContain('anthropic_api_key');
|
|
||||||
expect(content).not.toContain('openai_api_key');
|
describe('resolveAnthropicApiKey', () => {
|
||||||
});
|
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
});
|
|
||||||
|
beforeEach(() => {
|
||||||
describe('resolveAnthropicApiKey', () => {
|
invalidateGlobalConfigCache();
|
||||||
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
});
|
||||||
beforeEach(() => {
|
|
||||||
invalidateGlobalConfigCache();
|
afterEach(() => {
|
||||||
mkdirSync(taktDir, { recursive: true });
|
if (originalEnv !== undefined) {
|
||||||
});
|
process.env['TAKT_ANTHROPIC_API_KEY'] = originalEnv;
|
||||||
|
} else {
|
||||||
afterEach(() => {
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
if (originalEnv !== undefined) {
|
}
|
||||||
process.env['TAKT_ANTHROPIC_API_KEY'] = originalEnv;
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
} else {
|
});
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
|
||||||
}
|
it('should return env var when set', () => {
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
process.env['TAKT_ANTHROPIC_API_KEY'] = 'sk-ant-from-env';
|
||||||
});
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
it('should return env var when set', () => {
|
'default_piece: default',
|
||||||
process.env['TAKT_ANTHROPIC_API_KEY'] = 'sk-ant-from-env';
|
'log_level: info',
|
||||||
const yaml = [
|
'provider: claude',
|
||||||
'language: en',
|
'anthropic_api_key: sk-ant-from-yaml',
|
||||||
'trusted_directories: []',
|
].join('\n');
|
||||||
'default_piece: default',
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'log_level: info',
|
|
||||||
'provider: claude',
|
const key = resolveAnthropicApiKey();
|
||||||
'anthropic_api_key: sk-ant-from-yaml',
|
expect(key).toBe('sk-ant-from-env');
|
||||||
].join('\n');
|
});
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
it('should fall back to config when env var is not set', () => {
|
||||||
const key = resolveAnthropicApiKey();
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
expect(key).toBe('sk-ant-from-env');
|
const yaml = [
|
||||||
});
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
it('should fall back to config when env var is not set', () => {
|
'log_level: info',
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
'provider: claude',
|
||||||
const yaml = [
|
'anthropic_api_key: sk-ant-from-yaml',
|
||||||
'language: en',
|
].join('\n');
|
||||||
'trusted_directories: []',
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
const key = resolveAnthropicApiKey();
|
||||||
'provider: claude',
|
expect(key).toBe('sk-ant-from-yaml');
|
||||||
'anthropic_api_key: sk-ant-from-yaml',
|
});
|
||||||
].join('\n');
|
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
it('should return undefined when neither env var nor config is set', () => {
|
||||||
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
const key = resolveAnthropicApiKey();
|
const yaml = [
|
||||||
expect(key).toBe('sk-ant-from-yaml');
|
'language: en',
|
||||||
});
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
it('should return undefined when neither env var nor config is set', () => {
|
'provider: claude',
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
].join('\n');
|
||||||
const yaml = [
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'language: en',
|
|
||||||
'trusted_directories: []',
|
const key = resolveAnthropicApiKey();
|
||||||
'default_piece: default',
|
expect(key).toBeUndefined();
|
||||||
'log_level: info',
|
});
|
||||||
'provider: claude',
|
|
||||||
].join('\n');
|
it('should return undefined when config file does not exist', () => {
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
|
// No config file created
|
||||||
const key = resolveAnthropicApiKey();
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
expect(key).toBeUndefined();
|
|
||||||
});
|
const key = resolveAnthropicApiKey();
|
||||||
|
expect(key).toBeUndefined();
|
||||||
it('should return undefined when config file does not exist', () => {
|
});
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
});
|
||||||
// No config file created
|
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
describe('resolveOpenaiApiKey', () => {
|
||||||
|
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
|
||||||
const key = resolveAnthropicApiKey();
|
|
||||||
expect(key).toBeUndefined();
|
beforeEach(() => {
|
||||||
});
|
invalidateGlobalConfigCache();
|
||||||
});
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
});
|
||||||
describe('resolveOpenaiApiKey', () => {
|
|
||||||
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
beforeEach(() => {
|
process.env['TAKT_OPENAI_API_KEY'] = originalEnv;
|
||||||
invalidateGlobalConfigCache();
|
} else {
|
||||||
mkdirSync(taktDir, { recursive: true });
|
delete process.env['TAKT_OPENAI_API_KEY'];
|
||||||
});
|
}
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
afterEach(() => {
|
});
|
||||||
if (originalEnv !== undefined) {
|
|
||||||
process.env['TAKT_OPENAI_API_KEY'] = originalEnv;
|
it('should return env var when set', () => {
|
||||||
} else {
|
process.env['TAKT_OPENAI_API_KEY'] = 'sk-openai-from-env';
|
||||||
delete process.env['TAKT_OPENAI_API_KEY'];
|
const yaml = [
|
||||||
}
|
'language: en',
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
'default_piece: default',
|
||||||
});
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
it('should return env var when set', () => {
|
'openai_api_key: sk-openai-from-yaml',
|
||||||
process.env['TAKT_OPENAI_API_KEY'] = 'sk-openai-from-env';
|
].join('\n');
|
||||||
const yaml = [
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'language: en',
|
|
||||||
'trusted_directories: []',
|
const key = resolveOpenaiApiKey();
|
||||||
'default_piece: default',
|
expect(key).toBe('sk-openai-from-env');
|
||||||
'log_level: info',
|
});
|
||||||
'provider: claude',
|
|
||||||
'openai_api_key: sk-openai-from-yaml',
|
it('should fall back to config when env var is not set', () => {
|
||||||
].join('\n');
|
delete process.env['TAKT_OPENAI_API_KEY'];
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
const key = resolveOpenaiApiKey();
|
'default_piece: default',
|
||||||
expect(key).toBe('sk-openai-from-env');
|
'log_level: info',
|
||||||
});
|
'provider: claude',
|
||||||
|
'openai_api_key: sk-openai-from-yaml',
|
||||||
it('should fall back to config when env var is not set', () => {
|
].join('\n');
|
||||||
delete process.env['TAKT_OPENAI_API_KEY'];
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
const yaml = [
|
|
||||||
'language: en',
|
const key = resolveOpenaiApiKey();
|
||||||
'trusted_directories: []',
|
expect(key).toBe('sk-openai-from-yaml');
|
||||||
'default_piece: default',
|
});
|
||||||
'log_level: info',
|
|
||||||
'provider: claude',
|
it('should return undefined when neither env var nor config is set', () => {
|
||||||
'openai_api_key: sk-openai-from-yaml',
|
delete process.env['TAKT_OPENAI_API_KEY'];
|
||||||
].join('\n');
|
const yaml = [
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
const key = resolveOpenaiApiKey();
|
'log_level: info',
|
||||||
expect(key).toBe('sk-openai-from-yaml');
|
'provider: claude',
|
||||||
});
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
it('should return undefined when neither env var nor config is set', () => {
|
|
||||||
delete process.env['TAKT_OPENAI_API_KEY'];
|
const key = resolveOpenaiApiKey();
|
||||||
const yaml = [
|
expect(key).toBeUndefined();
|
||||||
'language: en',
|
});
|
||||||
'trusted_directories: []',
|
});
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
|
||||||
'provider: claude',
|
|
||||||
].join('\n');
|
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
|
||||||
const key = resolveOpenaiApiKey();
|
|
||||||
expect(key).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
37
src/__tests__/cli-wrapper.test.ts
Normal file
37
src/__tests__/cli-wrapper.test.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Tests for the CLI wrapper URL handling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { posix, win32, resolve } from 'node:path';
|
||||||
|
|
||||||
|
describe('cli wrapper import URL', () => {
|
||||||
|
it('builds a file URL for Windows paths', () => {
|
||||||
|
const winPath = win32.join('C:\\', 'work', 'git', 'takt', 'dist', 'app', 'cli', 'index.js');
|
||||||
|
const url = pathToFileURL(winPath).href;
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
expect(url).toBe('file:///C:/work/git/takt/dist/app/cli/index.js');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(url).toMatch(/C:%5Cwork%5Cgit%5Ctakt%5Cdist%5Capp%5Ccli%5Cindex\.js$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds a file URL for POSIX paths', () => {
|
||||||
|
const posixPath = posix.join('/', 'usr', 'local', 'lib', 'takt', 'dist', 'app', 'cli', 'index.js');
|
||||||
|
const url = pathToFileURL(posixPath).href;
|
||||||
|
|
||||||
|
expect(url).toBe('file:///usr/local/lib/takt/dist/app/cli/index.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses pathToFileURL in the npm wrapper', async () => {
|
||||||
|
const wrapperPath = resolve('bin', 'takt');
|
||||||
|
const wrapperContents = await readFile(wrapperPath, 'utf8');
|
||||||
|
|
||||||
|
expect(wrapperContents).toContain('pathToFileURL');
|
||||||
|
expect(wrapperContents).toContain('pathToFileURL(cliPath)');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -39,7 +39,6 @@ describe('loadGlobalConfig', () => {
|
|||||||
const config = loadGlobalConfig();
|
const config = loadGlobalConfig();
|
||||||
|
|
||||||
expect(config.language).toBe('en');
|
expect(config.language).toBe('en');
|
||||||
expect(config.trustedDirectories).toEqual([]);
|
|
||||||
expect(config.defaultPiece).toBe('default');
|
expect(config.defaultPiece).toBe('default');
|
||||||
expect(config.logLevel).toBe('info');
|
expect(config.logLevel).toBe('info');
|
||||||
expect(config.provider).toBe('claude');
|
expect(config.provider).toBe('claude');
|
||||||
|
|||||||
@ -156,6 +156,12 @@ describe('worktree branch deletion', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete regular (non-worktree) branches normally', () => {
|
it('should delete regular (non-worktree) branches normally', () => {
|
||||||
|
const defaultBranch = execFileSync('git', ['branch', '--show-current'], {
|
||||||
|
cwd: testDir,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
// Create a regular local branch
|
// Create a regular local branch
|
||||||
const branchName = 'takt/20260203T1002-regular-branch';
|
const branchName = 'takt/20260203T1002-regular-branch';
|
||||||
execFileSync('git', ['checkout', '-b', branchName], { cwd: testDir });
|
execFileSync('git', ['checkout', '-b', branchName], { cwd: testDir });
|
||||||
@ -166,7 +172,7 @@ describe('worktree branch deletion', () => {
|
|||||||
execFileSync('git', ['commit', '-m', 'Test change'], { cwd: testDir });
|
execFileSync('git', ['commit', '-m', 'Test change'], { cwd: testDir });
|
||||||
|
|
||||||
// Switch back to main
|
// Switch back to main
|
||||||
execFileSync('git', ['checkout', 'master'], { cwd: testDir });
|
execFileSync('git', ['checkout', defaultBranch || 'main'], { cwd: testDir });
|
||||||
|
|
||||||
// Verify branch exists
|
// Verify branch exists
|
||||||
const branchesBefore = listTaktBranches(testDir);
|
const branchesBefore = listTaktBranches(testDir);
|
||||||
|
|||||||
@ -201,7 +201,6 @@ describe('GlobalConfigSchema', () => {
|
|||||||
const config = {};
|
const config = {};
|
||||||
const result = GlobalConfigSchema.parse(config);
|
const result = GlobalConfigSchema.parse(config);
|
||||||
|
|
||||||
expect(result.trusted_directories).toEqual([]);
|
|
||||||
expect(result.default_piece).toBe('default');
|
expect(result.default_piece).toBe('default');
|
||||||
expect(result.log_level).toBe('info');
|
expect(result.log_level).toBe('info');
|
||||||
expect(result.provider).toBe('claude');
|
expect(result.provider).toBe('claude');
|
||||||
@ -209,13 +208,11 @@ describe('GlobalConfigSchema', () => {
|
|||||||
|
|
||||||
it('should accept valid config', () => {
|
it('should accept valid config', () => {
|
||||||
const config = {
|
const config = {
|
||||||
trusted_directories: ['/home/user/projects'],
|
|
||||||
default_piece: 'custom',
|
default_piece: 'custom',
|
||||||
log_level: 'debug' as const,
|
log_level: 'debug' as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = GlobalConfigSchema.parse(config);
|
const result = GlobalConfigSchema.parse(config);
|
||||||
expect(result.trusted_directories).toHaveLength(1);
|
|
||||||
expect(result.log_level).toBe('debug');
|
expect(result.log_level).toBe('debug');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,438 +1,434 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for pipeline execution
|
* Tests for pipeline execution
|
||||||
*
|
*
|
||||||
* Tests the orchestration logic with mocked dependencies.
|
* Tests the orchestration logic with mocked dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
// Mock all external dependencies
|
// Mock all external dependencies
|
||||||
const mockFetchIssue = vi.fn();
|
const mockFetchIssue = vi.fn();
|
||||||
const mockCheckGhCli = vi.fn().mockReturnValue({ available: true });
|
const mockCheckGhCli = vi.fn().mockReturnValue({ available: true });
|
||||||
vi.mock('../infra/github/issue.js', () => ({
|
vi.mock('../infra/github/issue.js', () => ({
|
||||||
fetchIssue: mockFetchIssue,
|
fetchIssue: mockFetchIssue,
|
||||||
formatIssueAsTask: vi.fn((issue: { title: string; body: string; number: number }) =>
|
formatIssueAsTask: vi.fn((issue: { title: string; body: string; number: number }) =>
|
||||||
`## GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}`
|
`## GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}`
|
||||||
),
|
),
|
||||||
checkGhCli: mockCheckGhCli,
|
checkGhCli: mockCheckGhCli,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockCreatePullRequest = vi.fn();
|
const mockCreatePullRequest = vi.fn();
|
||||||
const mockPushBranch = vi.fn();
|
const mockPushBranch = vi.fn();
|
||||||
const mockBuildPrBody = vi.fn(() => 'Default PR body');
|
const mockBuildPrBody = vi.fn(() => 'Default PR body');
|
||||||
vi.mock('../infra/github/pr.js', () => ({
|
vi.mock('../infra/github/pr.js', () => ({
|
||||||
createPullRequest: mockCreatePullRequest,
|
createPullRequest: mockCreatePullRequest,
|
||||||
pushBranch: mockPushBranch,
|
pushBranch: mockPushBranch,
|
||||||
buildPrBody: mockBuildPrBody,
|
buildPrBody: mockBuildPrBody,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockExecuteTask = vi.fn();
|
const mockExecuteTask = vi.fn();
|
||||||
vi.mock('../features/tasks/index.js', () => ({
|
vi.mock('../features/tasks/index.js', () => ({
|
||||||
executeTask: mockExecuteTask,
|
executeTask: mockExecuteTask,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock loadGlobalConfig
|
// Mock loadGlobalConfig
|
||||||
const mockLoadGlobalConfig = vi.fn();
|
const mockLoadGlobalConfig = vi.fn();
|
||||||
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => ({ ...(await importOriginal<Record<string, unknown>>()),
|
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => ({ ...(await importOriginal<Record<string, unknown>>()),
|
||||||
loadGlobalConfig: mockLoadGlobalConfig,
|
loadGlobalConfig: mockLoadGlobalConfig,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock execFileSync for git operations
|
// Mock execFileSync for git operations
|
||||||
const mockExecFileSync = vi.fn();
|
const mockExecFileSync = vi.fn();
|
||||||
vi.mock('node:child_process', () => ({
|
vi.mock('node:child_process', () => ({
|
||||||
execFileSync: mockExecFileSync,
|
execFileSync: mockExecFileSync,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock UI
|
// Mock UI
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
status: vi.fn(),
|
status: vi.fn(),
|
||||||
blankLine: vi.fn(),
|
blankLine: vi.fn(),
|
||||||
header: vi.fn(),
|
header: vi.fn(),
|
||||||
section: vi.fn(),
|
section: vi.fn(),
|
||||||
warn: vi.fn(),
|
warn: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
}));
|
}));
|
||||||
// Mock debug logger
|
// Mock debug logger
|
||||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
...(await importOriginal<Record<string, unknown>>()),
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { executePipeline } = await import('../features/pipeline/index.js');
|
const { executePipeline } = await import('../features/pipeline/index.js');
|
||||||
|
|
||||||
describe('executePipeline', () => {
|
describe('executePipeline', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Default: git operations succeed
|
// Default: git operations succeed
|
||||||
mockExecFileSync.mockReturnValue('abc1234\n');
|
mockExecFileSync.mockReturnValue('abc1234\n');
|
||||||
// Default: no pipeline config
|
// Default: no pipeline config
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
trustedDirectories: [],
|
defaultPiece: 'default',
|
||||||
defaultPiece: 'default',
|
logLevel: 'info',
|
||||||
logLevel: 'info',
|
provider: 'claude',
|
||||||
provider: 'claude',
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it('should return exit code 2 when neither --issue nor --task is specified', async () => {
|
||||||
it('should return exit code 2 when neither --issue nor --task is specified', async () => {
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
expect(exitCode).toBe(2);
|
});
|
||||||
});
|
|
||||||
|
it('should return exit code 2 when gh CLI is not available', async () => {
|
||||||
it('should return exit code 2 when gh CLI is not available', async () => {
|
mockCheckGhCli.mockReturnValueOnce({ available: false, error: 'gh not found' });
|
||||||
mockCheckGhCli.mockReturnValueOnce({ available: false, error: 'gh not found' });
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
issueNumber: 99,
|
||||||
issueNumber: 99,
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
expect(exitCode).toBe(2);
|
});
|
||||||
});
|
|
||||||
|
it('should return exit code 2 when issue fetch fails', async () => {
|
||||||
it('should return exit code 2 when issue fetch fails', async () => {
|
mockFetchIssue.mockImplementationOnce(() => {
|
||||||
mockFetchIssue.mockImplementationOnce(() => {
|
throw new Error('Issue not found');
|
||||||
throw new Error('Issue not found');
|
});
|
||||||
});
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
issueNumber: 999,
|
||||||
issueNumber: 999,
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
expect(exitCode).toBe(2);
|
});
|
||||||
});
|
|
||||||
|
it('should return exit code 3 when piece fails', async () => {
|
||||||
it('should return exit code 3 when piece fails', async () => {
|
mockFetchIssue.mockReturnValueOnce({
|
||||||
mockFetchIssue.mockReturnValueOnce({
|
number: 99,
|
||||||
number: 99,
|
title: 'Test issue',
|
||||||
title: 'Test issue',
|
body: 'Test body',
|
||||||
body: 'Test body',
|
labels: [],
|
||||||
labels: [],
|
comments: [],
|
||||||
comments: [],
|
});
|
||||||
});
|
mockExecuteTask.mockResolvedValueOnce(false);
|
||||||
mockExecuteTask.mockResolvedValueOnce(false);
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
issueNumber: 99,
|
||||||
issueNumber: 99,
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(3);
|
||||||
expect(exitCode).toBe(3);
|
});
|
||||||
});
|
|
||||||
|
it('should return exit code 0 on successful task-only execution', async () => {
|
||||||
it('should return exit code 0 on successful task-only execution', async () => {
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
task: 'Fix the bug',
|
||||||
task: 'Fix the bug',
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
expect(exitCode).toBe(0);
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith({
|
task: 'Fix the bug',
|
||||||
task: 'Fix the bug',
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
pieceIdentifier: 'default',
|
||||||
pieceIdentifier: 'default',
|
projectCwd: '/tmp/test',
|
||||||
projectCwd: '/tmp/test',
|
agentOverrides: undefined,
|
||||||
agentOverrides: undefined,
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it('passes provider/model overrides to task execution', async () => {
|
||||||
it('passes provider/model overrides to task execution', async () => {
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
task: 'Fix the bug',
|
||||||
task: 'Fix the bug',
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
provider: 'codex',
|
||||||
provider: 'codex',
|
model: 'codex-model',
|
||||||
model: 'codex-model',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
expect(exitCode).toBe(0);
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith({
|
task: 'Fix the bug',
|
||||||
task: 'Fix the bug',
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
pieceIdentifier: 'default',
|
||||||
pieceIdentifier: 'default',
|
projectCwd: '/tmp/test',
|
||||||
projectCwd: '/tmp/test',
|
agentOverrides: { provider: 'codex', model: 'codex-model' },
|
||||||
agentOverrides: { provider: 'codex', model: 'codex-model' },
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it('should return exit code 5 when PR creation fails', async () => {
|
||||||
it('should return exit code 5 when PR creation fails', async () => {
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
mockCreatePullRequest.mockReturnValueOnce({ success: false, error: 'PR failed' });
|
||||||
mockCreatePullRequest.mockReturnValueOnce({ success: false, error: 'PR failed' });
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
task: 'Fix the bug',
|
||||||
task: 'Fix the bug',
|
piece: 'default',
|
||||||
piece: 'default',
|
autoPr: true,
|
||||||
autoPr: true,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(5);
|
||||||
expect(exitCode).toBe(5);
|
});
|
||||||
});
|
|
||||||
|
it('should create PR with correct branch when --auto-pr', async () => {
|
||||||
it('should create PR with correct branch when --auto-pr', async () => {
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/test/pr/1' });
|
||||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/test/pr/1' });
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
task: 'Fix the bug',
|
||||||
task: 'Fix the bug',
|
piece: 'default',
|
||||||
piece: 'default',
|
branch: 'fix/my-branch',
|
||||||
branch: 'fix/my-branch',
|
autoPr: true,
|
||||||
autoPr: true,
|
repo: 'owner/repo',
|
||||||
repo: 'owner/repo',
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
expect(exitCode).toBe(0);
|
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
'/tmp/test',
|
||||||
'/tmp/test',
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
branch: 'fix/my-branch',
|
||||||
branch: 'fix/my-branch',
|
repo: 'owner/repo',
|
||||||
repo: 'owner/repo',
|
}),
|
||||||
}),
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
it('should use --task when both --task and positional task are provided', async () => {
|
||||||
it('should use --task when both --task and positional task are provided', async () => {
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
const exitCode = await executePipeline({
|
task: 'From --task flag',
|
||||||
task: 'From --task flag',
|
piece: 'magi',
|
||||||
piece: 'magi',
|
autoPr: false,
|
||||||
autoPr: false,
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
expect(exitCode).toBe(0);
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith({
|
task: 'From --task flag',
|
||||||
task: 'From --task flag',
|
cwd: '/tmp/test',
|
||||||
cwd: '/tmp/test',
|
pieceIdentifier: 'magi',
|
||||||
pieceIdentifier: 'magi',
|
projectCwd: '/tmp/test',
|
||||||
projectCwd: '/tmp/test',
|
agentOverrides: undefined,
|
||||||
agentOverrides: undefined,
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
describe('PipelineConfig template expansion', () => {
|
||||||
describe('PipelineConfig template expansion', () => {
|
it('should use commit_message_template when configured', async () => {
|
||||||
it('should use commit_message_template when configured', async () => {
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
language: 'en',
|
||||||
language: 'en',
|
defaultPiece: 'default',
|
||||||
trustedDirectories: [],
|
logLevel: 'info',
|
||||||
defaultPiece: 'default',
|
provider: 'claude',
|
||||||
logLevel: 'info',
|
pipeline: {
|
||||||
provider: 'claude',
|
commitMessageTemplate: 'fix: {title} (#{issue})',
|
||||||
pipeline: {
|
},
|
||||||
commitMessageTemplate: 'fix: {title} (#{issue})',
|
});
|
||||||
},
|
|
||||||
});
|
mockFetchIssue.mockReturnValueOnce({
|
||||||
|
number: 42,
|
||||||
mockFetchIssue.mockReturnValueOnce({
|
title: 'Login broken',
|
||||||
number: 42,
|
body: 'Cannot login.',
|
||||||
title: 'Login broken',
|
labels: [],
|
||||||
body: 'Cannot login.',
|
comments: [],
|
||||||
labels: [],
|
});
|
||||||
comments: [],
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
});
|
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
await executePipeline({
|
||||||
|
issueNumber: 42,
|
||||||
await executePipeline({
|
piece: 'default',
|
||||||
issueNumber: 42,
|
branch: 'test-branch',
|
||||||
piece: 'default',
|
autoPr: false,
|
||||||
branch: 'test-branch',
|
cwd: '/tmp/test',
|
||||||
autoPr: false,
|
});
|
||||||
cwd: '/tmp/test',
|
|
||||||
});
|
// Verify commit was called with expanded template
|
||||||
|
const commitCall = mockExecFileSync.mock.calls.find(
|
||||||
// Verify commit was called with expanded template
|
(call: unknown[]) => call[0] === 'git' && (call[1] as string[])[0] === 'commit',
|
||||||
const commitCall = mockExecFileSync.mock.calls.find(
|
);
|
||||||
(call: unknown[]) => call[0] === 'git' && (call[1] as string[])[0] === 'commit',
|
expect(commitCall).toBeDefined();
|
||||||
);
|
expect((commitCall![1] as string[])[2]).toBe('fix: Login broken (#42)');
|
||||||
expect(commitCall).toBeDefined();
|
});
|
||||||
expect((commitCall![1] as string[])[2]).toBe('fix: Login broken (#42)');
|
|
||||||
});
|
it('should use default_branch_prefix when configured', async () => {
|
||||||
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
it('should use default_branch_prefix when configured', async () => {
|
language: 'en',
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
defaultPiece: 'default',
|
||||||
language: 'en',
|
logLevel: 'info',
|
||||||
trustedDirectories: [],
|
provider: 'claude',
|
||||||
defaultPiece: 'default',
|
pipeline: {
|
||||||
logLevel: 'info',
|
defaultBranchPrefix: 'feat/',
|
||||||
provider: 'claude',
|
},
|
||||||
pipeline: {
|
});
|
||||||
defaultBranchPrefix: 'feat/',
|
|
||||||
},
|
mockFetchIssue.mockReturnValueOnce({
|
||||||
});
|
number: 10,
|
||||||
|
title: 'Add feature',
|
||||||
mockFetchIssue.mockReturnValueOnce({
|
body: 'Please add.',
|
||||||
number: 10,
|
labels: [],
|
||||||
title: 'Add feature',
|
comments: [],
|
||||||
body: 'Please add.',
|
});
|
||||||
labels: [],
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
comments: [],
|
|
||||||
});
|
await executePipeline({
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
issueNumber: 10,
|
||||||
|
piece: 'default',
|
||||||
await executePipeline({
|
autoPr: false,
|
||||||
issueNumber: 10,
|
cwd: '/tmp/test',
|
||||||
piece: 'default',
|
});
|
||||||
autoPr: false,
|
|
||||||
cwd: '/tmp/test',
|
// Verify checkout -b was called with prefix
|
||||||
});
|
const checkoutCall = mockExecFileSync.mock.calls.find(
|
||||||
|
(call: unknown[]) => call[0] === 'git' && (call[1] as string[])[0] === 'checkout' && (call[1] as string[])[1] === '-b',
|
||||||
// Verify checkout -b was called with prefix
|
);
|
||||||
const checkoutCall = mockExecFileSync.mock.calls.find(
|
expect(checkoutCall).toBeDefined();
|
||||||
(call: unknown[]) => call[0] === 'git' && (call[1] as string[])[0] === 'checkout' && (call[1] as string[])[1] === '-b',
|
const branchName = (checkoutCall![1] as string[])[2];
|
||||||
);
|
expect(branchName).toMatch(/^feat\/issue-10-\d+$/);
|
||||||
expect(checkoutCall).toBeDefined();
|
});
|
||||||
const branchName = (checkoutCall![1] as string[])[2];
|
|
||||||
expect(branchName).toMatch(/^feat\/issue-10-\d+$/);
|
it('should use pr_body_template when configured for PR creation', async () => {
|
||||||
});
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
|
language: 'en',
|
||||||
it('should use pr_body_template when configured for PR creation', async () => {
|
defaultPiece: 'default',
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
logLevel: 'info',
|
||||||
language: 'en',
|
provider: 'claude',
|
||||||
trustedDirectories: [],
|
pipeline: {
|
||||||
defaultPiece: 'default',
|
prBodyTemplate: '## Summary\n{issue_body}\n\nCloses #{issue}',
|
||||||
logLevel: 'info',
|
},
|
||||||
provider: 'claude',
|
});
|
||||||
pipeline: {
|
|
||||||
prBodyTemplate: '## Summary\n{issue_body}\n\nCloses #{issue}',
|
mockFetchIssue.mockReturnValueOnce({
|
||||||
},
|
number: 50,
|
||||||
});
|
title: 'Fix auth',
|
||||||
|
body: 'Auth is broken.',
|
||||||
mockFetchIssue.mockReturnValueOnce({
|
labels: [],
|
||||||
number: 50,
|
comments: [],
|
||||||
title: 'Fix auth',
|
});
|
||||||
body: 'Auth is broken.',
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
labels: [],
|
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/pr/1' });
|
||||||
comments: [],
|
|
||||||
});
|
await executePipeline({
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
issueNumber: 50,
|
||||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/pr/1' });
|
piece: 'default',
|
||||||
|
branch: 'fix-auth',
|
||||||
await executePipeline({
|
autoPr: true,
|
||||||
issueNumber: 50,
|
cwd: '/tmp/test',
|
||||||
piece: 'default',
|
});
|
||||||
branch: 'fix-auth',
|
|
||||||
autoPr: true,
|
// When prBodyTemplate is set, buildPrBody (mock) should NOT be called
|
||||||
cwd: '/tmp/test',
|
// Instead, the template is expanded directly
|
||||||
});
|
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||||
|
'/tmp/test',
|
||||||
// When prBodyTemplate is set, buildPrBody (mock) should NOT be called
|
expect.objectContaining({
|
||||||
// Instead, the template is expanded directly
|
body: '## Summary\nAuth is broken.\n\nCloses #50',
|
||||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
}),
|
||||||
'/tmp/test',
|
);
|
||||||
expect.objectContaining({
|
});
|
||||||
body: '## Summary\nAuth is broken.\n\nCloses #50',
|
|
||||||
}),
|
it('should fall back to buildPrBody when no template is configured', async () => {
|
||||||
);
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
});
|
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/pr/1' });
|
||||||
|
|
||||||
it('should fall back to buildPrBody when no template is configured', async () => {
|
await executePipeline({
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
task: 'Fix bug',
|
||||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/pr/1' });
|
piece: 'default',
|
||||||
|
branch: 'fix-branch',
|
||||||
await executePipeline({
|
autoPr: true,
|
||||||
task: 'Fix bug',
|
cwd: '/tmp/test',
|
||||||
piece: 'default',
|
});
|
||||||
branch: 'fix-branch',
|
|
||||||
autoPr: true,
|
// Should use buildPrBody (the mock)
|
||||||
cwd: '/tmp/test',
|
expect(mockBuildPrBody).toHaveBeenCalled();
|
||||||
});
|
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||||
|
'/tmp/test',
|
||||||
// Should use buildPrBody (the mock)
|
expect.objectContaining({
|
||||||
expect(mockBuildPrBody).toHaveBeenCalled();
|
body: 'Default PR body',
|
||||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
}),
|
||||||
'/tmp/test',
|
);
|
||||||
expect.objectContaining({
|
});
|
||||||
body: 'Default PR body',
|
});
|
||||||
}),
|
|
||||||
);
|
describe('--skip-git', () => {
|
||||||
});
|
it('should skip branch creation, commit, push when skipGit is true', async () => {
|
||||||
});
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
describe('--skip-git', () => {
|
const exitCode = await executePipeline({
|
||||||
it('should skip branch creation, commit, push when skipGit is true', async () => {
|
task: 'Fix the bug',
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
piece: 'default',
|
||||||
|
autoPr: false,
|
||||||
const exitCode = await executePipeline({
|
skipGit: true,
|
||||||
task: 'Fix the bug',
|
cwd: '/tmp/test',
|
||||||
piece: 'default',
|
});
|
||||||
autoPr: false,
|
|
||||||
skipGit: true,
|
expect(exitCode).toBe(0);
|
||||||
cwd: '/tmp/test',
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
});
|
task: 'Fix the bug',
|
||||||
|
cwd: '/tmp/test',
|
||||||
expect(exitCode).toBe(0);
|
pieceIdentifier: 'default',
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith({
|
projectCwd: '/tmp/test',
|
||||||
task: 'Fix the bug',
|
agentOverrides: undefined,
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
pieceIdentifier: 'default',
|
|
||||||
projectCwd: '/tmp/test',
|
// No git operations should have been called
|
||||||
agentOverrides: undefined,
|
const gitCalls = mockExecFileSync.mock.calls.filter(
|
||||||
});
|
(call: unknown[]) => call[0] === 'git',
|
||||||
|
);
|
||||||
// No git operations should have been called
|
expect(gitCalls).toHaveLength(0);
|
||||||
const gitCalls = mockExecFileSync.mock.calls.filter(
|
expect(mockPushBranch).not.toHaveBeenCalled();
|
||||||
(call: unknown[]) => call[0] === 'git',
|
});
|
||||||
);
|
|
||||||
expect(gitCalls).toHaveLength(0);
|
it('should ignore --auto-pr when skipGit is true', async () => {
|
||||||
expect(mockPushBranch).not.toHaveBeenCalled();
|
mockExecuteTask.mockResolvedValueOnce(true);
|
||||||
});
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
it('should ignore --auto-pr when skipGit is true', async () => {
|
task: 'Fix the bug',
|
||||||
mockExecuteTask.mockResolvedValueOnce(true);
|
piece: 'default',
|
||||||
|
autoPr: true,
|
||||||
const exitCode = await executePipeline({
|
skipGit: true,
|
||||||
task: 'Fix the bug',
|
cwd: '/tmp/test',
|
||||||
piece: 'default',
|
});
|
||||||
autoPr: true,
|
|
||||||
skipGit: true,
|
expect(exitCode).toBe(0);
|
||||||
cwd: '/tmp/test',
|
expect(mockCreatePullRequest).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
it('should still return piece failure exit code when skipGit is true', async () => {
|
||||||
expect(mockCreatePullRequest).not.toHaveBeenCalled();
|
mockExecuteTask.mockResolvedValueOnce(false);
|
||||||
});
|
|
||||||
|
const exitCode = await executePipeline({
|
||||||
it('should still return piece failure exit code when skipGit is true', async () => {
|
task: 'Fix the bug',
|
||||||
mockExecuteTask.mockResolvedValueOnce(false);
|
piece: 'default',
|
||||||
|
autoPr: false,
|
||||||
const exitCode = await executePipeline({
|
skipGit: true,
|
||||||
task: 'Fix the bug',
|
cwd: '/tmp/test',
|
||||||
piece: 'default',
|
});
|
||||||
autoPr: false,
|
|
||||||
skipGit: true,
|
expect(exitCode).toBe(3);
|
||||||
cwd: '/tmp/test',
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
expect(exitCode).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,273 +1,271 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for summarizeTaskName
|
* Tests for summarizeTaskName
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
vi.mock('../infra/providers/index.js', () => ({
|
vi.mock('../infra/providers/index.js', () => ({
|
||||||
getProvider: vi.fn(),
|
getProvider: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||||
loadGlobalConfig: vi.fn(),
|
loadGlobalConfig: vi.fn(),
|
||||||
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
...(await importOriginal<Record<string, unknown>>()),
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getProvider } from '../infra/providers/index.js';
|
import { getProvider } from '../infra/providers/index.js';
|
||||||
import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
|
import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
|
||||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||||
|
|
||||||
const mockGetProvider = vi.mocked(getProvider);
|
const mockGetProvider = vi.mocked(getProvider);
|
||||||
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
||||||
|
|
||||||
const mockProviderCall = vi.fn();
|
const mockProviderCall = vi.fn();
|
||||||
const mockProvider = {
|
const mockProvider = {
|
||||||
call: mockProviderCall,
|
call: mockProviderCall,
|
||||||
callCustom: vi.fn(),
|
callCustom: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockGetProvider.mockReturnValue(mockProvider);
|
mockGetProvider.mockReturnValue(mockProvider);
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
language: 'ja',
|
language: 'ja',
|
||||||
trustedDirectories: [],
|
defaultPiece: 'default',
|
||||||
defaultPiece: 'default',
|
logLevel: 'info',
|
||||||
logLevel: 'info',
|
provider: 'claude',
|
||||||
provider: 'claude',
|
model: 'haiku',
|
||||||
model: 'haiku',
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
describe('summarizeTaskName', () => {
|
||||||
describe('summarizeTaskName', () => {
|
it('should return AI-generated slug for task name', async () => {
|
||||||
it('should return AI-generated slug for Japanese task name', async () => {
|
// Given: AI returns a slug for input
|
||||||
// Given: AI returns a slug for Japanese input
|
mockProviderCall.mockResolvedValue({
|
||||||
mockProviderCall.mockResolvedValue({
|
agent: 'summarizer',
|
||||||
agent: 'summarizer',
|
status: 'done',
|
||||||
status: 'done',
|
content: 'add-auth',
|
||||||
content: 'add-auth',
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
});
|
||||||
});
|
|
||||||
|
// When
|
||||||
// When
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
const result = await summarizeTaskName('認証機能を追加する', { cwd: '/project' });
|
|
||||||
|
// Then
|
||||||
// Then
|
expect(result).toBe('add-auth');
|
||||||
expect(result).toBe('add-auth');
|
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
||||||
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
'summarizer',
|
||||||
'summarizer',
|
'long task name for testing',
|
||||||
'認証機能を追加する',
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
cwd: '/project',
|
||||||
cwd: '/project',
|
model: 'haiku',
|
||||||
model: 'haiku',
|
allowedTools: [],
|
||||||
allowedTools: [],
|
})
|
||||||
})
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
it('should return AI-generated slug for English task name', async () => {
|
||||||
it('should return AI-generated slug for English task name', async () => {
|
// Given
|
||||||
// Given
|
mockProviderCall.mockResolvedValue({
|
||||||
mockProviderCall.mockResolvedValue({
|
agent: 'summarizer',
|
||||||
agent: 'summarizer',
|
status: 'done',
|
||||||
status: 'done',
|
content: 'fix-login-bug',
|
||||||
content: 'fix-login-bug',
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
});
|
||||||
});
|
|
||||||
|
// When
|
||||||
// When
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
const result = await summarizeTaskName('Fix the login bug', { cwd: '/project' });
|
|
||||||
|
// Then
|
||||||
// Then
|
expect(result).toBe('fix-login-bug');
|
||||||
expect(result).toBe('fix-login-bug');
|
});
|
||||||
});
|
|
||||||
|
it('should clean up AI response with extra characters', async () => {
|
||||||
it('should clean up AI response with extra characters', async () => {
|
// Given: AI response has extra whitespace or formatting
|
||||||
// Given: AI response has extra whitespace or formatting
|
mockProviderCall.mockResolvedValue({
|
||||||
mockProviderCall.mockResolvedValue({
|
agent: 'summarizer',
|
||||||
agent: 'summarizer',
|
status: 'done',
|
||||||
status: 'done',
|
content: ' Add-User-Auth! \n',
|
||||||
content: ' Add-User-Auth! \n',
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
});
|
||||||
});
|
|
||||||
|
// When
|
||||||
// When
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
const result = await summarizeTaskName('ユーザー認証を追加', { cwd: '/project' });
|
|
||||||
|
// Then
|
||||||
// Then
|
expect(result).toBe('add-user-auth');
|
||||||
expect(result).toBe('add-user-auth');
|
});
|
||||||
});
|
|
||||||
|
it('should truncate long slugs to 30 characters without trailing hyphen', async () => {
|
||||||
it('should truncate long slugs to 30 characters without trailing hyphen', async () => {
|
// Given: AI returns a long slug
|
||||||
// Given: AI returns a long slug
|
mockProviderCall.mockResolvedValue({
|
||||||
mockProviderCall.mockResolvedValue({
|
agent: 'summarizer',
|
||||||
agent: 'summarizer',
|
status: 'done',
|
||||||
status: 'done',
|
content: 'this-is-a-very-long-slug-that-exceeds-thirty-characters',
|
||||||
content: 'this-is-a-very-long-slug-that-exceeds-thirty-characters',
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
});
|
||||||
});
|
|
||||||
|
// When
|
||||||
// When
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
const result = await summarizeTaskName('長いタスク名', { cwd: '/project' });
|
|
||||||
|
// Then
|
||||||
// Then
|
expect(result.length).toBeLessThanOrEqual(30);
|
||||||
expect(result.length).toBeLessThanOrEqual(30);
|
expect(result).toBe('this-is-a-very-long-slug-that');
|
||||||
expect(result).toBe('this-is-a-very-long-slug-that');
|
expect(result).not.toMatch(/-$/); // No trailing hyphen
|
||||||
expect(result).not.toMatch(/-$/); // No trailing hyphen
|
});
|
||||||
});
|
|
||||||
|
it('should return "task" as fallback for empty AI response', async () => {
|
||||||
it('should return "task" as fallback for empty AI response', async () => {
|
// Given: AI returns empty string
|
||||||
// Given: AI returns empty string
|
mockProviderCall.mockResolvedValue({
|
||||||
mockProviderCall.mockResolvedValue({
|
agent: 'summarizer',
|
||||||
agent: 'summarizer',
|
status: 'done',
|
||||||
status: 'done',
|
content: '',
|
||||||
content: '',
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
});
|
||||||
});
|
|
||||||
|
// When
|
||||||
// When
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
const result = await summarizeTaskName('test', { cwd: '/project' });
|
|
||||||
|
// Then
|
||||||
// Then
|
expect(result).toBe('task');
|
||||||
expect(result).toBe('task');
|
});
|
||||||
});
|
|
||||||
|
it('should use custom model if specified in options', async () => {
|
||||||
it('should use custom model if specified in options', async () => {
|
// Given
|
||||||
// Given
|
mockProviderCall.mockResolvedValue({
|
||||||
mockProviderCall.mockResolvedValue({
|
agent: 'summarizer',
|
||||||
agent: 'summarizer',
|
status: 'done',
|
||||||
status: 'done',
|
content: 'custom-task',
|
||||||
content: 'custom-task',
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
});
|
||||||
});
|
|
||||||
|
// When
|
||||||
// When
|
await summarizeTaskName('test', { cwd: '/project', model: 'sonnet' });
|
||||||
await summarizeTaskName('test', { cwd: '/project', model: 'sonnet' });
|
|
||||||
|
// Then
|
||||||
// Then
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
'summarizer',
|
||||||
'summarizer',
|
expect.any(String),
|
||||||
expect.any(String),
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
model: 'sonnet',
|
||||||
model: 'sonnet',
|
})
|
||||||
})
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
it('should use provider from config.yaml', async () => {
|
||||||
it('should use provider from config.yaml', async () => {
|
// Given: config has codex provider
|
||||||
// Given: config has codex provider
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
language: 'ja',
|
||||||
language: 'ja',
|
defaultPiece: 'default',
|
||||||
trustedDirectories: [],
|
logLevel: 'info',
|
||||||
defaultPiece: 'default',
|
provider: 'codex',
|
||||||
logLevel: 'info',
|
model: 'gpt-4',
|
||||||
provider: 'codex',
|
});
|
||||||
model: 'gpt-4',
|
mockProviderCall.mockResolvedValue({
|
||||||
});
|
agent: 'summarizer',
|
||||||
mockProviderCall.mockResolvedValue({
|
status: 'done',
|
||||||
agent: 'summarizer',
|
content: 'codex-task',
|
||||||
status: 'done',
|
timestamp: new Date(),
|
||||||
content: 'codex-task',
|
});
|
||||||
timestamp: new Date(),
|
|
||||||
});
|
// When
|
||||||
|
await summarizeTaskName('test', { cwd: '/project' });
|
||||||
// When
|
|
||||||
await summarizeTaskName('test', { cwd: '/project' });
|
// Then
|
||||||
|
expect(mockGetProvider).toHaveBeenCalledWith('codex');
|
||||||
// Then
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||||
expect(mockGetProvider).toHaveBeenCalledWith('codex');
|
'summarizer',
|
||||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
expect.any(String),
|
||||||
'summarizer',
|
expect.objectContaining({
|
||||||
expect.any(String),
|
model: 'gpt-4',
|
||||||
expect.objectContaining({
|
})
|
||||||
model: 'gpt-4',
|
);
|
||||||
})
|
});
|
||||||
);
|
|
||||||
});
|
it('should remove consecutive hyphens', async () => {
|
||||||
|
// Given: AI response has consecutive hyphens
|
||||||
it('should remove consecutive hyphens', async () => {
|
mockProviderCall.mockResolvedValue({
|
||||||
// Given: AI response has consecutive hyphens
|
agent: 'summarizer',
|
||||||
mockProviderCall.mockResolvedValue({
|
status: 'done',
|
||||||
agent: 'summarizer',
|
content: 'fix---multiple---hyphens',
|
||||||
status: 'done',
|
timestamp: new Date(),
|
||||||
content: 'fix---multiple---hyphens',
|
});
|
||||||
timestamp: new Date(),
|
|
||||||
});
|
// When
|
||||||
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
// When
|
|
||||||
const result = await summarizeTaskName('test', { cwd: '/project' });
|
// Then
|
||||||
|
expect(result).toBe('fix-multiple-hyphens');
|
||||||
// Then
|
});
|
||||||
expect(result).toBe('fix-multiple-hyphens');
|
|
||||||
});
|
it('should remove leading and trailing hyphens', async () => {
|
||||||
|
// Given: AI response has leading/trailing hyphens
|
||||||
it('should remove leading and trailing hyphens', async () => {
|
mockProviderCall.mockResolvedValue({
|
||||||
// Given: AI response has leading/trailing hyphens
|
agent: 'summarizer',
|
||||||
mockProviderCall.mockResolvedValue({
|
status: 'done',
|
||||||
agent: 'summarizer',
|
content: '-leading-trailing-',
|
||||||
status: 'done',
|
timestamp: new Date(),
|
||||||
content: '-leading-trailing-',
|
});
|
||||||
timestamp: new Date(),
|
|
||||||
});
|
// When
|
||||||
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
||||||
// When
|
|
||||||
const result = await summarizeTaskName('test', { cwd: '/project' });
|
// Then
|
||||||
|
expect(result).toBe('leading-trailing');
|
||||||
// Then
|
});
|
||||||
expect(result).toBe('leading-trailing');
|
|
||||||
});
|
it('should throw error when config load fails', async () => {
|
||||||
|
// Given: config loading throws error
|
||||||
it('should throw error when config load fails', async () => {
|
mockLoadGlobalConfig.mockImplementation(() => {
|
||||||
// Given: config loading throws error
|
throw new Error('Config not found');
|
||||||
mockLoadGlobalConfig.mockImplementation(() => {
|
});
|
||||||
throw new Error('Config not found');
|
|
||||||
});
|
// When/Then
|
||||||
|
await expect(summarizeTaskName('test', { cwd: '/project' })).rejects.toThrow('Config not found');
|
||||||
// When/Then
|
});
|
||||||
await expect(summarizeTaskName('test', { cwd: '/project' })).rejects.toThrow('Config not found');
|
|
||||||
});
|
it('should use romanization when useLLM is false', async () => {
|
||||||
|
// When: useLLM is explicitly false
|
||||||
it('should use romanization when useLLM is false', async () => {
|
const result = await summarizeTaskName('romanization test', { cwd: '/project', useLLM: false });
|
||||||
// When: useLLM is explicitly false
|
|
||||||
const result = await summarizeTaskName('認証機能を追加する', { cwd: '/project', useLLM: false });
|
// Then: should not call provider, should return romaji
|
||||||
|
expect(mockProviderCall).not.toHaveBeenCalled();
|
||||||
// Then: should not call provider, should return romaji
|
expect(result).toMatch(/^[a-z0-9-]+$/);
|
||||||
expect(mockProviderCall).not.toHaveBeenCalled();
|
expect(result.length).toBeLessThanOrEqual(30);
|
||||||
expect(result).toMatch(/^[a-z0-9-]+$/);
|
});
|
||||||
expect(result.length).toBeLessThanOrEqual(30);
|
|
||||||
});
|
it('should handle mixed Japanese/English with romanization', async () => {
|
||||||
|
// When
|
||||||
it('should handle mixed Japanese/English with romanization', async () => {
|
const result = await summarizeTaskName('Add romanization', { cwd: '/project', useLLM: false });
|
||||||
// When
|
|
||||||
const result = await summarizeTaskName('Add 認証機能', { cwd: '/project', useLLM: false });
|
// Then
|
||||||
|
expect(result).toMatch(/^[a-z0-9-]+$/);
|
||||||
// Then
|
expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens
|
||||||
expect(result).toMatch(/^[a-z0-9-]+$/);
|
});
|
||||||
expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens
|
|
||||||
});
|
it('should use LLM by default', async () => {
|
||||||
|
// Given
|
||||||
it('should use LLM by default', async () => {
|
mockProviderCall.mockResolvedValue({
|
||||||
// Given
|
agent: 'summarizer',
|
||||||
mockProviderCall.mockResolvedValue({
|
status: 'done',
|
||||||
agent: 'summarizer',
|
content: 'add-auth',
|
||||||
status: 'done',
|
timestamp: new Date(),
|
||||||
content: 'add-auth',
|
});
|
||||||
timestamp: new Date(),
|
|
||||||
});
|
// When: useLLM not specified (defaults to true)
|
||||||
|
await summarizeTaskName('test', { cwd: '/project' });
|
||||||
// When: useLLM not specified (defaults to true)
|
|
||||||
await summarizeTaskName('test', { cwd: '/project' });
|
// Then: should call provider
|
||||||
|
expect(mockProviderCall).toHaveBeenCalled();
|
||||||
// Then: should call provider
|
});
|
||||||
expect(mockProviderCall).toHaveBeenCalled();
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export interface PipelineConfig {
|
|||||||
/** Global configuration for takt */
|
/** Global configuration for takt */
|
||||||
export interface GlobalConfig {
|
export interface GlobalConfig {
|
||||||
language: Language;
|
language: Language;
|
||||||
trustedDirectories: string[];
|
|
||||||
defaultPiece: string;
|
defaultPiece: string;
|
||||||
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
||||||
provider?: 'claude' | 'codex' | 'mock';
|
provider?: 'claude' | 'codex' | 'mock';
|
||||||
|
|||||||
@ -216,7 +216,6 @@ export const PieceCategoryConfigSchema = z.record(z.string(), PieceCategoryConfi
|
|||||||
/** Global config schema */
|
/** Global config schema */
|
||||||
export const GlobalConfigSchema = z.object({
|
export const GlobalConfigSchema = z.object({
|
||||||
language: LanguageSchema.optional().default(DEFAULT_LANGUAGE),
|
language: LanguageSchema.optional().default(DEFAULT_LANGUAGE),
|
||||||
trusted_directories: z.array(z.string()).optional().default([]),
|
|
||||||
default_piece: z.string().optional().default('default'),
|
default_piece: z.string().optional().default('default'),
|
||||||
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
|
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
|
||||||
provider: z.enum(['claude', 'codex', 'mock']).optional().default('claude'),
|
provider: z.enum(['claude', 'codex', 'mock']).optional().default('claude'),
|
||||||
|
|||||||
@ -285,6 +285,11 @@ export async function interactiveMode(
|
|||||||
|
|
||||||
const result = await callAIWithRetry(initialInput, prompts.systemPrompt);
|
const result = await callAIWithRetry(initialInput, prompts.systemPrompt);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if (!result.success) {
|
||||||
|
error(result.content);
|
||||||
|
blankLine();
|
||||||
|
return { confirmed: false, task: '' };
|
||||||
|
}
|
||||||
history.push({ role: 'assistant', content: result.content });
|
history.push({ role: 'assistant', content: result.content });
|
||||||
blankLine();
|
blankLine();
|
||||||
} else {
|
} else {
|
||||||
@ -332,6 +337,11 @@ export async function interactiveMode(
|
|||||||
info(prompts.ui.summarizeFailed);
|
info(prompts.ui.summarizeFailed);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!summaryResult.success) {
|
||||||
|
error(summaryResult.content);
|
||||||
|
blankLine();
|
||||||
|
return { confirmed: false, task: '' };
|
||||||
|
}
|
||||||
const task = summaryResult.content.trim();
|
const task = summaryResult.content.trim();
|
||||||
const confirmed = await confirmTask(
|
const confirmed = await confirmTask(
|
||||||
task,
|
task,
|
||||||
@ -362,6 +372,12 @@ export async function interactiveMode(
|
|||||||
|
|
||||||
const result = await callAIWithRetry(trimmed, prompts.systemPrompt);
|
const result = await callAIWithRetry(trimmed, prompts.systemPrompt);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if (!result.success) {
|
||||||
|
error(result.content);
|
||||||
|
blankLine();
|
||||||
|
history.pop();
|
||||||
|
return { confirmed: false, task: '' };
|
||||||
|
}
|
||||||
history.push({ role: 'assistant', content: result.content });
|
history.push({ role: 'assistant', content: result.content });
|
||||||
blankLine();
|
blankLine();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||||
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
||||||
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
||||||
@ -17,7 +16,6 @@ import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
|||||||
function createDefaultGlobalConfig(): GlobalConfig {
|
function createDefaultGlobalConfig(): GlobalConfig {
|
||||||
return {
|
return {
|
||||||
language: DEFAULT_LANGUAGE,
|
language: DEFAULT_LANGUAGE,
|
||||||
trustedDirectories: [],
|
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
@ -68,7 +66,6 @@ export class GlobalConfigManager {
|
|||||||
const parsed = GlobalConfigSchema.parse(raw);
|
const parsed = GlobalConfigSchema.parse(raw);
|
||||||
const config: GlobalConfig = {
|
const config: GlobalConfig = {
|
||||||
language: parsed.language,
|
language: parsed.language,
|
||||||
trustedDirectories: parsed.trusted_directories,
|
|
||||||
defaultPiece: parsed.default_piece,
|
defaultPiece: parsed.default_piece,
|
||||||
logLevel: parsed.log_level,
|
logLevel: parsed.log_level,
|
||||||
provider: parsed.provider,
|
provider: parsed.provider,
|
||||||
@ -100,7 +97,6 @@ export class GlobalConfigManager {
|
|||||||
const configPath = getGlobalConfigPath();
|
const configPath = getGlobalConfigPath();
|
||||||
const raw: Record<string, unknown> = {
|
const raw: Record<string, unknown> = {
|
||||||
language: config.language,
|
language: config.language,
|
||||||
trusted_directories: config.trustedDirectories,
|
|
||||||
default_piece: config.defaultPiece,
|
default_piece: config.defaultPiece,
|
||||||
log_level: config.logLevel,
|
log_level: config.logLevel,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
@ -203,23 +199,6 @@ export function setProvider(provider: 'claude' | 'codex'): void {
|
|||||||
saveGlobalConfig(config);
|
saveGlobalConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addTrustedDirectory(dir: string): void {
|
|
||||||
const config = loadGlobalConfig();
|
|
||||||
const resolvedDir = join(dir);
|
|
||||||
if (!config.trustedDirectories.includes(resolvedDir)) {
|
|
||||||
config.trustedDirectories.push(resolvedDir);
|
|
||||||
saveGlobalConfig(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDirectoryTrusted(dir: string): boolean {
|
|
||||||
const config = loadGlobalConfig();
|
|
||||||
const resolvedDir = join(dir);
|
|
||||||
return config.trustedDirectories.some(
|
|
||||||
(trusted) => resolvedDir === trusted || resolvedDir.startsWith(trusted + '/')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the Anthropic API key.
|
* Resolve the Anthropic API key.
|
||||||
* Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
* Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
||||||
@ -290,4 +269,3 @@ export function getEffectiveDebugConfig(projectDir?: string): DebugConfig | unde
|
|||||||
|
|
||||||
return debugConfig;
|
return debugConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,6 @@ export {
|
|||||||
getLanguage,
|
getLanguage,
|
||||||
setLanguage,
|
setLanguage,
|
||||||
setProvider,
|
setProvider,
|
||||||
addTrustedDirectory,
|
|
||||||
isDirectoryTrusted,
|
|
||||||
resolveAnthropicApiKey,
|
resolveAnthropicApiKey,
|
||||||
resolveOpenaiApiKey,
|
resolveOpenaiApiKey,
|
||||||
loadProjectDebugConfig,
|
loadProjectDebugConfig,
|
||||||
|
|||||||
@ -28,8 +28,6 @@ export {
|
|||||||
loadGlobalConfig,
|
loadGlobalConfig,
|
||||||
saveGlobalConfig,
|
saveGlobalConfig,
|
||||||
invalidateGlobalConfigCache,
|
invalidateGlobalConfigCache,
|
||||||
addTrustedDirectory,
|
|
||||||
isDirectoryTrusted,
|
|
||||||
loadProjectDebugConfig,
|
loadProjectDebugConfig,
|
||||||
getEffectiveDebugConfig,
|
getEffectiveDebugConfig,
|
||||||
} from '../global/globalConfig.js';
|
} from '../global/globalConfig.js';
|
||||||
|
|||||||
@ -2,14 +2,40 @@
|
|||||||
* Codex provider implementation
|
* Codex provider implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js';
|
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js';
|
||||||
import { resolveOpenaiApiKey } from '../config/index.js';
|
import { resolveOpenaiApiKey } from '../config/index.js';
|
||||||
import type { AgentResponse } from '../../core/models/index.js';
|
import type { AgentResponse } from '../../core/models/index.js';
|
||||||
import type { Provider, ProviderCallOptions } from './types.js';
|
import type { Provider, ProviderCallOptions } from './types.js';
|
||||||
|
|
||||||
|
const NOT_GIT_REPO_MESSAGE =
|
||||||
|
'Codex をご利用の場合 Git 管理下のディレクトリでのみ動作します。';
|
||||||
|
|
||||||
|
function isInsideGitRepo(cwd: string): boolean {
|
||||||
|
try {
|
||||||
|
const result = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
||||||
|
cwd,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
}).trim();
|
||||||
|
return result === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Codex provider - wraps existing Codex client */
|
/** Codex provider - wraps existing Codex client */
|
||||||
export class CodexProvider implements Provider {
|
export class CodexProvider implements Provider {
|
||||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
||||||
|
if (!isInsideGitRepo(options.cwd)) {
|
||||||
|
return {
|
||||||
|
agent: agentName,
|
||||||
|
status: 'blocked',
|
||||||
|
content: NOT_GIT_REPO_MESSAGE,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const callOptions: CodexCallOptions = {
|
const callOptions: CodexCallOptions = {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
sessionId: options.sessionId,
|
sessionId: options.sessionId,
|
||||||
@ -24,6 +50,15 @@ export class CodexProvider implements Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
async callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
||||||
|
if (!isInsideGitRepo(options.cwd)) {
|
||||||
|
return {
|
||||||
|
agent: agentName,
|
||||||
|
status: 'blocked',
|
||||||
|
content: NOT_GIT_REPO_MESSAGE,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const callOptions: CodexCallOptions = {
|
const callOptions: CodexCallOptions = {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
sessionId: options.sessionId,
|
sessionId: options.sessionId,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user