takt/e2e/helpers/isolated-env.ts
nrslib 798e89605d feat: TeamLeader に refill threshold と動的パート追加を導入
TeamLeaderRunner を4モジュールに分割(execution, aggregation, common, streaming)し、
パート完了時にキュー残数が refill_threshold 以下になると追加タスクを動的に生成する
worker pool 型の実行モデルを実装。ParallelLogger に LineTimeSliceBuffer を追加し
ストリーミング出力を改善。deep-research ピースに team_leader 設定を追加。
2026-02-26 22:33:22 +09:00

138 lines
4.3 KiB
TypeScript

import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
export interface IsolatedEnv {
runId: string;
taktDir: string;
env: NodeJS.ProcessEnv;
cleanup: () => void;
}
type E2EConfig = Record<string, unknown>;
type NotificationSoundEvents = Record<string, unknown>;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const E2E_CONFIG_FIXTURE_PATH = resolve(__dirname, '../fixtures/config.e2e.yaml');
function readE2EFixtureConfig(): E2EConfig {
const raw = readFileSync(E2E_CONFIG_FIXTURE_PATH, 'utf-8');
const parsed = parseYaml(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`Invalid E2E config fixture: ${E2E_CONFIG_FIXTURE_PATH}`);
}
return parsed as E2EConfig;
}
function writeConfigFile(taktDir: string, config: E2EConfig): void {
writeFileSync(join(taktDir, 'config.yaml'), `${stringifyYaml(config)}`);
}
function parseNotificationSoundEvents(
source: E2EConfig,
sourceName: string,
): NotificationSoundEvents | undefined {
const value = source.notification_sound_events;
if (value === undefined) {
return undefined;
}
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(
`Invalid notification_sound_events in ${sourceName}: expected object`,
);
}
return value as NotificationSoundEvents;
}
function mergeIsolatedConfig(
fixture: E2EConfig,
current: E2EConfig,
patch: E2EConfig,
): E2EConfig {
const merged: E2EConfig = { ...fixture, ...current, ...patch };
const fixtureEvents = parseNotificationSoundEvents(fixture, 'fixture');
const currentEvents = parseNotificationSoundEvents(current, 'current config');
const patchEvents = parseNotificationSoundEvents(patch, 'patch');
if (!fixtureEvents && !currentEvents && !patchEvents) {
return merged;
}
merged.notification_sound_events = {
...(fixtureEvents ?? {}),
...(currentEvents ?? {}),
...(patchEvents ?? {}),
};
return merged;
}
export function updateIsolatedConfig(taktDir: string, patch: E2EConfig): void {
const current = readE2EFixtureConfig();
const configPath = join(taktDir, 'config.yaml');
const raw = readFileSync(configPath, 'utf-8');
const parsed = parseYaml(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`Invalid isolated config: ${configPath}`);
}
writeConfigFile(taktDir, mergeIsolatedConfig(current, parsed as E2EConfig, patch));
}
/**
* Create an isolated environment for E2E testing.
*
* - Sets TAKT_CONFIG_DIR to a temporary directory
* - Sets GIT_CONFIG_GLOBAL to an isolated .gitconfig file
* - Uses the real ~/.claude/ for Claude authentication
*/
export function createIsolatedEnv(): IsolatedEnv {
const runId = `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const baseDir = mkdtempSync(join(tmpdir(), `takt-e2e-${runId}-`));
const taktDir = join(baseDir, '.takt');
const gitConfigPath = join(baseDir, '.gitconfig');
// Create TAKT config directory and config.yaml
mkdirSync(taktDir, { recursive: true });
const baseConfig = readE2EFixtureConfig();
const provider = process.env.TAKT_E2E_PROVIDER;
const model = process.env.TAKT_E2E_MODEL;
if (provider === 'opencode' && !model) {
throw new Error('TAKT_E2E_PROVIDER=opencode requires TAKT_E2E_MODEL (e.g. opencode/big-pickle)');
}
const config = provider
? {
...baseConfig,
provider,
...(model ? { model } : {}),
}
: baseConfig;
writeConfigFile(taktDir, config);
// Create isolated Git config file
writeFileSync(
gitConfigPath,
['[user]', ' name = TAKT E2E Test', ' email = e2e@example.com'].join(
'\n',
),
);
// ...process.env inherits all env vars including TAKT_OPENAI_API_KEY (for Codex)
const env: NodeJS.ProcessEnv = {
...process.env,
TAKT_CONFIG_DIR: taktDir,
GIT_CONFIG_GLOBAL: gitConfigPath,
TAKT_NO_TTY: '1',
TAKT_NOTIFY_WEBHOOK: undefined,
};
return {
runId,
taktDir,
env,
cleanup: () => {
rmSync(baseDir, { recursive: true, force: true });
},
};
}