GitNexus/gitnexus/test/unit/run-analyze.test.ts
Gergő Magyar 0418cbb347
Some checks are pending
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
fix(cli): keep GitNexus ignores inside .gitnexus (#1248)
* fix(cli): keep GitNexus ignores inside .gitnexus

Avoid mutating analyzed repositories' root .gitignore while keeping generated GitNexus state untracked via .gitnexus/.gitignore.

Made-with: Cursor

* fix(cli): also use git info exclude for GitNexus storage

When an analyzed repo has a real .git directory, add .gitnexus/ to .git/info/exclude so local Git metadata ignores generated storage without touching root .gitignore.

Made-with: Cursor

* fix(cli): keep skip-git subdir indexes ignored

Ensure full analyze always writes the internal GitNexus ignore file so parent Git repositories stay clean for --skip-git subdirectory indexes.

Made-with: Cursor
2026-05-01 16:46:05 +01:00

138 lines
5.7 KiB
TypeScript

import { execSync } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import { describe, it, expect } from 'vitest';
import { deriveEmbeddingMode } from '../../src/core/embedding-mode.js';
import { getStoragePaths, saveMeta, type RepoMeta } from '../../src/storage/repo-manager.js';
import { createTempDir } from '../helpers/test-db.js';
describe('run-analyze module', () => {
it('exports runFullAnalysis as a function', async () => {
const mod = await import('../../src/core/run-analyze.js');
expect(typeof mod.runFullAnalysis).toBe('function');
});
it('exports PHASE_LABELS', async () => {
const mod = await import('../../src/core/run-analyze.js');
expect(mod.PHASE_LABELS).toBeDefined();
expect(mod.PHASE_LABELS.parsing).toBe('Parsing code');
});
it('creates .gitnexus/.gitignore on the already-up-to-date fast path (#1233)', async () => {
const tmpRepo = await createTempDir('gitnexus-run-analyze-fast-path-');
try {
execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' });
execSync('git -c user.name=test -c user.email=test@test commit --allow-empty -m init', {
cwd: tmpRepo.dbPath,
stdio: 'pipe',
});
const currentCommit = execSync('git rev-parse HEAD', {
cwd: tmpRepo.dbPath,
encoding: 'utf-8',
}).trim();
const { storagePath } = getStoragePaths(tmpRepo.dbPath);
const meta: RepoMeta = {
repoPath: tmpRepo.dbPath,
lastCommit: currentCommit,
indexedAt: new Date().toISOString(),
};
await saveMeta(storagePath, meta);
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
const result = await runFullAnalysis(
tmpRepo.dbPath,
{},
{
onProgress: () => {},
},
);
expect(result.alreadyUpToDate).toBe(true);
await expect(
fs.readFile(path.join(tmpRepo.dbPath, '.gitnexus', '.gitignore'), 'utf-8'),
).resolves.toBe('*\n');
} finally {
await tmpRepo.cleanup();
}
});
});
describe('deriveEmbeddingMode', () => {
// Default `analyze` on a repo with existing embeddings: must preserve, must
// NOT regenerate, must load the cache so phase 3.5 can re-insert vectors.
it('default + existing>0 → preserve only (load cache, no generation)', () => {
const m = deriveEmbeddingMode({}, 1234);
expect(m.preserveExistingEmbeddings).toBe(true);
expect(m.shouldGenerateEmbeddings).toBe(false);
expect(m.forceRegenerateEmbeddings).toBe(false);
expect(m.shouldLoadCache).toBe(true);
});
it('default + existing=0 → no-op (no preserve, no generation, no cache load)', () => {
const m = deriveEmbeddingMode({}, 0);
expect(m.preserveExistingEmbeddings).toBe(false);
expect(m.shouldGenerateEmbeddings).toBe(false);
expect(m.forceRegenerateEmbeddings).toBe(false);
expect(m.shouldLoadCache).toBe(false);
});
// The headline behavior change requested in PR feedback: --force on an
// already-embedded repo must regenerate (top up new/changed nodes), not
// silently downgrade to "preserve only".
it('--force + existing>0 → forceRegenerate + generate + load cache', () => {
const m = deriveEmbeddingMode({ force: true }, 500);
expect(m.forceRegenerateEmbeddings).toBe(true);
expect(m.shouldGenerateEmbeddings).toBe(true);
expect(m.preserveExistingEmbeddings).toBe(false);
expect(m.shouldLoadCache).toBe(true);
});
it('--force + existing=0 → no embedding work (force keeps prior semantics)', () => {
const m = deriveEmbeddingMode({ force: true }, 0);
expect(m.forceRegenerateEmbeddings).toBe(false);
expect(m.shouldGenerateEmbeddings).toBe(false);
expect(m.preserveExistingEmbeddings).toBe(false);
expect(m.shouldLoadCache).toBe(false);
});
it('--embeddings → generate + load cache (incremental top-up)', () => {
const m = deriveEmbeddingMode({ embeddings: true }, 500);
expect(m.shouldGenerateEmbeddings).toBe(true);
expect(m.preserveExistingEmbeddings).toBe(false);
expect(m.shouldLoadCache).toBe(true);
});
it('--embeddings + existing=0 → generate; cache load still fires (harmless empty load)', () => {
const m = deriveEmbeddingMode({ embeddings: true }, 0);
expect(m.shouldGenerateEmbeddings).toBe(true);
// Cache load is gated at the call site by `existingMeta`, not by count;
// when explicit `--embeddings` is set we always attempt the load so any
// stray vectors from a partial prior run get picked up.
expect(m.shouldLoadCache).toBe(true);
});
// --drop-embeddings is the explicit wipe path; it must suppress cache load
// even when --force is also set (the dominant escape hatch).
it('--drop-embeddings → suppresses cache load, no generation', () => {
const m = deriveEmbeddingMode({ dropEmbeddings: true }, 1234);
expect(m.shouldLoadCache).toBe(false);
expect(m.shouldGenerateEmbeddings).toBe(false);
expect(m.preserveExistingEmbeddings).toBe(false);
expect(m.forceRegenerateEmbeddings).toBe(false);
});
it('--force + --drop-embeddings → drop wins (no cache load, no generation)', () => {
const m = deriveEmbeddingMode({ force: true, dropEmbeddings: true }, 1234);
expect(m.shouldLoadCache).toBe(false);
expect(m.shouldGenerateEmbeddings).toBe(false);
expect(m.forceRegenerateEmbeddings).toBe(false);
});
it('--embeddings + --drop-embeddings → drop suppresses cache load (no preservation)', () => {
// --embeddings still generates, but the prior vectors are wiped first.
const m = deriveEmbeddingMode({ embeddings: true, dropEmbeddings: true }, 1234);
expect(m.shouldLoadCache).toBe(false);
expect(m.shouldGenerateEmbeddings).toBe(true);
expect(m.preserveExistingEmbeddings).toBe(false);
});
});