test(server): make clone-root assertions GITNEXUS_HOME-aware; cover server-mapping fallback

Addresses PR review (#2229):

- git-clone.test.ts hardcoded path.join(os.homedir(), '.gitnexus', 'repos')
  for the clone root, so the "direct child of the clone root" and containment
  assertions failed when GITNEXUS_HOME was set in the ambient env (e.g. a CI
  runner). CLONE_ROOT now derives from getGlobalDir(); mirror that derivation
  via EXPECTED_CLONE_ROOT (GITNEXUS_HOME || ~/.gitnexus), computed at module
  load — the same point CLONE_ROOT is frozen — so the two always agree.

- The GITNEXUS_HOME-unset fallback test covered clone + upload roots but not
  server-mapping (one of the three changed modules). Add a fallback case for
  readServerMapping. It redirects HOME/USERPROFILE to a tmp dir (os.homedir()
  honors them) so it exercises the real ~/.gitnexus fallback without writing
  into the developer's actual ~/.gitnexus/server-mapping.json.

Both files pass with and without GITNEXUS_HOME set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Borozdenets Ilya 2026-06-17 11:36:26 +03:00
parent 1bbc685e18
commit 06f8888c7b
2 changed files with 43 additions and 6 deletions

View file

@ -32,6 +32,15 @@ import fs from 'node:fs/promises';
import { spawn } from 'node:child_process';
import { getRemoteOriginUrl } from '../../src/storage/git.js';
// CLONE_ROOT now derives from getGlobalDir() (GITNEXUS_HOME || ~/.gitnexus), so
// assertions must mirror that derivation rather than hardcoding ~/.gitnexus —
// otherwise an ambient GITNEXUS_HOME (e.g. a CI runner that sets it) makes the
// "direct child of the clone root" assertions fail. Computed at module load,
// the same point CLONE_ROOT is frozen, so the two always agree.
const EXPECTED_CLONE_ROOT = path.resolve(
path.join(process.env.GITNEXUS_HOME || path.join(os.homedir(), '.gitnexus'), 'repos'),
);
describe('git-clone', () => {
describe('extractRepoName', () => {
it('extracts name from HTTPS URL', () => {
@ -112,11 +121,9 @@ describe('git-clone', () => {
});
describe('getCloneDir', () => {
it('returns path under ~/.gitnexus/repos/', () => {
it('returns path under the clone root (getGlobalDir()/repos/)', () => {
const dir = getCloneDir('my-repo');
expect(dir).toContain('.gitnexus');
expect(dir).toMatch(/repos/);
expect(dir).toContain('my-repo');
expect(dir).toBe(path.join(EXPECTED_CLONE_ROOT, 'my-repo'));
});
it('rejects ".." to prevent path-traversal escape from the clone root', () => {
@ -131,7 +138,7 @@ describe('git-clone', () => {
});
it('returned path is always a direct child of the clone root', () => {
const cloneRoot = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
const cloneRoot = EXPECTED_CLONE_ROOT;
const dir = getCloneDir('my-repo');
const rel = path.relative(cloneRoot, path.resolve(dir));
// path.relative from the parent to the child must be just the child name —
@ -476,7 +483,7 @@ describe('git-clone', () => {
//
// These tests do NOT mock spawn — the barrier throws synchronously
// before git is invoked, so the rejection is observable directly.
const cloneRoot = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
const cloneRoot = EXPECTED_CLONE_ROOT;
it('rejects an absolute target outside CLONE_ROOT', async () => {
await expect(cloneOrPull('https://github.com/a/b.git', '/etc/passwd')).rejects.toThrow(

View file

@ -67,4 +67,34 @@ describe('GITNEXUS_HOME path roots', () => {
);
expect(UPLOAD_ROOT).toBe(path.resolve(os.homedir(), '.gitnexus', 'uploads'));
});
it('server-mapping falls back to ~/.gitnexus when GITNEXUS_HOME is unset', async () => {
// os.homedir() honors $HOME (and $USERPROFILE on Windows), so redirect the
// home dir to a tmp path to exercise the real fallback (getGlobalDir() ->
// ~/.gitnexus) without writing into the developer's actual
// ~/.gitnexus/server-mapping.json.
const fakeHome = path.join(os.tmpdir(), 'gitnexus-fallback-home');
const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE;
delete process.env.GITNEXUS_HOME;
process.env.HOME = fakeHome;
process.env.USERPROFILE = fakeHome;
try {
await fs.mkdir(path.join(fakeHome, '.gitnexus'), { recursive: true });
await fs.writeFile(
path.join(fakeHome, '.gitnexus', 'server-mapping.json'),
JSON.stringify({ 'my-repo': 'fallback-service' }),
'utf-8',
);
vi.resetModules();
const { readServerMapping } = await import('../../src/core/embeddings/server-mapping.js');
expect(await readServerMapping('my-repo')).toBe('fallback-service');
} finally {
if (savedHome === undefined) delete process.env.HOME;
else process.env.HOME = savedHome;
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = savedUserProfile;
await fs.rm(fakeHome, { recursive: true, force: true });
}
});
});