fix(FN-000): compact selected memory file
This commit is contained in:
@@ -12804,13 +12804,20 @@ describe("PUT /api/memory", () => {
|
||||
|
||||
describe("POST /api/memory/compact", () => {
|
||||
let store: TaskStore;
|
||||
let rootDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "fusion-memory-compact-"));
|
||||
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
|
||||
store = createMockStore({
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getRootDir: vi.fn().mockReturnValue(rootDir),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -12823,26 +12830,9 @@ describe("POST /api/memory/compact", () => {
|
||||
memoryEnabled: true,
|
||||
memoryBackendType: "file",
|
||||
});
|
||||
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue("/tmp/test");
|
||||
writeFileSync(join(rootDir, ".fusion", "memory", "DREAMS.md"), "Short content");
|
||||
|
||||
// Mock the memory-backend module to return short content
|
||||
vi.doMock("../../core/src/memory-backend.js", () => ({
|
||||
readMemory: vi.fn().mockResolvedValue({ content: "Short content", exists: true, backend: "file" }),
|
||||
writeMemory: vi.fn().mockResolvedValue({ success: true, backend: "file" }),
|
||||
resolveMemoryBackend: vi.fn(),
|
||||
MemoryBackendError: class MemoryBackendError extends Error {
|
||||
code: string;
|
||||
backend: string;
|
||||
constructor(code: string, message: string, backend: string) {
|
||||
super(message);
|
||||
this.name = "MemoryBackendError";
|
||||
this.code = code;
|
||||
this.backend = backend;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/memory/compact", "", {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/memory/compact", JSON.stringify({ path: ".fusion/memory/DREAMS.md" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
@@ -12850,29 +12840,23 @@ describe("POST /api/memory/compact", () => {
|
||||
expect(res.body.error).toContain("too short to compact");
|
||||
});
|
||||
|
||||
// Note: Full AI integration tests for successful compaction and AI failure
|
||||
// are covered in the core package tests (memory-compaction.test.ts).
|
||||
// This test follows the same pattern as POST /api/ai/summarize-title:
|
||||
// accept [200, 503] since the AI service may not be available in the test environment.
|
||||
// The vi.doMock from the previous test persists, returning short content → 400.
|
||||
it("accepts compaction request (returns 200 or 503)", async () => {
|
||||
it("returns 409 for read-only memory backends before compacting", async () => {
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
memoryEnabled: true,
|
||||
memoryBackendType: "file",
|
||||
memoryBackendType: "readonly",
|
||||
});
|
||||
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue("/test/project");
|
||||
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Long memory content.\n".repeat(20));
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/memory/compact",
|
||||
JSON.stringify({}),
|
||||
JSON.stringify({ path: ".fusion/memory/MEMORY.md" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
// vi.doMock persists, content is short → 400
|
||||
// This test verifies the route exists and accepts requests
|
||||
expect([200, 400, 503]).toContain(res.status);
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.error).toContain("read-only");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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, writeProjectMemoryFile, readMemory, writeMemory, searchProjectMemory, isQmdAvailable, QMD_INSTALL_COMMAND, MemoryBackendError, 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, QMD_INSTALL_COMMAND, MemoryBackendError, discoverPiExtensions, updatePiExtensionDisabledIds, getFusionAgentDir, getLegacyPiAgentDir } from "@fusion/core";
|
||||
import type { ServerOptions } from "./server.js";
|
||||
import { GitHubClient, parseBadgeUrl } from "./github.js";
|
||||
import { githubRateLimiter } from "./github-poll.js";
|
||||
@@ -2714,9 +2714,9 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
/**
|
||||
* POST /api/memory/compact
|
||||
* AI-powered memory compaction using the memory compaction service.
|
||||
* Reads current memory, compacts it using AI, and writes back the result.
|
||||
* Reads one selected memory file, compacts it using AI, and writes back the result.
|
||||
*
|
||||
* Body: none (reads current memory from backend)
|
||||
* Body: { path?: string } (defaults to .fusion/memory/MEMORY.md)
|
||||
*
|
||||
* Error mapping:
|
||||
* - Memory content too short (< 200 chars) → 400 Bad Request
|
||||
@@ -2727,12 +2727,19 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
*/
|
||||
router.post("/memory/compact", async (req, res) => {
|
||||
try {
|
||||
const requestedPath = typeof req.body?.path === "string" && req.body.path.trim()
|
||||
? req.body.path
|
||||
: ".fusion/memory/MEMORY.md";
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const settings = await scopedStore.getSettings();
|
||||
const backend = resolveMemoryBackend(settings);
|
||||
if (!backend.capabilities.writable) {
|
||||
throw new MemoryBackendError("READ_ONLY", "This backend is read-only and cannot write memory", backend.type);
|
||||
}
|
||||
const rootDir = scopedStore.getRootDir();
|
||||
|
||||
// Read current memory
|
||||
const result = await readMemory(rootDir, settings);
|
||||
// Read the selected file in full before compaction.
|
||||
const result = await readProjectMemoryFileContent(rootDir, requestedPath);
|
||||
const content = result.content;
|
||||
|
||||
// Validate content length (must be at least 200 chars to compact)
|
||||
@@ -2759,10 +2766,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
const { compactMemoryWithAi } = await import("@fusion/core");
|
||||
const compacted = await compactMemoryWithAi(content, rootDir, resolvedProvider, resolvedModelId);
|
||||
|
||||
// Write compacted content back
|
||||
await writeMemory(rootDir, compacted, settings);
|
||||
// Write compacted content back to the same selected memory file.
|
||||
await writeProjectMemoryFile(rootDir, result.path, compacted);
|
||||
|
||||
res.json({ content: compacted });
|
||||
res.json({ path: result.path, content: compacted });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
|
||||
Reference in New Issue
Block a user