diff --git a/.changeset/skill-scanner-test-portability.md b/.changeset/skill-scanner-test-portability.md new file mode 100644 index 00000000..75360fef --- /dev/null +++ b/.changeset/skill-scanner-test-portability.md @@ -0,0 +1,4 @@ +--- +--- + +Make skill scanner path assertions match the normalized paths returned by `resolveSkillRoots` on Windows. diff --git a/packages/agent-core/test/skill/scanner.test.ts b/packages/agent-core/test/skill/scanner.test.ts index ecd0ba05..7d5c6c6b 100644 --- a/packages/agent-core/test/skill/scanner.test.ts +++ b/packages/agent-core/test/skill/scanner.test.ts @@ -14,6 +14,10 @@ afterEach(async () => { } }); +async function normalizedRealpath(p: string): Promise { + return (await realpath(p)).replaceAll('\\', '/'); +} + describe('skill discovery', () => { it('resolves documented roots in precedence order with brand merging enabled by default', async () => { const { homeDir, repoDir, workDir } = await makeWorkspace(); @@ -22,7 +26,7 @@ describe('skill discovery', () => { await mkdir(path.join(homeDir, '.kimi-code', 'skills'), { recursive: true }); await mkdir(path.join(homeDir, '.agents', 'skills'), { recursive: true }); await mkdir(path.join(repoDir, 'team-skills'), { recursive: true }); - const realRepoDir = await realpath(repoDir); + const realRepoDir = await normalizedRealpath(repoDir); const roots = await resolveSkillRoots({ paths: { userHomeDir: homeDir, workDir }, @@ -32,8 +36,14 @@ describe('skill discovery', () => { expect(roots.map((root) => path.relative(realRepoDir, root.path))).toEqual([ '.kimi-code/skills', '.agents/skills', - path.relative(realRepoDir, await realpath(path.join(homeDir, '.kimi-code', 'skills'))), - path.relative(realRepoDir, await realpath(path.join(homeDir, '.agents', 'skills'))), + path.relative( + realRepoDir, + await normalizedRealpath(path.join(homeDir, '.kimi-code', 'skills')), + ), + path.relative( + realRepoDir, + await normalizedRealpath(path.join(homeDir, '.agents', 'skills')), + ), 'team-skills', ]); expect(roots.map((root) => root.source)).toEqual([ @@ -56,8 +66,8 @@ describe('skill discovery', () => { }); expect(roots.map((root) => root.path)).toEqual([ - await realpath(path.join(repoDir, '.kimi-code', 'skills')), - await realpath(path.join(homeDir, '.kimi-code', 'skills')), + await normalizedRealpath(path.join(repoDir, '.kimi-code', 'skills')), + await normalizedRealpath(path.join(homeDir, '.kimi-code', 'skills')), ]); }); @@ -73,7 +83,7 @@ describe('skill discovery', () => { explicitDirs: ['explicit-skills'], extraDirs: ['extra-skills'], }); - const realRepoDir = await realpath(repoDir); + const realRepoDir = await normalizedRealpath(repoDir); expect(roots.map((root) => [path.relative(realRepoDir, root.path), root.source])).toEqual([ ['explicit-skills', 'user'], @@ -541,11 +551,11 @@ describe('resolveSkillRoots ordering and priority', () => { }); expect(roots.map((r) => r.path)).toEqual([ - await realpath(projBrand), - await realpath(projGeneric), - await realpath(userBrand), - await realpath(userGeneric), - await realpath(builtin), + await normalizedRealpath(projBrand), + await normalizedRealpath(projGeneric), + await normalizedRealpath(userBrand), + await normalizedRealpath(userGeneric), + await normalizedRealpath(builtin), ]); }); @@ -574,7 +584,9 @@ describe('resolveSkillRoots ordering and priority', () => { const roots = await resolveSkillRoots({ paths: { userHomeDir: homeDir, workDir } }); const paths = roots.map((r) => r.path); - expect(paths).toContain(await realpath(path.join(homeDir, '.kimi-code', 'skills'))); + expect(paths).toContain( + await normalizedRealpath(path.join(homeDir, '.kimi-code', 'skills')), + ); }); }); @@ -589,7 +601,7 @@ describe('resolveSkillRoots extra dirs', () => { extraDirs: [extra], }); - expect(roots.map((r) => r.path)).toContain(await realpath(extra)); + expect(roots.map((r) => r.path)).toContain(await normalizedRealpath(extra)); }); it('expands a leading ~/ in extra dirs against the user home directory', async () => { @@ -602,7 +614,7 @@ describe('resolveSkillRoots extra dirs', () => { extraDirs: ['~/my-skills'], }); - expect(roots.map((r) => r.path)).toContain(await realpath(target)); + expect(roots.map((r) => r.path)).toContain(await normalizedRealpath(target)); }); it('resolves a relative extra dir against the project root (.git ancestor), not the work dir', async () => { @@ -618,7 +630,7 @@ describe('resolveSkillRoots extra dirs', () => { }); const paths = roots.map((r) => r.path); - expect(paths).toContain(await realpath(extraAtRoot)); + expect(paths).toContain(await normalizedRealpath(extraAtRoot)); expect(paths).not.toContain(path.join(nested, 'my-dir')); }); @@ -632,7 +644,7 @@ describe('resolveSkillRoots extra dirs', () => { extraDirs: [absExtra], }); - expect(roots.map((r) => r.path)).toContain(await realpath(absExtra)); + expect(roots.map((r) => r.path)).toContain(await normalizedRealpath(absExtra)); }); it('silently drops missing extra-dir entries', async () => { @@ -646,7 +658,7 @@ describe('resolveSkillRoots extra dirs', () => { }); const paths = roots.map((r) => r.path); - expect(paths).toContain(await realpath(real)); + expect(paths).toContain(await normalizedRealpath(real)); expect(paths).not.toContain(path.join(repoDir, 'nowhere')); }); @@ -660,7 +672,7 @@ describe('resolveSkillRoots extra dirs', () => { extraDirs: [real, real], }); - const realResolved = await realpath(real); + const realResolved = await normalizedRealpath(real); const matches = roots.filter((r) => r.path === realResolved); expect(matches).toHaveLength(1); }); @@ -685,7 +697,7 @@ describe('resolveSkillRoots extra dirs', () => { ], }); - const realResolved = await realpath(real); + const realResolved = await normalizedRealpath(real); const matches = roots.filter((r) => r.path === realResolved); expect(matches).toHaveLength(1); expect(matches[0]?.plugin).toEqual({ @@ -769,10 +781,10 @@ describe('resolveSkillRoots extra dirs', () => { }); const paths = roots.map((r) => r.path); - expect(paths).toContain(await realpath(cli)); - expect(paths).toContain(await realpath(extra)); - expect(paths).not.toContain(await realpath(userBrand)); - expect(paths).not.toContain(await realpath(projectBrand)); + expect(paths).toContain(await normalizedRealpath(cli)); + expect(paths).toContain(await normalizedRealpath(extra)); + expect(paths).not.toContain(await normalizedRealpath(userBrand)); + expect(paths).not.toContain(await normalizedRealpath(projectBrand)); }); it('collapses a real dir and a symlink to the same target into one root', async () => { @@ -789,7 +801,7 @@ describe('resolveSkillRoots extra dirs', () => { const extras = roots.filter((r) => r.source === 'extra'); expect(extras).toHaveLength(1); - expect(extras[0]?.path).toBe(await realpath(real)); + expect(extras[0]?.path).toBe(await normalizedRealpath(real)); }); it('collapses entries differing only by trailing slash', async () => { @@ -837,7 +849,7 @@ describe('resolveSkillRoots extra dirs', () => { const extras = roots.filter((r) => r.source === 'extra'); expect(extras).toHaveLength(1); - expect(extras[0]?.path).toBe(await realpath(real)); + expect(extras[0]?.path).toBe(await normalizedRealpath(real)); expect(extras[0]?.path).not.toBe(link); }); @@ -857,7 +869,7 @@ describe('resolveSkillRoots extra dirs', () => { extraDirs: [userBrand], }); - const realUserBrand = await realpath(userBrand); + const realUserBrand = await normalizedRealpath(userBrand); const matching = roots.filter((r) => r.path === realUserBrand); expect(matching).toHaveLength(1); expect(matching[0]?.source).toBe('user'); @@ -1125,11 +1137,11 @@ describe('explicit dir override and scope stamping', () => { }); const paths = roots.map((r) => r.path); - expect(paths).toContain(await realpath(extraA)); - expect(paths).toContain(await realpath(extraB)); - expect(paths).toContain(await realpath(builtin)); - expect(paths).not.toContain(await realpath(userBrand)); - expect(paths).not.toContain(await realpath(projBrand)); + expect(paths).toContain(await normalizedRealpath(extraA)); + expect(paths).toContain(await normalizedRealpath(extraB)); + expect(paths).toContain(await normalizedRealpath(builtin)); + expect(paths).not.toContain(await normalizedRealpath(userBrand)); + expect(paths).not.toContain(await normalizedRealpath(projBrand)); }); it('returns both brand and generic user dirs when generic is empty (no shadowing)', async () => { @@ -1142,10 +1154,10 @@ describe('explicit dir override and scope stamping', () => { const roots = await resolveSkillRoots({ paths: { userHomeDir: homeDir, workDir } }); const userPaths = roots.filter((r) => r.source === 'user').map((r) => r.path); - expect(userPaths).toContain(await realpath(brand)); - expect(userPaths).toContain(await realpath(generic)); - expect(userPaths.indexOf(await realpath(brand))).toBeLessThan( - userPaths.indexOf(await realpath(generic)), + expect(userPaths).toContain(await normalizedRealpath(brand)); + expect(userPaths).toContain(await normalizedRealpath(generic)); + expect(userPaths.indexOf(await normalizedRealpath(brand))).toBeLessThan( + userPaths.indexOf(await normalizedRealpath(generic)), ); }); @@ -1211,7 +1223,7 @@ describe('project root discovery (.git walk-up)', () => { }); const projectPaths = roots.filter((r) => r.source === 'project').map((r) => r.path); - expect(projectPaths).toContain(await realpath(repoKimi)); + expect(projectPaths).toContain(await normalizedRealpath(repoKimi)); }); it('falls back to the work dir when no .git marker is found anywhere up the chain', async () => {