mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 15:55:50 +00:00
feat: 添加 skill learning 技能学习闭环系统
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
160
src/services/skillLearning/__tests__/projectContext.test.ts
Normal file
160
src/services/skillLearning/__tests__/projectContext.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { execFileSync } from 'child_process'
|
||||
import { getClaudeConfigHomeDir } from '../../../utils/envUtils.js'
|
||||
import {
|
||||
getProjectContextPath,
|
||||
getProjectsRegistryPath,
|
||||
getSkillLearningRootDir,
|
||||
resolveProjectContext,
|
||||
} from '../projectContext.js'
|
||||
import { isSkillLearningEnabled } from '../featureCheck.js'
|
||||
|
||||
const tempBase = mkdtempSync(join(tmpdir(), 'skill-learning-context-test-'))
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
beforeEach(() => {
|
||||
resetEnv()
|
||||
const tempHome = mkdtempSync(join(tempBase, 'home-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tempHome
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = { ...originalEnv }
|
||||
clearConfigDirCache()
|
||||
rmSync(tempBase, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('isSkillLearningEnabled', () => {
|
||||
test('honors explicit SKILL_LEARNING_ENABLED overrides', () => {
|
||||
process.env.SKILL_LEARNING_ENABLED = '1'
|
||||
expect(isSkillLearningEnabled()).toBe(true)
|
||||
|
||||
process.env.SKILL_LEARNING_ENABLED = '0'
|
||||
expect(isSkillLearningEnabled()).toBe(false)
|
||||
})
|
||||
|
||||
test('honors FEATURE_SKILL_LEARNING env fallback', () => {
|
||||
delete process.env.SKILL_LEARNING_ENABLED
|
||||
process.env.FEATURE_SKILL_LEARNING = '1'
|
||||
expect(isSkillLearningEnabled()).toBe(true)
|
||||
|
||||
process.env.FEATURE_SKILL_LEARNING = '0'
|
||||
expect(isSkillLearningEnabled()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveProjectContext', () => {
|
||||
test('prefers CLAUDE_PROJECT_DIR and writes registry files', () => {
|
||||
const cwd = mkdirTempDir('cwd-')
|
||||
const projectDir = mkdirTempDir('project-')
|
||||
process.env.CLAUDE_PROJECT_DIR = projectDir
|
||||
|
||||
const context = resolveProjectContext(cwd)
|
||||
|
||||
expect(context.source).toBe('claude_project_dir')
|
||||
expect(context.scope).toBe('project')
|
||||
expect(context.projectRoot).toBe(projectDir)
|
||||
expect(context.projectName).toBe(lastPathSegment(projectDir))
|
||||
expect(context.storageDir).toContain(context.projectId)
|
||||
|
||||
expect(existsSync(getProjectsRegistryPath())).toBe(true)
|
||||
expect(existsSync(getProjectContextPath(context.projectId))).toBe(true)
|
||||
|
||||
const registry = readJson(getProjectsRegistryPath())
|
||||
expect(registry.projects[context.projectId].source).toBe(
|
||||
'claude_project_dir',
|
||||
)
|
||||
})
|
||||
|
||||
test('uses git remote as stable identity across different checkouts', () => {
|
||||
const first = createGitRepo('remote-a-', 'https://example.com/acme/app.git')
|
||||
const second = createGitRepo(
|
||||
'remote-b-',
|
||||
'https://example.com/acme/app.git',
|
||||
)
|
||||
|
||||
const firstContext = resolveProjectContext(first)
|
||||
const secondContext = resolveProjectContext(second)
|
||||
|
||||
expect(firstContext.source).toBe('git_remote')
|
||||
expect(secondContext.source).toBe('git_remote')
|
||||
expect(firstContext.projectId).toBe(secondContext.projectId)
|
||||
expect(firstContext.gitRemote).toBe('https://example.com/acme/app')
|
||||
expect(firstContext.projectName).toBe('app')
|
||||
|
||||
const registry = readJson(getProjectsRegistryPath())
|
||||
expect(Object.keys(registry.projects)).toContain(firstContext.projectId)
|
||||
expect(registry.projects[firstContext.projectId].gitRemote).toBe(
|
||||
'https://example.com/acme/app',
|
||||
)
|
||||
})
|
||||
|
||||
test('falls back to git root when origin remote is missing', () => {
|
||||
const repo = createGitRepo('root-only-')
|
||||
|
||||
const context = resolveProjectContext(join(repo, 'nested'))
|
||||
|
||||
expect(context.source).toBe('git_root')
|
||||
expect(context.scope).toBe('project')
|
||||
expect(context.projectRoot).toBe(repo)
|
||||
expect(context.projectName).toBe(lastPathSegment(repo))
|
||||
})
|
||||
|
||||
test('falls back to global context outside a git repository', () => {
|
||||
const cwd = mkdirTempDir('not-git-')
|
||||
|
||||
const context = resolveProjectContext(cwd)
|
||||
|
||||
expect(context.source).toBe('global')
|
||||
expect(context.scope).toBe('global')
|
||||
expect(context.projectId).toBe('global')
|
||||
expect(context.projectName).toBe('Global')
|
||||
expect(context.storageDir).toBe(join(getSkillLearningRootDir(), 'global'))
|
||||
expect(existsSync(getProjectContextPath('global'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
function createGitRepo(prefix: string, remote?: string): string {
|
||||
const dir = mkdirTempDir(prefix)
|
||||
mkdirSync(join(dir, 'nested'), { recursive: true })
|
||||
execFileSync('git', ['init'], { cwd: dir, stdio: 'ignore' })
|
||||
if (remote) {
|
||||
execFileSync('git', ['remote', 'add', 'origin', remote], {
|
||||
cwd: dir,
|
||||
stdio: 'ignore',
|
||||
})
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
function mkdirTempDir(prefix: string): string {
|
||||
return mkdtempSync(join(tempBase, prefix))
|
||||
}
|
||||
|
||||
function readJson(path: string): any {
|
||||
return JSON.parse(readFileSync(path, 'utf8'))
|
||||
}
|
||||
|
||||
function lastPathSegment(path: string): string {
|
||||
return path.split(/[\\/]/).filter(Boolean).at(-1) ?? path
|
||||
}
|
||||
|
||||
function resetEnv(): void {
|
||||
process.env = { ...originalEnv }
|
||||
delete process.env.CLAUDE_PROJECT_DIR
|
||||
delete process.env.SKILL_LEARNING_ENABLED
|
||||
delete process.env.FEATURE_SKILL_LEARNING
|
||||
clearConfigDirCache()
|
||||
}
|
||||
|
||||
function clearConfigDirCache(): void {
|
||||
if (
|
||||
typeof getClaudeConfigHomeDir === 'function' &&
|
||||
'cache' in getClaudeConfigHomeDir
|
||||
) {
|
||||
;(getClaudeConfigHomeDir as any).cache.clear?.()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user