import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { makeLocalGitRepo } from '../../../test/make-local-git-repo.js'; import { fetchLookmlRepo } from '../../../../../src/context/ingest/adapters/lookml/fetch.js'; import type { LookmlPullConfig } from '../../../../../src/context/ingest/adapters/lookml/pull-config.js'; const FIXTURE_ROOT = join(__dirname, '../../../../fixtures/lookml'); function pullConfig(overrides: Partial & Pick): LookmlPullConfig { return { branch: 'main', path: null, authToken: null, expectedLookerConnectionName: null, parsedTargetTables: {}, ...overrides, }; } describe('fetchLookmlRepo', () => { let tmpRoot: string; beforeEach(async () => { tmpRoot = await mkdtemp(join(tmpdir(), 'fetch-lookml-')); }); afterEach(async () => rm(tmpRoot, { recursive: true, force: true })); it('clones a local file:// repo and materializes only .lkml/.lookml files into stagedDir', async () => { const repo = await makeLocalGitRepo(join(FIXTURE_ROOT, 'single-model'), join(tmpRoot, 'origin')); // Add a non-LookML file to prove we filter it out. await repo.writeFile('README.md', '# readme\n'); await repo.commit('add readme'); const stagedDir = join(tmpRoot, 'staged'); const cacheDir = join(tmpRoot, 'cache', 'conn-1'); await mkdir(stagedDir, { recursive: true }); const result = await fetchLookmlRepo({ config: pullConfig({ repoUrl: repo.repoUrl }), cacheDir, stagedDir, }); expect(result.filesCopied).toBe(3); // orders.model.lkml + 2 views expect(result.commitHash).toMatch(/^[0-9a-f]{40}$/); await expect(readFile(join(stagedDir, 'orders.model.lkml'), 'utf-8')).resolves.toMatch(/connection:/); await expect(readFile(join(stagedDir, 'views', 'orders.view.lkml'), 'utf-8')).resolves.toMatch(/view: orders/); // README.md is present in the cache but NOT in stagedDir. await expect(readFile(join(stagedDir, 'README.md'), 'utf-8')).rejects.toThrow(); await expect(readFile(join(cacheDir, 'README.md'), 'utf-8')).resolves.toMatch(/readme/); }); it('pulls an existing cache dir (second call) and surfaces the new commit', async () => { const repo = await makeLocalGitRepo(join(FIXTURE_ROOT, 'single-model'), join(tmpRoot, 'origin')); const stagedDir1 = join(tmpRoot, 'staged-1'); const stagedDir2 = join(tmpRoot, 'staged-2'); const cacheDir = join(tmpRoot, 'cache', 'conn-1'); await mkdir(stagedDir1, { recursive: true }); await mkdir(stagedDir2, { recursive: true }); const r1 = await fetchLookmlRepo({ config: pullConfig({ repoUrl: repo.repoUrl }), cacheDir, stagedDir: stagedDir1, }); // Commit a new revision in the origin — a modified view. await repo.writeFile('views/orders.view.lkml', 'view: orders { sql_table_name: public.orders_v2 ;; }\n'); await repo.commit('bump'); const r2 = await fetchLookmlRepo({ config: pullConfig({ repoUrl: repo.repoUrl }), cacheDir, stagedDir: stagedDir2, }); expect(r2.commitHash).not.toBe(r1.commitHash); await expect(readFile(join(stagedDir2, 'views', 'orders.view.lkml'), 'utf-8')).resolves.toMatch(/orders_v2/); }); it('respects config.path — only files under that subtree land in stagedDir', async () => { // Build a multi-subdir repo: models/... + views/... const originRoot = join(tmpRoot, 'origin'); await mkdir(originRoot, { recursive: true }); await mkdir(join(originRoot, 'fixture-src', 'models'), { recursive: true }); await mkdir(join(originRoot, 'fixture-src', 'views'), { recursive: true }); await writeFile(join(originRoot, 'fixture-src', 'models', 'orders.model.lkml'), 'connection: "c"\n', 'utf-8'); await writeFile(join(originRoot, 'fixture-src', 'views', 'orders.view.lkml'), 'view: orders {}\n', 'utf-8'); const repo = await makeLocalGitRepo(join(originRoot, 'fixture-src'), join(originRoot, 'git')); const stagedDir = join(tmpRoot, 'staged'); const cacheDir = join(tmpRoot, 'cache', 'conn-path'); await mkdir(stagedDir, { recursive: true }); const result = await fetchLookmlRepo({ config: pullConfig({ repoUrl: repo.repoUrl, path: 'views' }), cacheDir, stagedDir, }); expect(result.filesCopied).toBe(1); await expect(readFile(join(stagedDir, 'orders.view.lkml'), 'utf-8')).resolves.toMatch(/view: orders/); // The model under `models/` is NOT copied because we scoped to `views/`. await expect(readFile(join(stagedDir, 'orders.model.lkml'), 'utf-8')).rejects.toThrow(); }); it('falls back to fresh clone when the cache dir is corrupt', async () => { const repo = await makeLocalGitRepo(join(FIXTURE_ROOT, 'single-model'), join(tmpRoot, 'origin')); const stagedDir = join(tmpRoot, 'staged'); const cacheDir = join(tmpRoot, 'cache', 'conn-bad'); await mkdir(stagedDir, { recursive: true }); // Pre-create a cacheDir that looks like a git repo but is corrupt. await mkdir(join(cacheDir, '.git'), { recursive: true }); await writeFile(join(cacheDir, '.git', 'HEAD'), 'garbage\n', 'utf-8'); const result = await fetchLookmlRepo({ config: pullConfig({ repoUrl: repo.repoUrl }), cacheDir, stagedDir, }); expect(result.filesCopied).toBeGreaterThan(0); }); it('sanitizes auth tokens out of error messages when clone fails', async () => { const stagedDir = join(tmpRoot, 'staged'); const cacheDir = join(tmpRoot, 'cache', 'conn-bad-url'); await mkdir(stagedDir, { recursive: true }); await expect( fetchLookmlRepo({ config: pullConfig({ repoUrl: 'http://definitely-not-a-real-host.test/r.git', authToken: 'supersecret-token', }), cacheDir, stagedDir, }), ).rejects.toThrow( // Error is thrown with sanitized message — the token is replaced by '***'. // The exact message depends on simple-git's failure mode; we assert the token does NOT appear. expect.objectContaining({ message: expect.not.stringContaining('supersecret-token') }), ); }); });