Merge pull request #49 from timothyjlaurent/fix/mission-interview-default-model
fix(dashboard): resolve default model from settings for mission interview
This commit is contained in:
@@ -2878,6 +2878,163 @@ describe("Mission API", () => {
|
||||
lockedByTab: "other-tab",
|
||||
});
|
||||
});
|
||||
|
||||
it("POST /api/missions/interview/start resolves default model from settings when no override provided", async () => {
|
||||
// Configure scoped store with default model settings
|
||||
scopedStore = {
|
||||
getRootDir: vi.fn().mockReturnValue(scopedRootDir),
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
promptOverrides: {},
|
||||
defaultProvider: "zai",
|
||||
defaultModelId: "glm-5.1",
|
||||
}),
|
||||
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
||||
} as unknown as TaskStore;
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
|
||||
const createSpy = vi
|
||||
.spyOn(missionInterviewModule, "createMissionInterviewSession")
|
||||
.mockResolvedValueOnce("resolved-model-session-id");
|
||||
|
||||
const { app } = buildApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
`/api/missions/interview/start?projectId=${projectId}`,
|
||||
JSON.stringify({ missionTitle: "Default Model Mission" }),
|
||||
{ "content-type": "application/json" }
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// The route should resolve the default model from settings and pass it through
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
"Default Model Mission",
|
||||
scopedRootDir,
|
||||
{},
|
||||
"zai",
|
||||
"glm-5.1",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /api/missions/interview/start uses planning-specific model over global default", async () => {
|
||||
// Configure scoped store with both planning-specific and global defaults
|
||||
scopedStore = {
|
||||
getRootDir: vi.fn().mockReturnValue(scopedRootDir),
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
promptOverrides: {},
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
defaultProvider: "zai",
|
||||
defaultModelId: "glm-5.1",
|
||||
}),
|
||||
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
||||
} as unknown as TaskStore;
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
|
||||
const createSpy = vi
|
||||
.spyOn(missionInterviewModule, "createMissionInterviewSession")
|
||||
.mockResolvedValueOnce("planning-model-session-id");
|
||||
|
||||
const { app } = buildApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
`/api/missions/interview/start?projectId=${projectId}`,
|
||||
JSON.stringify({ missionTitle: "Planning Model Mission" }),
|
||||
{ "content-type": "application/json" }
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Planning-specific model should take priority over global default
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
"Planning Model Mission",
|
||||
scopedRootDir,
|
||||
{},
|
||||
"anthropic",
|
||||
"claude-sonnet-4-5",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /api/missions/interview/start explicit model override takes precedence over settings defaults", async () => {
|
||||
// Configure scoped store with default model settings
|
||||
scopedStore = {
|
||||
getRootDir: vi.fn().mockReturnValue(scopedRootDir),
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
promptOverrides: {},
|
||||
defaultProvider: "zai",
|
||||
defaultModelId: "glm-5.1",
|
||||
}),
|
||||
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
||||
} as unknown as TaskStore;
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
|
||||
const createSpy = vi
|
||||
.spyOn(missionInterviewModule, "createMissionInterviewSession")
|
||||
.mockResolvedValueOnce("override-session-id");
|
||||
|
||||
const { app } = buildApp();
|
||||
// Send explicit model override in request body
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
`/api/missions/interview/start?projectId=${projectId}`,
|
||||
JSON.stringify({
|
||||
missionTitle: "Override Mission",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
}),
|
||||
{ "content-type": "application/json" }
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Explicit override should win over settings defaults
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
"Override Mission",
|
||||
scopedRootDir,
|
||||
{},
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /api/missions/interview/start passes undefined model when no defaults configured", async () => {
|
||||
// Settings with no model configuration at all (the "no defaults" case)
|
||||
scopedStore = {
|
||||
getRootDir: vi.fn().mockReturnValue(scopedRootDir),
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
promptOverrides: {},
|
||||
}),
|
||||
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
||||
} as unknown as TaskStore;
|
||||
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);
|
||||
|
||||
const createSpy = vi
|
||||
.spyOn(missionInterviewModule, "createMissionInterviewSession")
|
||||
.mockResolvedValueOnce("no-defaults-session-id");
|
||||
|
||||
const { app } = buildApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
`/api/missions/interview/start?projectId=${projectId}`,
|
||||
JSON.stringify({ missionTitle: "No Defaults Mission" }),
|
||||
{ "content-type": "application/json" }
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// When no defaults are configured, provider/model should be undefined
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
"No Defaults Mission",
|
||||
scopedRootDir,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Regression: Generated ID format acceptance ─────────────────────────
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import { Router, type Request, type Response, type NextFunction } from "express";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { TaskStore } from "@fusion/core";
|
||||
import { TaskStore, resolvePlanningSettingsModel } from "@fusion/core";
|
||||
import { getOrCreateProjectStore } from "./project-store-resolver.js";
|
||||
import type {
|
||||
Mission,
|
||||
@@ -411,13 +411,19 @@ export function createMissionRouter(
|
||||
|
||||
const { createMissionInterviewSession } = await import("./mission-interview.js");
|
||||
|
||||
// Resolve effective model: explicit override wins, then fall back to
|
||||
// planning settings chain (planning-specific → project defaults → global defaults).
|
||||
const effectiveModel = resolvePlanningSettingsModel(settings);
|
||||
const resolvedProvider = modelProvider ?? effectiveModel.provider;
|
||||
const resolvedModelId = modelId ?? effectiveModel.modelId;
|
||||
|
||||
const sessionId = await createMissionInterviewSession(
|
||||
ip,
|
||||
missionTitle.trim(),
|
||||
rootDir,
|
||||
settings.promptOverrides,
|
||||
modelProvider,
|
||||
modelId,
|
||||
resolvedProvider,
|
||||
resolvedModelId,
|
||||
);
|
||||
res.status(201).json({ sessionId });
|
||||
} catch (err: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user