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:
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user