fix: 不正なtasks.yamlで削除せず停止するように修正 (#418)

* test: add regression coverage for invalid tasks.yaml preservation

* fix: keep invalid tasks.yaml untouched and fail fast
This commit is contained in:
Junichi Kato 2026-02-28 13:02:03 +09:00 committed by GitHub
parent ac4cb9c8a5
commit fe0b7237a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 14 deletions

View File

@ -94,18 +94,29 @@ describe('TaskRunner (tasks.yaml)', () => {
expect(recovered).toBe(0);
});
it('should recover from corrupted tasks.yaml and allow adding tasks again', () => {
it('should preserve corrupted tasks.yaml and throw', () => {
mkdirSync(join(testDir, '.takt'), { recursive: true });
writeFileSync(join(testDir, '.takt', 'tasks.yaml'), 'tasks:\n - name: [broken', 'utf-8');
expect(() => runner.listTasks()).not.toThrow();
expect(runner.listTasks()).toEqual([]);
expect(existsSync(join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
const tasksFilePath = join(testDir, '.takt', 'tasks.yaml');
expect(() => runner.listTasks()).toThrow(/Invalid tasks\.yaml/);
expect(existsSync(tasksFilePath)).toBe(true);
});
const task = runner.addTask('Task after recovery');
expect(task.name).toContain('task-after-recovery');
expect(existsSync(join(testDir, '.takt', 'tasks.yaml'))).toBe(true);
expect(runner.listTasks()).toHaveLength(1);
it('should preserve tasks.yaml and throw when pending record has started_at', () => {
writeTasksFile(testDir, [{
name: 'broken-pending-task',
status: 'pending',
content: 'Broken pending',
created_at: '2026-02-09T00:00:00.000Z',
started_at: '2026-02-09T00:01:00.000Z',
completed_at: null,
owner_pid: null,
}]);
const tasksFilePath = join(testDir, '.takt', 'tasks.yaml');
expect(() => runner.claimNextTasks(1)).toThrow();
expect(existsSync(tasksFilePath)).toBe(true);
});
it('should load pending content from relative content_file', () => {
@ -164,7 +175,7 @@ describe('TaskRunner (tasks.yaml)', () => {
expect(() => runner.listTasks()).toThrow(/Task spec file is missing/i);
});
it('should reset tasks file when both content and content_file are set', () => {
it('should preserve tasks file and throw when both content and content_file are set', () => {
writeTasksFile(testDir, [{
name: 'task-a',
status: 'pending',
@ -176,8 +187,9 @@ describe('TaskRunner (tasks.yaml)', () => {
owner_pid: null,
}]);
expect(runner.listTasks()).toEqual([]);
expect(existsSync(join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
const tasksFilePath = join(testDir, '.takt', 'tasks.yaml');
expect(() => runner.listTasks()).toThrow(/Invalid tasks\.yaml/);
expect(existsSync(tasksFilePath)).toBe(true);
});
it('should throw when content_file target is missing', () => {

View File

@ -56,9 +56,11 @@ export class TaskStore {
const parsed = parseYaml(raw) as unknown;
return TasksFileSchema.parse(parsed);
} catch (err) {
log.error('tasks.yaml is broken. Resetting file.', { file: this.tasksFile, error: String(err) });
fs.unlinkSync(this.tasksFile);
return { tasks: [] };
log.error('tasks.yaml is broken. Keeping file untouched.', { file: this.tasksFile, error: String(err) });
const reason = err instanceof Error ? err.message : String(err);
throw new Error(
`Invalid tasks.yaml: ${this.tasksFile}. Please fix the file and retry. Cause: ${reason}`,
);
}
}