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 02249d9baf
commit d87ab0e26f
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);
});
});
});