Files
bot-hermes 66edc74fac feat(web,ui): per-project git provider API base URL overrides
Per provider (github|gitlab|gitea) a project override stored in
~/.config/openchamber/projects/<projectId>.json under gitProviders wins
over the global settings.json value (precedence: project override > global
> built-in default). Server forge routes resolve the override per request
directory (worktree-aware via git-common-dir + containment + path fallback,
60s TTL cache); the override host is also accepted for remote parsing and
client detection. New GET/PUT /api/projects/:projectId/git-providers route;
client openchamberConfig preserves the server-owned gitProviders key;
Projects page gains a Git provider API base URLs section; detection store
hydrates per-project overrides (memory-only, server-authoritative).
2026-08-17 09:57:54 +00:00

84 lines
3.0 KiB
JavaScript

import { beforeEach, describe, expect, test, vi } from 'vitest';
// getOctokitOrNull reads auth + config modules; mock them all so the base URL
// resolution can be asserted without a real token, data dir, or Octokit client.
const mockState = vi.hoisted(() => ({
octokitConfigs: [],
getGitHubAuth: vi.fn(),
isGhCliActive: vi.fn(),
isGhCliDisabled: vi.fn(),
getGhCliToken: vi.fn(),
getProviderApiBaseUrl: vi.fn(),
getEffectiveProviderApiBaseUrl: vi.fn(),
}));
vi.mock('@octokit/rest', () => ({
Octokit: class {
constructor(config) {
mockState.octokitConfigs.push(config);
}
},
}));
vi.mock('./auth.js', () => ({
getGitHubAuth: mockState.getGitHubAuth,
isGhCliActive: mockState.isGhCliActive,
isGhCliDisabled: mockState.isGhCliDisabled,
}));
vi.mock('./gh-cli-credential.js', () => ({
getGhCliToken: mockState.getGhCliToken,
}));
vi.mock('../git-providers/config.js', () => ({
getProviderApiBaseUrl: mockState.getProviderApiBaseUrl,
}));
vi.mock('../git-providers/project-config.js', () => ({
getEffectiveProviderApiBaseUrl: mockState.getEffectiveProviderApiBaseUrl,
}));
const { getOctokitOrNull } = await import('./octokit.js');
beforeEach(() => {
mockState.octokitConfigs.length = 0;
mockState.getGitHubAuth.mockReset();
mockState.isGhCliActive.mockReset().mockReturnValue(false);
mockState.isGhCliDisabled.mockReset().mockReturnValue(false);
mockState.getGhCliToken.mockReset().mockReturnValue(null);
mockState.getProviderApiBaseUrl.mockReset();
mockState.getEffectiveProviderApiBaseUrl.mockReset();
});
describe('getOctokitOrNull base URL resolution', () => {
test('uses the global base URL without a directory and never consults project overrides', () => {
mockState.getGitHubAuth.mockReturnValue({ accessToken: 'ghp-test' });
mockState.getProviderApiBaseUrl.mockReturnValue('https://api.github.com');
const octokit = getOctokitOrNull();
expect(octokit).not.toBeNull();
expect(mockState.octokitConfigs).toHaveLength(1);
expect(mockState.octokitConfigs[0].auth).toBe('ghp-test');
expect(mockState.octokitConfigs[0].baseUrl).toBe('https://api.github.com');
expect(mockState.getEffectiveProviderApiBaseUrl).not.toHaveBeenCalled();
});
test('resolves the per-project override base URL for a directory', () => {
mockState.getGitHubAuth.mockReturnValue({ accessToken: 'ghp-test' });
mockState.getEffectiveProviderApiBaseUrl.mockReturnValue('https://github.enterprise.example');
const octokit = getOctokitOrNull('/work/override-project');
expect(octokit).not.toBeNull();
expect(mockState.getEffectiveProviderApiBaseUrl).toHaveBeenCalledWith('github', '/work/override-project');
expect(mockState.octokitConfigs[0].baseUrl).toBe('https://github.enterprise.example');
});
test('returns null without a token', () => {
mockState.getGitHubAuth.mockReturnValue(null);
expect(getOctokitOrNull('/work/override-project')).toBeNull();
expect(mockState.octokitConfigs).toHaveLength(0);
});
});