a9c71cd905
Usage panel (status bar): - every account shows its plan (Claude subscriptionType, Codex chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier) - new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's own file, sent only to its own host while valid, never refreshed - in-flight quota reads are shared between tabs AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added; Gemini CLI, Hermes and OpenClaw removed. Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat on an editor selection, Settings in three groups, JetBrains-style Changes panel (ticked files/hunks, ignore, blame, file history), terminal font. --no-verify: :3000 is taken by another dev server (lane-detect tests); both suites run by hand — server 2636/2639 (the 3 are :3000), client 1131/1131.
105 lines
4.1 KiB
JavaScript
105 lines
4.1 KiB
JavaScript
/**
|
|
* @file Tests for server/lib/commit-message.js against a real git repo and a
|
|
* fake `claude` script: only staged, in-lane, non-secret changes reach the
|
|
* model; its argv keeps tools, hooks and persistence off; its answer is
|
|
* cleaned; and every failure is a coded error.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { execFileSync } = require("node:child_process");
|
|
|
|
const { suggestCommitMessage, buildInput } = require("../lib/commit-message");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-commitmsg-"));
|
|
const REPO = path.join(ROOT, "repo");
|
|
const SUB = path.join(REPO, "sub");
|
|
const ARGS = path.join(ROOT, "args.txt");
|
|
const STDIN = path.join(ROOT, "stdin.txt");
|
|
|
|
const g = (...args) => {
|
|
const env = { ...process.env };
|
|
delete env.GIT_DIR;
|
|
delete env.GIT_INDEX_FILE;
|
|
execFileSync("git", ["-c", "user.email=t@h", "-c", "user.name=t", ...args], { cwd: REPO, env });
|
|
};
|
|
const write = (file, body) => {
|
|
fs.mkdirSync(path.dirname(path.join(REPO, file)), { recursive: true });
|
|
fs.writeFileSync(path.join(REPO, file), body);
|
|
};
|
|
/** A fake claude: records argv and stdin, prints `reply`, exits `code`. */
|
|
function fakeClaude(reply, code = 0) {
|
|
const bin = path.join(ROOT, `claude-${Math.random().toString(36).slice(2)}`);
|
|
fs.writeFileSync(
|
|
bin,
|
|
`#!/bin/sh\nprintf '%s\\n' "$@" > '${ARGS}'\ncat > '${STDIN}'\nprintf '${reply}'\nexit ${code}\n`,
|
|
{ mode: 0o755 }
|
|
);
|
|
return bin;
|
|
}
|
|
|
|
before(() => {
|
|
fs.mkdirSync(REPO);
|
|
g("init", "-q", "-b", "main");
|
|
write("a.txt", "one\n");
|
|
g("add", "-A");
|
|
g("commit", "-q", "-m", "feat: seed");
|
|
});
|
|
after(() => fs.rmSync(ROOT, { recursive: true, force: true }));
|
|
|
|
describe("commit-message", () => {
|
|
it("refuses with ENOSTAGED when nothing is staged", async () => {
|
|
write("a.txt", "one\nunstaged\n");
|
|
await assert.rejects(buildInput(REPO), { code: "ENOSTAGED" });
|
|
});
|
|
|
|
it("sends only staged, non-secret changes and keeps tools/hooks/persistence off", async () => {
|
|
write("b.txt", "staged line\n");
|
|
write(".claude/settings.local.json", '{"env":{"KEY":"sk-secret"}}\n');
|
|
g("add", "-f", "b.txt", ".claude/settings.local.json");
|
|
const msg = await suggestCommitMessage(REPO, { bin: fakeClaude("```\\nfeat: add b\\n```") });
|
|
assert.equal(msg, "feat: add b");
|
|
const stdin = fs.readFileSync(STDIN, "utf8");
|
|
assert.match(stdin, /feat: seed/);
|
|
assert.match(stdin, /staged line/);
|
|
assert.doesNotMatch(stdin, /unstaged|sk-secret|settings\.local/);
|
|
const argv = fs.readFileSync(ARGS, "utf8").split("\n");
|
|
for (const flag of ["-p", "--safe-mode", "--no-session-persistence", "--tools"]) {
|
|
assert.ok(argv.includes(flag), flag);
|
|
}
|
|
assert.equal(argv[argv.indexOf("--tools") + 1], "");
|
|
});
|
|
|
|
it("scopes a subdirectory lane to its own staged files", async () => {
|
|
write("sub/c.txt", "inside\n");
|
|
g("add", "sub/c.txt");
|
|
const input = await buildInput(SUB);
|
|
assert.match(input, /inside/);
|
|
assert.doesNotMatch(input, /staged line/);
|
|
});
|
|
|
|
it("with paths, reads exactly those files' working-tree changes, untracked included", async () => {
|
|
write("a.txt", "one\nticked edit\n");
|
|
write("new.txt", "brand new\n");
|
|
write("other.txt", "not ticked\n");
|
|
write(".claude/settings.local.json", '{"env":{"KEY":"sk-ticked"}}\n');
|
|
const input = await buildInput(REPO, ["a.txt", "new.txt", ".claude/settings.local.json"]);
|
|
assert.match(input, /ticked edit/);
|
|
assert.match(input, /brand new/);
|
|
assert.doesNotMatch(input, /not ticked|staged line|sk-ticked|settings\.local/);
|
|
await assert.rejects(buildInput(REPO, ["nope.txt"]), { code: "EBADPATH" });
|
|
await assert.rejects(buildInput(REPO, []), { code: "ENOSTAGED" });
|
|
});
|
|
|
|
it("reports a failing or silent CLI as EAI", async () => {
|
|
await assert.rejects(suggestCommitMessage(REPO, { bin: fakeClaude("boom", 1) }), {
|
|
code: "EAI",
|
|
});
|
|
await assert.rejects(suggestCommitMessage(REPO, { bin: fakeClaude("") }), { code: "EAI" });
|
|
});
|
|
});
|