f514602697
Part A — a fetch/pull/push against a remote that needs a login no longer dies on "terminal prompts disabled": the server answers 409 EGITAUTH with the host, the git bar asks for a username + password/access token and resends the same action. AgentHub keeps its OWN credential store (<CLAUDE_HOME>/agenthub-git-credentials/, 0700/0600), one login per host, shared by every user of the install, managed from Settings -> Git credentials. A login is only sent to one of the repo's own remote hosts, saved only after git accepted it, never returned by any route, and reaches git through an exclusive credential helper (never argv). Part B — merge a branch (or the diverged upstream) from the git bar; a merge that stops on conflicts lists them in the Changes panel with Keep mine / Take theirs / Mark resolved, then Complete merge or Abort merge (confirm gate). Clean tree, repo-root lane and an existing ref only; pull/switch/hunk actions are blocked mid-merge; staging a conflicted file with markers left is refused. Rebase/reset/stash stay out.
86 lines
3.3 KiB
JavaScript
86 lines
3.3 KiB
JavaScript
/**
|
|
* @file Issue #45: a dashboard fetch/pull/push against a remote that needs a
|
|
* login. A local HTTP "remote" answers 401 to every request and records the
|
|
* Authorization header it was sent, so the tests see exactly what git sent:
|
|
* nothing → EGITAUTH naming the host; a typed credential → that credential,
|
|
* as Basic auth, to that host; saved only when git succeeded AND asked to.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it, before, after, beforeEach } = 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 http = require("node:http");
|
|
const { execFileSync } = require("node:child_process");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "git-ops-auth-"));
|
|
process.env.CLAUDE_HOME = path.join(ROOT, ".claude");
|
|
// The machine's own helpers must not answer for the fake host.
|
|
process.env.GIT_CONFIG_NOSYSTEM = "1";
|
|
process.env.HOME = ROOT;
|
|
|
|
const { runGitAction } = require("../lib/git-ops");
|
|
const creds = require("../lib/git-credentials");
|
|
|
|
let server;
|
|
let host;
|
|
const seen = [];
|
|
const LANE = path.join(ROOT, "lane");
|
|
|
|
before(async () => {
|
|
server = http.createServer((req, res) => {
|
|
seen.push(req.headers.authorization || null);
|
|
res.writeHead(401, { "WWW-Authenticate": 'Basic realm="t"' });
|
|
res.end();
|
|
});
|
|
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
host = `127.0.0.1:${server.address().port}`;
|
|
execFileSync("git", ["init", "-q", LANE]);
|
|
execFileSync("git", ["-C", LANE, "remote", "add", "origin", `http://${host}/o/r.git`]);
|
|
});
|
|
after(() => new Promise((r) => server.close(r)));
|
|
beforeEach(() => {
|
|
seen.length = 0;
|
|
fs.rmSync(process.env.CLAUDE_HOME, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("git auth on fetch", () => {
|
|
it("answers EGITAUTH naming the host when git has no credential", async () => {
|
|
await assert.rejects(runGitAction(LANE, "fetch"), (e) => {
|
|
assert.equal(e.code, "EGITAUTH");
|
|
assert.deepEqual(e.auth, { protocol: "http", host, username: null, rejected: false });
|
|
return true;
|
|
});
|
|
});
|
|
|
|
it("sends a typed credential as Basic auth, and does not save a failed one", async () => {
|
|
await assert.rejects(
|
|
runGitAction(LANE, "fetch", {
|
|
credentials: { protocol: "http", host, username: "vi", password: "tok", remember: true },
|
|
}),
|
|
(e) => e.code === "EGITAUTH" && e.auth.rejected === true
|
|
);
|
|
const basic = `Basic ${Buffer.from("vi:tok").toString("base64")}`;
|
|
assert.ok(seen.includes(basic), `expected ${basic} in ${JSON.stringify(seen)}`);
|
|
assert.deepEqual(creds.list(), [], "a credential git rejected is never saved");
|
|
});
|
|
|
|
it("uses a stored credential without being asked", async () => {
|
|
creds.save({ protocol: "http", host, username: "saved", password: "s3" });
|
|
await assert.rejects(runGitAction(LANE, "fetch"), { code: "EGITAUTH" });
|
|
assert.ok(seen.includes(`Basic ${Buffer.from("saved:s3").toString("base64")}`));
|
|
});
|
|
|
|
it("refuses a credential for a host that is not this repo's remote", async () => {
|
|
await assert.rejects(
|
|
runGitAction(LANE, "fetch", {
|
|
credentials: { protocol: "https", host: "evil.test", username: "u", password: "p" },
|
|
}),
|
|
{ code: "EBADHOST" }
|
|
);
|
|
assert.equal(seen.length, 0);
|
|
});
|
|
});
|