Revert "fix(chat): align message writer store resolution with reader"
This reverts commit 802fecb245.
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix regular chat (ChatView) messages disappearing after leaving and returning to a conversation. The chat message **writer** (`POST /api/chat/sessions/:id/messages`, plus cancel and `isGenerating` enrichment) resolved its per-project `ChatManager`/`ChatStore` through `getOrCreateProjectStore`, while the **reader** (`GET /api/chat/sessions/:id/messages`) resolves through the engine-aware `resolveProjectChatContext`. When those resolved to different store instances, a sent message persisted to one store but the reload read from another, so it vanished on return. The writer now resolves through the same `resolveProjectChatContext` path as the reader, guaranteeing writes and reads share one store. Quick Chat masked the bug by keeping its thread warm in memory (no server reload).
|
||||
@@ -1664,39 +1664,20 @@ describe("multi-project chat routing", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("POST /cancel resolves the scoped manager via the engine-aware context (same store as the reader)", async () => {
|
||||
// FNXC:ChatPersistence regression — the scoped writer (cancel/isGenerating/
|
||||
// messages) must resolve through resolveProjectChatContext, the SAME engine-
|
||||
// aware path the reader uses, so writes and reads land in one store. The old
|
||||
// writer diverged via getOrCreateProjectStore and dropped messages on reload.
|
||||
it("POST /cancel uses scoped ChatManager when projectId is provided", async () => {
|
||||
mockCancelGeneration.mockReturnValue(false);
|
||||
const engineChatStore = { ...mockChatStoreInstance };
|
||||
const getChatStore = vi.fn(() => engineChatStore);
|
||||
const getTaskStore = vi.fn(() => store);
|
||||
const mockEngine = { getChatStore, getTaskStore };
|
||||
const getEngine = vi.fn((id: string) =>
|
||||
id === secondarySession.projectId ? mockEngine : undefined,
|
||||
);
|
||||
const { createServer } = await import("../server.js");
|
||||
const appWithEngine = createServer(store as any, {
|
||||
chatStore: mockChatStore as any,
|
||||
chatManager: mockChatManager as any,
|
||||
engineManager: { getEngine, getAllEngines: vi.fn().mockReturnValue(new Map()) } as any,
|
||||
});
|
||||
|
||||
const response = await request(
|
||||
appWithEngine,
|
||||
app,
|
||||
"POST",
|
||||
`/api/chat/sessions/${secondarySession.id}/cancel?projectId=${secondarySession.projectId}`,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
// Writer consulted the engine for this project (engine-aware resolution)
|
||||
expect(getEngine).toHaveBeenCalledWith(secondarySession.projectId);
|
||||
expect(getChatStore).toHaveBeenCalled();
|
||||
// The divergent getOrCreateProjectStore path is no longer used
|
||||
expect(mockGetOrCreateProjectStore).not.toHaveBeenCalled();
|
||||
// cancelGeneration still runs on the scoped manager
|
||||
expect((response.body as any).success).toBe(false);
|
||||
// Scoped path: getOrCreateProjectStore is called with the secondary projectId
|
||||
expect(mockGetOrCreateProjectStore).toHaveBeenCalledWith(secondarySession.projectId);
|
||||
// cancelGeneration was called on the scoped manager
|
||||
expect(mockCancelGeneration).toHaveBeenCalledWith(secondarySession.id);
|
||||
});
|
||||
|
||||
@@ -1729,9 +1710,8 @@ describe("multi-project chat routing", () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect((response.body as any).sessions).toHaveLength(1);
|
||||
// FNXC:ChatPersistence — scoped isGenerating no longer resolves through the
|
||||
// divergent getOrCreateProjectStore path; it shares the reader's store.
|
||||
expect(mockGetOrCreateProjectStore).not.toHaveBeenCalled();
|
||||
// Scoped path: getOrCreateProjectStore is called for isGenerating resolution
|
||||
expect(mockGetOrCreateProjectStore).toHaveBeenCalledWith(secondarySession.projectId);
|
||||
// isGenerating defaults to false (MockChatManager has no getGeneratingSessionIds)
|
||||
expect((response.body as any).sessions[0].isGenerating).toBe(false);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ import { CHAT_ALLOWED_MIME_TYPES, CHAT_MAX_ATTACHMENT_SIZE } from "./chat-attach
|
||||
import { rateLimit, RATE_LIMITS } from "../rate-limit.js";
|
||||
import { writeSSEEvent, type SessionBufferedEvent } from "../sse-buffer.js";
|
||||
import type { ApiRoutesContext } from "./types.js";
|
||||
import { getOrCreateScopedChatManager } from "../chat-project-services.js";
|
||||
import { getOrCreateScopedChatManager, getOrCreateScopedChatStore } from "../chat-project-services.js";
|
||||
import { getOrCreateProjectStore } from "../project-store-resolver.js";
|
||||
|
||||
interface ChatRouteDeps {
|
||||
parseLastEventId: (req: import("express").Request) => number | undefined;
|
||||
@@ -111,25 +112,8 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
||||
if (!options?.chatManager) throw new ApiError(503, "Chat manager not available");
|
||||
return options.chatManager;
|
||||
}
|
||||
/*
|
||||
FNXC:ChatPersistence 2026-06-16-00:00:
|
||||
The POST /chat/sessions/:id/messages writer MUST resolve the same per-project ChatStore the
|
||||
GET /chat/sessions/:id/messages reader uses. Otherwise a sent message persists to one store while
|
||||
the reload reads another, so the message vanishes when the user leaves and returns to the chat.
|
||||
The reader (resolveScopedChatStore -> resolveProjectChatContext) prefers the engine's per-project
|
||||
store/chatStore when an engine exists for the project. This writer previously diverged by going
|
||||
through getOrCreateProjectStore + a fresh getOrCreateScopedChatStore, bypassing the engine, which
|
||||
could bind the ChatManager to a different store instance than the reader. Resolve both writer and
|
||||
reader through resolveProjectChatContext so they always share one store. Quick Chat masked this
|
||||
bug by keeping its thread warm in memory (no server reload); ChatView reloads from the server on
|
||||
return and surfaced the missing rows.
|
||||
*/
|
||||
const { store: projectStore, chatStore } = await resolveProjectChatContext({
|
||||
projectId,
|
||||
defaultStore: store,
|
||||
defaultChatStore: options?.chatStore,
|
||||
engineManager: options?.engineManager,
|
||||
});
|
||||
const projectStore = await getOrCreateProjectStore(projectId);
|
||||
const chatStore = getOrCreateScopedChatStore(projectStore);
|
||||
return getOrCreateScopedChatManager(projectStore, chatStore, options?.pluginRunner);
|
||||
}
|
||||
// ── Chat Routes ────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user