969b06253c
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from Settings → Dashboard to the Claude Code tab's AgentHub sub-tab (ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub. A home that is or contains the OS home is refused (EBADHOME). Settings persistence: dashboard-saved settings go to ~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin cache an update wipes. Load order: real env > the checkout's .env > that file. The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put. C1: codex-usage.js reads Codex rollout journals into sessions (new column tool) and token_usage, tracked per file in codex_rollouts; forks deduped by total vector, sub-agent threads folded into the root; pricing-sync also prices OpenAI models; PricingSettings with a vendor filter. C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on a click, through a stable forwarder; Codex trust is never written. The hook route creates tool='codex' sessions and folds sub-agent hooks into the root. D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab; Gemini pricing. Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=, clear-data covering the new tables, Codex child-thread events moving no status, hooks owning status only while current, EFOREIGN for a forwarder that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE honoured from .env. Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
86 lines
3.6 KiB
TypeScript
86 lines
3.6 KiB
TypeScript
/**
|
|
* @file AntigravityHooksSettings.test.tsx
|
|
* @description Settings → Antigravity → Antigravity hooks: not installed until
|
|
* the user clicks Install, then "Installed" with the note that agy asks no
|
|
* trust and has no permission hook; a hook switched off in hooks.json is
|
|
* reported; Refresh re-reads; Remove; a server refusal shown inline.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import i18n from "i18next";
|
|
import { AntigravityHooksSettings } from "../AntigravityHooksSettings";
|
|
import { api } from "../../../lib/api";
|
|
import type { AntigravityHooksStatus } from "../../../lib/types";
|
|
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return {
|
|
...actual,
|
|
api: { settings: { antigravityHooks: { get: vi.fn(), install: vi.fn(), remove: vi.fn() } } },
|
|
};
|
|
});
|
|
|
|
function status(installed: boolean, enabled = installed): AntigravityHooksStatus {
|
|
return {
|
|
supported: true,
|
|
readable: true,
|
|
path: "/home/u/.gemini/config/hooks.json",
|
|
script: "/home/u/.gemini/config/agenthub-hook.sh",
|
|
installed,
|
|
enabled,
|
|
events: Object.fromEntries(
|
|
["PreInvocation", "PostToolUse", "Stop"].map((e) => [e, { installed }])
|
|
),
|
|
};
|
|
}
|
|
|
|
const hooks = api.settings.antigravityHooks;
|
|
|
|
beforeEach(() => {
|
|
i18n.changeLanguage("en");
|
|
vi.mocked(hooks.get).mockReset().mockResolvedValue(status(false));
|
|
vi.mocked(hooks.install).mockReset().mockResolvedValue(status(true));
|
|
vi.mocked(hooks.remove).mockReset().mockResolvedValue(status(false));
|
|
});
|
|
|
|
describe("AntigravityHooksSettings", () => {
|
|
it("installs on a click, and says agy has no permission hook", async () => {
|
|
const user = userEvent.setup();
|
|
render(<AntigravityHooksSettings />);
|
|
expect(await screen.findByText("Not installed")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Remove" })).toBeNull();
|
|
expect(screen.getByText(/no hook for a permission prompt/)).toBeInTheDocument();
|
|
await user.click(screen.getByRole("button", { name: "Install" }));
|
|
expect(hooks.install).toHaveBeenCalled();
|
|
expect(await screen.findByRole("button", { name: "Reinstall" })).toBeInTheDocument();
|
|
expect(screen.getByText("PreInvocation")).toBeInTheDocument();
|
|
expect(screen.queryByText("PreToolUse")).toBeNull();
|
|
});
|
|
|
|
it("reports a hook switched off, re-reads on Refresh, and removes", async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(hooks.get)
|
|
.mockResolvedValueOnce(status(true, false))
|
|
.mockResolvedValueOnce(status(true, true));
|
|
render(<AntigravityHooksSettings />);
|
|
expect(await screen.findByText(/switched off/)).toBeInTheDocument();
|
|
await user.click(screen.getByRole("button", { name: "Refresh" }));
|
|
expect(await screen.findByRole("button", { name: "Reinstall" })).toBeInTheDocument();
|
|
expect(screen.queryByText(/switched off/)).toBeNull();
|
|
await user.click(screen.getByRole("button", { name: "Remove" }));
|
|
expect(hooks.remove).toHaveBeenCalled();
|
|
expect(await screen.findByText("Not installed")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a server refusal inline", async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(hooks.install).mockRejectedValue(new Error("hooks.json is not a JSON object"));
|
|
render(<AntigravityHooksSettings />);
|
|
await user.click(await screen.findByRole("button", { name: "Install" }));
|
|
expect(await screen.findByText(/is not a JSON object/)).toBeInTheDocument();
|
|
});
|
|
});
|