fix(git): pin simple-git to project path for session discovery

simple-git without baseDir inherits process.cwd(), so launching
OpenChamber from a neutral directory (e.g. $HOME) and opening a git
project elsewhere produced repeated "not a git repository" status
errors and could abort project/session enumeration. Always require an
explicit baseDir, soft-handle non-repo GitErrors on status/check
routes, and cover non-git, foreign-cwd, and nested-repo cases.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-31 09:53:09 +00:00
co-authored by Serhii Dziupin
parent 09f0c64839
commit d839c8b0f8
5 changed files with 239 additions and 26 deletions
+74 -1
View File
@@ -3,11 +3,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
const gitLibraries = {
stageFiles: vi.fn(),
unstageFiles: vi.fn(),
isGitRepository: vi.fn(),
getStatus: vi.fn(),
};
vi.mock('./index.js', () => ({
stageFiles: gitLibraries.stageFiles,
unstageFiles: gitLibraries.unstageFiles,
isGitRepository: gitLibraries.isGitRepository,
getStatus: gitLibraries.getStatus,
}));
const { registerGitRoutes } = await import('./routes.js');
@@ -47,7 +51,6 @@ const createMockResponse = () => {
},
json(payload) {
body = payload;
return this;
},
get statusCode() {
return statusCode;
@@ -62,6 +65,8 @@ describe('git routes index mutations', () => {
beforeEach(() => {
gitLibraries.stageFiles.mockReset();
gitLibraries.unstageFiles.mockReset();
gitLibraries.isGitRepository.mockReset();
gitLibraries.getStatus.mockReset();
});
it('accepts legacy stage path payloads', async () => {
@@ -135,3 +140,71 @@ describe('git routes index mutations', () => {
expect(gitLibraries.stageFiles).not.toHaveBeenCalled();
});
});
describe('git routes status discovery', () => {
beforeEach(() => {
gitLibraries.isGitRepository.mockReset();
gitLibraries.getStatus.mockReset();
});
it('returns a soft non-repo payload for non-git folders', async () => {
gitLibraries.isGitRepository.mockResolvedValue(false);
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('GET', '/api/git/status')(
{ query: { directory: '/tmp/not-a-repo' } },
response,
);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({
isGitRepository: false,
files: [],
branch: null,
ahead: 0,
behind: 0,
});
expect(gitLibraries.getStatus).not.toHaveBeenCalled();
});
it('does not abort when getStatus throws a non-repo GitError', async () => {
gitLibraries.isGitRepository.mockResolvedValue(true);
gitLibraries.getStatus.mockRejectedValue(
Object.assign(new Error('fatal: not a git repository (or any of the parent directories): .git'), {
task: { commands: ['status'] },
}),
);
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('GET', '/api/git/status')(
{ query: { directory: '/opened/project' } },
response,
);
expect(response.statusCode).toBe(200);
expect(response.body).toMatchObject({ isGitRepository: false });
expect(gitLibraries.getStatus).toHaveBeenCalledWith('/opened/project', { mode: undefined });
});
it('uses the opened project path from query arrays without falling back to cwd', async () => {
gitLibraries.isGitRepository.mockResolvedValue(true);
gitLibraries.getStatus.mockResolvedValue({ current: 'main', files: [], isClean: true, ahead: 0, behind: 0 });
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('GET', '/api/git/status')(
{ query: { directory: ['/opened/git-project', '/ignored'] } },
response,
);
expect(response.statusCode).toBe(200);
expect(gitLibraries.isGitRepository).toHaveBeenCalledWith('/opened/git-project');
expect(gitLibraries.getStatus).toHaveBeenCalledWith('/opened/git-project', { mode: undefined });
expect(response.body).toMatchObject({ current: 'main' });
});
});