fix(tasks): sync markdown loops when listing tasks
This commit is contained in:
@@ -25,7 +25,7 @@ Check **Run as goal** to make the run pursue its prompt to completion instead of
|
||||
|
||||
## Loops: scheduled tasks as markdown files
|
||||
|
||||
A **loop** is a scheduled task defined as a portable markdown file you can commit to your repo. Drop a file into `.agents/loops/` and the task appears on the next sync — no dialog needed:
|
||||
A **loop** is a scheduled task defined as a portable markdown file you can commit to your repo. Drop a file into `.agents/loops/` and open the Scheduled Tasks list to sync it — no server restart needed:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
@@ -21,10 +21,11 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
||||
- `packages/web/server/lib/scheduled-tasks/loops.js`
|
||||
- Discovery of `.agents/loops/*.md` (project scope, ancestors up to the worktree root) and `~/.agents/loops/*.md` (user scope)
|
||||
- Frontmatter parsing into scheduled-task definitions
|
||||
- `syncProject` reconciles discovered loops with the persisted task list on every project sync (startup, task save/delete)
|
||||
- `syncProject` reconciles discovered loops with the persisted task list on every project sync (startup, task list load, task save/delete)
|
||||
|
||||
- `packages/web/server/lib/scheduled-tasks/routes.js`
|
||||
- Scheduled task CRUD endpoints
|
||||
- Listing tasks reconciles loop files first, so opening the Scheduled Tasks UI discovers file additions, edits, and removals without a server restart
|
||||
- Manual run endpoint
|
||||
- OpenChamber events SSE stream endpoint
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export const createScheduledTaskService = (dependencies) => {
|
||||
|
||||
const list = async (projectID) => {
|
||||
await findProjectByID(projectID);
|
||||
return projectConfigRuntime.listScheduledTasks(projectID);
|
||||
return scheduledTasksRuntime.syncProject(projectID);
|
||||
};
|
||||
|
||||
const upsert = async (projectID, taskInput) => {
|
||||
|
||||
@@ -34,6 +34,36 @@ const loopTask = {
|
||||
execution: { prompt: 'digest', providerID: 'openai', modelID: 'gpt-4.1' },
|
||||
};
|
||||
|
||||
describe('scheduled-task service list', () => {
|
||||
it('reconciles loop files before returning tasks', async () => {
|
||||
const syncedTasks = [loopTask];
|
||||
const { service, projectConfigRuntime, scheduledTasksRuntime } = createService({
|
||||
scheduledTasksRuntime: {
|
||||
syncProject: vi.fn(async () => syncedTasks),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(service.list('project-test')).resolves.toBe(syncedTasks);
|
||||
expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledOnce();
|
||||
expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledWith('project-test');
|
||||
expect(projectConfigRuntime.listScheduledTasks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces reconciliation failure instead of returning a stale list', async () => {
|
||||
const syncError = new Error('loop reconciliation failed');
|
||||
const { service, projectConfigRuntime } = createService({
|
||||
scheduledTasksRuntime: {
|
||||
syncProject: vi.fn(async () => {
|
||||
throw syncError;
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(service.list('project-test')).rejects.toBe(syncError);
|
||||
expect(projectConfigRuntime.listScheduledTasks).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduled-task service remove', () => {
|
||||
it('rejects deleting a loop-sourced task while its loop file still exists', async () => {
|
||||
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'oc-loop-delete-'));
|
||||
|
||||
Reference in New Issue
Block a user