
























@@ -0,0 +1,223 @@
1+import { describe, expect, it } from "vitest";
2+import {
3+detectGhConfigDirMismatch,
4+formatGhConfigDirMismatchHint,
5+type GhConfigDirMismatch,
6+type GhConfigDiscoveryInput,
7+} from "./gh-config-discovery.js";
8+9+function makeInput(overrides: Partial<GhConfigDiscoveryInput>): GhConfigDiscoveryInput {
10+return {
11+platform: "linux",
12+env: {},
13+fileExists: () => false,
14+ ...overrides,
15+};
16+}
17+18+function fileSet(...paths: readonly string[]): (absolutePath: string) => boolean {
19+const set = new Set(paths);
20+return (absolutePath) => set.has(absolutePath);
21+}
22+23+describe("detectGhConfigDirMismatch", () => {
24+it("returns 'explicit-gh-config-dir-set' when GH_CONFIG_DIR is already set", () => {
25+const result = detectGhConfigDirMismatch(
26+makeInput({
27+env: { HOME: "/agent/home", GH_CONFIG_DIR: "/etc/openclaw/gh" },
28+}),
29+);
30+expect(result).toEqual({ kind: "explicit-gh-config-dir-set", ghConfigDir: "/etc/openclaw/gh" });
31+});
32+33+it("returns 'no-process-home' when HOME and XDG and APPDATA are missing", () => {
34+const result = detectGhConfigDirMismatch(makeInput({ env: {} }));
35+expect(result).toEqual({ kind: "no-process-home" });
36+});
37+38+it("returns 'auth-discoverable' when the effective config dir already has hosts.yml", () => {
39+const result = detectGhConfigDirMismatch(
40+makeInput({
41+env: { HOME: "/agent/home" },
42+fileExists: fileSet("/agent/home/.config/gh/hosts.yml"),
43+}),
44+);
45+expect(result).toEqual({
46+kind: "auth-discoverable",
47+effectiveConfigDir: "/agent/home/.config/gh",
48+});
49+});
50+51+it("flags a mismatch when /root/.config/gh has hosts.yml but the agent HOME does not", () => {
52+const result = detectGhConfigDirMismatch(
53+makeInput({
54+env: { HOME: "/root/.openclaw/agents/main/agent/codex-home/home" },
55+fileExists: fileSet("/root/.config/gh/hosts.yml"),
56+}),
57+);
58+expect(result).toMatchObject({
59+kind: "mismatch",
60+effectiveConfigDir: "/root/.openclaw/agents/main/agent/codex-home/home/.config/gh",
61+alternateConfigDir: "/root/.config/gh",
62+alternateHostsFile: "/root/.config/gh/hosts.yml",
63+alternateHomeHint: "/root",
64+suggestedEnvValue: "/root/.config/gh",
65+});
66+});
67+68+it("uses SUDO_USER home as a candidate when set", () => {
69+const result = detectGhConfigDirMismatch(
70+makeInput({
71+env: { HOME: "/var/lib/openclaw/agent", SUDO_USER: "alice" },
72+fileExists: fileSet("/home/alice/.config/gh/hosts.yml"),
73+}),
74+);
75+expect(result).toMatchObject({
76+kind: "mismatch",
77+alternateConfigDir: "/home/alice/.config/gh",
78+alternateHomeHint: "/home/alice",
79+});
80+});
81+82+it("uses USER home as a fallback candidate when SUDO_USER is missing", () => {
83+const result = detectGhConfigDirMismatch(
84+makeInput({
85+env: { HOME: "/var/lib/openclaw/agent", USER: "ops" },
86+fileExists: fileSet("/home/ops/.config/gh/hosts.yml"),
87+}),
88+);
89+expect(result).toMatchObject({
90+kind: "mismatch",
91+alternateConfigDir: "/home/ops/.config/gh",
92+alternateHomeHint: "/home/ops",
93+});
94+});
95+96+it("ignores USER=root since /root is already part of the default candidate set", () => {
97+const result = detectGhConfigDirMismatch(
98+makeInput({
99+env: { HOME: "/agent/home", USER: "root" },
100+fileExists: fileSet("/root/.config/gh/hosts.yml"),
101+}),
102+);
103+expect(result.kind).toBe("mismatch");
104+});
105+106+it("returns 'no-known-auth' when no candidate has hosts.yml", () => {
107+const result = detectGhConfigDirMismatch(
108+makeInput({
109+env: { HOME: "/agent/home" },
110+fileExists: () => false,
111+}),
112+);
113+expect(result).toEqual({
114+kind: "no-known-auth",
115+effectiveConfigDir: "/agent/home/.config/gh",
116+});
117+});
118+119+it("does not flag a mismatch when the agent HOME equals the operator HOME", () => {
120+const result = detectGhConfigDirMismatch(
121+makeInput({
122+env: { HOME: "/root" },
123+fileExists: fileSet("/root/.config/gh/hosts.yml"),
124+}),
125+);
126+expect(result).toEqual({
127+kind: "auth-discoverable",
128+effectiveConfigDir: "/root/.config/gh",
129+});
130+});
131+132+it("respects XDG_CONFIG_HOME for the effective config dir on Linux", () => {
133+const result = detectGhConfigDirMismatch(
134+makeInput({
135+env: { HOME: "/agent/home", XDG_CONFIG_HOME: "/agent/xdg" },
136+fileExists: fileSet("/agent/xdg/gh/hosts.yml"),
137+}),
138+);
139+expect(result).toEqual({
140+kind: "auth-discoverable",
141+effectiveConfigDir: "/agent/xdg/gh",
142+});
143+});
144+145+it("uses HOME/.config/gh on darwin (matches gh's documented macOS lookup)", () => {
146+const result = detectGhConfigDirMismatch(
147+makeInput({
148+platform: "darwin",
149+env: { HOME: "/Users/agent" },
150+fileExists: fileSet("/Users/operator/.config/gh/hosts.yml"),
151+candidateOperatorHomes: ["/Users/operator"],
152+}),
153+);
154+expect(result).toMatchObject({
155+kind: "mismatch",
156+effectiveConfigDir: "/Users/agent/.config/gh",
157+alternateConfigDir: "/Users/operator/.config/gh",
158+alternateHomeHint: "/Users/operator",
159+});
160+});
161+162+it("uses APPDATA/GitHub CLI on win32", () => {
163+const result = detectGhConfigDirMismatch(
164+makeInput({
165+platform: "win32",
166+env: { APPDATA: "C:\\Users\\agent\\AppData\\Roaming" },
167+fileExists: fileSet("C:\\Users\\agent\\AppData\\Roaming\\GitHub CLI\\hosts.yml"),
168+}),
169+);
170+expect(result).toMatchObject({
171+kind: "auth-discoverable",
172+effectiveConfigDir: "C:\\Users\\agent\\AppData\\Roaming\\GitHub CLI",
173+});
174+});
175+176+it("respects an explicit candidateOperatorHomes list", () => {
177+const result = detectGhConfigDirMismatch(
178+makeInput({
179+env: { HOME: "/agent/home" },
180+fileExists: fileSet("/srv/automation/.config/gh/hosts.yml"),
181+candidateOperatorHomes: ["/srv/automation"],
182+}),
183+);
184+expect(result).toMatchObject({
185+kind: "mismatch",
186+alternateConfigDir: "/srv/automation/.config/gh",
187+alternateHomeHint: "/srv/automation",
188+});
189+});
190+});
191+192+describe("formatGhConfigDirMismatchHint", () => {
193+it("formats the mismatch into operator-actionable lines", () => {
194+const mismatch: GhConfigDirMismatch = {
195+effectiveConfigDir: "/agent/home/.config/gh",
196+alternateConfigDir: "/root/.config/gh",
197+alternateHostsFile: "/root/.config/gh/hosts.yml",
198+alternateHomeHint: "/root",
199+suggestedEnvValue: "/root/.config/gh",
200+};
201+expect(formatGhConfigDirMismatchHint(mismatch)).toEqual([
202+"GitHub CLI auth was found at a different HOME than the one this OpenClaw process uses.",
203+" Process gh config dir: /agent/home/.config/gh",
204+" Authenticated config: /root/.config/gh (contains hosts.yml)",
205+" Authenticated HOME: /root",
206+" Fix: set GH_CONFIG_DIR=/root/.config/gh on the OpenClaw service environment, then restart the gateway.",
207+]);
208+});
209+210+it("omits the home hint line when the alternate has no associated HOME", () => {
211+const mismatch: GhConfigDirMismatch = {
212+effectiveConfigDir: "/agent/home/.config/gh",
213+alternateConfigDir: "/srv/automation/.config/gh",
214+alternateHostsFile: "/srv/automation/.config/gh/hosts.yml",
215+suggestedEnvValue: "/srv/automation/.config/gh",
216+};
217+const lines = formatGhConfigDirMismatchHint(mismatch);
218+expect(lines.some((line) => line.includes("Authenticated HOME"))).toBe(false);
219+expect(lines).toContain(
220+" Fix: set GH_CONFIG_DIR=/srv/automation/.config/gh on the OpenClaw service environment, then restart the gateway.",
221+);
222+});
223+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。