feat(FN-1726): add saturated-slot regression tests for utility AI routes

- Add heartbeat wake saturation regression tests covering timer, assignment, and on-demand triggers
- Add mission interview saturated-slot regression tests for milestone and slice interviews
- Add saturated-slot regression tests for planning and subtask breakdown routes
- Tests verify 503 responses and correct slot-checking behavior under maxConcurrent saturation
This commit is contained in:
Fusion
2026-04-15 02:08:57 -07:00
committed by gsxdsm
parent 7f830626db
commit ae3716de44
2 changed files with 840 additions and 62 deletions

View File

@@ -4316,7 +4316,7 @@ describe("Mission API", () => {
*/
describe("Mission interview routes are independent of task-lane saturation", () => {
// Helper to create a mock AI session store for interview routes
function createMockAiSessionStore() {
function createMockAiSessionStore(options?: { lockConflict?: boolean }) {
const store = new Map<string, any>();
return {
store,
@@ -4324,13 +4324,18 @@ describe("Mission interview routes are independent of task-lane saturation", ()
get: vi.fn((id) => store.get(id) ?? null),
delete: vi.fn((id) => store.delete(id)),
listRecoverable: vi.fn(() => Array.from(store.values())),
acquireLock: vi.fn().mockReturnValue({ acquired: true, currentHolder: null }),
acquireLock: vi.fn().mockImplementation((_id: string, _tabId: string) => {
if (options?.lockConflict) {
return { acquired: false, currentHolder: "tab-owner" };
}
return { acquired: true, currentHolder: null };
}),
};
}
// Helper to build an app with saturated settings
function buildAppWithSaturatedSettings() {
const aiSessionStore = createMockAiSessionStore();
function buildAppWithSaturatedSettings(options?: { aiSessionStore?: ReturnType<typeof createMockAiSessionStore> }) {
const aiSessionStore = options?.aiSessionStore ?? createMockAiSessionStore();
const { app, missionStore } = buildApp({ aiSessionStore });
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
@@ -4343,78 +4348,269 @@ describe("Mission interview routes are independent of task-lane saturation", ()
return { app, missionStore: ms, aiSessionStore };
}
it("POST /api/missions/interview/start succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
describe("start endpoints", () => {
it("POST /api/missions/interview/start succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Mock createMissionInterviewSession to return a session
const createSessionMock = vi.fn().mockResolvedValue("mission-saturation-test-session");
vi.spyOn(missionInterviewModule, "createMissionInterviewSession").mockImplementation(createSessionMock);
// Mock createMissionInterviewSession to return a session
const createSessionMock = vi.fn().mockResolvedValue("mission-saturation-test-session");
vi.spyOn(missionInterviewModule, "createMissionInterviewSession").mockImplementation(createSessionMock);
const res = await request(
app,
"POST",
"/api/missions/interview/start",
JSON.stringify({ missionTitle: "Build auth system" }),
{ "content-type": "application/json" },
);
const res = await request(
app,
"POST",
"/api/missions/interview/start",
JSON.stringify({ missionTitle: "Build auth system" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(201);
expect(res.body.sessionId).toBe("mission-saturation-test-session");
// Verify no saturation error was introduced
expect(res.body.error).toBeUndefined();
expect(res.status).toBe(201);
expect(res.body.sessionId).toBe("mission-saturation-test-session");
// Verify no saturation error was introduced
expect(res.body.error).toBeUndefined();
});
it("POST /api/missions/milestones/:milestoneId/interview/start succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Create a milestone
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
// Mock createTargetInterviewSession to return a session (from milestone-slice-interview module)
const createSessionMock = vi.fn().mockResolvedValue("milestone-saturation-test-session");
vi.spyOn(milestoneSliceInterviewModule, "createTargetInterviewSession").mockImplementation(createSessionMock);
const res = await request(
app,
"POST",
`/api/missions/milestones/${milestone.id}/interview/start`,
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(201);
expect(res.body.sessionId).toBe("milestone-saturation-test-session");
// Verify no saturation error was introduced
expect(res.body.error).toBeUndefined();
});
it("POST /api/missions/slices/:sliceId/interview/start succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Create a slice
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Test Slice" });
// Mock createTargetInterviewSession to return a session (from milestone-slice-interview module)
const createSessionMock = vi.fn().mockResolvedValue("slice-saturation-test-session");
vi.spyOn(milestoneSliceInterviewModule, "createTargetInterviewSession").mockImplementation(createSessionMock);
const res = await request(
app,
"POST",
`/api/missions/slices/${slice.id}/interview/start`,
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(201);
expect(res.body.sessionId).toBe("slice-saturation-test-session");
// Verify no saturation error was introduced
expect(res.body.error).toBeUndefined();
});
});
it("POST /api/missions/milestones/:milestoneId/interview/start succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
describe("respond endpoints", () => {
it("POST /api/missions/interview/respond succeeds under saturated settings", async () => {
const { app } = buildAppWithSaturatedSettings();
// Create a milestone
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
// Mock submitMissionInterviewResponse to return a valid response
const respondMock = vi.fn().mockResolvedValue({
type: "question",
data: { id: "q-2", type: "text", question: "Next question?" },
});
vi.spyOn(missionInterviewModule, "submitMissionInterviewResponse").mockImplementation(respondMock);
// Mock createTargetInterviewSession to return a session (from milestone-slice-interview module)
const createSessionMock = vi.fn().mockResolvedValue("milestone-saturation-test-session");
vi.spyOn(milestoneSliceInterviewModule, "createTargetInterviewSession").mockImplementation(createSessionMock);
const res = await request(
app,
"POST",
"/api/missions/interview/respond",
JSON.stringify({ sessionId: "test-session", responses: { "q-1": "answer" } }),
{ "content-type": "application/json" },
);
const res = await request(
app,
"POST",
`/api/missions/milestones/${milestone.id}/interview/start`,
JSON.stringify({}),
{ "content-type": "application/json" },
);
// UTILITY PATH: Respond must NOT be gated on maxConcurrent
expect(res.status).toBe(200);
expect(res.body.type).toBe("question");
});
expect(res.status).toBe(201);
expect(res.body.sessionId).toBe("milestone-saturation-test-session");
// Verify no saturation error was introduced
expect(res.body.error).toBeUndefined();
it("preserves lock-conflict 409 semantics for respond under saturation", async () => {
const aiSessionStore = createMockAiSessionStore({ lockConflict: true });
const { app } = buildAppWithSaturatedSettings({ aiSessionStore });
const res = await request(
app,
"POST",
"/api/missions/interview/respond",
JSON.stringify({ sessionId: "locked-session", responses: { "q-1": "answer" }, tabId: "tab-other" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(409);
expect(res.body).toEqual({
error: "Session locked by another tab",
lockedByTab: "tab-owner",
});
});
it("POST /api/missions/milestones/:milestoneId/interview/respond succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Create a milestone
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
// Mock submitTargetInterviewResponse to return a valid response
const respondMock = vi.fn().mockResolvedValue({
type: "question",
data: { id: "ms-q-2", type: "text", question: "Milestone question?" },
});
vi.spyOn(milestoneSliceInterviewModule, "submitTargetInterviewResponse").mockImplementation(respondMock);
const res = await request(
app,
"POST",
`/api/missions/milestones/${milestone.id}/interview/respond`,
JSON.stringify({ sessionId: "milestone-test-session", responses: { "ms-q-1": "answer" } }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.type).toBe("question");
});
it("POST /api/missions/slices/:sliceId/interview/respond succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Create a slice
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Test Slice" });
// Mock submitTargetInterviewResponse to return a valid response
const respondMock = vi.fn().mockResolvedValue({
type: "complete",
data: { title: "Slice Plan", description: "Done" },
});
vi.spyOn(milestoneSliceInterviewModule, "submitTargetInterviewResponse").mockImplementation(respondMock);
const res = await request(
app,
"POST",
`/api/missions/slices/${slice.id}/interview/respond`,
JSON.stringify({ sessionId: "slice-test-session", responses: { "sl-q-1": "answer" } }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.type).toBe("complete");
});
});
it("POST /api/missions/slices/:sliceId/interview/start succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
describe("retry endpoints", () => {
it("POST /api/missions/interview/:sessionId/retry succeeds under saturated settings", async () => {
const { app } = buildAppWithSaturatedSettings();
// Create a slice
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Test Slice" });
// Mock retryMissionInterviewSession to succeed
const retryMock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(missionInterviewModule, "retryMissionInterviewSession").mockImplementation(retryMock);
// Mock createTargetInterviewSession to return a session (from milestone-slice-interview module)
const createSessionMock = vi.fn().mockResolvedValue("slice-saturation-test-session");
vi.spyOn(milestoneSliceInterviewModule, "createTargetInterviewSession").mockImplementation(createSessionMock);
const res = await request(
app,
"POST",
"/api/missions/interview/failed-session/retry",
JSON.stringify({}),
{ "content-type": "application/json" },
);
const res = await request(
app,
"POST",
`/api/missions/slices/${slice.id}/interview/start`,
JSON.stringify({}),
{ "content-type": "application/json" },
);
// UTILITY PATH: Retry must NOT be gated on maxConcurrent
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
expect(res.status).toBe(201);
expect(res.body.sessionId).toBe("slice-saturation-test-session");
// Verify no saturation error was introduced
expect(res.body.error).toBeUndefined();
it("preserves lock-conflict 409 for mission retry under saturation", async () => {
const aiSessionStore = createMockAiSessionStore({ lockConflict: true });
const { app } = buildAppWithSaturatedSettings({ aiSessionStore });
const res = await request(
app,
"POST",
"/api/missions/interview/locked-retry-session/retry",
JSON.stringify({ tabId: "tab-conflict" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(409);
expect(res.body).toEqual({
error: "Session locked by another tab",
lockedByTab: "tab-owner",
});
});
it("POST /api/missions/milestones/:milestoneId/interview/:sessionId/retry succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Create a milestone
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
// Mock retryTargetInterviewSession to succeed
const retryMock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(milestoneSliceInterviewModule, "retryTargetInterviewSession").mockImplementation(retryMock);
const res = await request(
app,
"POST",
`/api/missions/milestones/${milestone.id}/interview/milestone-retry-session/retry`,
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
it("POST /api/missions/slices/:sliceId/interview/:sessionId/retry succeeds under saturated settings", async () => {
const { app, missionStore } = buildAppWithSaturatedSettings();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
// Create a slice
const mission = ms.createMission({ title: "Test Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Test Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Test Slice" });
// Mock retryTargetInterviewSession to succeed
const retryMock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(milestoneSliceInterviewModule, "retryTargetInterviewSession").mockImplementation(retryMock);
const res = await request(
app,
"POST",
`/api/missions/slices/${slice.id}/interview/slice-retry-session/retry`,
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
});
});

View File

@@ -8202,6 +8202,588 @@ describe("Planning Mode Routes", () => {
});
});
/**
* Saturated-slot regression coverage for utility AI routes.
* These tests prove that planning, subtask, and interview routes remain executable
* when the task-lane is saturated (maxConcurrent = 0).
*
* UTILITY PATH contract: These routes are on the heartbeat control-plane lane
* and must NOT be gated on task-lane saturation (maxConcurrent, semaphore, queue depth).
*
* See .fusion/memory.md "Heartbeat Control-Plane Lane (FN-1487)"
*/
describe("Saturated-slot regression: utility AI routes", () => {
/**
* Helper to create a store with saturated settings (maxConcurrent = 0).
* This simulates the task-lane being fully saturated.
*/
function createSaturatedStore(overrides: Partial<TaskStore> = {}): TaskStore {
return createMockStore({
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 0, // SATURATED: zero task slots available
promptOverrides: {},
}),
getSettingsFast: vi.fn().mockResolvedValue({
maxConcurrent: 0,
}),
...overrides,
} as Partial<TaskStore>);
}
/**
* Helper to create an app with saturated settings and optional aiSessionStore.
*/
function buildSaturatedApp(options: { aiSessionStore?: any } = {}) {
const store = createSaturatedStore();
const app = express();
app.use(express.json());
const routeOptions = options.aiSessionStore ? { aiSessionStore: options.aiSessionStore } : undefined;
app.use("/api", createApiRoutes(store, routeOptions as any));
return { app, store };
}
/**
* Setup a mock agent for planning session tests.
*/
function setupSaturatedPlanningMockAgent() {
const questionResponses = [
JSON.stringify({
type: "question",
data: {
id: "q-scope",
type: "single_select",
question: "What is the scope?",
description: "Choose scope.",
options: [
{ id: "small", label: "Small" },
{ id: "medium", label: "Medium" },
{ id: "large", label: "Large" },
],
},
}),
JSON.stringify({
type: "question",
data: {
id: "q-req",
type: "text",
question: "Requirements?",
},
}),
JSON.stringify({
type: "complete",
data: {
title: "Saturated Plan",
description: "A plan created under saturation",
suggestedSize: "M",
suggestedDependencies: [],
keyDeliverables: ["Item 1", "Item 2"],
},
}),
];
const messages: Array<{ role: string; content: string }> = [];
let callIndex = 0;
const mockAgent = {
session: {
state: { messages },
prompt: vi.fn(async (msg: string) => {
messages.push({ role: "user", content: msg });
const response = questionResponses[callIndex++] ?? questionResponses[questionResponses.length - 1];
messages.push({ role: "assistant", content: response });
}),
dispose: vi.fn(),
},
};
__setCreateKbAgent(async () => mockAgent);
}
describe("POST /api/planning/start — utility lane independence", () => {
beforeEach(() => {
__resetPlanningState();
setupSaturatedPlanningMockAgent();
});
afterEach(() => {
__setCreateKbAgent(undefined as any);
});
it("executes successfully when task-lane is saturated (maxConcurrent=0)", async () => {
const { app } = buildSaturatedApp();
const res = await REQUEST(
app,
"POST",
"/api/planning/start",
JSON.stringify({ initialPlan: "Plan a feature under saturated task-lane" }),
{ "Content-Type": "application/json" },
);
// UTILITY PATH: Planning start must NOT be gated on maxConcurrent
expect(res.status).toBe(201);
expect(res.body.sessionId).toBeDefined();
expect(res.body.firstQuestion).toBeDefined();
});
});
describe("POST /api/planning/start-streaming — utility lane independence", () => {
beforeEach(() => {
__resetPlanningState();
});
afterEach(() => {
__setCreateKbAgent(undefined as any);
});
it("executes successfully when task-lane is saturated (maxConcurrent=0)", async () => {
// Mock agent for streaming
const messages: Array<{ role: string; content: string }> = [];
const mockAgent = {
session: {
state: { messages },
prompt: vi.fn(async (msg: string) => {
messages.push({ role: "user", content: msg });
messages.push({
role: "assistant",
content: JSON.stringify({
type: "question",
data: { id: "q-scope", type: "text", question: "What to plan?" },
}),
});
}),
dispose: vi.fn(),
},
};
__setCreateKbAgent(async () => mockAgent);
const { app } = buildSaturatedApp();
const res = await REQUEST(
app,
"POST",
"/api/planning/start-streaming",
JSON.stringify({ initialPlan: "Streaming plan under saturation" }),
{ "Content-Type": "application/json" },
);
// UTILITY PATH: Planning streaming must NOT be gated on maxConcurrent
expect(res.status).toBe(201);
expect(res.body.sessionId).toBeDefined();
});
});
describe("POST /api/planning/respond — utility lane independence", () => {
beforeEach(() => {
__resetPlanningState();
setupSaturatedPlanningMockAgent();
});
afterEach(() => {
__setCreateKbAgent(undefined as any);
});
it("executes successfully when task-lane is saturated (maxConcurrent=0)", async () => {
const { app } = buildSaturatedApp();
// First create a session
const startRes = await REQUEST(
app,
"POST",
"/api/planning/start",
JSON.stringify({ initialPlan: "Test respond under saturation" }),
{ "Content-Type": "application/json" },
);
expect(startRes.status).toBe(201);
const sessionId = startRes.body.sessionId;
// Submit response - must NOT be blocked by maxConcurrent
const res = await REQUEST(
app,
"POST",
"/api/planning/respond",
JSON.stringify({ sessionId, responses: { scope: "medium" } }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.type).toBe("question");
});
it("preserves lock-conflict 409 semantics when task-lane is saturated", async () => {
// Create mock aiSessionStore that returns conflict on acquire
const mockAiSessionStore = {
acquireLock: vi.fn().mockReturnValue({ acquired: false, currentHolder: "tab-a" }),
releaseLock: vi.fn(),
};
const { app } = buildSaturatedApp({ aiSessionStore: mockAiSessionStore });
// Create session
const startRes = await REQUEST(
app,
"POST",
"/api/planning/start",
JSON.stringify({ initialPlan: "Test respond lock under saturation" }),
{ "Content-Type": "application/json" },
);
expect(startRes.status).toBe(201);
const sessionId = startRes.body.sessionId;
// Respond with conflicting tabId - mock returns conflict
const conflictRes = await REQUEST(
app,
"POST",
"/api/planning/respond",
JSON.stringify({ sessionId, responses: { scope: "medium" }, tabId: "tab-b" }),
{ "Content-Type": "application/json" },
);
expect(conflictRes.status).toBe(409);
expect(conflictRes.body).toEqual({
error: "Session locked by another tab",
lockedByTab: "tab-a",
});
});
});
describe("POST /api/planning/:sessionId/retry — utility lane independence", () => {
it("executes successfully when task-lane is saturated (maxConcurrent=0)", async () => {
const retrySpy = vi.spyOn(planningModule, "retrySession").mockResolvedValue();
const { app } = buildSaturatedApp();
const res = await REQUEST(app, "POST", "/api/planning/session-sat-retry/retry");
// UTILITY PATH: Planning retry must NOT be gated on maxConcurrent
expect(res.status).toBe(200);
expect(res.body).toEqual({ success: true, sessionId: "session-sat-retry" });
expect(retrySpy).toHaveBeenCalled();
});
it("preserves lock-conflict 409 semantics when task-lane is saturated", async () => {
// Create mock that returns conflict
const mockAiSessionStore = {
acquireLock: vi.fn().mockReturnValue({ acquired: false, currentHolder: "tab-x" }),
releaseLock: vi.fn(),
};
const { app } = buildSaturatedApp({ aiSessionStore: mockAiSessionStore });
const conflictRes = await REQUEST(
app,
"POST",
"/api/planning/session-locked-retry/retry",
JSON.stringify({ tabId: "tab-y" }),
{ "Content-Type": "application/json" },
);
expect(conflictRes.status).toBe(409);
expect(conflictRes.body).toEqual({
error: "Session locked by another tab",
lockedByTab: "tab-x",
});
});
});
describe("POST /api/subtasks/start-streaming — utility lane independence", () => {
it("executes successfully when task-lane is saturated (maxConcurrent=0)", async () => {
// Mock the subtask breakdown module to return proper format
const mockCreateSubtaskSession = vi.fn().mockResolvedValue({ sessionId: "subtask-sat-session" });
vi.spyOn(subtaskBreakdownModule, "createSubtaskSession").mockImplementation(mockCreateSubtaskSession);
try {
const { app } = buildSaturatedApp();
const res = await REQUEST(
app,
"POST",
"/api/subtasks/start-streaming",
JSON.stringify({ description: "Break into subtasks under saturation" }),
{ "Content-Type": "application/json" },
);
// UTILITY PATH: Subtask start must NOT be gated on maxConcurrent
expect(res.status).toBe(201);
expect(res.body.sessionId).toBeDefined();
expect(mockCreateSubtaskSession).toHaveBeenCalled();
} finally {
vi.restoreAllMocks();
}
});
});
describe("POST /api/subtasks/:sessionId/retry — utility lane independence", () => {
it("executes successfully when task-lane is saturated (maxConcurrent=0)", async () => {
const retrySpy = vi.spyOn(subtaskBreakdownModule, "retrySubtaskSession").mockResolvedValue();
const { app } = buildSaturatedApp();
const res = await REQUEST(app, "POST", "/api/subtasks/session-sat-retry/retry");
// UTILITY PATH: Subtask retry must NOT be gated on maxConcurrent
expect(res.status).toBe(200);
expect(retrySpy).toHaveBeenCalled();
});
it("preserves lock-conflict 409 semantics when task-lane is saturated", async () => {
// Create mock that returns conflict
const mockAiSessionStore = {
acquireLock: vi.fn().mockReturnValue({ acquired: false, currentHolder: "tab-locked" }),
releaseLock: vi.fn(),
};
const { app } = buildSaturatedApp({ aiSessionStore: mockAiSessionStore });
const conflictRes = await REQUEST(
app,
"POST",
"/api/subtasks/subtask-locked-retry/retry",
JSON.stringify({ tabId: "tab-conflict" }),
{ "Content-Type": "application/json" },
);
expect(conflictRes.status).toBe(409);
expect(conflictRes.body).toEqual({
error: "Session locked by another tab",
lockedByTab: "tab-locked",
});
});
});
});
/**
* Saturated-slot regression coverage for heartbeat wake routes.
* These tests prove that comment-triggered and steering-triggered heartbeat wake
* paths remain executable when the task-lane is saturated.
*
* UTILITY PATH contract: Wake delegation (heartbeatMonitor.executeHeartbeat) is on
* the heartbeat control-plane lane and must NOT be gated on task-lane saturation.
*
* See .fusion/memory.md "Heartbeat Control-Plane Lane (FN-1487)"
*/
describe("Saturated-slot regression: heartbeat wake routes", () => {
/**
* Helper to create a store with saturated settings (maxConcurrent = 0).
*/
function createSaturatedStore(overrides: Partial<TaskStore> = {}): TaskStore {
return createMockStore({
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 0, // SATURATED: zero task slots available
promptOverrides: {},
}),
getSettingsFast: vi.fn().mockResolvedValue({
maxConcurrent: 0,
}),
...overrides,
} as Partial<TaskStore>);
}
describe("POST /api/tasks/:id/comments — utility lane independence", () => {
it("triggers heartbeat wake for assigned agent when task-lane is saturated", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "kb-routes-sat-comment-"));
const fusionDir = join(tempDir, ".fusion");
mkdirSync(fusionDir, { recursive: true });
try {
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: fusionDir });
await agentStore.init();
const agent = await agentStore.createAgent({ name: "Saturated Wake Agent", role: "executor" });
await agentStore.updateAgent(agent.id, {
runtimeConfig: { messageResponseMode: "immediate" },
});
const heartbeatMonitor = {
executeHeartbeat: vi.fn().mockResolvedValue({ id: "run-sat-1" }),
};
const updatedTask = {
...FAKE_TASK_DETAIL,
id: "KB-SAT-001",
assignedAgentId: agent.id,
comments: [{ id: "comment-sat-1", text: "Hello", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
};
const store = createSaturatedStore({
addTaskComment: vi.fn().mockResolvedValue(updatedTask),
getFusionDir: vi.fn().mockReturnValue(fusionDir),
});
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store, { heartbeatMonitor } as any));
const res = await REQUEST(
app,
"POST",
"/api/tasks/KB-SAT-001/comments",
JSON.stringify({ text: "Hello" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
// UTILITY PATH: Wake must NOT be blocked by maxConcurrent=0
await vi.waitFor(() => {
expect(heartbeatMonitor.executeHeartbeat).toHaveBeenCalledWith(expect.objectContaining({
agentId: agent.id,
source: "on_demand",
taskId: "KB-SAT-001",
triggeringCommentIds: ["comment-sat-1"],
triggeringCommentType: "task",
}));
}, { timeout: 1000 });
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it("preserves active-run conflict 409 semantics when task-lane is saturated", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "kb-routes-sat-active-run-"));
const fusionDir = join(tempDir, ".fusion");
mkdirSync(fusionDir, { recursive: true });
try {
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: fusionDir });
await agentStore.init();
const agent = await agentStore.createAgent({ name: "Active Run Agent", role: "executor" });
await agentStore.updateAgent(agent.id, {
runtimeConfig: { messageResponseMode: "immediate" },
});
// Start an active run (simulates existing heartbeat)
await agentStore.startHeartbeatRun(agent.id);
const heartbeatMonitor = {
executeHeartbeat: vi.fn().mockResolvedValue({ id: "run-sat-2" }),
};
const updatedTask = {
...FAKE_TASK_DETAIL,
id: "KB-SAT-002",
assignedAgentId: agent.id,
comments: [{ id: "comment-sat-2", text: "Hello", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
};
const store = createSaturatedStore({
addTaskComment: vi.fn().mockResolvedValue(updatedTask),
getFusionDir: vi.fn().mockReturnValue(fusionDir),
});
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store, { heartbeatMonitor } as any));
const res = await REQUEST(
app,
"POST",
"/api/tasks/KB-SAT-002/comments",
JSON.stringify({ text: "Hello" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
// Active run conflict must still work under saturation
await vi.waitFor(() => {
expect(heartbeatMonitor.executeHeartbeat).not.toHaveBeenCalled();
}, { timeout: 1000 });
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
describe("POST /api/agents/:id/runs — utility lane independence", () => {
it("accepts triggering comment wake fields when task-lane is saturated", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "kb-routes-sat-agent-runs-"));
const fusionDir = join(tempDir, ".fusion");
mkdirSync(fusionDir, { recursive: true });
try {
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: fusionDir });
await agentStore.init();
const agent = await agentStore.createAgent({
name: "Saturated Run Agent",
role: "executor",
});
const store = createSaturatedStore({
getFusionDir: vi.fn().mockReturnValue(fusionDir),
} as any);
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
const res = await REQUEST(
app,
"POST",
`/api/agents/${agent.id}/runs`,
JSON.stringify({
source: "on_demand",
triggerDetail: "task-comment",
taskId: "FN-SAT-001",
triggeringCommentIds: ["c-sat-1", "c-sat-2"],
triggeringCommentType: "task",
}),
{ "Content-Type": "application/json" },
);
// UTILITY PATH: Agent run creation must NOT be blocked by maxConcurrent=0
expect(res.status).toBe(201);
expect(res.body.contextSnapshot).toMatchObject({
wakeReason: "on_demand",
triggerDetail: "task-comment",
taskId: "FN-SAT-001",
triggeringCommentIds: ["c-sat-1", "c-sat-2"],
triggeringCommentType: "task",
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it("preserves validation 400 for invalid triggeringCommentIds when task-lane is saturated", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "kb-routes-sat-validation-"));
const fusionDir = join(tempDir, ".fusion");
mkdirSync(fusionDir, { recursive: true });
try {
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: fusionDir });
await agentStore.init();
const agent = await agentStore.createAgent({
name: "Validation Agent",
role: "executor",
});
const store = createSaturatedStore({
getFusionDir: vi.fn().mockReturnValue(fusionDir),
} as any);
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
// Invalid: triggeringCommentIds is a string, not array
const res = await REQUEST(
app,
"POST",
`/api/agents/${agent.id}/runs`,
JSON.stringify({
source: "on_demand",
triggeringCommentIds: "not-an-array",
}),
{ "Content-Type": "application/json" },
);
// Validation must still work under saturation
expect(res.status).toBe(400);
expect(res.body.error).toContain("triggeringCommentIds must be an array");
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
});
describe("DELETE /api/ai-sessions/cleanup", () => {
let store: TaskStore;