Files
AgentHub/client/src/pages/__tests__/CcConfig.member.test.tsx
T
nntrivi2001 8546748759 chore: maintenance — dependency majors, rename step 4, member Claude config, OpenAPI coverage
Dependencies
- Security: body-parser and multer DoS advisories fixed (npm audit: 0).
- Server: Express 5 (query parser kept "extended", req.body defaults to {},
  SPA catch-all /{*splat}, send-file helper restores Express 4's dot-folder
  rule for sendFile/download, alerts ack route without inline regex),
  better-sqlite3 13, js-yaml 5 (server/lib/yaml.js keeps js-yaml 4's default
  schema), uuid dropped for crypto.randomUUID, node-pty beta.18, multer 2.5.
- Client: React 19, xterm 6 (WebGL renderer replaces the removed canvas one,
  DOM fallback on context loss; Alt+arrow word moves re-mapped), lucide-react 1,
  TypeScript 7, i18next/react-i18next/codemirror minors.

Workspace rename step 4
- `agenthub lanes` and `--lane` exit 1 with "renamed: use …"; client api.ts,
  the CLI and the MCP server call /api/workspaces; /api/lanes stays mounted.

Server mode (S3): members manage their own Claude Code config
- server/lib/cc-target.js resolves whose config a /cc-config request touches
  and gates the router by allowlist; /profiles* and /plugins/update-all stay
  admin-only. Member writes go through write-as, CLI calls through sudo as the
  member (never -y), reads through openSafe; cc_config_changed reaches only
  its owner. Not verified live on an S3 install yet.

Other
- Chat: an HTML file is an artifact only when written whole and not a project
  page (index.html, or under src/, public/, templates/, …).
- OpenAPI covers every /api route; openapi-coverage.test.js guards it.
- read-pool test keeps its event loop alive (its worker is unref'd).
- Docs: plan statuses, checklists, CLI.md, LANES.md, SERVER-MODE.md, API.md.
2026-10-26 14:22:41 +07:00

134 lines
4.3 KiB
TypeScript

/**
* @file Covers the Claude Code config page as a MEMBER's own config (server
* mode, a non-admin with their own Linux account): it says whose config it is,
* and the host-wide controls — Switch profile, Update all, AgentHub's own
* Claude settings — are not shown (the server refuses them anyway). An admin
* still sees all three.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
const perms = vi.hoisted(() => ({ isAdmin: false }));
vi.mock("../../components/AuthGate", async (importOriginal) => {
const m = await importOriginal<typeof import("../../components/AuthGate")>();
return {
...m,
usePermissions: () => ({ ...m.usePermissions(), local: false, isAdmin: perms.isAdmin }),
};
});
vi.mock("../../components/settings/ClaudeAgentHubSettings", () => ({
ClaudeAgentHubSettings: () => <div data-testid="agenthub-claude-settings" />,
}));
const HOME = "/srv/users/mia/.claude";
const ccConfigMock = vi.hoisted(() => {
const r = (value: unknown) => vi.fn().mockResolvedValue(value);
return {
target: r({ kind: "own", readOnly: false, claudeHome: "/srv/users/mia/.claude" }),
overview: r({
roots: {
claudeHome: "/srv/users/mia/.claude",
projectClaudeDir: null,
projectRoot: null,
claudeJson: "/srv/users/mia/.claude.json",
},
counts: {
skills: { user: 0, project: 0 },
agents: { user: 0, project: 0 },
commands: { user: 0, project: 0 },
outputStyles: { user: 0, project: 0 },
mcpServers: { user: 0, project: 0 },
hooks: {},
plugins: 0,
marketplaces: 0,
keybindings: 0,
settingsFiles: 0,
memory: 0,
},
}),
skills: r({ items: [] }),
agents: r({ items: [] }),
commands: r({ items: [] }),
outputStyles: r({ items: [] }),
memory: r({ items: [] }),
plugins: r({ manifestPath: null, manifestExists: false, plugins: [] }),
marketplaces: r({ items: [] }),
mcp: r({ user: [], projectScoped: [] }),
hooks: r({ items: [] }),
settings: r({ items: [] }),
keybindings: r({ groups: [] }),
statusline: r({}),
hookScripts: r({ items: [] }),
backups: r({ items: [] }),
pluginsAvailable: r({ installed: [], available: [] }),
updateAllStatus: r({ batch: null }),
};
});
vi.mock("../../lib/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return {
...actual,
api: { ...(actual.api as Record<string, unknown>), ccConfig: ccConfigMock },
};
});
vi.mock("../../lib/eventBus", () => ({
eventBus: {
subscribe: () => () => {},
publish: () => {},
onConnection: () => () => {},
connected: true,
setConnected: () => {},
},
}));
import { CcConfig } from "../CcConfig";
async function settle() {
await act(async () => {
await new Promise((res) => setTimeout(res, 0));
await new Promise((res) => setTimeout(res, 0));
});
}
async function open() {
render(
<MemoryRouter>
<CcConfig />
</MemoryRouter>
);
await settle();
}
beforeEach(() => {
vi.clearAllMocks();
perms.isAdmin = false;
});
describe("CcConfig — a member's own config", () => {
it("says whose config it is and hides the host-wide controls", async () => {
await open();
expect(screen.getByTestId("cc-own-config")).toHaveTextContent(HOME);
expect(screen.queryByRole("link", { name: /switch profile/i })).not.toBeInTheDocument();
expect(screen.queryByTestId("agenthub-claude-settings")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("tab", { name: /plugins/i }));
await settle();
expect(screen.queryByRole("button", { name: /^update all$/i })).not.toBeInTheDocument();
expect(ccConfigMock.updateAllStatus).not.toHaveBeenCalled();
});
it("keeps all three for an admin", async () => {
perms.isAdmin = true;
await open();
expect(screen.queryByTestId("cc-own-config")).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: /switch profile/i })).toBeInTheDocument();
expect(screen.getByTestId("agenthub-claude-settings")).toBeInTheDocument();
expect(ccConfigMock.target).not.toHaveBeenCalled();
});
});