/** * Unit tests for branch-aware `list` and `status` rendering (#2106). * * The data layer (repo-manager / git) is mocked so these assert only the * console rendering: branch indexes appear when present, single-branch output * is unchanged, and `status` reflects the checked-out branch. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import fs from 'fs/promises'; import os from 'os'; import path from 'path'; const { runnerIdentity } = vi.hoisted(() => ({ runnerIdentity: { schemaVersion: 4 as const, runtime: { executablePath: '/usr/bin/node', version: 'v22.0.0', platform: 'linux', architecture: 'x64', modulesAbi: '127', libc: 'glibc:2.39', }, cliVersion: '1.6.9', invokedArtifact: { path: '/opt/gitnexus/dist/cli/index.js', digest: 'sha256:entry' }, build: { kind: 'distribution' as const, rootPath: '/opt/gitnexus/dist', canonicalization: 'gitnexus-analyzer-build-v2' as const, digest: 'sha256:build', }, dependencyRuntime: { manifestPath: '/opt/gitnexus/package.json', lockfilePath: '/opt/package-lock.json', canonicalization: 'gitnexus-analyzer-dependency-runtime-v4' as const, packageCount: 42, artifactCount: 12, digest: 'sha256:dependencies', }, }, })); vi.mock('../../src/storage/repo-manager.js', () => ({ listRegisteredRepos: vi.fn(), getStoragePaths: vi.fn((repoPath: string, branch?: string, resolvedStoragePath?: string) => ({ storagePath: resolvedStoragePath ?? `${repoPath}/.gitnexus`, lbugPath: branch ? `${resolvedStoragePath ?? `${repoPath}/.gitnexus`}/branches/${branch}/lbug` : `${resolvedStoragePath ?? `${repoPath}/.gitnexus`}/lbug`, metaPath: branch ? `${resolvedStoragePath ?? `${repoPath}/.gitnexus`}/branches/${branch}/meta.json` : `${resolvedStoragePath ?? `${repoPath}/.gitnexus`}/meta.json`, })), loadMeta: vi.fn(), hasKuzuIndex: vi.fn().mockResolvedValue(false), readRegistryStrict: vi.fn(), resolveRegistryEntry: vi.fn(), RegistryNotFoundError: class RegistryNotFoundError extends Error {}, RegistryAmbiguousTargetError: class RegistryAmbiguousTargetError extends Error {}, })); vi.mock('../../src/core/analyzer-identity.js', () => ({ resolveAnalyzerRunnerIdentity: vi.fn(() => runnerIdentity), analyzerRunnerIdentitiesEqual: vi.fn( (indexedIdentity: unknown, currentIdentity: unknown) => indexedIdentity === currentIdentity, ), })); vi.mock('../../src/storage/storage-resolver.js', async (importOriginal) => ({ ...(await importOriginal()), requireStoragePath: vi.fn().mockResolvedValue('/repo/.gitnexus'), requireRegisteredStoragePath: vi.fn().mockResolvedValue('/repo/.gitnexus'), })); vi.mock('../../src/storage/git.js', () => ({ isGitRepo: vi.fn().mockReturnValue(true), getCurrentCommit: vi.fn().mockReturnValue('headsha0'), getCurrentBranch: vi.fn().mockReturnValue('main'), getGitRoot: vi.fn().mockReturnValue('/repo'), isWorkingTreeDirty: vi.fn().mockReturnValue(false), listWorkingTreeDirtyPaths: vi.fn().mockReturnValue([]), })); import { listCommand } from '../../src/cli/list.js'; import { statusCommand } from '../../src/cli/status.js'; import { listRegisteredRepos, loadMeta, readRegistryStrict, resolveRegistryEntry, } from '../../src/storage/repo-manager.js'; import { getCurrentBranch, getCurrentCommit, isWorkingTreeDirty } from '../../src/storage/git.js'; import { requireRegisteredStoragePath, requireStoragePath, STATUS_STORAGE_REQUIREMENTS, StorageRequirementError, } from '../../src/storage/storage-resolver.js'; let logSpy: ReturnType; const output = () => logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); beforeEach(() => { vi.clearAllMocks(); logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); (loadMeta as any).mockResolvedValue({ repoPath: '/repo', lastCommit: 'headsha0', indexedAt: '2026-06-10T12:00:00.000Z', branch: 'main', runnerIdentity, scopeExtractionReceipt: 1 as const, }); }); describe('list branch rendering (#2106)', () => { it('renders the primary branch and a Branch indexes section', async () => { (listRegisteredRepos as any).mockResolvedValue([ { name: 'multi', path: '/repo', storagePath: '/repo/.gitnexus', indexedAt: '2026-06-10T12:00:00.000Z', lastCommit: 'aaaaaaa1', branch: 'main', branches: [ { branch: 'feature/x', indexedAt: '2026-06-10T13:00:00.000Z', lastCommit: 'bbbbbbb2' }, ], stats: { files: 1, nodes: 2, edges: 3 }, }, ]); await listCommand(); const out = output(); expect(out).toContain('Branch: main'); expect(out).toContain('Branch indexes:'); expect(out).toContain('feature/x'); expect(out).toContain('bbbbbbb'); // 7-char short commit of bbbbbbb2 }); it('single-branch entry renders no branch lines (unchanged)', async () => { (listRegisteredRepos as any).mockResolvedValue([ { name: 'solo', path: '/solo', storagePath: '/solo/.gitnexus', indexedAt: '2026-06-10T12:00:00.000Z', lastCommit: 'ccccccc3', stats: { files: 1, nodes: 1, edges: 1 }, }, ]); await listCommand(); const out = output(); expect(out).not.toContain('Branch:'); expect(out).not.toContain('Branch indexes:'); }); }); describe('status branch rendering (#2106)', () => { const baseRepo = { repoPath: '/repo', storagePath: '/repo/.gitnexus', lbugPath: '/repo/.gitnexus/lbug', metaPath: '/repo/.gitnexus/meta.json', meta: { repoPath: '/repo', lastCommit: 'headsha0', indexedAt: '2026-06-10T12:00:00.000Z', branch: 'main', runnerIdentity, scopeExtractionReceipt: 1 as const, }, }; it.each([false, true])('uses the strict registry reader for status --repo%s', async (json) => { const corrupt = new Error('registry is corrupt: entry 0 is malformed'); vi.mocked(readRegistryStrict).mockRejectedValueOnce(corrupt); await expect(statusCommand({ repo: 'demo', json })).rejects.toBe(corrupt); expect(resolveRegistryEntry).not.toHaveBeenCalled(); }); it('resolves the Git root before checking the default storage path', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); await statusCommand({ json: true }); expect(requireStoragePath).toHaveBeenCalledWith('/repo', STATUS_STORAGE_REQUIREMENTS); }); it('checks the exact registry storage path for status --repo', async () => { const entry = { path: '/repo', storagePath: '/external/repo-slot' }; (readRegistryStrict as any).mockResolvedValue([entry]); (resolveRegistryEntry as any).mockReturnValue(entry); (requireRegisteredStoragePath as any).mockResolvedValue(entry.storagePath); (loadMeta as any).mockResolvedValue({ ...baseRepo.meta, repoPath: entry.path }); await statusCommand({ repo: 'repo', json: true }); expect(requireRegisteredStoragePath).toHaveBeenCalledWith(entry, STATUS_STORAGE_REQUIREMENTS); expect(JSON.parse(output())).toMatchObject({ storagePath: entry.storagePath }); }); it.each(['symbol', 'none'] as const)( 'status --repo reports source-unavailable when checkout exists but contentRetention is %s', async (contentRetention) => { const checkout = await fs.mkdtemp(path.join(os.tmpdir(), 'gnx-status-src-')); try { const entry = { path: checkout, storagePath: '/external/repo-slot' }; (readRegistryStrict as any).mockResolvedValue([entry]); (resolveRegistryEntry as any).mockReturnValue(entry); (requireRegisteredStoragePath as any).mockResolvedValue(entry.storagePath); (loadMeta as any).mockResolvedValue({ ...baseRepo.meta, repoPath: entry.path, contentRetention, }); await statusCommand({ repo: 'repo', json: true }); expect(JSON.parse(output())).toMatchObject({ sourceAvailable: false, status: 'source-unavailable', index: { contentRetention }, }); } finally { await fs.rm(checkout, { recursive: true, force: true }); } }, ); it('status --repo reports sourceAvailable when checkout exists and retention is full', async () => { const checkout = await fs.mkdtemp(path.join(os.tmpdir(), 'gnx-status-full-')); try { const entry = { path: checkout, storagePath: '/external/repo-slot' }; (readRegistryStrict as any).mockResolvedValue([entry]); (resolveRegistryEntry as any).mockReturnValue(entry); (requireRegisteredStoragePath as any).mockResolvedValue(entry.storagePath); (loadMeta as any).mockResolvedValue({ ...baseRepo.meta, repoPath: entry.path, contentRetention: 'full', }); await statusCommand({ repo: 'repo', json: true }); expect(JSON.parse(output())).toMatchObject({ sourceAvailable: true, status: 'registered', }); } finally { await fs.rm(checkout, { recursive: true, force: true }); } }); it('rejects a foreign registered storage path instead of treating it as an index', async () => { const entry = { path: '/repo', storagePath: '/external/repo-slot' }; const inspection = { repoPath: entry.path, storagePath: entry.storagePath, state: 'foreign' as const, hasCodeIndexDB: true, }; (readRegistryStrict as any).mockResolvedValue([entry]); (resolveRegistryEntry as any).mockReturnValue(entry); (requireRegisteredStoragePath as any).mockRejectedValue( new StorageRequirementError(inspection, STATUS_STORAGE_REQUIREMENTS), ); await expect(statusCommand({ repo: 'repo', json: true })).rejects.toBeInstanceOf( StorageRequirementError, ); }); it('renders indexed and current typed runner receipts for exact comparison', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); await statusCommand(); const out = output(); expect(out).toContain(`Indexed analyzer runner identity: ${JSON.stringify(runnerIdentity)}`); expect(out).toContain(`Current analyzer runner identity: ${JSON.stringify(runnerIdentity)}`); }); it('renders stable machine-readable provenance with --json', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); await statusCommand({ json: true }); const parsed = JSON.parse(output()); expect(parsed).toMatchObject({ schemaVersion: 1, repository: '/repo', index: { commit: 'headsha0', runnerIdentity, runnerIdentityStatus: 'current' }, current: { commit: 'headsha0', runnerIdentity }, status: 'up-to-date', }); }); it('reports a dirty working tree as stale in --json even when the commit matches', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); (isWorkingTreeDirty as any).mockReturnValueOnce(true); await statusCommand({ json: true }); expect(JSON.parse(output())).toMatchObject({ index: { commit: 'headsha0' }, current: { commit: 'headsha0' }, status: 'stale', }); }); it('reports a dirty working tree as stale in the human output at a matching commit', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); (isWorkingTreeDirty as any).mockReturnValueOnce(true); await statusCommand(); expect(output()).not.toContain('up-to-date'); }); it('never certifies dirty or checkpointed metadata and reports stable incomplete reasons', async () => { (loadMeta as any).mockResolvedValue({ ...baseRepo.meta, incrementalInProgress: { startedAt: 1, toWriteCount: 2 }, embeddingCheckpoint: { at: '2026-07-18T00:00:00.000Z', nodesProcessed: 1, totalNodes: 2, chunksProcessed: 1, model: 'fixture', dimensions: 3, provider: 'local', }, }); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); await statusCommand({ json: true }); expect(JSON.parse(output())).toMatchObject({ index: { incompleteReasons: ['incremental-in-progress', 'embedding-checkpoint-pending'], runnerIdentityStatus: 'current', }, status: 'stale', }); }); it('treats an older runner receipt schema as stale at the same commit', async () => { (loadMeta as any).mockResolvedValue({ ...baseRepo.meta, runnerIdentity: { ...runnerIdentity, schemaVersion: 1 }, }); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); await statusCommand({ json: true }); expect(JSON.parse(output())).toMatchObject({ index: { runnerIdentityStatus: 'stale-or-unknown' }, status: 'stale', }); }); it('shows the current branch and up-to-date on the primary', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); (getCurrentBranch as any).mockReturnValue('main'); (getCurrentCommit as any).mockReturnValue('headsha0'); await statusCommand(); const out = output(); expect(out).toContain('Branch: main'); expect(out).toContain('up-to-date'); }); it('falls through to the workspace index when the branch has no pinned index (#2354)', async () => { (loadMeta as any).mockResolvedValueOnce(baseRepo.meta).mockResolvedValueOnce(null); // feature/y has no pinned index (getCurrentBranch as any).mockReturnValue('feature/y'); (getCurrentCommit as any).mockReturnValue('headsha9'); await statusCommand(); const out = output(); expect(out).toContain('Branch: feature/y'); // The flat workspace index (last analyzed on main) is reported, with the // commit comparison saying it lags this branch's tree. expect(out).toContain("Workspace index: last analyzed on 'main'"); expect(out).toContain('stale'); }); it('same-commit branch flip reports up-to-date against the workspace index (#2354)', async () => { (loadMeta as any).mockResolvedValueOnce(baseRepo.meta).mockResolvedValueOnce(null); // feature/y has no pinned index (getCurrentBranch as any).mockReturnValue('feature/y'); (getCurrentCommit as any).mockReturnValue('headsha0'); // same commit as flat meta await statusCommand(); const out = output(); expect(out).toContain("Workspace index: last analyzed on 'main'"); expect(out).toContain('up-to-date'); }); it('compares against the branch index when the current branch has one', async () => { (loadMeta as any).mockResolvedValueOnce(baseRepo.meta).mockResolvedValueOnce({ repoPath: '/repo', lastCommit: 'zzzzsha0', indexedAt: '2026-06-10T14:00:00.000Z', branch: 'feature/z', runnerIdentity, scopeExtractionReceipt: 1, }); (getCurrentBranch as any).mockReturnValue('feature/z'); (getCurrentCommit as any).mockReturnValue('zzzzsha0'); await statusCommand(); const out = output(); expect(out).toContain('Branch: feature/z'); expect(out).toContain('up-to-date'); }); it('shows detached HEAD and compares against the flat index', async () => { (loadMeta as any).mockResolvedValue(baseRepo.meta); (getCurrentBranch as any).mockReturnValue(null); // detached (getCurrentCommit as any).mockReturnValue('headsha0'); await statusCommand(); const out = output(); expect(out).toContain('(detached HEAD)'); expect(out).toContain('up-to-date'); }); it('reports stale when the branch index is behind the branch tip', async () => { (loadMeta as any).mockResolvedValueOnce(baseRepo.meta).mockResolvedValueOnce({ repoPath: '/repo', lastCommit: 'oldsha00', indexedAt: '2026-06-10T14:00:00.000Z', branch: 'feature/z', }); (getCurrentBranch as any).mockReturnValue('feature/z'); (getCurrentCommit as any).mockReturnValue('newsha99'); // moved past the index await statusCommand(); const out = output(); expect(out).toContain('Branch: feature/z'); expect(out).toContain('stale'); }); });