feat(FN-1448): merge fusion/fn-1448
This commit is contained in:
@@ -9367,6 +9367,371 @@ describe("POST /api/ai/summarize-title", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /planning/start-streaming with projectId scoping", () => {
|
||||
const projectId = "proj-planning-scoped";
|
||||
|
||||
let defaultStore: TaskStore;
|
||||
let scopedStore: TaskStore;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultStore = createMockStore();
|
||||
scopedStore = createMockStore({
|
||||
getRootDir: vi.fn().mockReturnValue("/scoped/planning/project"),
|
||||
});
|
||||
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
__resetPlanningState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
__setCreateKbAgent(undefined as any);
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(defaultStore));
|
||||
return app;
|
||||
}
|
||||
|
||||
it("uses scoped store settings for prompt resolution when projectId is provided", async () => {
|
||||
const customPlanningPrompt = "CUSTOM SCOPED PLANNING PROMPT";
|
||||
(scopedStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
promptOverrides: {
|
||||
"planning-system": customPlanningPrompt,
|
||||
},
|
||||
defaultProvider: "scoped-provider",
|
||||
defaultModelId: "scoped-model",
|
||||
});
|
||||
|
||||
const mockAgent = {
|
||||
session: {
|
||||
state: { messages: [] },
|
||||
prompt: vi.fn(async () => {
|
||||
return JSON.stringify({
|
||||
type: "question",
|
||||
data: { id: "q-1", type: "text", question: "What is the scope?" },
|
||||
});
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
};
|
||||
const createKbAgentSpy = vi.fn(async () => mockAgent);
|
||||
__setCreateKbAgent(createKbAgentSpy as any);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
`/api/planning/start-streaming?projectId=${projectId}`,
|
||||
JSON.stringify({ initialPlan: "Test planning" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(projectStoreResolver.getOrCreateProjectStore).toHaveBeenCalledWith(projectId);
|
||||
expect(scopedStore.getSettings).toHaveBeenCalled();
|
||||
expect(scopedStore.getRootDir()).toBe("/scoped/planning/project");
|
||||
// Verify scoped settings were used for prompt resolution
|
||||
await vi.waitFor(() => {
|
||||
expect(createKbAgentSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
systemPrompt: customPlanningPrompt,
|
||||
defaultProvider: "scoped-provider",
|
||||
defaultModelId: "scoped-model",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses request body model override when provided alongside projectId", async () => {
|
||||
(scopedStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
defaultProvider: "scoped-provider",
|
||||
defaultModelId: "scoped-model",
|
||||
});
|
||||
|
||||
const mockAgent = {
|
||||
session: {
|
||||
state: { messages: [] },
|
||||
prompt: vi.fn(async () => {
|
||||
return JSON.stringify({
|
||||
type: "question",
|
||||
data: { id: "q-1", type: "text", question: "What is the scope?" },
|
||||
});
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
};
|
||||
const createKbAgentSpy = vi.fn(async () => mockAgent);
|
||||
__setCreateKbAgent(createKbAgentSpy as any);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
`/api/planning/start-streaming?projectId=${projectId}`,
|
||||
JSON.stringify({
|
||||
initialPlan: "Test planning",
|
||||
planningModelProvider: "google",
|
||||
planningModelId: "gemini-2.5-pro",
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Request body override takes precedence over scoped settings
|
||||
await vi.waitFor(() => {
|
||||
expect(createKbAgentSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultProvider: "google",
|
||||
defaultModelId: "gemini-2.5-pro",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to scoped settings when no request override provided", async () => {
|
||||
(scopedStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4",
|
||||
});
|
||||
|
||||
const mockAgent = {
|
||||
session: {
|
||||
state: { messages: [] },
|
||||
prompt: vi.fn(async () => {
|
||||
return JSON.stringify({
|
||||
type: "question",
|
||||
data: { id: "q-1", type: "text", question: "What is the scope?" },
|
||||
});
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
};
|
||||
const createKbAgentSpy = vi.fn(async () => mockAgent);
|
||||
__setCreateKbAgent(createKbAgentSpy as any);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
`/api/planning/start-streaming?projectId=${projectId}`,
|
||||
JSON.stringify({ initialPlan: "Test planning" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Scoped settings planning lane should be used
|
||||
await vi.waitFor(() => {
|
||||
expect(createKbAgentSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses default store when projectId is omitted", async () => {
|
||||
// When projectId is omitted, default store should be used
|
||||
const defaultRootDir = "/fake/root";
|
||||
(defaultStore.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(defaultRootDir);
|
||||
(defaultStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
defaultProvider: "default-provider",
|
||||
defaultModelId: "default-model",
|
||||
});
|
||||
|
||||
const mockAgent = {
|
||||
session: {
|
||||
state: { messages: [] },
|
||||
prompt: vi.fn(async () => {
|
||||
return JSON.stringify({
|
||||
type: "question",
|
||||
data: { id: "q-1", type: "text", question: "What is the scope?" },
|
||||
});
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
};
|
||||
const createKbAgentSpy = vi.fn(async () => mockAgent);
|
||||
__setCreateKbAgent(createKbAgentSpy as any);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/planning/start-streaming",
|
||||
JSON.stringify({ initialPlan: "Test planning" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Default store should be used (getOrCreateProjectStore should not be called)
|
||||
expect(projectStoreResolver.getOrCreateProjectStore).not.toHaveBeenCalled();
|
||||
await vi.waitFor(() => {
|
||||
expect(createKbAgentSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultProvider: "default-provider",
|
||||
defaultModelId: "default-model",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /subtasks/start-streaming with projectId scoping", () => {
|
||||
const projectId = "proj-subtask-scoped";
|
||||
|
||||
let defaultStore: TaskStore;
|
||||
let scopedStore: TaskStore;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultStore = createMockStore();
|
||||
scopedStore = createMockStore({
|
||||
getRootDir: vi.fn().mockReturnValue("/scoped/subtask/project"),
|
||||
});
|
||||
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
__resetSubtaskBreakdownState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(defaultStore));
|
||||
return app;
|
||||
}
|
||||
|
||||
it("uses scoped store settings for prompt resolution when projectId is provided", async () => {
|
||||
const customSubtaskPrompt = "CUSTOM SCOPED SUBTASK BREAKDOWN PROMPT";
|
||||
(scopedStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
promptOverrides: {
|
||||
"subtask-breakdown-system": customSubtaskPrompt,
|
||||
},
|
||||
});
|
||||
|
||||
const mockCreateSubtaskSession = vi.fn().mockResolvedValue({ sessionId: "scoped-subtask-session" });
|
||||
vi.spyOn(subtaskBreakdownModule, "createSubtaskSession").mockImplementation(mockCreateSubtaskSession);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
`/api/subtasks/start-streaming?projectId=${projectId}`,
|
||||
JSON.stringify({ description: "Break into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.sessionId).toBe("scoped-subtask-session");
|
||||
expect(projectStoreResolver.getOrCreateProjectStore).toHaveBeenCalledWith(projectId);
|
||||
expect(scopedStore.getSettings).toHaveBeenCalled();
|
||||
expect(scopedStore.getRootDir()).toBe("/scoped/subtask/project");
|
||||
// Verify scoped settings were passed for prompt resolution
|
||||
expect(mockCreateSubtaskSession).toHaveBeenCalledWith(
|
||||
"Break into subtasks",
|
||||
scopedStore,
|
||||
"/scoped/subtask/project",
|
||||
expect.objectContaining({
|
||||
"subtask-breakdown-system": customSubtaskPrompt,
|
||||
}),
|
||||
projectId,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses scoped rootDir for subtask generation", async () => {
|
||||
const scopedRootDir = "/different/scoped/root";
|
||||
scopedStore = createMockStore({
|
||||
getRootDir: vi.fn().mockReturnValue(scopedRootDir),
|
||||
});
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
|
||||
(scopedStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
promptOverrides: {},
|
||||
});
|
||||
|
||||
const mockCreateSubtaskSession = vi.fn().mockResolvedValue({ sessionId: "session-2" });
|
||||
vi.spyOn(subtaskBreakdownModule, "createSubtaskSession").mockImplementation(mockCreateSubtaskSession);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
`/api/subtasks/start-streaming?projectId=${projectId}`,
|
||||
JSON.stringify({ description: "Break into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockCreateSubtaskSession).toHaveBeenCalledWith(
|
||||
"Break into subtasks",
|
||||
scopedStore,
|
||||
scopedRootDir,
|
||||
expect.any(Object),
|
||||
projectId,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses default store when projectId is omitted", async () => {
|
||||
// When projectId is omitted, default store should be used
|
||||
const defaultRootDir = "/fake/root";
|
||||
(defaultStore.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(defaultRootDir);
|
||||
(defaultStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
promptOverrides: {
|
||||
"subtask-breakdown-system": "Default subtask prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const mockCreateSubtaskSession = vi.fn().mockResolvedValue({ sessionId: "default-session" });
|
||||
vi.spyOn(subtaskBreakdownModule, "createSubtaskSession").mockImplementation(mockCreateSubtaskSession);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/start-streaming",
|
||||
JSON.stringify({ description: "Break into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Default store should be used (getOrCreateProjectStore should not be called)
|
||||
expect(projectStoreResolver.getOrCreateProjectStore).not.toHaveBeenCalled();
|
||||
expect(mockCreateSubtaskSession).toHaveBeenCalledWith(
|
||||
"Break into subtasks",
|
||||
defaultStore,
|
||||
defaultRootDir,
|
||||
expect.any(Object),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("passes projectId to subtask session for multi-project scoping", async () => {
|
||||
(scopedStore.getSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
promptOverrides: {},
|
||||
});
|
||||
|
||||
const mockCreateSubtaskSession = vi.fn().mockResolvedValue({ sessionId: "session-with-projectid" });
|
||||
vi.spyOn(subtaskBreakdownModule, "createSubtaskSession").mockImplementation(mockCreateSubtaskSession);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
`/api/subtasks/start-streaming?projectId=${projectId}`,
|
||||
JSON.stringify({ description: "Scoped subtask" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Ensure projectId is passed through for multi-project scoping
|
||||
expect(mockCreateSubtaskSession).toHaveBeenCalledWith(
|
||||
"Scoped subtask",
|
||||
scopedStore,
|
||||
"/scoped/subtask/project",
|
||||
expect.any(Object),
|
||||
projectId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/ai/summarize-title with projectId scoping", () => {
|
||||
const projectId = "proj-summarize-scoped";
|
||||
let defaultStore: TaskStore;
|
||||
|
||||
Reference in New Issue
Block a user