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.
82 lines
3.3 KiB
JavaScript
82 lines
3.3 KiB
JavaScript
/**
|
|
* @file Tests setClaudeHome: the shared home-dir guard (a directory that is or
|
|
* contains the user's home is refused), persistence to the settings file
|
|
* (never the repo `.env`, which a plugin update wipes), and pinning the data
|
|
* dir, so a new CLAUDE_HOME can never open an empty database at the next start.
|
|
* The settings file is a temp path (AGENTHUB_SETTINGS_FILE).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const os = require("node:os");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { describe, it, before, beforeEach, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-claude-home-set-"));
|
|
const SETTINGS = path.join(ROOT, "settings.env");
|
|
const REPO_ENV = path.resolve(__dirname, "..", "..", ".env");
|
|
const SAVED = {
|
|
AGENTHUB_SETTINGS_FILE: process.env.AGENTHUB_SETTINGS_FILE,
|
|
CLAUDE_HOME: process.env.CLAUDE_HOME,
|
|
DASHBOARD_DATA_DIR: process.env.DASHBOARD_DATA_DIR,
|
|
};
|
|
process.env.AGENTHUB_SETTINGS_FILE = SETTINGS;
|
|
|
|
const { setClaudeHome, getClaudeHome, getDataDir } = require("../lib/claude-home");
|
|
|
|
describe("setClaudeHome", () => {
|
|
let repoEnvBefore;
|
|
before(() => {
|
|
repoEnvBefore = fs.existsSync(REPO_ENV) ? fs.readFileSync(REPO_ENV, "utf8") : null;
|
|
});
|
|
beforeEach(() => {
|
|
fs.rmSync(SETTINGS, { force: true });
|
|
process.env.CLAUDE_HOME = fs.mkdtempSync(path.join(ROOT, "start-"));
|
|
delete process.env.DASHBOARD_DATA_DIR;
|
|
});
|
|
after(() => {
|
|
for (const [k, v] of Object.entries(SAVED)) {
|
|
if (v === undefined) delete process.env[k];
|
|
else process.env[k] = v;
|
|
}
|
|
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
});
|
|
|
|
for (const bad of ["/", os.homedir(), path.dirname(os.homedir()), "~/"]) {
|
|
it(`refuses ${JSON.stringify(bad)} as too broad`, () => {
|
|
const current = getClaudeHome();
|
|
assert.throws(() => setClaudeHome(bad), { code: "EBADHOME" });
|
|
assert.equal(getClaudeHome(), current);
|
|
assert.ok(!fs.existsSync(SETTINGS));
|
|
});
|
|
}
|
|
|
|
it("accepts a real directory, persisting it to the settings file only", () => {
|
|
const next = fs.mkdtempSync(path.join(ROOT, "next-"));
|
|
assert.equal(setClaudeHome(next), next);
|
|
assert.equal(getClaudeHome(), next);
|
|
assert.match(fs.readFileSync(SETTINGS, "utf8"), new RegExp(`^CLAUDE_HOME=${next}$`, "m"));
|
|
const repoEnvNow = fs.existsSync(REPO_ENV) ? fs.readFileSync(REPO_ENV, "utf8") : null;
|
|
assert.equal(repoEnvNow, repoEnvBefore, "the repo .env is never written");
|
|
});
|
|
|
|
it("pins the data dir in use before CLAUDE_HOME moves, so the database stays put", () => {
|
|
const dataDirBefore = getDataDir();
|
|
setClaudeHome(fs.mkdtempSync(path.join(ROOT, "next-")));
|
|
assert.equal(getDataDir(), dataDirBefore);
|
|
assert.equal(process.env.DASHBOARD_DATA_DIR, dataDirBefore);
|
|
assert.match(
|
|
fs.readFileSync(SETTINGS, "utf8"),
|
|
new RegExp(`^DASHBOARD_DATA_DIR=${dataDirBefore}$`, "m")
|
|
);
|
|
});
|
|
|
|
it("leaves an explicit DASHBOARD_DATA_DIR alone", () => {
|
|
process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "explicit-data");
|
|
setClaudeHome(fs.mkdtempSync(path.join(ROOT, "next-")));
|
|
assert.equal(process.env.DASHBOARD_DATA_DIR, path.join(ROOT, "explicit-data"));
|
|
assert.doesNotMatch(fs.readFileSync(SETTINGS, "utf8"), /DASHBOARD_DATA_DIR/);
|
|
});
|
|
});
|