docs: add naming conventions and structure check script (Task 5.2)
- Add docs/development/naming.md with complete naming convention reference - Add scripts/check-structure.js to verify file sizes (target: ≤300 lines) - Note: 9 files slightly exceed limit (form-heavy tabs, complex hooks, test files, utilities.css) — documented as acceptable deviations Quality gates: tsc (pass), eslint (pass) Refs: repo-restructure Task 5.2
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Verifies repository structure conventions.
|
||||||
|
* Run with: node scripts/check-structure.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const SRC_DIR = path.join(__dirname, "..", "src");
|
||||||
|
|
||||||
|
let errors = 0;
|
||||||
|
let warnings = 0;
|
||||||
|
|
||||||
|
function checkFileSize(filePath, maxLines = 300) {
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = content.split("\n").length;
|
||||||
|
if (lines > maxLines) {
|
||||||
|
console.error(`❌ OVERSIZED (${lines} lines): ${path.relative(SRC_DIR, filePath)}`);
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(dir, callback) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
||||||
|
walk(fullPath, callback);
|
||||||
|
} else {
|
||||||
|
callback(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Checking file sizes...\n");
|
||||||
|
walk(SRC_DIR, (filePath) => {
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
if ([".ts", ".tsx", ".py", ".css"].includes(ext)) {
|
||||||
|
checkFileSize(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\n---");
|
||||||
|
if (errors === 0 && warnings === 0) {
|
||||||
|
console.log("✅ All checks passed!");
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ ${errors} error(s), ${warnings} warning(s)`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -5,58 +5,58 @@ import { FileBrowser } from "./FileBrowser";
|
|||||||
|
|
||||||
// Mock apiClient
|
// Mock apiClient
|
||||||
vi.mock("../../../api/client", () => ({
|
vi.mock("../../../api/client", () => ({
|
||||||
apiClient: {
|
apiClient: {
|
||||||
get: vi.fn(),
|
get: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { apiClient } from "../../../api/client";
|
import { apiClient } from "../../../api/client";
|
||||||
|
|
||||||
describe("FileBrowser", () => {
|
describe("FileBrowser", () => {
|
||||||
it("renders loading state initially", () => {
|
it("renders loading state initially", () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText(/loading files/i)).toBeInTheDocument();
|
expect(screen.getByText(/loading files/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders file entries after loading", async () => {
|
it("renders file entries after loading", async () => {
|
||||||
const mockedGet = apiClient.get as ReturnType<typeof vi.fn>;
|
const mockedGet = apiClient.get as ReturnType<typeof vi.fn>;
|
||||||
mockedGet.mockResolvedValueOnce({
|
mockedGet.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
entries: [
|
entries: [
|
||||||
{ name: "src", type: "directory", path: "src" },
|
{ name: "src", type: "directory", path: "src" },
|
||||||
{ name: "README.md", type: "file", path: "README.md" },
|
{ name: "README.md", type: "file", path: "README.md" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("src")).toBeInTheDocument();
|
expect(screen.getByText("src")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(screen.getByText("README.md")).toBeInTheDocument();
|
expect(screen.getByText("README.md")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders error state on failure", async () => {
|
it("renders error state on failure", async () => {
|
||||||
const mockedGet = apiClient.get as ReturnType<typeof vi.fn>;
|
const mockedGet = apiClient.get as ReturnType<typeof vi.fn>;
|
||||||
mockedGet.mockRejectedValueOnce(new Error("Network error"));
|
mockedGet.mockRejectedValueOnce(new Error("Network error"));
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText(/failed to load files/i)).toBeInTheDocument();
|
expect(screen.getByText(/failed to load files/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,91 +5,143 @@ import type { Project } from "@/types/project";
|
|||||||
import type { ToolType } from "@/types/tool-type";
|
import type { ToolType } from "@/types/tool-type";
|
||||||
|
|
||||||
vi.mock("@/api/git_repositories", () => ({
|
vi.mock("@/api/git_repositories", () => ({
|
||||||
listRepositories: vi.fn(),
|
listRepositories: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/api/sessions", () => ({
|
vi.mock("@/api/sessions", () => ({
|
||||||
createInstance: vi.fn(),
|
createInstance: vi.fn(),
|
||||||
startInstance: vi.fn(),
|
startInstance: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/api/settings", () => ({
|
vi.mock("@/api/settings", () => ({
|
||||||
updateUserConfig: vi.fn(),
|
updateUserConfig: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { listRepositories } from "@/api/git_repositories";
|
import { listRepositories } from "@/api/git_repositories";
|
||||||
import { createInstance } from "@/api/sessions";
|
import { createInstance } from "@/api/sessions";
|
||||||
|
|
||||||
const mockProjects = [
|
const mockProjects = [
|
||||||
{ id: "p1", name: "Project One", description: null, owner_id: "u1", default_ssh_key_id: null },
|
{
|
||||||
{ id: "p2", name: "Project Two", description: null, owner_id: "u1", default_ssh_key_id: null },
|
id: "p1",
|
||||||
|
name: "Project One",
|
||||||
|
description: null,
|
||||||
|
owner_id: "u1",
|
||||||
|
default_ssh_key_id: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p2",
|
||||||
|
name: "Project Two",
|
||||||
|
description: null,
|
||||||
|
owner_id: "u1",
|
||||||
|
default_ssh_key_id: null,
|
||||||
|
},
|
||||||
] as Project[];
|
] as Project[];
|
||||||
|
|
||||||
const mockToolTypes = [
|
const mockToolTypes = [
|
||||||
{ id: "t1", name: "vscode", display_name: "VS Code", description: null, category: "editor", interfaces: ["web"], default_port: 8443, definition_type: "compose", compose_template: "", dockerfile_template: null, readiness_probe: null, required_variables: [], is_builtin: true, build_context: null, created_by_id: "u1", created_at: "", updated_at: "" },
|
{
|
||||||
{ id: "t2", name: "terminal", display_name: "Terminal", description: null, category: "shell", interfaces: ["terminal"], default_port: 22, definition_type: "dockerfile", compose_template: null, dockerfile_template: "", readiness_probe: null, required_variables: [], is_builtin: true, build_context: null, created_by_id: "u1", created_at: "", updated_at: "" },
|
id: "t1",
|
||||||
|
name: "vscode",
|
||||||
|
display_name: "VS Code",
|
||||||
|
description: null,
|
||||||
|
category: "editor",
|
||||||
|
interfaces: ["web"],
|
||||||
|
default_port: 8443,
|
||||||
|
definition_type: "compose",
|
||||||
|
compose_template: "",
|
||||||
|
dockerfile_template: null,
|
||||||
|
readiness_probe: null,
|
||||||
|
required_variables: [],
|
||||||
|
is_builtin: true,
|
||||||
|
build_context: null,
|
||||||
|
created_by_id: "u1",
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "t2",
|
||||||
|
name: "terminal",
|
||||||
|
display_name: "Terminal",
|
||||||
|
description: null,
|
||||||
|
category: "shell",
|
||||||
|
interfaces: ["terminal"],
|
||||||
|
default_port: 22,
|
||||||
|
definition_type: "dockerfile",
|
||||||
|
compose_template: null,
|
||||||
|
dockerfile_template: "",
|
||||||
|
readiness_probe: null,
|
||||||
|
required_variables: [],
|
||||||
|
is_builtin: true,
|
||||||
|
build_context: null,
|
||||||
|
created_by_id: "u1",
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
},
|
||||||
] as ToolType[];
|
] as ToolType[];
|
||||||
|
|
||||||
describe("CreateSessionForm", () => {
|
describe("CreateSessionForm", () => {
|
||||||
it("renders form with create button", () => {
|
it("renders form with create button", () => {
|
||||||
render(
|
render(
|
||||||
<CreateSessionForm
|
<CreateSessionForm
|
||||||
projects={mockProjects}
|
projects={mockProjects}
|
||||||
toolTypes={mockToolTypes}
|
toolTypes={mockToolTypes}
|
||||||
onCreated={vi.fn()}
|
onCreated={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Create New Session")).toBeInTheDocument();
|
expect(screen.getByText("Create New Session")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Create Session")).toBeInTheDocument();
|
expect(screen.getByText("Create Session")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows validation error when fields are missing", async () => {
|
it("shows validation error when fields are missing", async () => {
|
||||||
render(
|
render(
|
||||||
<CreateSessionForm
|
<CreateSessionForm
|
||||||
projects={mockProjects}
|
projects={mockProjects}
|
||||||
toolTypes={mockToolTypes}
|
toolTypes={mockToolTypes}
|
||||||
onCreated={vi.fn()}
|
onCreated={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<CreateSessionForm
|
<CreateSessionForm
|
||||||
projects={mockProjects}
|
projects={mockProjects}
|
||||||
toolTypes={mockToolTypes}
|
toolTypes={mockToolTypes}
|
||||||
onCreated={vi.fn()}
|
onCreated={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitBtn = container.querySelector('button[type="submit"]') as HTMLButtonElement;
|
const submitBtn = container.querySelector(
|
||||||
fireEvent.click(submitBtn);
|
'button[type="submit"]',
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
fireEvent.click(submitBtn);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/project, repository, and tool type are required/i)
|
screen.getByText(/project, repository, and tool type are required/i),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(createInstance).not.toHaveBeenCalled();
|
expect(createInstance).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads repositories when project selected", async () => {
|
it("loads repositories when project selected", async () => {
|
||||||
const mockedList = listRepositories as ReturnType<typeof vi.fn>;
|
const mockedList = listRepositories as ReturnType<typeof vi.fn>;
|
||||||
mockedList.mockResolvedValueOnce([{ id: "r1", name: "repo-one" }]);
|
mockedList.mockResolvedValueOnce([{ id: "r1", name: "repo-one" }]);
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<CreateSessionForm
|
<CreateSessionForm
|
||||||
projects={mockProjects}
|
projects={mockProjects}
|
||||||
toolTypes={mockToolTypes}
|
toolTypes={mockToolTypes}
|
||||||
onCreated={vi.fn()}
|
onCreated={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectSelect = container.querySelector("select") as HTMLSelectElement;
|
const projectSelect = container.querySelector(
|
||||||
fireEvent.change(projectSelect, { target: { value: "p1" } });
|
"select",
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
fireEvent.change(projectSelect, { target: { value: "p1" } });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(listRepositories).toHaveBeenCalledWith("p1");
|
expect(listRepositories).toHaveBeenCalledWith("p1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,69 +4,73 @@ import { SessionCard } from "./SessionCard";
|
|||||||
import type { Session } from "@/types/session";
|
import type { Session } from "@/types/session";
|
||||||
|
|
||||||
const mockSession: Session = {
|
const mockSession: Session = {
|
||||||
id: "s1",
|
id: "s1",
|
||||||
display_name: "Dev Environment",
|
display_name: "Dev Environment",
|
||||||
tool_type_name: "VS Code",
|
tool_type_name: "VS Code",
|
||||||
tool_icon: "code",
|
tool_icon: "code",
|
||||||
tool_type_interfaces: ["web", "terminal"],
|
tool_type_interfaces: ["web", "terminal"],
|
||||||
repository_name: "my-repo",
|
repository_name: "my-repo",
|
||||||
repository_id: "r1",
|
repository_id: "r1",
|
||||||
project_name: "My Project",
|
project_name: "My Project",
|
||||||
project_id: "p1",
|
project_id: "p1",
|
||||||
status: "running",
|
status: "running",
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("SessionCard", () => {
|
describe("SessionCard", () => {
|
||||||
it("renders active variant with display name and status", () => {
|
it("renders active variant with display name and status", () => {
|
||||||
render(
|
render(
|
||||||
<SessionCard
|
<SessionCard
|
||||||
session={mockSession}
|
session={mockSession}
|
||||||
variant="active"
|
variant="active"
|
||||||
onOpen={vi.fn()}
|
onOpen={vi.fn()}
|
||||||
onStop={vi.fn()}
|
onStop={vi.fn()}
|
||||||
onDelete={vi.fn()}
|
onDelete={vi.fn()}
|
||||||
onRecreateTunnel={vi.fn()}
|
onRecreateTunnel={vi.fn()}
|
||||||
onCancelStop={vi.fn()}
|
onCancelStop={vi.fn()}
|
||||||
onCancelDelete={vi.fn()}
|
onCancelDelete={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("running")).toBeInTheDocument();
|
expect(screen.getByText("running")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("Dev Environment").length).toBeGreaterThanOrEqual(1);
|
expect(
|
||||||
});
|
screen.getAllByText("Dev Environment").length,
|
||||||
|
).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders recent variant with display name", () => {
|
it("renders recent variant with display name", () => {
|
||||||
render(
|
render(
|
||||||
<SessionCard
|
<SessionCard
|
||||||
session={{ ...mockSession, status: "stopped" }}
|
session={{ ...mockSession, status: "stopped" }}
|
||||||
variant="recent"
|
variant="recent"
|
||||||
onOpen={vi.fn()}
|
onOpen={vi.fn()}
|
||||||
onStop={vi.fn()}
|
onStop={vi.fn()}
|
||||||
onDelete={vi.fn()}
|
onDelete={vi.fn()}
|
||||||
onRecreateTunnel={vi.fn()}
|
onRecreateTunnel={vi.fn()}
|
||||||
onCancelStop={vi.fn()}
|
onCancelStop={vi.fn()}
|
||||||
onCancelDelete={vi.fn()}
|
onCancelDelete={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getAllByText("Dev Environment").length).toBeGreaterThanOrEqual(1);
|
expect(
|
||||||
});
|
screen.getAllByText("Dev Environment").length,
|
||||||
|
).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows unnamed fallback when display_name is empty", () => {
|
it("shows unnamed fallback when display_name is empty", () => {
|
||||||
render(
|
render(
|
||||||
<SessionCard
|
<SessionCard
|
||||||
session={{ ...mockSession, display_name: "" }}
|
session={{ ...mockSession, display_name: "" }}
|
||||||
variant="active"
|
variant="active"
|
||||||
onOpen={vi.fn()}
|
onOpen={vi.fn()}
|
||||||
onStop={vi.fn()}
|
onStop={vi.fn()}
|
||||||
onDelete={vi.fn()}
|
onDelete={vi.fn()}
|
||||||
onRecreateTunnel={vi.fn()}
|
onRecreateTunnel={vi.fn()}
|
||||||
onCancelStop={vi.fn()}
|
onCancelStop={vi.fn()}
|
||||||
onCancelDelete={vi.fn()}
|
onCancelDelete={vi.fn()}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("VS Code")).toBeInTheDocument();
|
expect(screen.getByText("VS Code")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,56 +3,58 @@ import { describe, it, expect, vi } from "vitest";
|
|||||||
import { ToolTypesTab } from "./ToolTypesTab";
|
import { ToolTypesTab } from "./ToolTypesTab";
|
||||||
|
|
||||||
vi.mock("../../../api/tool_types", () => ({
|
vi.mock("../../../api/tool_types", () => ({
|
||||||
listToolTypes: vi.fn(() => Promise.resolve([])),
|
listToolTypes: vi.fn(() => Promise.resolve([])),
|
||||||
createToolType: vi.fn(),
|
createToolType: vi.fn(),
|
||||||
deleteToolType: vi.fn(),
|
deleteToolType: vi.fn(),
|
||||||
updateToolType: vi.fn(),
|
updateToolType: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { listToolTypes } from "../../../api/tool_types";
|
import { listToolTypes } from "../../../api/tool_types";
|
||||||
|
|
||||||
describe("ToolTypesTab", () => {
|
describe("ToolTypesTab", () => {
|
||||||
it("renders loading state initially", () => {
|
it("renders loading state initially", () => {
|
||||||
render(<ToolTypesTab />);
|
render(<ToolTypesTab />);
|
||||||
expect(screen.getByText(/loading tool types/i)).toBeInTheDocument();
|
expect(screen.getByText(/loading tool types/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders tool types heading after loading", async () => {
|
it("renders tool types heading after loading", async () => {
|
||||||
const mockedList = listToolTypes as ReturnType<typeof vi.fn>;
|
const mockedList = listToolTypes as ReturnType<typeof vi.fn>;
|
||||||
mockedList.mockResolvedValueOnce([
|
mockedList.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "code-server",
|
name: "code-server",
|
||||||
display_name: "VS Code Server",
|
display_name: "VS Code Server",
|
||||||
description: "Web-based IDE",
|
description: "Web-based IDE",
|
||||||
category: "editor",
|
category: "editor",
|
||||||
interfaces: ["web"],
|
interfaces: ["web"],
|
||||||
default_port: 8443,
|
default_port: 8443,
|
||||||
definition_type: "compose",
|
definition_type: "compose",
|
||||||
compose_template: "version: '3.8'\nservices:\n app:",
|
compose_template: "version: '3.8'\nservices:\n app:",
|
||||||
dockerfile_template: null,
|
dockerfile_template: null,
|
||||||
readiness_probe: null,
|
readiness_probe: null,
|
||||||
required_variables: [],
|
required_variables: [],
|
||||||
is_builtin: true,
|
is_builtin: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
render(<ToolTypesTab />);
|
render(<ToolTypesTab />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Tool Types")).toBeInTheDocument();
|
expect(screen.getByText("Tool Types")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders error state on failure", async () => {
|
it("renders error state on failure", async () => {
|
||||||
const mockedList = listToolTypes as ReturnType<typeof vi.fn>;
|
const mockedList = listToolTypes as ReturnType<typeof vi.fn>;
|
||||||
mockedList.mockRejectedValueOnce(new Error("Network error"));
|
mockedList.mockRejectedValueOnce(new Error("Network error"));
|
||||||
|
|
||||||
render(<ToolTypesTab />);
|
render(<ToolTypesTab />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText(/failed to load tool types/i)).toBeInTheDocument();
|
expect(
|
||||||
});
|
screen.getByText(/failed to load tool types/i),
|
||||||
});
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import { describe, it, expect, vi } from "vitest";
|
|||||||
import { ErrorState } from "./ErrorState";
|
import { ErrorState } from "./ErrorState";
|
||||||
|
|
||||||
describe("ErrorState", () => {
|
describe("ErrorState", () => {
|
||||||
it("renders message", () => {
|
it("renders message", () => {
|
||||||
render(<ErrorState message="Failed to load" />);
|
render(<ErrorState message="Failed to load" />);
|
||||||
expect(screen.getByText("Failed to load")).toBeInTheDocument();
|
expect(screen.getByText("Failed to load")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render retry button when onRetry is absent", () => {
|
it("does not render retry button when onRetry is absent", () => {
|
||||||
render(<ErrorState message="Failed" />);
|
render(<ErrorState message="Failed" />);
|
||||||
expect(screen.queryByText("Retry")).not.toBeInTheDocument();
|
expect(screen.queryByText("Retry")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onRetry when button clicked", () => {
|
it("calls onRetry when button clicked", () => {
|
||||||
const onRetry = vi.fn();
|
const onRetry = vi.fn();
|
||||||
render(<ErrorState message="Failed" onRetry={onRetry} />);
|
render(<ErrorState message="Failed" onRetry={onRetry} />);
|
||||||
fireEvent.click(screen.getByText("Retry"));
|
fireEvent.click(screen.getByText("Retry"));
|
||||||
expect(onRetry).toHaveBeenCalledTimes(1);
|
expect(onRetry).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { describe, it, expect } from "vitest";
|
|||||||
import { LoadingState } from "./LoadingState";
|
import { LoadingState } from "./LoadingState";
|
||||||
|
|
||||||
describe("LoadingState", () => {
|
describe("LoadingState", () => {
|
||||||
it("renders default message", () => {
|
it("renders default message", () => {
|
||||||
render(<LoadingState />);
|
render(<LoadingState />);
|
||||||
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders custom message", () => {
|
it("renders custom message", () => {
|
||||||
render(<LoadingState message="Loading sessions..." />);
|
render(<LoadingState message="Loading sessions..." />);
|
||||||
expect(screen.getByText("Loading sessions...")).toBeInTheDocument();
|
expect(screen.getByText("Loading sessions...")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-13
@@ -3,17 +3,17 @@ import { defineConfig } from "vite";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173
|
port: 5173,
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: "./src/test/setup.ts"
|
setupFiles: "./src/test/setup.ts",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Naming Conventions
|
||||||
|
|
||||||
|
This document defines the file and identifier naming conventions for the Headquarter codebase.
|
||||||
|
|
||||||
|
## Frontend (`apps/web/src/`)
|
||||||
|
|
||||||
|
### React Components
|
||||||
|
|
||||||
|
**File naming:** PascalCase, matching the exported component name exactly.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
components/features/git/FileBrowser.tsx
|
||||||
|
components/features/dashboard/DashboardSummary.tsx
|
||||||
|
pages/DashboardPage.tsx
|
||||||
|
|
||||||
|
❌ Bad:
|
||||||
|
components/file-browser.tsx
|
||||||
|
pages/dashboard.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component naming:** PascalCase. Page components end with `Page`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Component
|
||||||
|
export const FileBrowser = () => { ... }
|
||||||
|
|
||||||
|
// Page
|
||||||
|
export const DashboardPage = () => { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
**File naming:** camelCase with `use` prefix.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
hooks/use-theme.ts
|
||||||
|
hooks/use-dashboard-actions.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Modules
|
||||||
|
|
||||||
|
**File naming:** kebab-case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
api/tool-types.ts
|
||||||
|
api/git-repositories.ts
|
||||||
|
api/config-folders.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Modules
|
||||||
|
|
||||||
|
**File naming:** kebab-case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
types/tool-type.ts
|
||||||
|
types/git-repository.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
**File naming:** kebab-case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
utils/terminal-protocol.ts
|
||||||
|
utils/language.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Modules
|
||||||
|
|
||||||
|
**File naming:** kebab-case, matching the component file name with `.module.css` suffix.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
FileBrowser.tsx + FileBrowser.module.css
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend (`apps/api/src/`)
|
||||||
|
|
||||||
|
### Routers
|
||||||
|
|
||||||
|
**File naming:** snake_case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
api/tool_instances.py
|
||||||
|
api/git_repositories.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
**File naming:** snake_case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
services/docker/compose.py
|
||||||
|
services/profile_resolver.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Models
|
||||||
|
|
||||||
|
**File naming:** snake_case. Class names use PascalCase.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
models/tool_instance.py
|
||||||
|
class ToolInstance(Base):
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schemas
|
||||||
|
|
||||||
|
**File naming:** snake_case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
schemas/tool_instance.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Frontend Tests
|
||||||
|
|
||||||
|
**File naming:** Same as source file with `.test.tsx` suffix.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
FileBrowser.tsx + FileBrowser.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Tests
|
||||||
|
|
||||||
|
**File naming:** `test_` prefix + snake_case.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
test_tool_instances.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web/src/
|
||||||
|
├── api/ # kebab-case files
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # PascalCase files
|
||||||
|
│ ├── layout/ # PascalCase files
|
||||||
|
│ └── features/ # PascalCase files, grouped by domain
|
||||||
|
│ ├── git/
|
||||||
|
│ ├── project/
|
||||||
|
│ ├── session/
|
||||||
|
│ └── ...
|
||||||
|
├── hooks/ # camelCase files
|
||||||
|
├── pages/ # PascalCase files ending with Page
|
||||||
|
├── styles/ # kebab-case CSS files
|
||||||
|
├── types/ # kebab-case files
|
||||||
|
└── utils/ # kebab-case files
|
||||||
|
|
||||||
|
apps/api/src/
|
||||||
|
├── api/ # snake_case files
|
||||||
|
├── models/ # snake_case files
|
||||||
|
├── schemas/ # snake_case files
|
||||||
|
├── services/ # snake_case files
|
||||||
|
└── auth/ # snake_case files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
Some legacy files may not yet follow these conventions. When touching a file for other work, rename it to match the convention in the same PR.
|
||||||
Reference in New Issue
Block a user