Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/__tests__/globalConfig-defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('loadGlobalConfig', () => {
expect(config.logLevel).toBe('info');
expect(config.provider).toBe('claude');
expect(config.model).toBeUndefined();
expect(config.verbose).toBeUndefined();
expect(config.verbose).toBe(false);
expect(config.pipeline).toBeUndefined();
});

Expand Down
137 changes: 137 additions & 0 deletions src/__tests__/resolveConfigValue-no-defaultValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Tests for RESOLUTION_REGISTRY defaultValue removal.
*
* Verifies that piece, verbose, and autoFetch no longer rely on
* RESOLUTION_REGISTRY defaultValue but instead use schema defaults
* or other guaranteed sources.
*/

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';

const testId = randomUUID();
const testDir = join(tmpdir(), `takt-rcv-test-${testId}`);
const globalTaktDir = join(testDir, 'global-takt');
const globalConfigPath = join(globalTaktDir, 'config.yaml');

vi.mock('../infra/config/paths.js', async (importOriginal) => {
const original = await importOriginal() as Record<string, unknown>;
return {
...original,
getGlobalConfigPath: () => globalConfigPath,
getTaktDir: () => globalTaktDir,
};
});

const { resolveConfigValue, resolveConfigValueWithSource, invalidateAllResolvedConfigCache } = await import('../infra/config/resolveConfigValue.js');
const { invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
const { getProjectConfigDir } = await import('../infra/config/paths.js');

describe('RESOLUTION_REGISTRY defaultValue removal', () => {
let projectDir: string;

beforeEach(() => {
projectDir = join(testDir, `project-${randomUUID()}`);
mkdirSync(projectDir, { recursive: true });
mkdirSync(globalTaktDir, { recursive: true });
writeFileSync(globalConfigPath, 'language: en\n', 'utf-8');
invalidateGlobalConfigCache();
invalidateAllResolvedConfigCache();
});

afterEach(() => {
invalidateGlobalConfigCache();
invalidateAllResolvedConfigCache();
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});

describe('piece', () => {
it('should resolve piece from project config DEFAULT_PROJECT_CONFIG when not explicitly set', () => {
const value = resolveConfigValue(projectDir, 'piece');
expect(value).toBe('default');
});

it('should report source as project when piece comes from DEFAULT_PROJECT_CONFIG', () => {
const result = resolveConfigValueWithSource(projectDir, 'piece');
expect(result.value).toBe('default');
expect(result.source).toBe('project');
});

it('should resolve explicit project piece over default', () => {
const configDir = getProjectConfigDir(projectDir);
mkdirSync(configDir, { recursive: true });
writeFileSync(join(configDir, 'config.yaml'), 'piece: custom-piece\n');

const value = resolveConfigValue(projectDir, 'piece');
expect(value).toBe('custom-piece');
});

it('should resolve piece from global config when global has it', () => {
writeFileSync(globalConfigPath, 'language: en\npiece: global-piece\n', 'utf-8');
invalidateGlobalConfigCache();

const result = resolveConfigValueWithSource(projectDir, 'piece');
expect(result.value).toBe('default');
expect(result.source).toBe('project');
});
});

describe('verbose', () => {
it('should resolve verbose to false via schema default when not set anywhere', () => {
const value = resolveConfigValue(projectDir, 'verbose');
expect(value).toBe(false);
});

it('should report source as global when verbose comes from schema default', () => {
const result = resolveConfigValueWithSource(projectDir, 'verbose');
expect(result.value).toBe(false);
expect(result.source).toBe('global');
});

it('should resolve verbose from global config when explicitly set', () => {
writeFileSync(globalConfigPath, 'language: en\nverbose: true\n', 'utf-8');
invalidateGlobalConfigCache();

const value = resolveConfigValue(projectDir, 'verbose');
expect(value).toBe(true);
});

it('should resolve verbose from project config over global', () => {
writeFileSync(globalConfigPath, 'language: en\nverbose: false\n', 'utf-8');
invalidateGlobalConfigCache();

const configDir = getProjectConfigDir(projectDir);
mkdirSync(configDir, { recursive: true });
writeFileSync(join(configDir, 'config.yaml'), 'piece: default\nverbose: true\n');

const value = resolveConfigValue(projectDir, 'verbose');
expect(value).toBe(true);
});
});

describe('autoFetch', () => {
it('should resolve autoFetch to false via schema default when not set', () => {
const value = resolveConfigValue(projectDir, 'autoFetch');
expect(value).toBe(false);
});

it('should report source as global when autoFetch comes from schema default', () => {
const result = resolveConfigValueWithSource(projectDir, 'autoFetch');
expect(result.value).toBe(false);
expect(result.source).toBe('global');
});

it('should resolve autoFetch from global config when explicitly set', () => {
writeFileSync(globalConfigPath, 'language: en\nauto_fetch: true\n', 'utf-8');
invalidateGlobalConfigCache();

const value = resolveConfigValue(projectDir, 'autoFetch');
expect(value).toBe(true);
});
});
});
4 changes: 2 additions & 2 deletions src/core/models/persisted-global-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@ export interface PersistedGlobalConfig {
/** Number of movement previews to inject into interactive mode (0 to disable, max 10) */
interactivePreviewMovements?: number;
/** Verbose output mode */
verbose?: boolean;
verbose: boolean;
/** Number of tasks to run concurrently in takt run (default: 1 = sequential) */
concurrency: number;
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
taskPollIntervalMs: number;
/** Opt-in: fetch remote before cloning to keep clones up-to-date (default: false) */
autoFetch?: boolean;
autoFetch: boolean;
/** Base branch to clone from (default: current branch) */
baseBranch?: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/models/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ export const GlobalConfigSchema = z.object({
/** Number of movement previews to inject into interactive mode (0 to disable, max 10) */
interactive_preview_movements: z.number().int().min(0).max(10).optional().default(3),
/** Verbose output mode */
verbose: z.boolean().optional(),
verbose: z.boolean().optional().default(false),
/** Number of tasks to run concurrently in takt run (default: 1 = sequential, max: 10) */
concurrency: z.number().int().min(1).max(10).optional().default(1),
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
Expand Down
4 changes: 2 additions & 2 deletions src/infra/config/global/globalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export class GlobalConfigManager {
if (config.interactivePreviewMovements !== undefined) {
raw.interactive_preview_movements = config.interactivePreviewMovements;
}
if (config.verbose !== undefined) {
if (config.verbose) {
raw.verbose = config.verbose;
}
if (config.concurrency !== undefined && config.concurrency > 1) {
Expand All @@ -352,7 +352,7 @@ export class GlobalConfigManager {
if (config.taskPollIntervalMs !== undefined && config.taskPollIntervalMs !== 500) {
raw.task_poll_interval_ms = config.taskPollIntervalMs;
}
if (config.autoFetch !== undefined) {
if (config.autoFetch) {
raw.auto_fetch = config.autoFetch;
}
if (config.baseBranch) {
Expand Down
9 changes: 4 additions & 5 deletions src/infra/config/resolveConfigValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export interface ResolvedConfigValue<K extends ConfigParameterKey> {
type ResolutionLayer = 'local' | 'piece' | 'global';
interface ResolutionRule<K extends ConfigParameterKey> {
layers: readonly ResolutionLayer[];
defaultValue?: LoadedConfig[K];
mergeMode?: 'analytics';
pieceValue?: (pieceContext: PieceContext | undefined) => LoadedConfig[K] | undefined;
}
Expand Down Expand Up @@ -61,7 +60,7 @@ const PROVIDER_OPTIONS_ENV_PATHS = [
] as const;

const RESOLUTION_REGISTRY: Partial<{ [K in ConfigParameterKey]: ResolutionRule<K> }> = {
piece: { layers: ['local', 'global'], defaultValue: 'default' },
piece: { layers: ['local', 'global'] },
provider: {
layers: ['local', 'piece', 'global'],
pieceValue: (pieceContext) => pieceContext?.provider,
Expand All @@ -77,8 +76,8 @@ const RESOLUTION_REGISTRY: Partial<{ [K in ConfigParameterKey]: ResolutionRule<K
autoPr: { layers: ['local', 'global'] },
draftPr: { layers: ['local', 'global'] },
analytics: { layers: ['local', 'global'], mergeMode: 'analytics' },
verbose: { layers: ['local', 'global'], defaultValue: false },
autoFetch: { layers: ['global'], defaultValue: false },
verbose: { layers: ['local', 'global'] },
autoFetch: { layers: ['global'] },
baseBranch: { layers: ['local', 'global'] },
};

Expand Down Expand Up @@ -159,7 +158,7 @@ function resolveByRegistry<K extends ConfigParameterKey>(
}
}

return { value: rule.defaultValue as LoadedConfig[K], source: 'default' };
return { value: undefined as LoadedConfig[K], source: 'default' };
}

function hasProviderOptionsEnvOverride(): boolean {
Expand Down
3 changes: 1 addition & 2 deletions src/infra/config/resolvedConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { PersistedGlobalConfig } from '../../core/models/persisted-global-config.js';

export interface LoadedConfig extends Omit<PersistedGlobalConfig, 'verbose'> {
export interface LoadedConfig extends PersistedGlobalConfig {
piece: string;
verbose: boolean;
}

export type ConfigParameterKey = keyof LoadedConfig;