import { strict as assert } from 'node:assert' import { mkdtemp, mkdir, symlink, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' import { DEFAULT_EXTENSIONS, extensionOf, globToRegExp, listCodeFiles, matchesAny } from '../lib/walk.js' test('globToRegExp anchors and handles star and double-star', () => { assert.equal(globToRegExp('src/*.ts').test('src/a.ts'), true) assert.equal(globToRegExp('src/*.ts').test('src/nested/a.ts'), false) assert.equal(globToRegExp('**/*.ts').test('a/b/c.ts'), true) assert.equal(globToRegExp('**/*.ts').test('c.ts'), true) assert.equal(globToRegExp('src/**').test('src/a/b/c.ts'), true) assert.equal(globToRegExp('dist/').test('dist/a/b.js'), true) assert.equal(globToRegExp('src/a?.ts').test('src/ab.ts'), true) assert.equal(globToRegExp('src/a?.ts').test('src/abc.ts'), false) // Dots must be literal, not wildcards. assert.equal(globToRegExp('a.ts').test('axts'), false) }) test('matchesAny ignores empty patterns', () => { assert.equal(matchesAny('a/b.ts', ['']), false) assert.equal(matchesAny('a/b.ts', ['', 'a/*.ts']), true) }) test('extensionOf lowercases and rejects dotfiles', () => { assert.equal(extensionOf('a/B.TS'), 'ts') assert.equal(extensionOf('.gitignore'), '') assert.equal(extensionOf('Makefile'), '') }) test('listCodeFiles returns a deterministic sorted list of code files only', async () => { const root = await mkdtemp(join(tmpdir(), 'recall-walk-')) try { await mkdir(join(root, 'src', 'nested'), { recursive: true }) await mkdir(join(root, 'node_modules', 'dep'), { recursive: true }) await writeFile(join(root, 'src', 'b.ts'), 'export const b = 1\n') await writeFile(join(root, 'src', 'a.ts'), 'export const a = 1\n') await writeFile(join(root, 'src', 'nested', 'c.py'), 'c = 1\n') await writeFile(join(root, 'README.md'), '# not code\n') await writeFile(join(root, 'node_modules', 'dep', 'index.js'), 'module.exports = 1\n') const { files } = await listCodeFiles(root) assert.deepEqual( files.map((file) => file.path), ['src/a.ts', 'src/b.ts', 'src/nested/c.py'], ) } finally { await rm(root, { recursive: true, force: true }) } }) test('listCodeFiles honours an extension filter and an exclude glob', async () => { const root = await mkdtemp(join(tmpdir(), 'recall-walk-')) try { await mkdir(join(root, 'src', 'generated'), { recursive: true }) await writeFile(join(root, 'src', 'a.ts'), 'a\n') await writeFile(join(root, 'src', 'generated', 'b.ts'), 'b\n') await writeFile(join(root, 'src', 'c.py'), 'c\n') const onlyTs = await listCodeFiles(root, { extensions: ['ts'] }) assert.deepEqual(onlyTs.files.map((file) => file.path), ['src/a.ts', 'src/generated/b.ts']) const excluded = await listCodeFiles(root, { exclude: ['src/generated/'] }) assert.deepEqual(excluded.files.map((file) => file.path), ['src/a.ts', 'src/c.py']) } finally { await rm(root, { recursive: true, force: true }) } }) test('listCodeFiles reports truncation instead of silently dropping files', async () => { const root = await mkdtemp(join(tmpdir(), 'recall-walk-')) try { for (let at = 0; at < 5; at += 1) await writeFile(join(root, `f${at}.ts`), `${at}\n`) const { files, truncated } = await listCodeFiles(root, { maxFiles: 2 }) assert.equal(files.length, 2) assert.equal(truncated, true) } finally { await rm(root, { recursive: true, force: true }) } }) test('listCodeFiles does not follow symlinks', async () => { const root = await mkdtemp(join(tmpdir(), 'recall-walk-')) const outside = await mkdtemp(join(tmpdir(), 'recall-outside-')) try { await writeFile(join(outside, 'secret.ts'), 'export const secret = 1\n') await writeFile(join(root, 'real.ts'), 'export const real = 1\n') await symlink(outside, join(root, 'linked'), 'dir') const { files } = await listCodeFiles(root) assert.deepEqual(files.map((file) => file.path), ['real.ts']) } finally { await rm(root, { recursive: true, force: true }) await rm(outside, { recursive: true, force: true }) } }) test('the default extension set is code, not prose', () => { assert.ok(DEFAULT_EXTENSIONS.includes('ts')) assert.ok(DEFAULT_EXTENSIONS.includes('py')) assert.ok(!DEFAULT_EXTENSIONS.includes('md')) })