Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions packages/dashboard/src/__tests__/mission-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Comment on lines +2919 to +2957
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Global-planning tier not covered by route tests

The new tests exercise tier 1 (project-level planningProvider/planningModelId) and tier 4 (defaultProvider/defaultModelId), but the intermediate planningGlobalProvider/planningGlobalModelId tier in resolvePlanningSettingsModel has no route-layer coverage. If the resolver's priority order is ever changed, a bug could silently survive. Adding a test with only planningGlobalProvider/planningGlobalModelId set (and no project-level planning fields) would close this gap.

});

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 ─────────────────────────
Expand Down
12 changes: 9 additions & 3 deletions packages/dashboard/src/mission-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Loading