Files
AgentHub/server/__tests__/commit-message.test.js
T
nntrivi2001 f0500dc592 feat: AI commit messages, change tree, router behavesAs, sidebar times (v2.2.0)
- Changes panel: Files and Changes merged into one panel; the overview's
  groups drawn as a folder tree (compacted chains, file counts, fold,
  stage/unstage a whole folder)
- AI-suggested commit message for what is staged
  (POST /api/lanes/:id/git/commit-message, claude -p --safe-mode, haiku)
- AI Router: optional "Behaves as" Claude model id, written as
  modelPicker options into the router's --settings file (issue #56)
- Sidebar: session rows show last activity, live rows their start time;
  a closed Terminal row can be removed (DELETE /api/run/history/:id)
- Every pane's CLI starts only once a client is attached; claude panes
  no longer forced inline; tmux scrollbar one column wide
- Update notifier: "Check for updates" reopens a closed notice
2026-10-06 12:36:06 +07:00

92 lines
3.4 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("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" });
});
});