Stale ~/.claude/plugins/cache versions polluted token-hotspots ranking and
inflated CNF duplicate-hook findings with config that loads on zero turns.
installPaths point INTO the cache, so a blunt "skip all of plugins/cache"
would drop ACTIVE plugins — the filter is therefore version-aware: it reads
the adjacent installed_plugins.json, keeps active version dirs, drops only
stale ones (and exposes them via discovery.staleCacheVersions for B3b).
- file-discovery: cacheVersionKey() + applyCacheFilter() (active vs stale via
installed_plugins.json; HOME-independent, derives the manifest from the cache
path); discoverConfigFiles/Multi gain { excludeCache } + staleCacheVersions.
Absent/unparseable manifest -> no filtering (never silently drop live config).
- token-hotspots-cli + scan-orchestrator: --exclude-cache (default ON for these
live-cost scans) / --no-exclude-cache restores the full walk.
- Tests: cacheVersionKey unit cases; stale-dropped/active-kept/no-manifest
discovery cases; CNF-drop proof (soft-spot verified, not assumed:
include=1 -> exclude=0 duplicate-hook findings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
511 lines
21 KiB
JavaScript
511 lines
21 KiB
JavaScript
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { join } from 'node:path';
|
|
import { mkdir, writeFile, rm, stat } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import {
|
|
discoverConfigFiles,
|
|
discoverConfigFilesMulti,
|
|
discoverFullMachinePaths,
|
|
readTextFile,
|
|
cacheVersionKey,
|
|
} from '../../scanners/lib/file-discovery.mjs';
|
|
|
|
/**
|
|
* Create a temp directory with a unique name for test isolation.
|
|
*/
|
|
function tempDir(suffix) {
|
|
return join(tmpdir(), `config-audit-fd-test-${suffix}-${Date.now()}`);
|
|
}
|
|
|
|
/**
|
|
* Build a realistic ~/.claude/plugins/cache fixture under `root`.
|
|
* `versions` is a list of 'marketplace/plugin/version' keys to materialize on
|
|
* disk (each gets a CLAUDE.md + hooks/hooks.json). `active` is the subset of
|
|
* those keys that installed_plugins.json references via installPath — i.e. the
|
|
* versions Claude Code actually loads. Versions NOT in `active` are stale.
|
|
*/
|
|
async function buildCacheFixture(root, { versions, active }) {
|
|
const pluginsDir = join(root, 'plugins');
|
|
for (const key of versions) {
|
|
const verDir = join(pluginsDir, 'cache', ...key.split('/'));
|
|
await mkdir(join(verDir, 'hooks'), { recursive: true });
|
|
await writeFile(join(verDir, 'CLAUDE.md'), `# ${key}`);
|
|
await writeFile(
|
|
join(verDir, 'hooks', 'hooks.json'),
|
|
JSON.stringify({ hooks: { PreToolUse: [{ matcher: 'Edit' }] } })
|
|
);
|
|
}
|
|
const plugins = {};
|
|
for (const key of active) {
|
|
const [mkt, plug, version] = key.split('/');
|
|
plugins[`${plug}@${mkt}`] = [{
|
|
scope: 'user',
|
|
installPath: join(pluginsDir, 'cache', mkt, plug, version),
|
|
version,
|
|
}];
|
|
}
|
|
await writeFile(
|
|
join(pluginsDir, 'installed_plugins.json'),
|
|
JSON.stringify({ version: 2, plugins })
|
|
);
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 1: discoverConfigFiles — single path
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('discoverConfigFiles (single path)', () => {
|
|
let dir;
|
|
|
|
before(async () => {
|
|
dir = tempDir('single');
|
|
// Create a realistic project structure
|
|
await mkdir(join(dir, '.claude', 'rules'), { recursive: true });
|
|
await mkdir(join(dir, 'hooks'), { recursive: true });
|
|
await mkdir(join(dir, 'node_modules', 'pkg'), { recursive: true });
|
|
await mkdir(join(dir, 'src'), { recursive: true });
|
|
|
|
await writeFile(join(dir, 'CLAUDE.md'), '# Instructions');
|
|
await writeFile(join(dir, '.claude', 'settings.json'), '{}');
|
|
await writeFile(join(dir, '.claude', 'rules', 'my-rule.md'), '---\nglobs: "*.ts"\n---\nRule');
|
|
await writeFile(join(dir, '.mcp.json'), '{"mcpServers": {}}');
|
|
await writeFile(join(dir, 'hooks', 'hooks.json'), '{"hooks": {}}');
|
|
await writeFile(join(dir, 'src', 'index.ts'), 'export {}');
|
|
// Should be skipped
|
|
await writeFile(join(dir, 'node_modules', 'pkg', 'CLAUDE.md'), '# dep');
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('returns { files, skipped } shape', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
assert.ok(Array.isArray(result.files));
|
|
assert.equal(typeof result.skipped, 'number');
|
|
});
|
|
|
|
it('finds CLAUDE.md', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const claude = result.files.find(f => f.type === 'claude-md');
|
|
assert.ok(claude, 'should find CLAUDE.md');
|
|
assert.equal(claude.relPath, 'CLAUDE.md');
|
|
});
|
|
|
|
it('finds settings.json in .claude/', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const settings = result.files.find(f => f.type === 'settings-json');
|
|
assert.ok(settings, 'should find settings.json');
|
|
assert.ok(settings.relPath.includes('.claude'));
|
|
});
|
|
|
|
it('finds .mcp.json', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const mcp = result.files.find(f => f.type === 'mcp-json');
|
|
assert.ok(mcp, 'should find .mcp.json');
|
|
});
|
|
|
|
it('finds rules in .claude/rules/', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const rules = result.files.filter(f => f.type === 'rule');
|
|
assert.ok(rules.length > 0, 'should find at least one rule');
|
|
});
|
|
|
|
it('finds hooks.json', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const hooks = result.files.find(f => f.type === 'hooks-json');
|
|
assert.ok(hooks, 'should find hooks.json');
|
|
});
|
|
|
|
it('skips node_modules', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const inNodeModules = result.files.filter(f => f.absPath.includes('node_modules'));
|
|
assert.equal(inNodeModules.length, 0, 'should not include files in node_modules');
|
|
});
|
|
|
|
it('tracks skipped directories (counter fix)', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
assert.ok(result.skipped >= 1, 'should count at least node_modules as skipped');
|
|
});
|
|
|
|
it('does not include non-config files', async () => {
|
|
const result = await discoverConfigFiles(dir);
|
|
const ts = result.files.find(f => f.absPath.endsWith('.ts'));
|
|
assert.equal(ts, undefined, 'should not include .ts files');
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 2: File classification
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('file classification via discoverConfigFiles', () => {
|
|
let dir;
|
|
|
|
before(async () => {
|
|
dir = tempDir('classify');
|
|
await mkdir(join(dir, '.claude-plugin'), { recursive: true });
|
|
await mkdir(join(dir, 'agents'), { recursive: true });
|
|
await mkdir(join(dir, 'commands'), { recursive: true });
|
|
await mkdir(join(dir, 'skills', 'my-skill'), { recursive: true });
|
|
await mkdir(join(dir, 'hooks'), { recursive: true });
|
|
|
|
await writeFile(join(dir, '.claude-plugin', 'plugin.json'), '{}');
|
|
await writeFile(join(dir, 'agents', 'test-agent.md'), '---\nname: test\n---');
|
|
await writeFile(join(dir, 'commands', 'test-cmd.md'), '---\nname: test\n---');
|
|
await writeFile(join(dir, 'skills', 'my-skill', 'SKILL.md'), '# Skill');
|
|
await writeFile(join(dir, 'hooks', 'hooks.json'), '{}');
|
|
await writeFile(join(dir, 'CLAUDE.local.md'), '# Local');
|
|
// Not a config file
|
|
await writeFile(join(dir, 'random.txt'), 'hello');
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('classifies plugin.json in .claude-plugin/', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const plugin = files.find(f => f.type === 'plugin-json');
|
|
assert.ok(plugin, 'should find plugin.json');
|
|
});
|
|
|
|
it('classifies agent .md in agents/', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const agent = files.find(f => f.type === 'agent-md');
|
|
assert.ok(agent, 'should find agent markdown');
|
|
});
|
|
|
|
it('classifies command .md in commands/', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const cmd = files.find(f => f.type === 'command-md');
|
|
assert.ok(cmd, 'should find command markdown');
|
|
});
|
|
|
|
it('classifies SKILL.md', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const skill = files.find(f => f.type === 'skill-md');
|
|
assert.ok(skill, 'should find SKILL.md');
|
|
});
|
|
|
|
it('classifies hooks.json in hooks/', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const hooks = files.find(f => f.type === 'hooks-json');
|
|
assert.ok(hooks, 'should find hooks.json');
|
|
});
|
|
|
|
it('classifies CLAUDE.local.md', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const local = files.find(f => f.absPath.endsWith('CLAUDE.local.md'));
|
|
assert.ok(local, 'should find CLAUDE.local.md');
|
|
assert.equal(local.type, 'claude-md');
|
|
});
|
|
|
|
it('does not discover random.txt', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const txt = files.find(f => f.absPath.endsWith('random.txt'));
|
|
assert.equal(txt, undefined, 'should not discover .txt files');
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 3: Depth limit
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('depth limit', () => {
|
|
let dir;
|
|
|
|
before(async () => {
|
|
dir = tempDir('depth');
|
|
// Create structure: a/b/c/d/e/f/g/CLAUDE.md (depth 7 from root)
|
|
const shallow = join(dir, 'a', 'b');
|
|
const deep = join(dir, 'a', 'b', 'c', 'd', 'e', 'f', 'g');
|
|
await mkdir(shallow, { recursive: true });
|
|
await mkdir(deep, { recursive: true });
|
|
await writeFile(join(shallow, 'CLAUDE.md'), '# Shallow (depth 2)');
|
|
await writeFile(join(deep, 'CLAUDE.md'), '# Deep (depth 7)');
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('finds deep files with default maxDepth (10)', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
|
|
assert.ok(deep, 'should find CLAUDE.md at depth 7 with default maxDepth');
|
|
});
|
|
|
|
it('respects custom maxDepth: 3', async () => {
|
|
const { files } = await discoverConfigFiles(dir, { maxDepth: 3 });
|
|
const shallow = files.find(f => f.absPath.includes('a/b/CLAUDE.md'));
|
|
const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
|
|
assert.ok(shallow, 'should find shallow CLAUDE.md');
|
|
assert.equal(deep, undefined, 'should NOT find deep CLAUDE.md with maxDepth: 3');
|
|
});
|
|
|
|
it('old depth limit of 5 would have missed depth-7 files', async () => {
|
|
const { files } = await discoverConfigFiles(dir, { maxDepth: 5 });
|
|
const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
|
|
assert.equal(deep, undefined, 'maxDepth: 5 should miss depth-7 files');
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 4: discoverConfigFilesMulti
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('discoverConfigFilesMulti', () => {
|
|
let rootA, rootB;
|
|
|
|
before(async () => {
|
|
rootA = tempDir('multiA');
|
|
rootB = tempDir('multiB');
|
|
await mkdir(rootA, { recursive: true });
|
|
await mkdir(rootB, { recursive: true });
|
|
await mkdir(join(rootB, 'a', 'b', 'c', 'd'), { recursive: true });
|
|
await mkdir(join(rootA, 'node_modules'), { recursive: true });
|
|
|
|
await writeFile(join(rootA, 'CLAUDE.md'), '# Project A');
|
|
await writeFile(join(rootA, '.mcp.json'), '{}');
|
|
await writeFile(join(rootB, 'CLAUDE.md'), '# Project B');
|
|
await writeFile(join(rootB, 'a', 'b', 'c', 'd', 'CLAUDE.md'), '# Deep B');
|
|
// Skippable
|
|
await writeFile(join(rootA, 'node_modules', 'CLAUDE.md'), '# Skip');
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(rootA, { recursive: true, force: true });
|
|
await rm(rootB, { recursive: true, force: true });
|
|
});
|
|
|
|
it('discovers files from both roots', async () => {
|
|
const roots = [
|
|
{ path: rootA, maxDepth: 10 },
|
|
{ path: rootB, maxDepth: 10 },
|
|
];
|
|
const { files } = await discoverConfigFilesMulti(roots);
|
|
const fromA = files.find(f => f.absPath.includes(rootA) && f.type === 'claude-md');
|
|
const fromB = files.find(f => f.absPath === join(rootB, 'CLAUDE.md'));
|
|
assert.ok(fromA, 'should find CLAUDE.md from root A');
|
|
assert.ok(fromB, 'should find CLAUDE.md from root B');
|
|
});
|
|
|
|
it('deduplicates when same root listed twice', async () => {
|
|
const roots = [
|
|
{ path: rootA, maxDepth: 10 },
|
|
{ path: rootA, maxDepth: 10 },
|
|
];
|
|
const { files } = await discoverConfigFilesMulti(roots);
|
|
const claudeFiles = files.filter(f => f.absPath === join(rootA, 'CLAUDE.md'));
|
|
assert.equal(claudeFiles.length, 1, 'should deduplicate same file');
|
|
});
|
|
|
|
it('accumulates skipped count across roots', async () => {
|
|
const roots = [
|
|
{ path: rootA, maxDepth: 10 },
|
|
{ path: rootB, maxDepth: 10 },
|
|
];
|
|
const { skipped } = await discoverConfigFilesMulti(roots);
|
|
assert.ok(skipped >= 1, 'should accumulate skipped dirs from rootA');
|
|
});
|
|
|
|
it('respects per-root maxDepth', async () => {
|
|
const roots = [
|
|
{ path: rootA, maxDepth: 10 },
|
|
{ path: rootB, maxDepth: 2 }, // blocks depth-4 file in rootB
|
|
];
|
|
const { files } = await discoverConfigFilesMulti(roots);
|
|
const deepB = files.find(f => f.absPath.includes('c/d/CLAUDE.md'));
|
|
assert.equal(deepB, undefined, 'should not find deep file when maxDepth is 2');
|
|
});
|
|
|
|
it('respects global maxFiles cap', async () => {
|
|
const roots = [
|
|
{ path: rootA, maxDepth: 10 },
|
|
{ path: rootB, maxDepth: 10 },
|
|
];
|
|
const { files } = await discoverConfigFilesMulti(roots, { maxFiles: 1 });
|
|
assert.equal(files.length, 1, 'should stop after maxFiles');
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 5: discoverFullMachinePaths
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('discoverFullMachinePaths', () => {
|
|
it('returns an array', async () => {
|
|
const paths = await discoverFullMachinePaths();
|
|
assert.ok(Array.isArray(paths));
|
|
});
|
|
|
|
it('each entry has path (string) and maxDepth (number)', async () => {
|
|
const paths = await discoverFullMachinePaths();
|
|
for (const entry of paths) {
|
|
assert.equal(typeof entry.path, 'string', 'path should be string');
|
|
assert.equal(typeof entry.maxDepth, 'number', 'maxDepth should be number');
|
|
}
|
|
});
|
|
|
|
it('only returns existing directories', async () => {
|
|
const paths = await discoverFullMachinePaths();
|
|
for (const entry of paths) {
|
|
const s = await stat(entry.path);
|
|
assert.ok(s.isDirectory(), `${entry.path} should be a directory`);
|
|
}
|
|
});
|
|
|
|
it('includes ~/.claude if it exists', async () => {
|
|
const home = process.env.HOME || '';
|
|
const paths = await discoverFullMachinePaths();
|
|
const hasClaude = paths.some(p => p.path === join(home, '.claude'));
|
|
// Only assert if ~/.claude exists on this machine
|
|
try {
|
|
await stat(join(home, '.claude'));
|
|
assert.ok(hasClaude, 'should include ~/.claude');
|
|
} catch {
|
|
// ~/.claude doesn't exist, skip
|
|
}
|
|
});
|
|
|
|
it('has no duplicate paths', async () => {
|
|
const paths = await discoverFullMachinePaths();
|
|
const seen = new Set();
|
|
for (const entry of paths) {
|
|
assert.ok(!seen.has(entry.path), `duplicate path: ${entry.path}`);
|
|
seen.add(entry.path);
|
|
}
|
|
});
|
|
|
|
it('~/.claude gets maxDepth >= 6', async () => {
|
|
const home = process.env.HOME || '';
|
|
const paths = await discoverFullMachinePaths();
|
|
const claude = paths.find(p => p.path === join(home, '.claude'));
|
|
if (claude) {
|
|
assert.ok(claude.maxDepth >= 6, 'maxDepth for ~/.claude should be >= 6');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 6: readTextFile
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('readTextFile', () => {
|
|
let dir;
|
|
|
|
before(async () => {
|
|
dir = tempDir('readtext');
|
|
await mkdir(dir, { recursive: true });
|
|
await writeFile(join(dir, 'good.md'), '# Hello\nWorld');
|
|
await writeFile(join(dir, 'binary.bin'), Buffer.from([0x48, 0x65, 0x00, 0x6c, 0x6f]));
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('returns string for valid UTF-8 file', async () => {
|
|
const content = await readTextFile(join(dir, 'good.md'));
|
|
assert.equal(typeof content, 'string');
|
|
assert.ok(content.includes('Hello'));
|
|
});
|
|
|
|
it('returns null for binary file (null bytes)', async () => {
|
|
const content = await readTextFile(join(dir, 'binary.bin'));
|
|
assert.equal(content, null);
|
|
});
|
|
|
|
it('returns null for nonexistent file', async () => {
|
|
const content = await readTextFile(join(dir, 'nope.txt'));
|
|
assert.equal(content, null);
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
// Group 7: cache-aware filtering (B3) — version-aware exclusion of
|
|
// stale ~/.claude/plugins/cache versions; active versions kept.
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
describe('cacheVersionKey (pure)', () => {
|
|
it('extracts marketplace/plugin/version from a plugins/cache path', () => {
|
|
const p = '/home/u/.claude/plugins/cache/mkt/voyage/5.6.0/commands/x.md';
|
|
assert.equal(cacheVersionKey(p), 'mkt/voyage/5.6.0');
|
|
});
|
|
|
|
it('works for the version directory itself', () => {
|
|
assert.equal(cacheVersionKey('/x/plugins/cache/m/p/1.0.0'), 'm/p/1.0.0');
|
|
});
|
|
|
|
it('returns null for paths outside plugins/cache', () => {
|
|
assert.equal(cacheVersionKey('/home/u/repos/proj/cache/foo/bar/baz'), null);
|
|
assert.equal(cacheVersionKey('/home/u/.claude/agents/x.md'), null);
|
|
});
|
|
|
|
it('returns null when shallower than version level', () => {
|
|
assert.equal(cacheVersionKey('/x/plugins/cache/m/p'), null);
|
|
});
|
|
});
|
|
|
|
describe('discoverConfigFiles — cache-aware filtering', () => {
|
|
let dir;
|
|
const ACTIVE = 'mkt/voyage/5.6.0';
|
|
const STALE1 = 'mkt/voyage/5.1.1';
|
|
const STALE2 = 'mkt/config-audit/5.1.0';
|
|
|
|
before(async () => {
|
|
dir = tempDir('cache');
|
|
await buildCacheFixture(dir, {
|
|
versions: [ACTIVE, STALE1, STALE2],
|
|
active: [ACTIVE],
|
|
});
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('default (no excludeCache): keeps every cached version', async () => {
|
|
const { files } = await discoverConfigFiles(dir);
|
|
const keys = new Set(files.map(f => cacheVersionKey(f.absPath)).filter(Boolean));
|
|
assert.ok(keys.has(ACTIVE) && keys.has(STALE1) && keys.has(STALE2),
|
|
'all version keys present without excludeCache');
|
|
});
|
|
|
|
it('excludeCache: drops stale versions, keeps active', async () => {
|
|
const { files } = await discoverConfigFiles(dir, { excludeCache: true });
|
|
const keys = new Set(files.map(f => cacheVersionKey(f.absPath)).filter(Boolean));
|
|
assert.ok(keys.has(ACTIVE), 'active version retained');
|
|
assert.ok(!keys.has(STALE1), 'stale version 1 dropped');
|
|
assert.ok(!keys.has(STALE2), 'stale version 2 dropped');
|
|
});
|
|
|
|
it('reports staleCacheVersions regardless of excludeCache', async () => {
|
|
const { staleCacheVersions } = await discoverConfigFiles(dir, { excludeCache: true });
|
|
const staleKeys = staleCacheVersions.map(s => s.key).sort();
|
|
assert.deepEqual(staleKeys, [STALE2, STALE1].sort());
|
|
const s1 = staleCacheVersions.find(s => s.key === STALE1);
|
|
assert.ok(s1.fileCount >= 2, 'counts the cached config files (CLAUDE.md + hooks.json)');
|
|
});
|
|
|
|
it('does NOT filter when installed_plugins.json is absent (cannot tell active from stale)', async () => {
|
|
const noManifest = tempDir('cache-nomanifest');
|
|
// materialize cache versions but NO installed_plugins.json
|
|
await buildCacheFixture(noManifest, { versions: [ACTIVE, STALE1], active: [] });
|
|
await rm(join(noManifest, 'plugins', 'installed_plugins.json'), { force: true });
|
|
const { files, staleCacheVersions } = await discoverConfigFiles(noManifest, { excludeCache: true });
|
|
const keys = new Set(files.map(f => cacheVersionKey(f.absPath)).filter(Boolean));
|
|
assert.ok(keys.has(ACTIVE) && keys.has(STALE1), 'no filtering without a manifest');
|
|
assert.equal(staleCacheVersions.length, 0, 'no stale claims without a manifest');
|
|
await rm(noManifest, { recursive: true, force: true });
|
|
});
|
|
|
|
it('discoverConfigFilesMulti applies the same cache filter', async () => {
|
|
const { files } = await discoverConfigFilesMulti([{ path: dir, maxDepth: 10 }], { excludeCache: true });
|
|
const keys = new Set(files.map(f => cacheVersionKey(f.absPath)).filter(Boolean));
|
|
assert.ok(keys.has(ACTIVE), 'active retained via Multi');
|
|
assert.ok(!keys.has(STALE1) && !keys.has(STALE2), 'stale dropped via Multi');
|
|
});
|
|
});
|