feat(HAI-061): make scheduler and triage processor read settings dynamically

- Update Scheduler to read settings dynamically from the store instead of using static config
- Make TriageProcessor poll interval dynamic so it responds to settings changes
- Update dashboard.ts wiring to pass dynamic settings through to engine components
- Add scheduler and triage processor tests for dynamic settings behavior
- Remove unused InlineCreateCard and stale test files
This commit is contained in:
Dustin Byrne
2026-03-26 00:04:50 -04:00
parent c2166723ee
commit afcd074caa
5 changed files with 275 additions and 21 deletions

View File

@@ -99,6 +99,13 @@ describe("Scheduler concurrency", () => {
makeTask({ id: "HAI-003", column: "todo" }),
];
const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({
maxConcurrent: 3,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store, { maxConcurrent: 3 });
await runSchedule(scheduler);
@@ -123,6 +130,118 @@ describe("Scheduler concurrency", () => {
});
});
describe("Scheduler dynamic settings reload", () => {
beforeEach(() => {
vi.clearAllMocks();
});
async function runSchedule(scheduler: Scheduler): Promise<void> {
(scheduler as any).running = true;
await scheduler.schedule();
}
it("reads maxConcurrent from store settings on each schedule() call", async () => {
const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "todo" }),
];
const store = createMockStore(tasks);
// Start with maxConcurrent: 1 — no room
store.getSettings.mockResolvedValue({
maxConcurrent: 1,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store);
await runSchedule(scheduler);
expect(store.moveTask).not.toHaveBeenCalled();
// Now bump maxConcurrent to 2 — room for HAI-002
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
await runSchedule(scheduler);
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress");
});
it("reads maxWorktrees from store settings on each schedule() call", async () => {
const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "in-review", worktree: "/tmp/wt" }),
makeTask({ id: "HAI-003", column: "todo" }),
];
const store = createMockStore(tasks);
// Start with maxWorktrees: 2 — no room (2 active worktrees)
store.getSettings.mockResolvedValue({
maxConcurrent: 4,
maxWorktrees: 2,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store);
await runSchedule(scheduler);
expect(store.moveTask).not.toHaveBeenCalled();
// Bump maxWorktrees to 3 — room for HAI-003
store.getSettings.mockResolvedValue({
maxConcurrent: 4,
maxWorktrees: 3,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
await runSchedule(scheduler);
expect(store.moveTask).toHaveBeenCalledWith("HAI-003", "in-progress");
});
it("refreshes poll interval when settings.pollIntervalMs changes", async () => {
const store = createMockStore([]);
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store);
// Manually set running and activePollMs to simulate start()
(scheduler as any).running = true;
(scheduler as any).activePollMs = 15000;
(scheduler as any).pollInterval = setInterval(() => {}, 15000);
// First schedule — same interval, no change
await scheduler.schedule();
expect((scheduler as any).activePollMs).toBe(15000);
// Change pollIntervalMs
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 5000,
groupOverlappingFiles: false,
autoMerge: false,
});
await scheduler.schedule();
expect((scheduler as any).activePollMs).toBe(5000);
// Clean up
scheduler.stop();
});
});
describe("Scheduler file-scope overlap", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -206,6 +325,13 @@ describe("Scheduler worktree limit logging", () => {
makeTask({ id: "HAI-002", column: "in-progress" }),
];
const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 2,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store, { maxWorktrees: 2 });
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -223,6 +349,13 @@ describe("Scheduler worktree limit logging", () => {
makeTask({ id: "HAI-002", column: "in-progress" }),
];
const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 2,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store, { maxWorktrees: 2 });
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -250,6 +383,13 @@ describe("Scheduler worktree limit logging", () => {
];
const store = createMockStore(maxedTasks);
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 2,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
const scheduler = new Scheduler(store, { maxWorktrees: 2, maxConcurrent: 2 });
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});