8546748759
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.
134 lines
4.3 KiB
TypeScript
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();
|
|
});
|
|
});
|