feat(orchestrator): create_skill action + wire per-project watchers (§14.3)

- domain: OrchestratorCommand::CreateSkill with optional scope field
  (defaults to Project, case-insensitive, UnknownScope on bad value)
- application: dispatch CreateSkill through the CreateSkill use case
- app-tauri: build OrchestratorService and start/stop per-project request
  watchers on open/create/close_project (ensure/stop_orchestrator_watch)
- tests: domain validation, service dispatch, infra watcher e2e,
  app-tauri wiring lifecycle (idempotent, isolated, stop)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:06:41 +02:00
parent b9fd2fb925
commit d11eaaa8c0
13 changed files with 619 additions and 52 deletions

View File

@ -9,7 +9,7 @@
* bailed.
*/
import { describe, it, expect } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import type { Gateways } from "@/ports";
import { MockLayoutGateway, MockTerminalGateway } from "@/adapters/mock";
@ -90,6 +90,72 @@ describe("LayoutGrid (with MockLayoutGateway)", () => {
});
});
it("closing a cell removes THAT cell and keeps its sibling (closes the right terminal)", async () => {
const layout = new MockLayoutGateway();
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",
});
renderGrid(layout);
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);
});
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;