テストのメモリリークとハング問題を修正
- PieceEngineとTaskWatcherのクリーンアップ処理を追加 - vitestをシングルスレッド実行に変更してテストの安定性を向上
This commit is contained in:
parent
c4ebbdb6a6
commit
9bc60c21b5
@ -49,10 +49,12 @@ import {
|
||||
mockDetectMatchedRuleSequence,
|
||||
createTestTmpDir,
|
||||
applyDefaultMocks,
|
||||
cleanupPieceEngine,
|
||||
} from './engine-test-helpers.js';
|
||||
|
||||
describe('PieceEngine Integration: Happy Path', () => {
|
||||
let tmpDir: string;
|
||||
let engine: PieceEngine | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
@ -61,6 +63,10 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (engine) {
|
||||
cleanupPieceEngine(engine);
|
||||
engine = null;
|
||||
}
|
||||
if (existsSync(tmpDir)) {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
@ -72,7 +78,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
describe('Happy path', () => {
|
||||
it('should complete: plan → implement → ai_review → reviewers(all approved) → supervise → COMPLETE', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan complete' }),
|
||||
@ -111,7 +117,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
describe('Review reject and fix loop', () => {
|
||||
it('should handle: reviewers(needs_fix) → fix → reviewers(all approved) → supervise → COMPLETE', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
@ -152,7 +158,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
|
||||
it('should inject latest reviewers output as Previous Response for repeated fix steps', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
@ -221,7 +227,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
|
||||
it('should use the latest movement output across different steps for Previous Response', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
@ -279,7 +285,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
describe('AI review reject and fix', () => {
|
||||
it('should handle: ai_review(issues) → ai_fix → reviewers → supervise → COMPLETE', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
@ -316,7 +322,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
describe('ABORT transition', () => {
|
||||
it('should abort when movement transitions to ABORT', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Requirements unclear' }),
|
||||
@ -343,7 +349,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
describe('Event emissions', () => {
|
||||
it('should emit movement:start and movement:complete for each movement', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||
@ -390,7 +396,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
}),
|
||||
],
|
||||
};
|
||||
const engine = new PieceEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
@ -413,7 +419,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
|
||||
it('should pass empty instruction to movement:start for parallel movements', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||
@ -451,7 +457,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
|
||||
it('should emit iteration:limit when max iterations reached', async () => {
|
||||
const config = buildDefaultPieceConfig({ maxIterations: 1 });
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||
@ -475,7 +481,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
describe('Movement output tracking', () => {
|
||||
it('should store outputs for all executed movements', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan output' }),
|
||||
@ -520,7 +526,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
}),
|
||||
],
|
||||
};
|
||||
const engine = new PieceEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
@ -548,7 +554,7 @@ describe('PieceEngine Integration: Happy Path', () => {
|
||||
|
||||
it('should emit phase events for all movements in happy path', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||
|
||||
@ -173,3 +173,13 @@ export function applyDefaultMocks(): void {
|
||||
vi.mocked(runStatusJudgmentPhase).mockResolvedValue('');
|
||||
vi.mocked(generateReportDir).mockReturnValue('test-report-dir');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up PieceEngine instances to prevent EventEmitter memory leaks.
|
||||
* Call this in afterEach to ensure all event listeners are removed.
|
||||
*/
|
||||
export function cleanupPieceEngine(engine: any): void {
|
||||
if (engine && typeof engine.removeAllListeners === 'function') {
|
||||
engine.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import type { TaskInfo } from '../infra/task/types.js';
|
||||
|
||||
describe('TaskWatcher', () => {
|
||||
const testDir = `/tmp/takt-watcher-test-${Date.now()}`;
|
||||
let watcher: TaskWatcher | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
mkdirSync(join(testDir, '.takt', 'tasks'), { recursive: true });
|
||||
@ -17,6 +18,11 @@ describe('TaskWatcher', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Ensure watcher is stopped before cleanup
|
||||
if (watcher) {
|
||||
watcher.stop();
|
||||
watcher = null;
|
||||
}
|
||||
if (existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
@ -24,19 +30,19 @@ describe('TaskWatcher', () => {
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create watcher with default options', () => {
|
||||
const watcher = new TaskWatcher(testDir);
|
||||
watcher = new TaskWatcher(testDir);
|
||||
expect(watcher.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept custom poll interval', () => {
|
||||
const watcher = new TaskWatcher(testDir, { pollInterval: 500 });
|
||||
watcher = new TaskWatcher(testDir, { pollInterval: 500 });
|
||||
expect(watcher.isRunning()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
it('should detect and process a task file', async () => {
|
||||
const watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
const processed: string[] = [];
|
||||
|
||||
// Pre-create a task file
|
||||
@ -59,7 +65,7 @@ describe('TaskWatcher', () => {
|
||||
});
|
||||
|
||||
it('should wait when no tasks are available', async () => {
|
||||
const watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
let pollCount = 0;
|
||||
|
||||
// Start watching, add a task after a delay
|
||||
@ -81,7 +87,7 @@ describe('TaskWatcher', () => {
|
||||
});
|
||||
|
||||
it('should process multiple tasks sequentially', async () => {
|
||||
const watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
const processed: string[] = [];
|
||||
|
||||
// Pre-create two task files
|
||||
@ -111,7 +117,7 @@ describe('TaskWatcher', () => {
|
||||
|
||||
describe('stop', () => {
|
||||
it('should stop the watcher gracefully', async () => {
|
||||
const watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
watcher = new TaskWatcher(testDir, { pollInterval: 50 });
|
||||
|
||||
// Start watching, stop after a short delay
|
||||
const watchPromise = watcher.watch(async () => {
|
||||
@ -127,7 +133,7 @@ describe('TaskWatcher', () => {
|
||||
});
|
||||
|
||||
it('should abort sleep immediately when stopped', async () => {
|
||||
const watcher = new TaskWatcher(testDir, { pollInterval: 10000 });
|
||||
watcher = new TaskWatcher(testDir, { pollInterval: 10000 });
|
||||
|
||||
const start = Date.now();
|
||||
const watchPromise = watcher.watch(async () => {});
|
||||
|
||||
@ -5,6 +5,17 @@ export default defineConfig({
|
||||
include: ['src/__tests__/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
globals: false,
|
||||
// Ensure proper cleanup by forcing sequential execution and graceful shutdown
|
||||
pool: 'threads',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
},
|
||||
},
|
||||
// Increase timeout for tests with async cleanup
|
||||
testTimeout: 15000,
|
||||
// Force exit after tests complete to prevent hanging
|
||||
teardownTimeout: 5000,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user