diff --git a/.changeset/refresh-workspace-discovery.md b/.changeset/refresh-workspace-discovery.md new file mode 100644 index 00000000..a25be0da --- /dev/null +++ b/.changeset/refresh-workspace-discovery.md @@ -0,0 +1,7 @@ +--- +'@tanstack/intent': patch +--- + +Refresh workspace roots, patterns, and members between core operations. Keep workspace discovery reuse within the existing operation-local filesystem cache so listing and loading observe changed membership and source kinds. + +Avoid enumerating unrelated workspace members and reading unused skill metadata during direct loads. Preserve fresh policy reads and final path checks. diff --git a/packages/intent/src/core/intent-core.ts b/packages/intent/src/core/intent-core.ts index 374bef18..a84402c2 100644 --- a/packages/intent/src/core/intent-core.ts +++ b/packages/intent/src/core/intent-core.ts @@ -100,7 +100,7 @@ export function listIntentSkills( const cwd = resolveCoreCwd(options) const scanOptions = toScanOptions(options) const fsCache = createIntentFsCache() - const projectContext = resolveProjectContext({ cwd }) + const projectContext = resolveProjectContext({ cwd, fsCache }) const { hiddenSourceCount, hiddenSources, scan, excludePatterns } = scanForPolicedIntents({ cwd, @@ -283,7 +283,7 @@ function resolveIntentSkillInCwd( } const fsCache = createIntentFsCache() - const projectContext = resolveProjectContext({ cwd }) + const projectContext = resolveProjectContext({ cwd, fsCache }) const excludePatterns = getEffectiveExcludePatterns( options, projectContext, diff --git a/packages/intent/src/core/load-resolution.ts b/packages/intent/src/core/load-resolution.ts index b69dfc63..bed6a6c7 100644 --- a/packages/intent/src/core/load-resolution.ts +++ b/packages/intent/src/core/load-resolution.ts @@ -32,7 +32,7 @@ function readWorkspacePackageInfos( if (context.workspaceRoot) { dirs.add(context.workspaceRoot) - for (const dir of findWorkspacePackages(context.workspaceRoot)) { + for (const dir of findWorkspacePackages(context.workspaceRoot, fsCache)) { dirs.add(dir) } } @@ -224,6 +224,7 @@ function resolveFromPackageRoots( const scanned = scanIntentPackageAtRoot(packageRoot, { fallbackName: parsedUse.packageName, fsCache, + includeSkillMetadata: false, projectRoot: cwd, skillNameHint: parsedUse.skillName, }) diff --git a/packages/intent/src/core/project-context.ts b/packages/intent/src/core/project-context.ts index 6d9ad2b5..95d836e6 100644 --- a/packages/intent/src/core/project-context.ts +++ b/packages/intent/src/core/project-context.ts @@ -1,9 +1,11 @@ import { existsSync, statSync } from 'node:fs' import { dirname, join, relative, resolve } from 'node:path' +import { createIntentFsCache } from '../discovery/fs-cache.js' import { findWorkspaceRoot, readWorkspacePatterns, } from '../setup/workspace-patterns.js' +import type { IntentFsCache } from '../discovery/fs-cache.js' export type ProjectContext = { cwd: string @@ -23,18 +25,23 @@ export type ProjectContext = { export function resolveProjectContext({ cwd, targetPath, + fsCache = createIntentFsCache(), }: { cwd: string targetPath?: string + fsCache?: IntentFsCache }): ProjectContext { const resolvedCwd = resolve(cwd) const resolvedTargetPath = targetPath ? resolve(resolvedCwd, targetPath) : resolvedCwd const packageRoot = findOwningPackageRoot(resolvedTargetPath) - const workspaceRoot = findWorkspaceRoot(packageRoot ?? resolvedTargetPath) + const workspaceRoot = findWorkspaceRoot( + packageRoot ?? resolvedTargetPath, + fsCache, + ) const workspacePatterns = workspaceRoot - ? (readWorkspacePatterns(workspaceRoot) ?? []) + ? (readWorkspacePatterns(workspaceRoot, fsCache) ?? []) : [] return { diff --git a/packages/intent/src/discovery/scanner.ts b/packages/intent/src/discovery/scanner.ts index 27a9fa36..c7afdef3 100644 --- a/packages/intent/src/discovery/scanner.ts +++ b/packages/intent/src/discovery/scanner.ts @@ -24,6 +24,7 @@ import { import { findWorkspacePackages, findWorkspaceRoot, + readWorkspacePatterns, } from '../setup/workspace-patterns.js' import { createIntentFsCache } from './fs-cache.js' import { detectPackageManager } from './package-manager.js' @@ -325,6 +326,7 @@ function discoverSkillByNameHint( packageName: string, skillNameHint: string, readFs: ReadFs = nodeReadFs, + includeMetadata = true, ): Array { const skills: Array = [] const seen = new Set() @@ -339,7 +341,9 @@ function discoverSkillByNameHint( // Keep the hinted identity so loading can report its existing path error, // without reading metadata from an unreadable or escaping target. - const skill = readSkillEntry(skillsDir, childDir, skillFile, readFs) ?? { + const skill = (includeMetadata + ? readSkillEntry(skillsDir, childDir, skillFile, readFs) + : null) ?? { name: hint, path: skillFile, description: '', @@ -517,11 +521,44 @@ function getScanScope(options: ScanOptions): ScanScope { function createWorkspacePackageKeySet( workspaceRoot: string | null, fsCache: IntentFsCache, + candidateRoot?: string, ): Set { if (!workspaceRoot) return new Set() + if (candidateRoot) { + const patterns = readWorkspacePatterns(workspaceRoot, fsCache) ?? [] + const couldMatch = patterns.some((pattern) => { + if (pattern.startsWith('!')) return false + const segments = pattern.split('/') + const wildcard = segments.findIndex( + (segment) => segment === '*' || segment === '**', + ) + if ( + wildcard < 0 || + segments + .slice(wildcard) + .some((segment) => !['*', '**'].includes(segment)) + ) + return true + try { + const readFs = fsCache.getReadFs() + const prefix = readFs.realpathSync( + join(workspaceRoot, ...segments.slice(0, wildcard)), + ) + const candidate = readFs.realpathSync(candidateRoot) + const path = relative(prefix, candidate) + return ( + path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path) + ) + } catch { + return true + } + }) + if (!couldMatch) return new Set() + } + const packagesByParent = new Map>() - for (const dir of findWorkspacePackages(workspaceRoot)) { + for (const dir of findWorkspacePackages(workspaceRoot, fsCache)) { const parent = dirname(dir) const dirs = packagesByParent.get(parent) if (dirs) dirs.push(dir) @@ -576,7 +613,7 @@ export function scanForIntents( const scanScope = getScanScope(options) const fsCache = (options as ScanOptionsWithFsCache).fsCache ?? createIntentFsCache() - const workspaceRoot = findWorkspaceRoot(projectRoot) + const workspaceRoot = findWorkspaceRoot(projectRoot, fsCache) const packageManager = detectPackageManager( projectRoot, [workspaceRoot], @@ -835,6 +872,7 @@ export function scanForIntents( export interface ScanIntentPackageAtRootOptions { fallbackName?: string fsCache?: IntentFsCache + includeSkillMetadata?: boolean projectRoot?: string source?: IntentPackage['source'] skillNameHint?: string @@ -855,7 +893,11 @@ export function scanIntentPackageAtRoot( const packageIndexes = new Map() const fsCache = options.fsCache ?? createIntentFsCache() const getPackageKind = createPackageKindResolver( - createWorkspacePackageKeySet(findWorkspaceRoot(projectRoot), fsCache), + createWorkspacePackageKeySet( + findWorkspaceRoot(projectRoot, fsCache), + fsCache, + packageRoot, + ), fsCache.getFsIdentity, ) @@ -873,6 +915,7 @@ export function scanIntentPackageAtRoot( packageName, options.skillNameHint!, fsCache.getReadFs(), + options.includeSkillMetadata !== false, ) : (skillsDir, packageName) => discoverSkills(skillsDir, packageName, fsCache, warnings), diff --git a/packages/intent/src/discovery/walk.ts b/packages/intent/src/discovery/walk.ts index ac233cf0..1a2a111c 100644 --- a/packages/intent/src/discovery/walk.ts +++ b/packages/intent/src/discovery/walk.ts @@ -110,7 +110,7 @@ export function createDependencyWalker(opts: CreateDependencyWalkerOptions) { } function walkWorkspacePackages(): void { - for (const wsDir of findWorkspacePackages(opts.projectRoot)) { + for (const wsDir of findWorkspacePackages(opts.projectRoot, opts.fsCache)) { opts.scanNodeModulesDir(join(wsDir, 'node_modules')) const wsPkg = readPkgJsonWithWarning(wsDir, 'workspace') diff --git a/packages/intent/src/setup/workspace-patterns.ts b/packages/intent/src/setup/workspace-patterns.ts index 48a7488f..d4164cb4 100644 --- a/packages/intent/src/setup/workspace-patterns.ts +++ b/packages/intent/src/setup/workspace-patterns.ts @@ -1,10 +1,11 @@ -import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs' import { dirname, join } from 'node:path' import { parse as parseJsonc } from 'jsonc-parser' import { parse as parseYaml } from 'yaml' import { hasAnySkillFile } from '../shared/utils.js' import { readPackageJson } from '../core/package-json.js' import type { ParseError } from 'jsonc-parser' +import type { IntentFsCache } from '../discovery/fs-cache.js' function normalizeWorkspacePattern(pattern: string): string { return pattern.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '') @@ -105,7 +106,7 @@ function warnConfigError(path: string, err: unknown): void { type WorkspacePatternSource = { fileName: string - read: (path: string) => unknown + read: (path: string, fsCache?: IntentFsCache) => unknown getPatterns: (config: unknown) => Array | null } @@ -128,7 +129,7 @@ const workspacePatternSources: Array = [ }, { fileName: 'package.json', - read: (path) => readPackageJson(dirname(path)), + read: (path, fsCache) => readPackageJson(dirname(path), fsCache), getPatterns: (config) => parseWorkspacePatternField( isRecord(config) ? config.workspaces : undefined, @@ -158,25 +159,41 @@ const workspacePatternSources: Array = [ }, ] -const workspacePatternsCache = new Map | null>() -const workspaceRootCache = new Map() -const workspacePackageDirsCache = new Map | null>() -const workspaceInfoCache = new Map() - -export function readWorkspacePatterns(root: string): Array | null { - if (workspacePatternsCache.has(root)) { - return workspacePatternsCache.get(root) ?? null +const workspaceCaches = new WeakMap< + IntentFsCache, + { + patterns: Map | null> + roots: Map + packageDirs: Map> } - - const patterns = readWorkspacePatternsUncached(root) - workspacePatternsCache.set(root, patterns) - return patterns +>() + +function workspaceCache(fsCache?: IntentFsCache) { + if (!fsCache) return undefined + let cache = workspaceCaches.get(fsCache) + if (!cache) { + cache = { patterns: new Map(), roots: new Map(), packageDirs: new Map() } + workspaceCaches.set(fsCache, cache) + } + return cache } -function readWorkspacePatternsUncached(root: string): Array | null { +export function readWorkspacePatterns( + root: string, + fsCache?: IntentFsCache, +): Array | null { + const cache = workspaceCache(fsCache)?.patterns + const cached = cache?.get(root) + if (cached !== undefined) return cached for (const source of workspacePatternSources) { const path = join(root, source.fileName) + if ( + source.fileName === 'package.json' && + !lstatSync(path, { throwIfNoEntry: false }) + ) { + continue + } if (source.fileName !== 'package.json' && !existsSync(path)) { continue } @@ -184,12 +201,17 @@ function readWorkspacePatternsUncached(root: string): Array | null { // An unreadable ancestor may own inherited policy. Never turn it into // a cached "no workspace" result, even when a child has its own allowlist. const packageJson = - source.fileName === 'package.json' ? source.read(path) : undefined + source.fileName === 'package.json' + ? source.read(path, fsCache) + : undefined try { const patterns = source.getPatterns( - source.fileName === 'package.json' ? packageJson : source.read(path), + source.fileName === 'package.json' + ? packageJson + : source.read(path, fsCache), ) if (patterns) { + cache?.set(root, patterns) return patterns } } catch (err: unknown) { @@ -197,50 +219,38 @@ function readWorkspacePatternsUncached(root: string): Array | null { } } + cache?.set(root, null) return null } -function readWorkspacePackageDirs(root: string): Array | null { - if (workspacePackageDirsCache.has(root)) { - return workspacePackageDirsCache.get(root) ?? null - } - - const patterns = readWorkspacePatterns(root) - if (!patterns) { - workspacePackageDirsCache.set(root, null) - return null - } - - const packageDirs = resolveWorkspacePackages(root, patterns) - workspacePackageDirsCache.set(root, packageDirs) +export function findWorkspacePackages( + root: string, + fsCache?: IntentFsCache, +): Array { + const cache = workspaceCache(fsCache)?.packageDirs + const cached = cache?.get(root) + if (cached !== undefined) return cached + const patterns = readWorkspacePatterns(root, fsCache) + const packageDirs = patterns ? resolveWorkspacePackages(root, patterns) : [] + cache?.set(root, packageDirs) return packageDirs } export function getWorkspaceInfo(root: string): WorkspaceInfo | null { - if (workspaceInfoCache.has(root)) { - return workspaceInfoCache.get(root) ?? null - } - const patterns = readWorkspacePatterns(root) - if (!patterns) { - workspaceInfoCache.set(root, null) - return null - } + if (!patterns) return null - const packageDirs = readWorkspacePackageDirs(root) ?? [] + const packageDirs = resolveWorkspacePackages(root, patterns) const packageDirsWithSkills = packageDirs.filter((dir) => { const skillsDir = join(dir, 'skills') return existsSync(skillsDir) && hasAnySkillFile(skillsDir) }) - const info = { + return { root, patterns, packageDirs, packageDirsWithSkills, } - - workspaceInfoCache.set(root, info) - return info } export function resolveWorkspacePackages( @@ -325,26 +335,24 @@ function readChildDirectories(dir: string): Array { } } -export function findWorkspaceRoot(start: string): string | null { +export function findWorkspaceRoot( + start: string, + fsCache?: IntentFsCache, +): string | null { + const cache = workspaceCache(fsCache)?.roots let dir = start let prev: string | undefined const visited: Array = [] while (dir !== prev) { - const cached = workspaceRootCache.get(dir) + const cached = cache?.get(dir) if (cached !== undefined) { - for (const visitedDir of visited) { - workspaceRootCache.set(visitedDir, cached) - } + for (const visitedDir of visited) cache?.set(visitedDir, cached) return cached } - visited.push(dir) - - if (readWorkspacePatterns(dir)) { - for (const visitedDir of visited) { - workspaceRootCache.set(visitedDir, dir) - } + if (readWorkspacePatterns(dir, fsCache)) { + for (const visitedDir of visited) cache?.set(visitedDir, dir) return dir } @@ -356,16 +364,10 @@ export function findWorkspaceRoot(start: string): string | null { dir = dirname(dir) } - for (const visitedDir of visited) { - workspaceRootCache.set(visitedDir, null) - } + for (const visitedDir of visited) cache?.set(visitedDir, null) return null } export function findPackagesWithSkills(root: string): Array { return getWorkspaceInfo(root)?.packageDirsWithSkills ?? [] } - -export function findWorkspacePackages(root: string): Array { - return readWorkspacePackageDirs(root) ?? [] -} diff --git a/packages/intent/tests/core.test.ts b/packages/intent/tests/core.test.ts index 3e8ff672..6be0edde 100644 --- a/packages/intent/tests/core.test.ts +++ b/packages/intent/tests/core.test.ts @@ -8,7 +8,7 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { IntentCoreError, listIntentSkills, @@ -92,6 +92,87 @@ afterEach(() => { }) describe('listIntentSkills', () => { + it.each(['package.json', 'pnpm-workspace.yaml'])( + 'refreshes workspace members and patterns from %s between core operations', + async (workspaceFile) => { + const manifest = { + name: 'consumer', + private: true, + } + writeJson(join(root, 'package.json'), manifest) + const setPatterns = (patterns: Array) => { + if (workspaceFile === 'package.json') { + writeJson(join(root, 'package.json'), { + ...manifest, + workspaces: patterns, + }) + } else { + writeFileSync( + join(root, workspaceFile), + `packages: ${JSON.stringify(patterns)}\n`, + ) + } + } + const addMember = (member: string, dependency: string) => { + const memberDir = join(root, member) + writeJson(join(memberDir, 'package.json'), { + name: member, + dependencies: { [dependency]: '1.0.0' }, + }) + writeInstalledIntentPackage(memberDir, { + name: dependency, + version: '1.0.0', + skillName: 'core', + description: `Use ${dependency}.`, + }) + } + addMember('packages/first', 'first-library') + expect(listIntentSkills({ cwd: root }).skills).toEqual([]) + setPatterns(['packages/*']) + expect( + listIntentSkills({ cwd: root }).skills.map((skill) => skill.use), + ).toEqual(['first-library#core']) + + addMember('packages/second', 'second-library') + const added = listIntentSkills({ cwd: root }) + vi.resetModules() + const freshCore = await import('../src/core/index.js') + expect(added.skills).toEqual( + freshCore.listIntentSkills({ cwd: root }).skills, + ) + expect(added.skills.map((skill) => skill.use).sort()).toEqual([ + 'first-library#core', + 'second-library#core', + ]) + expect(loadIntentSkill('second-library#core', { cwd: root }).source).toBe( + 'local', + ) + + rmSync(join(root, 'packages/first'), { recursive: true }) + expect( + listIntentSkills({ cwd: root }).skills.map((skill) => skill.use), + ).toEqual(['second-library#core']) + + addMember('tools/third', 'third-library') + setPatterns(['tools/*']) + const changed = listIntentSkills({ cwd: root }) + vi.resetModules() + const refreshedCore = await import('../src/core/index.js') + expect(changed.skills).toEqual( + refreshedCore.listIntentSkills({ cwd: root }).skills, + ) + expect(changed.skills.map((skill) => skill.use)).toEqual([ + 'third-library#core', + ]) + expect(loadIntentSkill('third-library#core', { cwd: root }).source).toBe( + 'local', + ) + expect(() => + loadIntentSkill('second-library#core', { cwd: root }), + ).toThrow() + }, + ) + it('exposes purpose separately while keeping activation descriptions and older skills usable', () => { writeInstalledIntentPackage(root, { name: 'client', @@ -1132,6 +1213,53 @@ describe('loadIntentSkill — kind-mismatch late gate', () => { ) }) + it('refreshes source kind when workspace membership changes', () => { + const routerDir = join(root, 'packages', 'router-core') + const manifest = { + name: 'consumer', + private: true, + workspaces: ['packages/*'], + dependencies: { '@tanstack/router-core': '1.0.0' }, + intent: { skills: ['workspace:@tanstack/router-core'] }, + } + writeJson(join(root, 'package.json'), manifest) + writeJson(join(routerDir, 'package.json'), { + name: '@tanstack/router-core', + version: '1.0.0', + intent: { version: 1, repo: 'TanStack/router', docs: 'docs/' }, + }) + writeSkillMd({ + dir: join(routerDir, 'skills', 'core'), + frontmatter: { name: 'core', description: 'Router core' }, + }) + mkdirSync(join(root, 'node_modules', '@tanstack'), { recursive: true }) + symlinkSync( + routerDir, + join(root, 'node_modules', '@tanstack', 'router-core'), + 'dir', + ) + expect(listIntentSkills({ cwd: root }).skills).toHaveLength(1) + expect( + loadIntentSkill('@tanstack/router-core#core', { cwd: root }).packageName, + ).toBe('@tanstack/router-core') + + writeJson(join(root, 'package.json'), { ...manifest, workspaces: [] }) + expect(listIntentSkills({ cwd: root }).skills).toEqual([]) + expect(() => + loadIntentSkill('@tanstack/router-core#core', { cwd: root }), + ).toThrow('not listed in intent.skills') + + writeJson(join(root, 'package.json'), { + ...manifest, + workspaces: [], + intent: { skills: ['@tanstack/router-core'] }, + }) + expect(listIntentSkills({ cwd: root }).skills).toHaveLength(1) + expect( + loadIntentSkill('@tanstack/router-core#core', { cwd: root }).packageName, + ).toBe('@tanstack/router-core') + }) + it('allows a workspace member listed as workspace:', () => { const appDir = join(root, 'packages', 'app') const routerDir = join(root, 'packages', 'router-core') diff --git a/packages/intent/tests/repeated-work.test.ts b/packages/intent/tests/repeated-work.test.ts index 76917a66..25fdda3c 100644 --- a/packages/intent/tests/repeated-work.test.ts +++ b/packages/intent/tests/repeated-work.test.ts @@ -14,9 +14,14 @@ import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { main } from '../src/cli.js' -import { listIntentSkills, resolveIntentSkill } from '../src/core/index.js' +import { + listIntentSkills, + loadIntentSkill, + resolveIntentSkill, +} from '../src/core/index.js' import { rewriteLoadedSkillMarkdownDestinations } from '../src/core/markdown.js' import { checkStaleness } from '../src/staleness/check.js' +import { nodeReadFs } from '../src/shared/utils.js' import type * as NodeFs from 'node:fs' import type * as NodePath from 'node:path' @@ -74,6 +79,23 @@ afterEach(() => { }) describe('command work budgets', () => { + it('resolves direct skills without reading unused frontmatter', () => { + write( + join(root, 'package.json'), + JSON.stringify({ name: 'consumer', dependencies: { example: '1.0.0' } }), + ) + writePackage(join(root, 'node_modules', 'example'), 'example') + const metadataRead = vi.spyOn(nodeReadFs, 'readSync') + + expect(resolveIntentSkill('example#core', { cwd: root }).skillName).toBe( + 'core', + ) + expect(loadIntentSkill('example#core', { cwd: root }).content).toContain( + 'Guide.', + ) + expect(metadataRead).not.toHaveBeenCalled() + }) + it('indexes each skill path once when matching many artifact entries by name', async () => { write( join(root, 'package.json'), @@ -103,30 +125,71 @@ describe('command work budgets', () => { ).toHaveLength(1) } }) - it('classifies a direct dependency without statting every workspace package', () => { + it.each(['packages/*', 'packages/**'])( + 'classifies a direct dependency without enumerating %s workspace members', + (pattern) => { + write( + join(root, 'package.json'), + JSON.stringify({ + name: 'consumer', + dependencies: { example: '1.0.0' }, + intent: { skills: ['example'] }, + }), + ) + write(join(root, 'pnpm-workspace.yaml'), `packages:\n - ${pattern}\n`) + const packageDirs = Array.from({ length: 120 }, (_, index) => + join(root, 'packages', `pkg-${index}`), + ) + for (const dir of packageDirs) write(join(dir, 'package.json'), '{}') + writePackage(join(root, 'node_modules', 'example'), 'example') + + expect(resolveIntentSkill('example#core', { cwd: root }).skillName).toBe( + 'core', + ) + expect( + vi + .mocked(readdirSync) + .mock.calls.filter(([path]) => path === join(root, 'packages')), + ).toHaveLength(0) + expect( + vi + .mocked(lstatSync) + .mock.calls.filter(([path]) => packageDirs.includes(String(path))), + ).toHaveLength(0) + }, + ) + + it('keeps symlinked members after a wildcard in workspace classification', () => { write( join(root, 'package.json'), JSON.stringify({ name: 'consumer', - dependencies: { example: '1.0.0' }, - intent: { skills: ['example'] }, + intent: { skills: ['workspace:example'] }, }), ) - write(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n') - const packageDirs = Array.from({ length: 120 }, (_, index) => - join(root, 'packages', `pkg-${index}`), + write( + join(root, 'pnpm-workspace.yaml'), + 'packages:\n - groups/*/current\n', ) - for (const dir of packageDirs) write(join(dir, 'package.json'), '{}') - writePackage(join(root, 'node_modules', 'example'), 'example') - + const first = join(root, 'first') + const second = join(root, 'second') + writePackage(first, 'example') + writePackage(second, 'example') + const workspaceLink = join(root, 'groups', 'team', 'current') + const dependencyLink = join(root, 'node_modules', 'example') + mkdirSync(dirname(workspaceLink), { recursive: true }) + mkdirSync(dirname(dependencyLink), { recursive: true }) + symlinkSync(first, workspaceLink, 'dir') + symlinkSync(first, dependencyLink, 'dir') expect(resolveIntentSkill('example#core', { cwd: root }).skillName).toBe( 'core', ) - expect( - vi - .mocked(lstatSync) - .mock.calls.filter(([path]) => packageDirs.includes(String(path))), - ).toHaveLength(0) + + unlinkSync(workspaceLink) + symlinkSync(second, workspaceLink, 'dir') + expect(() => resolveIntentSkill('example#core', { cwd: root })).toThrow( + 'not listed', + ) }) it.each([false, true])(