137 lines
4.3 KiB
TypeScript
137 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,
|
|
...(provider === 'opencode' && 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',
|
|
};
|
|
|
|
return {
|
|
runId,
|
|
taktDir,
|
|
env,
|
|
cleanup: () => {
|
|
rmSync(baseDir, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|