Files
openchamber/packages/web/server/lib/git/routes.test.js
T
Cursor AgentandSerhii Dziupin d839c8b0f8 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>
2026-07-31 09:53:09 +00:00

211 lines
6.2 KiB
JavaScript

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');
const createRouteRegistry = () => {
const routes = new Map();
return {
app: {
get(routePath, handler) {
routes.set(`GET ${routePath}`, handler);
},
post(routePath, handler) {
routes.set(`POST ${routePath}`, handler);
},
put(routePath, handler) {
routes.set(`PUT ${routePath}`, handler);
},
delete(routePath, handler) {
routes.set(`DELETE ${routePath}`, handler);
},
},
getRoute(method, routePath) {
return routes.get(`${method} ${routePath}`);
},
};
};
const createMockResponse = () => {
let statusCode = 200;
let body = null;
return {
status(code) {
statusCode = code;
return this;
},
json(payload) {
body = payload;
},
get statusCode() {
return statusCode;
},
get body() {
return body;
},
};
};
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 () => {
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('POST', '/api/git/stage')(
{ query: { directory: '/repo' }, body: { path: 'a.ts' } },
response,
);
expect(response.statusCode).toBe(200);
expect(gitLibraries.stageFiles).toHaveBeenCalledWith('/repo', ['a.ts']);
});
it('accepts bulk stage paths payloads', async () => {
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('POST', '/api/git/stage')(
{ query: { directory: '/repo' }, body: { paths: ['a.ts', 'b.ts'] } },
response,
);
expect(response.statusCode).toBe(200);
expect(gitLibraries.stageFiles).toHaveBeenCalledWith('/repo', ['a.ts', 'b.ts']);
});
it('accepts legacy unstage path payloads', async () => {
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('POST', '/api/git/unstage')(
{ query: { directory: '/repo' }, body: { path: 'a.ts' } },
response,
);
expect(response.statusCode).toBe(200);
expect(gitLibraries.unstageFiles).toHaveBeenCalledWith('/repo', ['a.ts']);
});
it('accepts bulk unstage paths payloads', async () => {
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('POST', '/api/git/unstage')(
{ query: { directory: '/repo' }, body: { paths: ['a.ts', 'b.ts'] } },
response,
);
expect(response.statusCode).toBe(200);
expect(gitLibraries.unstageFiles).toHaveBeenCalledWith('/repo', ['a.ts', 'b.ts']);
});
it('rejects invalid path payloads before calling git', async () => {
const { app, getRoute } = createRouteRegistry();
registerGitRoutes(app);
const response = createMockResponse();
await getRoute('POST', '/api/git/stage')(
{ query: { directory: '/repo' }, body: { paths: [' ', null] } },
response,
);
expect(response.statusCode).toBe(400);
expect(response.body).toEqual({ error: 'path parameter is required' });
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' });
});
});