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
|
## 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
|
```markdown
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
|||||||
- `packages/web/server/lib/scheduled-tasks/loops.js`
|
- `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)
|
- 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
|
- 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`
|
- `packages/web/server/lib/scheduled-tasks/routes.js`
|
||||||
- Scheduled task CRUD endpoints
|
- 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
|
- Manual run endpoint
|
||||||
- OpenChamber events SSE stream endpoint
|
- OpenChamber events SSE stream endpoint
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const createScheduledTaskService = (dependencies) => {
|
|||||||
|
|
||||||
const list = async (projectID) => {
|
const list = async (projectID) => {
|
||||||
await findProjectByID(projectID);
|
await findProjectByID(projectID);
|
||||||
return projectConfigRuntime.listScheduledTasks(projectID);
|
return scheduledTasksRuntime.syncProject(projectID);
|
||||||
};
|
};
|
||||||
|
|
||||||
const upsert = async (projectID, taskInput) => {
|
const upsert = async (projectID, taskInput) => {
|
||||||
|
|||||||
@@ -34,6 +34,36 @@ const loopTask = {
|
|||||||
execution: { prompt: 'digest', providerID: 'openai', modelID: 'gpt-4.1' },
|
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', () => {
|
describe('scheduled-task service remove', () => {
|
||||||
it('rejects deleting a loop-sourced task while its loop file still exists', async () => {
|
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-'));
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'oc-loop-delete-'));
|
||||||
|
|||||||
Reference in New Issue
Block a user