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.
149 lines
5.5 KiB
JavaScript
149 lines
5.5 KiB
JavaScript
/**
|
|
* @file Tests GET/PUT /api/settings/codex-home and setCodexHome: the shared
|
|
* home-dir guard, persistence to the settings file, the `env_owned` flag, and
|
|
* the AI Router layer being re-written into the NEW home for every codex lane
|
|
* on a router (a `codex -p <name>` against a home without `<name>.config.toml`
|
|
* would fail at start); a home change from another site refused, and a
|
|
* CLAUDE_HOME change naming the lanes whose router profile the new home's
|
|
* store lacks. Everything lives in a temp dir.
|
|
* @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 http = require("node:http");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-codex-home-"));
|
|
process.env.CLAUDE_HOME = path.join(ROOT, ".claude");
|
|
process.env.CODEX_HOME = path.join(ROOT, "codex-a");
|
|
process.env.DASHBOARD_DB_PATH = path.join(ROOT, "dashboard.db");
|
|
process.env.AGENTHUB_SETTINGS_FILE = path.join(ROOT, "settings.env");
|
|
fs.mkdirSync(process.env.CODEX_HOME, { recursive: true });
|
|
|
|
const { describe, it, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const express = require("express");
|
|
|
|
const routers = require("../lib/ai-routers");
|
|
const lanesLib = require("../lib/lanes");
|
|
const { applyRouterToLane } = require("../lib/router-apply");
|
|
const { envOwned } = require("../lib/settings-env");
|
|
const settingsRouter = require("../routes/settings");
|
|
|
|
let server;
|
|
let base;
|
|
|
|
function request(method, url, body, extra = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const data = body === undefined ? null : Buffer.from(JSON.stringify(body));
|
|
const req = http.request(
|
|
`${base}${url}`,
|
|
{
|
|
method,
|
|
headers: {
|
|
...(data ? { "content-type": "application/json", "content-length": data.length } : {}),
|
|
...extra,
|
|
},
|
|
},
|
|
(res) => {
|
|
let text = "";
|
|
res.on("data", (c) => (text += c));
|
|
res.on("end", () => resolve({ status: res.statusCode, body: JSON.parse(text) }));
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
if (data) req.write(data);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
before(async () => {
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use("/api/settings", settingsRouter);
|
|
server = http.createServer(app);
|
|
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
base = `http://127.0.0.1:${server.address().port}`;
|
|
});
|
|
|
|
after(() => {
|
|
server.close();
|
|
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("/api/settings/codex-home", () => {
|
|
it("GET answers the current home and whether the environment owns it", async () => {
|
|
const res = await request("GET", "/api/settings/codex-home");
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.codex_home, process.env.CODEX_HOME);
|
|
assert.equal(res.body.env_owned, envOwned.has("CODEX_HOME"));
|
|
});
|
|
|
|
it("refuses a path that contains the user's home, changing nothing", async () => {
|
|
const before = process.env.CODEX_HOME;
|
|
const res = await request("PUT", "/api/settings/codex-home", { path: "/" });
|
|
assert.equal(res.status, 400);
|
|
assert.equal(res.body.error.code, "EBADHOME");
|
|
assert.equal(process.env.CODEX_HOME, before);
|
|
assert.ok(!fs.existsSync(process.env.AGENTHUB_SETTINGS_FILE));
|
|
});
|
|
|
|
it("refuses a missing path and a non-string body", async () => {
|
|
for (const body of [{ path: path.join(ROOT, "nope") }, { path: 7 }, {}]) {
|
|
const res = await request("PUT", "/api/settings/codex-home", body);
|
|
assert.equal(res.status, 400);
|
|
assert.equal(res.body.error.code, "INVALID_PATH");
|
|
}
|
|
});
|
|
|
|
it("switches, persists, and re-writes every codex lane's router layer into the new home", async () => {
|
|
const cwd = path.join(ROOT, "lane");
|
|
fs.mkdirSync(cwd, { recursive: true });
|
|
const { name } = routers.createRouter({
|
|
label: "9Router",
|
|
baseUrl: "http://localhost:20128/v1",
|
|
apiKey: "sk-secret-value",
|
|
model: "Highest",
|
|
});
|
|
const lane = lanesLib.createLane({ title: "c", cwd, tool: "codex", router_profile: name });
|
|
applyRouterToLane(lane, name);
|
|
assert.ok(fs.existsSync(path.join(process.env.CODEX_HOME, `${name}.config.toml`)));
|
|
|
|
const next = path.join(ROOT, "codex-b");
|
|
fs.mkdirSync(next);
|
|
const res = await request("PUT", "/api/settings/codex-home", { path: next });
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.codex_home, next);
|
|
assert.deepEqual(res.body.routers, { updated: 1, failed: [] });
|
|
assert.equal(process.env.CODEX_HOME, next);
|
|
assert.ok(fs.existsSync(path.join(next, `${name}.config.toml`)));
|
|
assert.match(
|
|
fs.readFileSync(process.env.AGENTHUB_SETTINGS_FILE, "utf8"),
|
|
new RegExp(`^CODEX_HOME=${next}$`, "m")
|
|
);
|
|
});
|
|
|
|
it("refuses a home change asked by another site", async () => {
|
|
const next = path.join(ROOT, "codex-evil");
|
|
fs.mkdirSync(next);
|
|
const res = await request(
|
|
"PUT",
|
|
"/api/settings/codex-home",
|
|
{ path: next },
|
|
{ Origin: "https://evil.example" }
|
|
);
|
|
assert.equal(res.status, 403);
|
|
assert.notEqual(process.env.CODEX_HOME, next);
|
|
});
|
|
|
|
it("a CLAUDE_HOME change names the lanes whose router the new store lacks", async () => {
|
|
const next = path.join(ROOT, "claude-b");
|
|
fs.mkdirSync(next);
|
|
const res = await request("PUT", "/api/settings/claude-home", { path: next });
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.routers.missing.length, 1);
|
|
assert.match(res.body.routers.missing[0], /\(c\): 9router/i);
|
|
});
|
|
});
|