feat(HAI-016): complete Step 2 — integrate semaphore into TriageProcessor

This commit is contained in:
Dustin Byrne
2026-03-25 21:16:16 -04:00
parent 6379008a88
commit 640c288832
3 changed files with 215 additions and 19 deletions

View File

@@ -0,0 +1,148 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { AgentSemaphore } from "./concurrency.js";
// Mock createHaiAgent before importing TriageProcessor
vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(),
}));
import { TriageProcessor } from "./triage.js";
import { createHaiAgent } from "./pi.js";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent);
function createMockStore(tasks: any[] = []) {
return {
listTasks: vi.fn().mockResolvedValue(tasks),
getTask: vi.fn().mockResolvedValue({
id: "HAI-001",
title: "Test",
description: "Test task",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
prompt: "# test",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
updateTask: vi.fn().mockResolvedValue({}),
moveTask: vi.fn().mockResolvedValue({}),
} as any;
}
describe("TriageProcessor with semaphore", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("acquires semaphore before creating agent and releases after", async () => {
const sem = new AgentSemaphore(1);
const store = createMockStore();
const acquireSpy = vi.spyOn(sem, "acquire");
const releaseSpy = vi.spyOn(sem, "release");
mockedCreateHaiAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
},
} as any);
const triage = new TriageProcessor(store, "/tmp/test", { semaphore: sem });
await triage.specifyTask({
id: "HAI-001",
title: "Test",
description: "Test",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Semaphore was used via run() which calls acquire + release
expect(acquireSpy).toHaveBeenCalledOnce();
expect(releaseSpy).toHaveBeenCalledOnce();
expect(mockedCreateHaiAgent).toHaveBeenCalledOnce();
expect(sem.activeCount).toBe(0);
});
it("releases semaphore on agent error", async () => {
const sem = new AgentSemaphore(1);
const store = createMockStore();
mockedCreateHaiAgent.mockRejectedValue(new Error("agent failed"));
const onError = vi.fn();
const triage = new TriageProcessor(store, "/tmp/test", {
semaphore: sem,
onSpecifyError: onError,
});
await triage.specifyTask({
id: "HAI-001",
title: "Test",
description: "Test",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
expect(sem.activeCount).toBe(0);
expect(onError).toHaveBeenCalled();
});
it("concurrent specifyTask calls respect semaphore limit", async () => {
const sem = new AgentSemaphore(1);
const store = createMockStore();
let concurrent = 0;
let maxConcurrent = 0;
mockedCreateHaiAgent.mockImplementation(async () => {
concurrent++;
maxConcurrent = Math.max(maxConcurrent, concurrent);
return {
session: {
prompt: vi.fn().mockImplementation(async () => {
await new Promise((r) => setTimeout(r, 10));
concurrent--;
}),
dispose: vi.fn(),
},
} as any;
});
const triage = new TriageProcessor(store, "/tmp/test", { semaphore: sem });
const task = (id: string) => ({
id,
title: "Test",
description: "Test",
column: "triage" as const,
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
await Promise.all([
triage.specifyTask(task("HAI-001")),
triage.specifyTask(task("HAI-002")),
triage.specifyTask(task("HAI-003")),
]);
expect(maxConcurrent).toBe(1);
expect(sem.activeCount).toBe(0);
});
});