From 2c807ae1c7a9af99cb895fb56d5d93a52a5adcfc Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Mon, 24 Aug 2026 15:35:07 +0800 Subject: [PATCH] fix(tgit): run gf auth commands from a neutral cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gf auth whoami` / `gf auth login` inspect the current git repository's `origin` remote and scope the authentication check to that remote's host. When `teamai` runs inside a repo whose origin is not git.woa.com (e.g. a GitHub mirror), gf reports "not logged in" even though a valid git.woa.com token exists — so `teamai init` wrongly re-triggers interactive login and fails with "gf auth login failed" in the very repo teamai itself lives in. Run the three auth calls (gfIsAuthenticated / gfAuthWhoami / gfAuthLogin) from os.tmpdir(), which is not a git repo, so the host-scoped credential is found reliably regardless of where teamai was invoked. Verified end-to-end: `teamai init` now authenticates and completes inside a repo whose origin is github.com (previously reproduced the failure). Co-Authored-By: Claude Fable 5 --- src/__tests__/gf-cli.test.ts | 65 +++++++++++++++++++++++++++++++++++- src/providers/tgit/gf-cli.ts | 21 ++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/__tests__/gf-cli.test.ts b/src/__tests__/gf-cli.test.ts index c8a8f9ad..18e242b5 100644 --- a/src/__tests__/gf-cli.test.ts +++ b/src/__tests__/gf-cli.test.ts @@ -41,9 +41,16 @@ vi.mock('node:fs', () => ({ }, })); +import os from 'node:os'; import { spawnSync } from 'node:child_process'; import { execSync } from 'node:child_process'; -import { gfGetOAuthToken, gfMrCreate } from '../providers/tgit/gf-cli.js'; +import { + gfGetOAuthToken, + gfMrCreate, + gfAuthWhoami, + gfIsAuthenticated, + gfAuthLogin, +} from '../providers/tgit/gf-cli.js'; describe('gfGetOAuthToken', () => { beforeEach(() => { @@ -219,3 +226,59 @@ describe('gfMrCreate', () => { ).toThrow('gf mr create failed: auth required'); }); }); + +describe('gf auth commands run from a neutral cwd', () => { + const mockSpawnSync = vi.mocked(spawnSync); + const mockExecSync = vi.mocked(execSync); + + beforeEach(() => { + vi.clearAllMocks(); + // Make getGfPath() find gf in PATH + mockExecSync.mockImplementation((cmd: string) => { + if (cmd.includes('test -x')) throw new Error('not found'); + if (cmd === 'which gf') return '/usr/bin/gf' as any; + throw new Error('unexpected'); + }); + }); + + // gf `auth` commands inspect the current git repo's origin remote and scope + // the auth check to that host. Running them from the repo cwd wrongly reports + // "not logged in" when origin is a non-git.woa.com mirror (e.g. GitHub). They + // must run from a neutral, non-git directory (os.tmpdir()) so the host-scoped + // credential is found regardless of where teamai was invoked. + it('gfAuthWhoami spawns with cwd = os.tmpdir()', () => { + mockSpawnSync.mockReturnValue({ + stdout: '当前登录用户:jeffyxu', + stderr: '', + status: 0, + } as any); + + expect(gfAuthWhoami()).toBe('jeffyxu'); + + const opts = mockSpawnSync.mock.calls[0][2] as { cwd?: string }; + expect(opts.cwd).toBe(os.tmpdir()); + }); + + it('gfIsAuthenticated spawns with cwd = os.tmpdir()', () => { + mockSpawnSync.mockReturnValue({ + stdout: '当前登录用户:jeffyxu', + stderr: '', + status: 0, + } as any); + + expect(gfIsAuthenticated()).toBe(true); + + const opts = mockSpawnSync.mock.calls[0][2] as { cwd?: string }; + expect(opts.cwd).toBe(os.tmpdir()); + }); + + it('gfAuthLogin spawns with cwd = os.tmpdir() and inherited stdio', () => { + mockSpawnSync.mockReturnValue({ status: 0 } as any); + + gfAuthLogin(); + + const opts = mockSpawnSync.mock.calls[0][2] as { cwd?: string; stdio?: string }; + expect(opts.cwd).toBe(os.tmpdir()); + expect(opts.stdio).toBe('inherit'); + }); +}); diff --git a/src/providers/tgit/gf-cli.ts b/src/providers/tgit/gf-cli.ts index 2aa90656..cfdf01ff 100644 --- a/src/providers/tgit/gf-cli.ts +++ b/src/providers/tgit/gf-cli.ts @@ -171,12 +171,27 @@ export async function ensureGfInstalled(): Promise { // ─── Authentication ────────────────────────────────────── +/** + * A neutral working directory for `gf auth` commands. + * + * `gf auth whoami` / `gf auth login` inspect the *current* git repository's + * `origin` remote and scope the authentication check to that remote's host. + * When teamai runs inside a repo whose origin is not git.woa.com (e.g. a GitHub + * mirror), gf reports "not logged in" even though a valid git.woa.com token + * exists — so teamai wrongly re-triggers interactive login and fails. + * + * Running these commands from the system temp dir (which is not a git repo) + * removes the cwd dependency, so the host-scoped credential is found reliably + * regardless of where teamai was invoked. + */ +const AUTH_CWD = os.tmpdir(); + /** * Check if gf is authenticated. Returns true if `gf auth whoami` succeeds. */ export function gfIsAuthenticated(): boolean { try { - const result = gfExec(['auth', 'whoami']); + const result = gfExec(['auth', 'whoami'], { cwd: AUTH_CWD }); return result.status === 0 && result.stdout.includes('当前登录用户'); } catch { return false; @@ -189,7 +204,7 @@ export function gfIsAuthenticated(): boolean { */ export function gfAuthWhoami(): string | null { try { - const result = gfExec(['auth', 'whoami']); + const result = gfExec(['auth', 'whoami'], { cwd: AUTH_CWD }); if (result.status !== 0) return null; // Parse "当前登录用户:" or similar @@ -207,7 +222,7 @@ export function gfAuthWhoami(): string | null { */ export function gfAuthLogin(): void { log.info('Starting gf authentication...'); - const result = gfExec(['auth', 'login'], { inheritStdio: true }); + const result = gfExec(['auth', 'login'], { inheritStdio: true, cwd: AUTH_CWD }); if (result.status !== 0) { throw new Error('gf auth login failed. Please try again.'); }