takt: github-issue-100-macosdesuriip
This commit is contained in:
parent
24361b34e3
commit
b9c47d29a8
@ -193,4 +193,51 @@ describe('loadGlobalConfig', () => {
|
||||
expect(config3.language).toBe('en');
|
||||
expect(config3).not.toBe(config1);
|
||||
});
|
||||
|
||||
it('should load prevent_sleep config from config.yaml', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(
|
||||
getGlobalConfigPath(),
|
||||
'language: en\nprevent_sleep: true\n',
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
|
||||
expect(config.preventSleep).toBe(true);
|
||||
});
|
||||
|
||||
it('should save and reload prevent_sleep config', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8');
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
config.preventSleep = true;
|
||||
saveGlobalConfig(config);
|
||||
invalidateGlobalConfigCache();
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect(reloaded.preventSleep).toBe(true);
|
||||
});
|
||||
|
||||
it('should save prevent_sleep: false when explicitly set', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8');
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
config.preventSleep = false;
|
||||
saveGlobalConfig(config);
|
||||
invalidateGlobalConfigCache();
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect(reloaded.preventSleep).toBe(false);
|
||||
});
|
||||
|
||||
it('should have undefined preventSleep by default', () => {
|
||||
const config = loadGlobalConfig();
|
||||
expect(config.preventSleep).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
114
src/__tests__/sleep.test.ts
Normal file
114
src/__tests__/sleep.test.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
|
||||
// Mock modules before importing the module under test
|
||||
vi.mock('node:child_process', () => ({
|
||||
spawn: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:os', () => ({
|
||||
platform: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the debug logger
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
createLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
enter: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Import after mocks are set up
|
||||
const { spawn } = await import('node:child_process');
|
||||
const { platform } = await import('node:os');
|
||||
const { existsSync } = await import('node:fs');
|
||||
const { preventSleep, resetPreventSleepState } = await import('../shared/utils/sleep.js');
|
||||
|
||||
describe('preventSleep', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetPreventSleepState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing on non-darwin platforms', () => {
|
||||
vi.mocked(platform).mockReturnValue('linux');
|
||||
|
||||
preventSleep();
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing on Windows', () => {
|
||||
vi.mocked(platform).mockReturnValue('win32');
|
||||
|
||||
preventSleep();
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should spawn caffeinate on macOS when available', () => {
|
||||
vi.mocked(platform).mockReturnValue('darwin');
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
|
||||
const mockChild = {
|
||||
unref: vi.fn(),
|
||||
pid: 12345,
|
||||
} as unknown as ChildProcess;
|
||||
vi.mocked(spawn).mockReturnValue(mockChild);
|
||||
|
||||
preventSleep();
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'/usr/bin/caffeinate',
|
||||
['-i', '-w', String(process.pid)],
|
||||
{ stdio: 'ignore', detached: true }
|
||||
);
|
||||
expect(mockChild.unref).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not spawn caffeinate if not found', () => {
|
||||
vi.mocked(platform).mockReturnValue('darwin');
|
||||
vi.mocked(existsSync).mockReturnValue(false);
|
||||
|
||||
preventSleep();
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should check for caffeinate at /usr/bin/caffeinate', () => {
|
||||
vi.mocked(platform).mockReturnValue('darwin');
|
||||
vi.mocked(existsSync).mockReturnValue(false);
|
||||
|
||||
preventSleep();
|
||||
|
||||
expect(existsSync).toHaveBeenCalledWith('/usr/bin/caffeinate');
|
||||
});
|
||||
|
||||
it('should only spawn caffeinate once even when called multiple times', () => {
|
||||
vi.mocked(platform).mockReturnValue('darwin');
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
|
||||
const mockChild = {
|
||||
unref: vi.fn(),
|
||||
pid: 12345,
|
||||
} as unknown as ChildProcess;
|
||||
vi.mocked(spawn).mockReturnValue(mockChild);
|
||||
|
||||
preventSleep();
|
||||
preventSleep();
|
||||
preventSleep();
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -63,6 +63,8 @@ export interface GlobalConfig {
|
||||
pieceCategoriesFile?: string;
|
||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||
branchNameStrategy?: 'romaji' | 'ai';
|
||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||
preventSleep?: boolean;
|
||||
}
|
||||
|
||||
/** Project-level configuration */
|
||||
|
||||
@ -272,6 +272,8 @@ export const GlobalConfigSchema = z.object({
|
||||
piece_categories_file: z.string().optional(),
|
||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||
prevent_sleep: z.boolean().optional(),
|
||||
});
|
||||
|
||||
/** Project config schema */
|
||||
|
||||
@ -46,7 +46,7 @@ import {
|
||||
type NdjsonInteractiveStart,
|
||||
type NdjsonInteractiveEnd,
|
||||
} from '../../../infra/fs/index.js';
|
||||
import { createLogger, notifySuccess, notifyError } from '../../../shared/utils/index.js';
|
||||
import { createLogger, notifySuccess, notifyError, preventSleep } from '../../../shared/utils/index.js';
|
||||
import { selectOption, promptInput } from '../../../shared/prompt/index.js';
|
||||
import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
|
||||
import { getLabel } from '../../../shared/i18n/index.js';
|
||||
@ -141,7 +141,13 @@ export async function executePiece(
|
||||
|
||||
// Load saved agent sessions for continuity (from project root or clone-specific storage)
|
||||
const isWorktree = cwd !== projectCwd;
|
||||
const currentProvider = loadGlobalConfig().provider ?? 'claude';
|
||||
const globalConfig = loadGlobalConfig();
|
||||
const currentProvider = globalConfig.provider ?? 'claude';
|
||||
|
||||
// Prevent macOS idle sleep if configured
|
||||
if (globalConfig.preventSleep) {
|
||||
preventSleep();
|
||||
}
|
||||
const savedSessions = isWorktree
|
||||
? loadWorktreeSessions(projectCwd, cwd, currentProvider)
|
||||
: loadAgentSessions(projectCwd, currentProvider);
|
||||
|
||||
@ -89,6 +89,7 @@ export class GlobalConfigManager {
|
||||
bookmarksFile: parsed.bookmarks_file,
|
||||
pieceCategoriesFile: parsed.piece_categories_file,
|
||||
branchNameStrategy: parsed.branch_name_strategy,
|
||||
preventSleep: parsed.prevent_sleep,
|
||||
};
|
||||
this.cachedConfig = config;
|
||||
return config;
|
||||
@ -151,6 +152,9 @@ export class GlobalConfigManager {
|
||||
if (config.branchNameStrategy) {
|
||||
raw.branch_name_strategy = config.branchNameStrategy;
|
||||
}
|
||||
if (config.preventSleep !== undefined) {
|
||||
raw.prevent_sleep = config.preventSleep;
|
||||
}
|
||||
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ export * from './debug.js';
|
||||
export * from './error.js';
|
||||
export * from './notification.js';
|
||||
export * from './reportDir.js';
|
||||
export * from './sleep.js';
|
||||
export * from './slug.js';
|
||||
export * from './text.js';
|
||||
export * from './types.js';
|
||||
|
||||
46
src/shared/utils/sleep.ts
Normal file
46
src/shared/utils/sleep.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { createLogger } from './debug.js';
|
||||
|
||||
const log = createLogger('sleep');
|
||||
|
||||
let caffeinateStarted = false;
|
||||
|
||||
/**
|
||||
* takt実行中のmacOSアイドルスリープを防止する。
|
||||
* 蓋を閉じた場合のスリープは防げない(-s はAC電源が必要なため)。
|
||||
*/
|
||||
export function preventSleep(): void {
|
||||
if (caffeinateStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (platform() !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
const caffeinatePath = '/usr/bin/caffeinate';
|
||||
if (!existsSync(caffeinatePath)) {
|
||||
log.info('caffeinate not found, sleep prevention disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const child = spawn(caffeinatePath, ['-i', '-w', String(process.pid)], {
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
});
|
||||
|
||||
child.unref();
|
||||
|
||||
caffeinateStarted = true;
|
||||
|
||||
log.debug('Started caffeinate for sleep prevention', { pid: child.pid });
|
||||
}
|
||||
|
||||
/**
|
||||
* テスト用: caffeinateStarted フラグをリセットする
|
||||
*/
|
||||
export function resetPreventSleepState(): void {
|
||||
caffeinateStarted = false;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user