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>
211 lines
6.2 KiB
JavaScript
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' });
|
|
});
|
|
});
|