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:
gsxdsm
2026-05-06 00:03:16 -07:00
committed by GitHub
2 changed files with 166 additions and 3 deletions

View File

@@ -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 ─────────────────────────

View File

@@ -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) {