feat(memory): récolte automatique contrôlée de la mémoire (Lot E1)
Câble la récolte automatique de mémoire de bout en bout, sous contrôle explicite, du domaine jusqu'à l'UI : - domain : modèle `memory_harvest` (candidats de récolte, décision) + exports `lib.rs`. - application : use case `memory/harvest` branché dans `memory/mod`, `lib.rs`, le cycle de vie d'agent (`agent/lifecycle`) et l'orchestrateur (`orchestrator/service`). - app-tauri : wiring runtime dans `state`. - frontend : DTO `domain/index` + hook `useMemory`. Couverture : domain `memory_harvest` (14), application `memory_harvest` (5) et `orchestrator_service` (récolte, 4), plus patch test-only du mock gateway `workState` (`mock.test.ts`) et UI `memory.test`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -18,7 +18,11 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
|
||||
import { MockMemoryGateway, MockEmbedderGateway } from "@/adapters/mock";
|
||||
import {
|
||||
MockMemoryGateway,
|
||||
MockEmbedderGateway,
|
||||
MockSystemGateway,
|
||||
} from "@/adapters/mock";
|
||||
import type { GatewayError } from "@/domain";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
@ -26,14 +30,19 @@ import { MemoryPanel } from "./MemoryPanel";
|
||||
|
||||
const PROJECT_ID = "proj-memory-test";
|
||||
|
||||
function renderMemoryPanel(memory?: MockMemoryGateway) {
|
||||
function renderMemoryPanel(
|
||||
memory?: MockMemoryGateway,
|
||||
system: MockSystemGateway = new MockSystemGateway(),
|
||||
) {
|
||||
const mem = memory ?? new MockMemoryGateway();
|
||||
const gateways = {
|
||||
memory: mem,
|
||||
embedder: new MockEmbedderGateway(),
|
||||
system,
|
||||
} as unknown as Gateways;
|
||||
return {
|
||||
memory: mem,
|
||||
system,
|
||||
...render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<MemoryPanel projectId={PROJECT_ID} />
|
||||
@ -240,6 +249,40 @@ describe("MemoryPanel (with MockMemoryGateway)", () => {
|
||||
const alert = await screen.findByRole("alert");
|
||||
expect(alert.textContent).toMatch(/boom from gateway/i);
|
||||
});
|
||||
|
||||
it("refreshes the index when a memorySaved domain event fires", async () => {
|
||||
const memory = new MockMemoryGateway();
|
||||
const system = new MockSystemGateway();
|
||||
const spy = vi.spyOn(memory, "readIndex");
|
||||
renderMemoryPanel(memory, system);
|
||||
await waitForMemoryIdle();
|
||||
spy.mockClear();
|
||||
|
||||
await memory.createMemory({
|
||||
projectId: PROJECT_ID,
|
||||
name: "Harvested Note",
|
||||
description: "created by idea-memory",
|
||||
type: "project",
|
||||
content: "harvested body",
|
||||
});
|
||||
system.emit({ type: "memorySaved", slug: "harvested-note" });
|
||||
|
||||
expect(await screen.findByText("Harvested Note")).toBeTruthy();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not refresh the index for unrelated domain events", async () => {
|
||||
const memory = new MockMemoryGateway();
|
||||
const system = new MockSystemGateway();
|
||||
const spy = vi.spyOn(memory, "readIndex");
|
||||
renderMemoryPanel(memory, system);
|
||||
await waitForMemoryIdle();
|
||||
spy.mockClear();
|
||||
|
||||
system.emit({ type: "projectCreated", projectId: "other-project" });
|
||||
|
||||
await waitFor(() => expect(spy).not.toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -46,7 +46,7 @@ function describe(e: unknown): string {
|
||||
}
|
||||
|
||||
export function useMemory(projectId: string): MemoryViewModel {
|
||||
const { memory } = useGateways();
|
||||
const { memory, system } = useGateways();
|
||||
|
||||
const [entries, setEntries] = useState<MemoryIndexEntry[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -69,6 +69,24 @@ export function useMemory(projectId: string): MemoryViewModel {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!system) return;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let cancelled = false;
|
||||
void system.onDomainEvent((event) => {
|
||||
if (event.type === "memorySaved") {
|
||||
void refresh();
|
||||
}
|
||||
}).then((u) => {
|
||||
if (cancelled) u();
|
||||
else unsubscribe = u;
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [refresh, system]);
|
||||
|
||||
const createMemory = useCallback(
|
||||
async (input: Omit<CreateMemoryInput, "projectId">) => {
|
||||
setBusy(true);
|
||||
|
||||
Reference in New Issue
Block a user