fix(dashboard): plumb TaskStore through mission-interview rehydration

Mirror the planning.ts fix: capture store/rootDir on MissionInterviewSession
at creation, fall back to the captured values in
ensureMissionInterviewAgent when callers don't plumb them through. Then
update the session/mission test files so their createSession,
createMissionInterviewSession, and rehydrated submitResponse/retry calls
pass a real TaskStore where they were previously passing undefined.

Fixes 10 failing tests across mission-interview.test.ts,
milestone-slice-interview.test.ts, session-error-recovery.test.ts,
session-persistence-roundtrip.test.ts, session-resume-history.test.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-19 18:26:18 -07:00
parent 329b74416d
commit 236bfeeabc
6 changed files with 61 additions and 17 deletions

View File

@@ -460,7 +460,7 @@ describe("milestone-slice-interview module", () => {
mockCreateFnAgent.mockImplementation(async () => createMockAgent([createQuestionJson()]));
await expect(retryTargetInterviewSession(sessionId, "/tmp/project")).resolves.not.toThrow();
await expect(retryTargetInterviewSession(sessionId, "/tmp/project", MOCK_TASK_STORE)).resolves.not.toThrow();
});
it("throws when retrying a non-error session", async () => {

View File

@@ -389,6 +389,7 @@ describe("mission-interview module", () => {
row.id,
{ "q-2": "Need launch in 4 weeks" },
"/tmp/project",
MOCK_TASK_STORE,
);
expect(result.type).toBe("question");

View File

@@ -10,7 +10,7 @@ import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "@fusion/core";
import { Database, TaskStore } from "@fusion/core";
import { AiSessionStore } from "../ai-session-store.js";
import {
__resetPlanningState,
@@ -82,8 +82,9 @@ describe("session error recovery", () => {
let tmpDir: string;
let db: Database;
let aiSessionStore: AiSessionStore;
let taskStore: TaskStore;
beforeEach(() => {
beforeEach(async () => {
vi.clearAllMocks();
__resetPlanningState();
__resetSubtaskBreakdownState();
@@ -93,6 +94,8 @@ describe("session error recovery", () => {
db = new Database(join(tmpDir, ".fusion"));
db.init();
aiSessionStore = new AiSessionStore(db);
taskStore = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });
await taskStore.init();
setPlanningAiSessionStore(aiSessionStore);
setSubtaskAiSessionStore(aiSessionStore);
@@ -105,6 +108,11 @@ describe("session error recovery", () => {
__resetSubtaskBreakdownState();
__resetMissionInterviewState();
try {
taskStore.close();
} catch {
// no-op
}
try {
db.close();
} catch {
@@ -133,7 +141,7 @@ describe("session error recovery", () => {
]),
);
const { sessionId } = await createSession("127.0.0.101", "Planning error flow", undefined, "/tmp/project");
const { sessionId } = await createSession("127.0.0.101", "Planning error flow", taskStore, "/tmp/project");
const unsubscribeError = planningStreamManager.subscribe(sessionId, (event) => {
if (event.type === "error") {
@@ -237,7 +245,7 @@ describe("session error recovery", () => {
]),
);
const sessionId = await createMissionInterviewSession("127.0.0.111", "Mission error flow", "/tmp/project");
const sessionId = await createMissionInterviewSession("127.0.0.111", "Mission error flow", "/tmp/project", taskStore);
await waitFor(() => Boolean(getMissionInterviewSession(sessionId)?.currentQuestion));
const unsubscribe = missionInterviewStreamManager.subscribe(sessionId, (event) => {

View File

@@ -10,7 +10,7 @@ import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "@fusion/core";
import { Database, TaskStore } from "@fusion/core";
import { AiSessionStore, type AiSessionRow } from "../ai-session-store.js";
import {
__resetPlanningState,
@@ -75,8 +75,9 @@ describe("session persistence round-trip", () => {
let tmpDir: string;
let db: Database;
let aiSessionStore: AiSessionStore;
let taskStore: TaskStore;
beforeEach(() => {
beforeEach(async () => {
vi.clearAllMocks();
__resetPlanningState();
__resetSubtaskBreakdownState();
@@ -86,6 +87,8 @@ describe("session persistence round-trip", () => {
db = new Database(join(tmpDir, ".fusion"));
db.init();
aiSessionStore = new AiSessionStore(db);
taskStore = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });
await taskStore.init();
setPlanningAiSessionStore(aiSessionStore);
setSubtaskAiSessionStore(aiSessionStore);
@@ -98,6 +101,11 @@ describe("session persistence round-trip", () => {
__resetSubtaskBreakdownState();
__resetMissionInterviewState();
try {
taskStore.close();
} catch {
// no-op
}
try {
db.close();
} catch {
@@ -132,7 +140,7 @@ describe("session persistence round-trip", () => {
]),
);
const { sessionId } = await createSession("127.0.0.31", "Plan persistence", undefined, "/tmp/project");
const { sessionId } = await createSession("127.0.0.31", "Plan persistence", taskStore, "/tmp/project");
const afterCreate = aiSessionStore.get(sessionId);
expect(afterCreate?.status).toBe("awaiting_input");
@@ -292,6 +300,7 @@ describe("session persistence round-trip", () => {
"127.0.0.44",
"Mission persistence",
"/tmp/project",
taskStore,
undefined,
undefined,
undefined,
@@ -324,7 +333,7 @@ describe("session persistence round-trip", () => {
]),
);
const { sessionId } = await createSession("127.0.0.55", "Cancel persistence", undefined, "/tmp/project");
const { sessionId } = await createSession("127.0.0.55", "Cancel persistence", taskStore, "/tmp/project");
expect(aiSessionStore.get(sessionId)).not.toBeNull();
await cancelSession(sessionId);

View File

@@ -10,7 +10,7 @@ import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "@fusion/core";
import { Database, TaskStore } from "@fusion/core";
import { AiSessionStore, type AiSessionRow } from "../ai-session-store.js";
import {
__resetPlanningState,
@@ -75,8 +75,9 @@ describe("session resume + history restore", () => {
let tmpDir: string;
let db: Database;
let aiSessionStore: AiSessionStore;
let taskStore: TaskStore;
beforeEach(() => {
beforeEach(async () => {
vi.clearAllMocks();
__resetPlanningState();
__resetSubtaskBreakdownState();
@@ -86,6 +87,8 @@ describe("session resume + history restore", () => {
db = new Database(join(tmpDir, ".fusion"));
db.init();
aiSessionStore = new AiSessionStore(db);
taskStore = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });
await taskStore.init();
setPlanningAiSessionStore(aiSessionStore);
setSubtaskAiSessionStore(aiSessionStore);
@@ -98,6 +101,11 @@ describe("session resume + history restore", () => {
__resetSubtaskBreakdownState();
__resetMissionInterviewState();
try {
taskStore.close();
} catch {
// no-op
}
try {
db.close();
} catch {
@@ -151,7 +159,7 @@ describe("session resume + history restore", () => {
expect(restored?.thinkingOutput).toBe("latest-thinking");
expect(restored?.lastGeneratedThinking).toBe("latest-thinking");
const response = await submitResponse(row.id, { "q-2": "No constraints" }, "/tmp/project");
const response = await submitResponse(row.id, { "q-2": "No constraints" }, "/tmp/project", undefined, taskStore);
expect(response.type).toBe("question");
if (response.type === "question") {
expect(response.data.id).toBe("q-3");
@@ -212,6 +220,7 @@ describe("session resume + history restore", () => {
row.id,
{ "q-m-2": "None" },
"/tmp/project",
taskStore,
);
expect(response.type).toBe("question");
@@ -288,8 +297,8 @@ describe("session resume + history restore", () => {
]),
);
const planning = await createSession("127.0.0.88", "Fresh planning", undefined, "/tmp/project");
const missionSessionId = await createMissionInterviewSession("127.0.0.89", "Fresh mission", "/tmp/project");
const planning = await createSession("127.0.0.88", "Fresh planning", taskStore, "/tmp/project");
const missionSessionId = await createMissionInterviewSession("127.0.0.89", "Fresh mission", "/tmp/project", taskStore);
await waitFor(() => Boolean(getMissionInterviewSession(missionSessionId)?.currentQuestion));

View File

@@ -264,6 +264,12 @@ interface MissionInterviewSession {
/** Model override for this interview session */
modelProvider?: string;
modelId?: string;
/**
* Captured at session creation so rehydrated sessions can rebuild their
* agent without the caller threading context through every API.
*/
store?: TaskStore;
rootDir?: string;
createdAt: Date;
updatedAt: Date;
}
@@ -910,18 +916,21 @@ async function ensureMissionInterviewAgent(
return;
}
if (!rootDir) {
const effectiveRootDir = rootDir ?? session.rootDir;
const effectiveStore = store ?? session.store;
if (!effectiveRootDir) {
throw new InvalidSessionStateError(
"AI agent not available for this session and cannot be resumed without project context",
);
}
if (!store) {
if (!effectiveStore) {
throw new InvalidSessionStateError(
"AI agent not available for this session and cannot be resumed without task store context",
);
}
session.agent = await createMissionInterviewAgent(session, rootDir, store, promptOverrides);
session.agent = await createMissionInterviewAgent(session, effectiveRootDir, effectiveStore, promptOverrides);
if (historyForReplay.length === 0) {
return;
@@ -1159,6 +1168,8 @@ export async function createMissionInterviewSession(
lastGeneratedThinking: "",
modelProvider,
modelId,
store,
rootDir,
createdAt: new Date(),
updatedAt: new Date(),
};
@@ -1195,6 +1206,9 @@ export async function submitMissionInterviewResponse(
throw new SessionNotFoundError(`Mission interview session ${sessionId} not found or expired`);
}
if (store && !session.store) session.store = store;
if (rootDir && !session.rootDir) session.rootDir = rootDir;
if (!session.currentQuestion) {
throw new InvalidSessionStateError("No active question in session");
}
@@ -1245,6 +1259,9 @@ export async function retryMissionInterviewSession(
throw new SessionNotFoundError(`Mission interview session ${sessionId} not found or expired`);
}
if (store && !session.store) session.store = store;
if (rootDir && !session.rootDir) session.rootDir = rootDir;
const persisted = _aiSessionStore?.get(sessionId);
if (persisted && persisted.type !== "mission_interview") {
throw new SessionNotFoundError(`Mission interview session ${sessionId} not found or expired`);