feat(FN-1675): merge fusion/fn-1675
This commit is contained in:
@@ -357,14 +357,41 @@ describe("Chat API Routes", () => {
|
||||
expect(response.status).toBe(201);
|
||||
expect((response.body as any).session.id).toBe("chat-abc123");
|
||||
// Model is resolved from agent's runtimeConfig.model
|
||||
// projectId is null when no projectId query param is provided
|
||||
expect(mockCreateSession).toHaveBeenCalledWith({
|
||||
agentId: "agent-001",
|
||||
title: null,
|
||||
projectId: null,
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
});
|
||||
|
||||
it("creates session with projectId when projectId query param is provided", async () => {
|
||||
const sessionWithProject = {
|
||||
...sampleSession,
|
||||
projectId: "proj-001",
|
||||
};
|
||||
mockCreateSession.mockReturnValue(sessionWithProject);
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/chat/sessions?projectId=proj-001",
|
||||
JSON.stringify({ agentId: "agent-001" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect((response.body as any).session.projectId).toBe("proj-001");
|
||||
// Verify projectId is passed to createSession
|
||||
expect(mockCreateSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectId: "proj-001",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("creates session with title and resolves model from agent config", async () => {
|
||||
const sessionWithOptions = {
|
||||
...sampleSession,
|
||||
@@ -387,6 +414,13 @@ describe("Chat API Routes", () => {
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect((response.body as any).session.title).toBe("Custom Title");
|
||||
// projectId is null when no projectId query param is provided
|
||||
expect(mockCreateSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Custom Title",
|
||||
projectId: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 400 when agentId is missing", async () => {
|
||||
@@ -455,9 +489,11 @@ describe("Chat API Routes", () => {
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
// No model resolved from agent config
|
||||
// projectId is null when no projectId query param is provided
|
||||
expect(mockCreateSession).toHaveBeenCalledWith({
|
||||
agentId: "agent-002",
|
||||
title: null,
|
||||
projectId: null,
|
||||
modelProvider: null,
|
||||
modelId: null,
|
||||
});
|
||||
|
||||
@@ -276,6 +276,31 @@ describe("Roadmap Routes", () => {
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.title).toBe("New Roadmap");
|
||||
});
|
||||
|
||||
it("returns 400 when title is missing", async () => {
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps", JSON.stringify({}), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("title is required");
|
||||
});
|
||||
|
||||
it("returns 400 when title is empty", async () => {
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps", JSON.stringify({ title: "" }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("title is required");
|
||||
});
|
||||
|
||||
it("returns 400 when title is whitespace-only", async () => {
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps", JSON.stringify({ title: " " }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("title is required");
|
||||
});
|
||||
|
||||
it("returns 400 when title exceeds 200 characters", async () => {
|
||||
const longTitle = "A".repeat(201);
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps", JSON.stringify({ title: longTitle }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("200 characters");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/roadmaps/:roadmapId", () => {
|
||||
@@ -325,6 +350,20 @@ describe("Roadmap Routes", () => {
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps/" + roadmap.id + "/milestones/reorder", JSON.stringify({ orderedMilestoneIds: [m2.id, m1.id] }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(204);
|
||||
});
|
||||
|
||||
it("returns 400 when orderedMilestoneIds is not an array", async () => {
|
||||
const roadmap = mockRoadmapStore.createRoadmap({ title: "Test" });
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps/" + roadmap.id + "/milestones/reorder", JSON.stringify({ orderedMilestoneIds: "not-an-array" }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("must be an array");
|
||||
});
|
||||
|
||||
it("returns 400 when orderedMilestoneIds contains non-strings", async () => {
|
||||
const roadmap = mockRoadmapStore.createRoadmap({ title: "Test" });
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps/" + roadmap.id + "/milestones/reorder", JSON.stringify({ orderedMilestoneIds: ["id1", 123, "id3"] }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("must be an array of strings");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/roadmaps/milestones/:milestoneId", () => {
|
||||
@@ -354,6 +393,32 @@ describe("Roadmap Routes", () => {
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.title).toBe("New Feature");
|
||||
});
|
||||
|
||||
it("returns 400 when title is missing", async () => {
|
||||
const roadmap = mockRoadmapStore.createRoadmap({ title: "Test" });
|
||||
const milestone = mockRoadmapStore.createMilestone(roadmap.id, { title: "MS" });
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps/milestones/" + milestone.id + "/features", JSON.stringify({}), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("title is required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/roadmaps/milestones/:milestoneId/features/reorder", () => {
|
||||
it("returns 400 when orderedFeatureIds is not an array", async () => {
|
||||
const roadmap = mockRoadmapStore.createRoadmap({ title: "Test" });
|
||||
const milestone = mockRoadmapStore.createMilestone(roadmap.id, { title: "MS" });
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps/milestones/" + milestone.id + "/features/reorder", JSON.stringify({ orderedFeatureIds: "not-an-array" }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("must be an array");
|
||||
});
|
||||
|
||||
it("returns 400 when orderedFeatureIds contains non-strings", async () => {
|
||||
const roadmap = mockRoadmapStore.createRoadmap({ title: "Test" });
|
||||
const milestone = mockRoadmapStore.createMilestone(roadmap.id, { title: "MS" });
|
||||
const response = await performRequest(app, "POST", "/api/roadmaps/milestones/" + milestone.id + "/features/reorder", JSON.stringify({ orderedFeatureIds: [123, "id2"] }), { "Content-Type": "application/json" });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain("must be an array of strings");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/roadmaps/features/:featureId", () => {
|
||||
|
||||
@@ -7817,6 +7817,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
* Create a new chat session.
|
||||
* Body: { agentId: string, title?: string }
|
||||
* The model is resolved from the agent's runtimeConfig.model setting.
|
||||
* The session is scoped to the project identified by projectId query param or header.
|
||||
*/
|
||||
router.post("/chat/sessions", rateLimit(RATE_LIMITS.mutation), async (req, res) => {
|
||||
try {
|
||||
@@ -7825,7 +7826,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
throw internalError("Chat store not available");
|
||||
}
|
||||
|
||||
const scopedStore = await getScopedStore(req);
|
||||
// Get project context to scope the session and resolve agent from the correct store
|
||||
const { store: scopedStore, projectId } = await getProjectContext(req);
|
||||
const { AgentStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
@@ -7852,9 +7854,11 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
const resolvedProvider = slashIdx > 0 ? runtimeModel.slice(0, slashIdx) : undefined;
|
||||
const resolvedModelId = slashIdx > 0 ? runtimeModel.slice(slashIdx + 1) : undefined;
|
||||
|
||||
// Create the chat session with projectId for multi-project scoping
|
||||
const session = chatStore.createSession({
|
||||
agentId: agentId.trim(),
|
||||
title: title?.trim() || null,
|
||||
projectId: projectId ?? null,
|
||||
modelProvider: resolvedProvider ?? null,
|
||||
modelId: resolvedModelId ?? null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user