feat(FN-2001): merge fusion/fn-2001

This commit is contained in:
gsxdsm
2026-04-17 14:09:45 -07:00
parent 6f12375725
commit 0a2dc6105d
4 changed files with 148 additions and 1 deletions

View File

@@ -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", () => {

View File

@@ -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 } });