import { mkdir, mkdtemp, 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 { describeLookerScope, hashLookerScope, isPathInLookerScope } from '../../../../../src/context/ingest/adapters/looker/scope.js'; async function writeJson(stagedDir: string, relPath: string, value: unknown): Promise { const abs = join(stagedDir, relPath); await mkdir(join(abs, '..'), { recursive: true }); await writeFile(abs, `${JSON.stringify(value, null, 2)}\n`, 'utf-8'); } describe('Looker runtime fetch scope', () => { let stagedDir: string; beforeEach(async () => { stagedDir = await mkdtemp(join(tmpdir(), 'looker-scope-')); }); afterEach(async () => { await rm(stagedDir, { recursive: true, force: true }); }); it('keeps omitted known-current entity files out of the deletion baseline', () => { const scope = { mode: 'incremental' as const, knownCurrentRawPaths: ['dashboards/10.json', 'dashboards/11.json', 'looks/20.json'], fetchedRawPaths: ['dashboards/11.json'], }; expect(isPathInLookerScope('dashboards/10.json', scope)).toBe(false); expect(isPathInLookerScope('looks/20.json', scope)).toBe(false); expect(isPathInLookerScope('dashboards/11.json', scope)).toBe(true); expect(isPathInLookerScope('looks/21.json', scope)).toBe(true); expect(isPathInLookerScope('signals/dashboard_usage.json', scope)).toBe(true); expect(isPathInLookerScope('explores/b2b/sales_pipeline.json', scope)).toBe(true); }); it('keeps omitted unchanged evidence documents out of incremental delete scope', () => { const scope = { mode: 'incremental' as const, knownCurrentRawPaths: ['dashboards/10.json', 'looks/20.json'], fetchedRawPaths: ['dashboards/10.json'], }; expect(isPathInLookerScope('evidence/dashboards/10/page.md', scope)).toBe(true); expect(isPathInLookerScope('evidence/dashboards/10/metadata.json', scope)).toBe(true); expect(isPathInLookerScope('evidence/looks/20/page.md', scope)).toBe(false); expect(isPathInLookerScope('evidence/looks/20/metadata.json', scope)).toBe(false); }); it('treats full scope as all raw paths in scope', () => { const scope = { mode: 'full' as const, knownCurrentRawPaths: ['dashboards/10.json'], fetchedRawPaths: ['dashboards/10.json'], }; expect(isPathInLookerScope('dashboards/10.json', scope)).toBe(true); expect(isPathInLookerScope('dashboards/99.json', scope)).toBe(true); expect(isPathInLookerScope('looks/20.json', scope)).toBe(true); }); it('hashes scope order-insensitively', () => { const a = hashLookerScope({ mode: 'incremental', knownCurrentRawPaths: ['looks/20.json', 'dashboards/10.json'], fetchedRawPaths: ['dashboards/10.json'], }); const b = hashLookerScope({ mode: 'incremental', knownCurrentRawPaths: ['dashboards/10.json', 'looks/20.json'], fetchedRawPaths: ['dashboards/10.json'], }); expect(a).toBe(b); expect(a).toMatch(/^[0-9a-f]{64}$/); }); it('reads staged scope and returns a SourceAdapter ScopeDescriptor', async () => { await writeJson(stagedDir, 'looker-scope.json', { mode: 'incremental', knownCurrentRawPaths: ['dashboards/10.json', 'looks/20.json'], fetchedRawPaths: ['dashboards/10.json'], }); const descriptor = await describeLookerScope(stagedDir); expect(descriptor.fingerprint).toMatch(/^[0-9a-f]{64}$/); expect(descriptor.isPathInScope('dashboards/10.json')).toBe(true); expect(descriptor.isPathInScope('looks/20.json')).toBe(false); expect(descriptor.isPathInScope('looks/99.json')).toBe(true); }); it('falls back to full scope when old fixtures do not have a scope file', async () => { const descriptor = await describeLookerScope(stagedDir); expect(descriptor.isPathInScope('dashboards/10.json')).toBe(true); expect(descriptor.isPathInScope('looks/20.json')).toBe(true); }); });