feat(FN-2001): merge fusion/fn-2001
This commit is contained in:
@@ -39,6 +39,7 @@ const {
|
||||
mockGetOrCreateProjectStore,
|
||||
mockListNodes,
|
||||
mockGetNode,
|
||||
mockEnsureMemoryFileWithBackend,
|
||||
} = vi.hoisted(() => ({
|
||||
mockListProjects: vi.fn().mockResolvedValue([]),
|
||||
mockGetProject: vi.fn().mockResolvedValue(null),
|
||||
@@ -90,6 +91,7 @@ const {
|
||||
mockGetOrCreateProjectStore: vi.fn(),
|
||||
mockListNodes: vi.fn().mockResolvedValue([]),
|
||||
mockGetNode: vi.fn().mockResolvedValue(null),
|
||||
mockEnsureMemoryFileWithBackend: vi.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", async () => {
|
||||
@@ -112,6 +114,7 @@ vi.mock("@fusion/core", async () => {
|
||||
listNodes: mockListNodes,
|
||||
getNode: mockGetNode,
|
||||
})),
|
||||
ensureMemoryFileWithBackend: mockEnsureMemoryFileWithBackend,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -579,6 +582,7 @@ describe("POST /api/projects route handler", () => {
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
mockEnsureMemoryFileWithBackend.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("calls updateProject with status 'active' after registration", async () => {
|
||||
@@ -627,6 +631,43 @@ describe("POST /api/projects route handler", () => {
|
||||
nodeId: "node-remote-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("calls ensureMemoryFileWithBackend after project activation", async () => {
|
||||
const store = new MockStoreForRoutes();
|
||||
const app = createServer(store as any);
|
||||
mockEnsureMemoryFileWithBackend.mockResolvedValue(true);
|
||||
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/projects",
|
||||
JSON.stringify({ name: "Test Project", path: "/tmp" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
// Allow fire-and-forget promise to complete
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(mockEnsureMemoryFileWithBackend).toHaveBeenCalledWith("/tmp");
|
||||
});
|
||||
|
||||
it("returns 201 even when memory bootstrap fails", async () => {
|
||||
const store = new MockStoreForRoutes();
|
||||
const app = createServer(store as any);
|
||||
mockEnsureMemoryFileWithBackend.mockRejectedValue(new Error("disk full"));
|
||||
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/projects",
|
||||
JSON.stringify({ name: "Test Project", path: "/tmp" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
// Project registration should still succeed
|
||||
expect(res.status).toBe(201);
|
||||
expect((res.body as any).status).toBe("active");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/projects route handler", () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as nodeFs from "node:fs";
|
||||
|
||||
import { promisify } from "node:util";
|
||||
import type { TaskStore, Column, ScheduleType, ActivityEventType, ModelPreset, MessageType, ParticipantType, RoutineTriggerType, ProjectSettings } from "@fusion/core";
|
||||
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, type PiExtensionEntry, type PiExtensionSettings, getCurrentRepo, isGhAuthenticated, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupRoutine, exportSettings, importSettings, validateImportData, MessageStore, RoutineStore, isWebhookTrigger, resolveMemoryBackend, getMemoryBackendCapabilities, listMemoryBackendTypes, listProjectMemoryFiles, readProjectMemoryFile, readProjectMemoryFileContent, writeProjectMemoryFile, readMemory, writeMemory, searchProjectMemory, isQmdAvailable, installQmd, refreshQmdProjectMemoryIndex, QMD_INSTALL_COMMAND, MemoryBackendError, scheduleQmdProjectMemoryRefresh, discoverPiExtensions, updatePiExtensionDisabledIds, getFusionAgentDir, getLegacyPiAgentDir } from "@fusion/core";
|
||||
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, type PiExtensionEntry, type PiExtensionSettings, getCurrentRepo, isGhAuthenticated, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupRoutine, exportSettings, importSettings, validateImportData, MessageStore, RoutineStore, isWebhookTrigger, resolveMemoryBackend, getMemoryBackendCapabilities, listMemoryBackendTypes, listProjectMemoryFiles, readProjectMemoryFile, readProjectMemoryFileContent, writeProjectMemoryFile, readMemory, writeMemory, searchProjectMemory, isQmdAvailable, installQmd, refreshQmdProjectMemoryIndex, QMD_INSTALL_COMMAND, MemoryBackendError, scheduleQmdProjectMemoryRefresh, discoverPiExtensions, updatePiExtensionDisabledIds, getFusionAgentDir, getLegacyPiAgentDir, ensureMemoryFileWithBackend } from "@fusion/core";
|
||||
import type { ServerOptions } from "./server.js";
|
||||
import { GitHubClient, parseBadgeUrl } from "./github.js";
|
||||
import { githubRateLimiter } from "./github-poll.js";
|
||||
@@ -14198,6 +14198,11 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Activate the project (registration sets it to 'initializing')
|
||||
const activeProject = await central.updateProject(project.id, { status: "active" });
|
||||
|
||||
// Bootstrap memory files (non-blocking, non-fatal)
|
||||
ensureMemoryFileWithBackend(path.trim()).catch(() => {
|
||||
// Memory bootstrap failure is non-fatal - project registration succeeded
|
||||
});
|
||||
|
||||
await central.close();
|
||||
|
||||
res.status(201).json({ ...activeProject, _meta: { hasFusionDir: hasFusionDir ? undefined : false } });
|
||||
|
||||
Reference in New Issue
Block a user