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