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.language).toBe('en');
|
||||||
expect(config3).not.toBe(config1);
|
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;
|
pieceCategoriesFile?: string;
|
||||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
branchNameStrategy?: 'romaji' | 'ai';
|
branchNameStrategy?: 'romaji' | 'ai';
|
||||||
|
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||||
|
preventSleep?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Project-level configuration */
|
/** Project-level configuration */
|
||||||
|
|||||||
@ -272,6 +272,8 @@ export const GlobalConfigSchema = z.object({
|
|||||||
piece_categories_file: z.string().optional(),
|
piece_categories_file: z.string().optional(),
|
||||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
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 */
|
/** Project config schema */
|
||||||
|
|||||||
@ -46,7 +46,7 @@ import {
|
|||||||
type NdjsonInteractiveStart,
|
type NdjsonInteractiveStart,
|
||||||
type NdjsonInteractiveEnd,
|
type NdjsonInteractiveEnd,
|
||||||
} from '../../../infra/fs/index.js';
|
} 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 { selectOption, promptInput } from '../../../shared/prompt/index.js';
|
||||||
import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
|
import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
|
||||||
import { getLabel } from '../../../shared/i18n/index.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)
|
// Load saved agent sessions for continuity (from project root or clone-specific storage)
|
||||||
const isWorktree = cwd !== projectCwd;
|
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
|
const savedSessions = isWorktree
|
||||||
? loadWorktreeSessions(projectCwd, cwd, currentProvider)
|
? loadWorktreeSessions(projectCwd, cwd, currentProvider)
|
||||||
: loadAgentSessions(projectCwd, currentProvider);
|
: loadAgentSessions(projectCwd, currentProvider);
|
||||||
|
|||||||
@ -89,6 +89,7 @@ export class GlobalConfigManager {
|
|||||||
bookmarksFile: parsed.bookmarks_file,
|
bookmarksFile: parsed.bookmarks_file,
|
||||||
pieceCategoriesFile: parsed.piece_categories_file,
|
pieceCategoriesFile: parsed.piece_categories_file,
|
||||||
branchNameStrategy: parsed.branch_name_strategy,
|
branchNameStrategy: parsed.branch_name_strategy,
|
||||||
|
preventSleep: parsed.prevent_sleep,
|
||||||
};
|
};
|
||||||
this.cachedConfig = config;
|
this.cachedConfig = config;
|
||||||
return config;
|
return config;
|
||||||
@ -151,6 +152,9 @@ export class GlobalConfigManager {
|
|||||||
if (config.branchNameStrategy) {
|
if (config.branchNameStrategy) {
|
||||||
raw.branch_name_strategy = config.branchNameStrategy;
|
raw.branch_name_strategy = config.branchNameStrategy;
|
||||||
}
|
}
|
||||||
|
if (config.preventSleep !== undefined) {
|
||||||
|
raw.prevent_sleep = config.preventSleep;
|
||||||
|
}
|
||||||
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||||
this.invalidateCache();
|
this.invalidateCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export * from './debug.js';
|
|||||||
export * from './error.js';
|
export * from './error.js';
|
||||||
export * from './notification.js';
|
export * from './notification.js';
|
||||||
export * from './reportDir.js';
|
export * from './reportDir.js';
|
||||||
|
export * from './sleep.js';
|
||||||
export * from './slug.js';
|
export * from './slug.js';
|
||||||
export * from './text.js';
|
export * from './text.js';
|
||||||
export * from './types.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