feat: add main features

Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View File

@ -0,0 +1,284 @@
/**
* L8 — git feature wired to the stateful `MockGitGateway` via the real
* `DIProvider`.
*
* Covers:
* - status shows staged/unstaged groupings
* - stage moves a file from Unstaged to Staged
* - commit with empty message is blocked (button disabled)
* - commit with message but nothing staged is blocked (button disabled)
* - commit moves staged files out and adds an entry to the log
* - checkout changes the current branch
* - MockGitGateway unit behaviour
*/
import { describe, it, expect } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { MockGitGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { GitPanel } from "./GitPanel";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const PROJECT_ID = "proj-git-test-001";
function renderPanel(git: MockGitGateway = new MockGitGateway()) {
const gateways = { git } as unknown as Gateways;
return {
git,
...render(
<DIProvider gateways={gateways}>
<GitPanel projectId={PROJECT_ID} />
</DIProvider>,
),
};
}
/** Waits until the "Changes" section is rendered (panel stabilised). */
async function waitForPanel() {
await waitFor(() => {
expect(screen.getByRole("region", { name: "Changes" })).toBeTruthy();
});
}
// ---------------------------------------------------------------------------
// Integration tests (GitPanel + MockGitGateway via DIProvider)
// ---------------------------------------------------------------------------
describe("GitPanel (with MockGitGateway)", () => {
it("shows demo files split between Staged and Unstaged sections", async () => {
const git = new MockGitGateway();
// Seed a staged file.
await git.init(PROJECT_ID);
await git.stage(PROJECT_ID, "src/main.rs");
renderPanel(git);
await waitForPanel();
expect(screen.getByText("Staged")).toBeTruthy();
expect(screen.getByText("Unstaged")).toBeTruthy();
// src/main.rs is staged → Unstage button exists
expect(
screen.getByRole("button", { name: "unstage src/main.rs" }),
).toBeTruthy();
// README.md is unstaged → Stage button exists
expect(
screen.getByRole("button", { name: "stage README.md" }),
).toBeTruthy();
});
it("clicking Stage moves a file to the Staged group", async () => {
renderPanel();
await waitForPanel();
// Stage README.md
const stageBtn = await screen.findByRole("button", {
name: "stage README.md",
});
fireEvent.click(stageBtn);
// After staging, an Unstage button should appear for README.md
await waitFor(() => {
expect(
screen.getByRole("button", { name: "unstage README.md" }),
).toBeTruthy();
});
});
it("clicking Unstage moves a file back to Unstaged", async () => {
const git = new MockGitGateway();
await git.init(PROJECT_ID);
await git.stage(PROJECT_ID, "src/main.rs");
renderPanel(git);
await waitForPanel();
const unstageBtn = await screen.findByRole("button", {
name: "unstage src/main.rs",
});
fireEvent.click(unstageBtn);
await waitFor(() => {
expect(
screen.getByRole("button", { name: "stage src/main.rs" }),
).toBeTruthy();
});
});
it("Commit button is disabled when message is empty", async () => {
renderPanel();
await waitForPanel();
const btn = screen.getByRole("button", { name: /commit/i });
expect((btn as HTMLButtonElement).disabled).toBe(true);
});
it("Commit button is disabled when message is set but nothing is staged", async () => {
renderPanel();
await waitForPanel();
fireEvent.change(screen.getByLabelText("commit message"), {
target: { value: "my message" },
});
const btn = screen.getByRole("button", { name: /commit/i });
expect((btn as HTMLButtonElement).disabled).toBe(true);
});
it("successful commit removes staged files and adds to the log", async () => {
const git = new MockGitGateway();
await git.init(PROJECT_ID);
await git.stage(PROJECT_ID, "src/main.rs");
await git.stage(PROJECT_ID, "README.md");
renderPanel(git);
await waitForPanel();
fireEvent.change(screen.getByLabelText("commit message"), {
target: { value: "initial commit" },
});
const btn = screen.getByRole("button", { name: /commit/i });
expect((btn as HTMLButtonElement).disabled).toBe(false);
fireEvent.click(btn);
// After commit, no files should remain (both were staged).
await waitFor(() => {
expect(screen.getByText("No changes.")).toBeTruthy();
});
// The commit should appear in the log.
await waitFor(() => {
expect(screen.getByText("initial commit")).toBeTruthy();
});
});
it("checkout changes the current branch label", async () => {
renderPanel();
await waitForPanel();
// Ensure the Branches section is rendered with "main" as current.
const branchesSection = await screen.findByRole("region", {
name: "Branches",
});
expect(branchesSection).toBeTruthy();
// Create & checkout a new branch via the form.
const branchInput = screen.getByLabelText("new branch name");
fireEvent.change(branchInput, { target: { value: "feature/new-thing" } });
fireEvent.click(screen.getByRole("button", { name: /create.*checkout/i }));
// After checkout, the new branch should appear (may show in current + list).
await waitFor(() => {
const matches = screen.getAllByText("feature/new-thing");
expect(matches.length).toBeGreaterThan(0);
});
// A checkout button for "main" should now appear (since it's no longer current).
await waitFor(() => {
expect(
screen.getByRole("button", { name: "checkout main" }),
).toBeTruthy();
});
});
it("shows 'No commits yet.' in the Log section initially", async () => {
renderPanel();
await waitForPanel();
expect(screen.getByText("No commits yet.")).toBeTruthy();
});
});
// ---------------------------------------------------------------------------
// MockGitGateway unit tests
// ---------------------------------------------------------------------------
describe("MockGitGateway (unit)", () => {
it("status returns seeded files for a new project", async () => {
const gw = new MockGitGateway();
const files = await gw.status("proj");
expect(files.length).toBeGreaterThan(0);
expect(files.every((f) => !f.staged)).toBe(true);
});
it("init creates state if absent", async () => {
const gw = new MockGitGateway();
await gw.init("new-proj");
const files = await gw.status("new-proj");
expect(Array.isArray(files)).toBe(true);
});
it("stage toggles a file to staged=true", async () => {
const gw = new MockGitGateway();
const before = await gw.status("p");
const target = before[0].path;
await gw.stage("p", target);
const after = await gw.status("p");
const file = after.find((f) => f.path === target)!;
expect(file.staged).toBe(true);
});
it("unstage toggles a file back to staged=false", async () => {
const gw = new MockGitGateway();
const files = await gw.status("p");
const target = files[0].path;
await gw.stage("p", target);
await gw.unstage("p", target);
const after = await gw.status("p");
expect(after.find((f) => f.path === target)!.staged).toBe(false);
});
it("commit removes staged files from status and adds to log", async () => {
const gw = new MockGitGateway();
const files = await gw.status("p");
const target = files[0].path;
await gw.stage("p", target);
const result = await gw.commit("p", "test commit\n\nmore text");
expect(result.summary).toBe("test commit");
expect(result.hash).toMatch(/^mock-/);
const after = await gw.status("p");
expect(after.find((f) => f.path === target)).toBeUndefined();
const log = await gw.log("p", 10);
expect(log[0].summary).toBe("test commit");
});
it("branches returns initial ['main'] with current='main'", async () => {
const gw = new MockGitGateway();
const b = await gw.branches("p");
expect(b.branches).toEqual(["main"]);
expect(b.current).toBe("main");
});
it("checkout creates a new branch and sets it as current", async () => {
const gw = new MockGitGateway();
await gw.checkout("p", "develop");
const b = await gw.branches("p");
expect(b.branches).toContain("develop");
expect(b.current).toBe("develop");
});
it("log(limit) returns at most limit entries", async () => {
const gw = new MockGitGateway();
const files = await gw.status("p");
// Stage and commit each file individually.
for (let i = 0; i < files.length; i++) {
await gw.stage("p", files[i].path);
await gw.commit("p", `commit ${i}`);
}
const log = await gw.log("p", 1);
expect(log).toHaveLength(1);
});
it("projects are isolated", async () => {
const gw = new MockGitGateway();
await gw.stage("proj-A", (await gw.status("proj-A"))[0].path);
await gw.commit("proj-A", "A commit");
const logB = await gw.log("proj-B", 10);
expect(logB).toHaveLength(0);
});
});