feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés, environnement local détecté, stratégies compilées). UI EmbedderSettings. - LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la mémoire dépasse le budget de recall sans embedder configuré (event EmbedderSuggested, anti-spam 1×/session, « ne plus demander »). - Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les agents/profils au lancement, avant la persona. UI ProjectContextPanel. Tests : backend workspace vert (0 échec) ; frontend 306/306. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -4,11 +4,11 @@
|
||||
* - The mock agent gateway mirrors the backend guard: launching an agent already
|
||||
* live in another cell is refused with `AGENT_ALREADY_RUNNING`, and the same
|
||||
* node is idempotent; `listLiveAgents` reports who runs where (T5).
|
||||
* - The per-cell dropdown disables (and `onChange` rejects) an agent already live
|
||||
* in another cell, while the agent pinned on its own cell stays selectable (T6).
|
||||
* - The per-cell dropdown blocks an agent already visible in another cell, but
|
||||
* can rebind a background live session when the backend exposes its session id.
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
|
||||
import type { Gateways, LiveAgent } from "@/ports";
|
||||
import {
|
||||
@ -65,7 +65,13 @@ describe("singleton agent — mock gateway guard (T5)", () => {
|
||||
);
|
||||
|
||||
const live: LiveAgent[] = await agent.listLiveAgents("p1");
|
||||
expect(live).toEqual([{ agentId: a.id, nodeId: "node-A" }]);
|
||||
expect(live).toEqual([
|
||||
{
|
||||
agentId: a.id,
|
||||
nodeId: "node-A",
|
||||
sessionId: "mock-agent-session-1",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("relaunching in the SAME node is allowed (idempotent), not refused", async () => {
|
||||
@ -104,34 +110,68 @@ describe("singleton agent — dropdown guard (T6)", () => {
|
||||
);
|
||||
}
|
||||
|
||||
it("disables an agent already running in another cell and rejects selecting it", async () => {
|
||||
it("opens an agent already running elsewhere by rebinding its live session", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const a = await agent.createAgent("p1", { name: "Busy", profileId: "p" });
|
||||
await agent.launchAgent(
|
||||
"p1",
|
||||
a.id,
|
||||
{ cwd: "/x", rows: 24, cols: 80, nodeId: "old-node" },
|
||||
noop,
|
||||
);
|
||||
|
||||
const tree = await layout.loadLayout("p1");
|
||||
const leafId = leaves(tree)[0].id;
|
||||
|
||||
renderGrid(layout, agent);
|
||||
|
||||
// The option is selectable: choosing it rebinds the running session here.
|
||||
const option = await screen.findByRole("option", {
|
||||
name: /Busy/,
|
||||
});
|
||||
expect((option as HTMLOptionElement).disabled).toBe(false);
|
||||
|
||||
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
fireEvent.change(select, { target: { value: a.id } });
|
||||
|
||||
await waitFor(async () => {
|
||||
const updated = await layout.loadLayout("p1");
|
||||
const leaf = leaves(updated).find((l) => l.id === leafId)!;
|
||||
expect(leaf.agent).toBe(a.id);
|
||||
expect(leaf.session).toBe("mock-agent-session-1");
|
||||
});
|
||||
expect(await agent.listLiveAgents("p1")).toEqual([
|
||||
{
|
||||
agentId: a.id,
|
||||
nodeId: leafId,
|
||||
sessionId: "mock-agent-session-1",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("explains the missing backend contract when a live agent has no attachable session", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const a = await agent.createAgent("p1", { name: "Busy", profileId: "p" });
|
||||
|
||||
// Mark the agent live in a DIFFERENT node than the (single) leaf we render.
|
||||
vi.spyOn(agent, "listLiveAgents").mockResolvedValue([
|
||||
{ agentId: a.id, nodeId: "some-other-node" },
|
||||
]);
|
||||
|
||||
renderGrid(layout, agent);
|
||||
|
||||
// The option for the live-elsewhere agent is rendered disabled with a label.
|
||||
const option = await screen.findByRole("option", {
|
||||
name: /Busy \(en cours ailleurs\)/,
|
||||
name: /Busy/,
|
||||
});
|
||||
expect((option as HTMLOptionElement).disabled).toBe(true);
|
||||
expect((option as HTMLOptionElement).disabled).toBe(false);
|
||||
|
||||
// Programmatically selecting it must be rejected (agent not pinned).
|
||||
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
const setCellAgentSpy = vi.spyOn(layout, "mutateLayout");
|
||||
fireEvent.change(select, { target: { value: a.id } });
|
||||
|
||||
// A notice is shown and no setCellAgent mutation was issued.
|
||||
expect(await screen.findByRole("status")).toBeTruthy();
|
||||
expect(
|
||||
setCellAgentSpy.mock.calls.filter((c) => c[1].type === "setCellAgent"),
|
||||
).toHaveLength(0);
|
||||
expect((await screen.findByRole("status")).textContent).toMatch(
|
||||
/Session active introuvable/,
|
||||
);
|
||||
});
|
||||
|
||||
it("the agent pinned on THIS cell stays selectable even while live (same node)", async () => {
|
||||
|
||||
Reference in New Issue
Block a user