- 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>
185 lines
5.9 KiB
TypeScript
185 lines
5.9 KiB
TypeScript
/**
|
|
* L4 — {@link LayoutGrid} wired through the real {@link DIProvider} with the
|
|
* in-memory {@link MockLayoutGateway} (+ {@link MockTerminalGateway} for the
|
|
* leaves' embedded {@link TerminalView}).
|
|
*
|
|
* Under jsdom xterm's `term.open` may bail gracefully (no real layout engine),
|
|
* so these tests assert the *structure* the grid renders (cells, no throw)
|
|
* rather than xterm's visual output. They stay robust whether or not xterm
|
|
* bailed.
|
|
*/
|
|
import { describe, it, expect, vi } from "vitest";
|
|
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
|
|
|
import type { Gateways } from "@/ports";
|
|
import { MockLayoutGateway, MockTerminalGateway } from "@/adapters/mock";
|
|
import { DIProvider } from "@/app/di";
|
|
import { leaves } from "./layout";
|
|
import { LayoutGrid } from "./LayoutGrid";
|
|
|
|
function renderGrid(layout: MockLayoutGateway, projectId = "p1") {
|
|
const gateways = {
|
|
layout,
|
|
terminal: new MockTerminalGateway(),
|
|
} as unknown as Gateways;
|
|
return render(
|
|
<DIProvider gateways={gateways}>
|
|
<LayoutGrid projectId={projectId} cwd="/home/me/proj" />
|
|
</DIProvider>,
|
|
);
|
|
}
|
|
|
|
describe("LayoutGrid (with MockLayoutGateway)", () => {
|
|
it("renders a single-leaf layout without throwing", async () => {
|
|
const layout = new MockLayoutGateway();
|
|
expect(() => renderGrid(layout)).not.toThrow();
|
|
|
|
// The grid container always renders.
|
|
expect(screen.getByTestId("layout-grid")).toBeTruthy();
|
|
// Once loaded, the single leaf cell is shown.
|
|
await waitFor(() => {
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it("renders N cells for a pre-split tree", async () => {
|
|
const layout = new MockLayoutGateway();
|
|
// Pre-split the project's tree into two leaves before rendering.
|
|
const initial = await layout.loadLayout("p1");
|
|
await layout.mutateLayout("p1", {
|
|
type: "split",
|
|
target: leaves(initial)[0].id,
|
|
direction: "row",
|
|
newLeaf: "b",
|
|
container: "c",
|
|
});
|
|
|
|
renderGrid(layout);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2);
|
|
});
|
|
// The split container is present with its direction recorded.
|
|
const split = screen.getByTestId("layout-split");
|
|
expect(split.getAttribute("data-direction")).toBe("row");
|
|
});
|
|
|
|
it("stays graceful (no throw) even though xterm bails under jsdom", async () => {
|
|
const layout = new MockLayoutGateway();
|
|
// Render a 3-leaf tree: nested splits, each leaf hosts a TerminalView.
|
|
const initial = await layout.loadLayout("p1");
|
|
const a = leaves(initial)[0].id;
|
|
await layout.mutateLayout("p1", {
|
|
type: "split",
|
|
target: a,
|
|
direction: "row",
|
|
newLeaf: "b",
|
|
container: "c",
|
|
});
|
|
await layout.mutateLayout("p1", {
|
|
type: "split",
|
|
target: "b",
|
|
direction: "column",
|
|
newLeaf: "d",
|
|
container: "e",
|
|
});
|
|
|
|
expect(() => renderGrid(layout)).not.toThrow();
|
|
await waitFor(() => {
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
it("closing a cell removes THAT cell and keeps its sibling without killing its session", async () => {
|
|
const layout = new MockLayoutGateway();
|
|
const terminal = new MockTerminalGateway();
|
|
const closeSpy = vi.spyOn(terminal, "closeTerminal");
|
|
const initial = await layout.loadLayout("p1");
|
|
const a = leaves(initial)[0].id;
|
|
// Split A → container c with children [A (index 0), b (index 1)].
|
|
await layout.mutateLayout("p1", {
|
|
type: "split",
|
|
target: a,
|
|
direction: "row",
|
|
newLeaf: "b",
|
|
container: "c",
|
|
});
|
|
await layout.mutateLayout("p1", {
|
|
type: "setSession",
|
|
target: "b",
|
|
session: "running-session-b",
|
|
});
|
|
|
|
const gateways = { layout, terminal } as unknown as Gateways;
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
|
|
</DIProvider>,
|
|
);
|
|
await waitFor(() =>
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2),
|
|
);
|
|
|
|
// Click the close button on cell `b` — `b` must disappear, `a` must remain.
|
|
fireEvent.click(screen.getByLabelText("close b"));
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1),
|
|
);
|
|
expect(
|
|
screen.getByTestId("layout-leaf").getAttribute("data-node-id"),
|
|
).toBe(a);
|
|
expect(closeSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("closing the other cell keeps the opposite sibling", async () => {
|
|
const layout = new MockLayoutGateway();
|
|
const initial = await layout.loadLayout("p1");
|
|
const a = leaves(initial)[0].id;
|
|
await layout.mutateLayout("p1", {
|
|
type: "split",
|
|
target: a,
|
|
direction: "row",
|
|
newLeaf: "b",
|
|
container: "c",
|
|
});
|
|
|
|
renderGrid(layout);
|
|
await waitFor(() =>
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2),
|
|
);
|
|
|
|
// Closing `a` (index 0) must keep `b` (index 1).
|
|
fireEvent.click(screen.getByLabelText(`close ${a}`));
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1),
|
|
);
|
|
expect(
|
|
screen.getByTestId("layout-leaf").getAttribute("data-node-id"),
|
|
).toBe("b");
|
|
});
|
|
|
|
it("the sole root cell has no close button (cannot be closed)", async () => {
|
|
const layout = new MockLayoutGateway();
|
|
renderGrid(layout);
|
|
await waitFor(() =>
|
|
expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1),
|
|
);
|
|
expect(screen.queryByLabelText(/^close /)).toBeNull();
|
|
});
|
|
|
|
it("renders the loading/empty container when the layout gateway is absent", () => {
|
|
// A DI subset without the layout gateway → useLayout is defensive (null).
|
|
const gateways = { terminal: new MockTerminalGateway() } as unknown as Gateways;
|
|
expect(() =>
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<LayoutGrid projectId="p1" cwd="/c" />
|
|
</DIProvider>,
|
|
),
|
|
).not.toThrow();
|
|
expect(screen.getByTestId("layout-grid")).toBeTruthy();
|
|
});
|
|
});
|