feat(FN-2848): add manual memory dream API trigger

- Add POST /api/memory/dream route that runs project and agent dream processing with AI prompt execution
- Expose triggerMemoryDreams() in dashboard legacy API client for direct dream invocation
- Switch SettingsModal and useMemoryData Dream Now actions from automation lookup to the new endpoint
- Update dashboard route, hook, and settings modal tests to cover success and error handling
- Add a patch changeset for @runfusion/fusion documenting the new endpoint and client helper
This commit is contained in:
Fusion
2026-04-28 03:15:28 -07:00
committed by gsxdsm
parent 1d9d9f4e7f
commit f19ecac8f5
8 changed files with 256 additions and 41 deletions

View File

@@ -125,6 +125,7 @@ vi.mock("@fusion/engine", () => ({
}));
import { AgentStore, isGhAvailable, isGhAuthenticated } from "@fusion/core";
import { createFnAgent } from "@fusion/engine";
const mockIsGhAvailable = vi.mocked(isGhAvailable);
const mockIsGhAuthenticated = vi.mocked(isGhAuthenticated);
@@ -14827,6 +14828,126 @@ describe("POST /api/memory/compact", () => {
});
});
describe("POST /api/memory/dream", () => {
let store: TaskStore;
let rootDir: string;
beforeEach(() => {
rootDir = mkdtempSync(join(tmpdir(), "fusion-memory-dream-"));
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "# Memory\n\nLong-term context");
writeFileSync(join(rootDir, ".fusion", "memory", `${new Date().toISOString().slice(0, 10)}.md`), "# Daily Memory\n\n- notable note");
store = createMockStore({
getRootDir: vi.fn().mockReturnValue(rootDir),
getFusionDir: vi.fn().mockReturnValue(join(rootDir, ".fusion")),
getSettings: vi.fn().mockResolvedValue({
memoryEnabled: true,
memoryDreamsEnabled: true,
memoryBackendType: "file",
}),
});
vi.spyOn(AgentStore.prototype, "init").mockResolvedValue(undefined);
vi.spyOn(AgentStore.prototype, "listAgents").mockResolvedValue([]);
});
afterEach(() => {
rmSync(rootDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
function buildApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
return app;
}
it("returns 200 with dream results on success", async () => {
vi.mocked(createFnAgent).mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue("## DREAMS\nSynthesis\n\n## LONG_TERM_UPDATES\nLesson"),
dispose: vi.fn(),
},
} as never);
const res = await REQUEST(buildApp(), "POST", "/api/memory/dream", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
success: true,
dreamsWritten: true,
longTermUpdatesWritten: true,
});
});
it("returns 200 with empty results when no daily notes to process", async () => {
writeFileSync(join(rootDir, ".fusion", "memory", `${new Date().toISOString().slice(0, 10)}.md`), "");
vi.mocked(createFnAgent).mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue("## DREAMS\n\n## LONG_TERM_UPDATES\n"),
dispose: vi.fn(),
},
} as never);
const res = await REQUEST(buildApp(), "POST", "/api/memory/dream", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
success: true,
dreamsWritten: false,
longTermUpdatesWritten: false,
});
});
it("returns 400 when dreams are disabled in settings", async () => {
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
memoryEnabled: true,
memoryDreamsEnabled: false,
memoryBackendType: "file",
});
const res = await REQUEST(buildApp(), "POST", "/api/memory/dream", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(400);
expect(res.body.error).toContain("Memory dreams are disabled");
});
it("returns 503 when AI service is unavailable", async () => {
vi.mocked(createFnAgent).mockRejectedValue(Object.assign(new Error("AI down"), { name: "AiServiceError" }));
const res = await REQUEST(buildApp(), "POST", "/api/memory/dream", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(503);
expect(res.body.error).toContain("AI");
});
it("returns 500 on unexpected processing failure", async () => {
vi.mocked(createFnAgent).mockResolvedValue({
session: {
prompt: vi.fn().mockRejectedValue(new Error("boom")),
dispose: vi.fn(),
},
} as never);
const res = await REQUEST(buildApp(), "POST", "/api/memory/dream", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(500);
expect(res.body.error).toContain("boom");
});
});
describe("GET /api/memory/insights", () => {
let store: TaskStore;
let rootDir: string;