Files
claude-code/src/services/skillLearning/__tests__/projectContext.test.ts
claude-code-best f43350e600 fix: 修复 4 个测试失败(路径规范化、SDK 签名变更、空消息防护)
- projectContext.test.ts: 使用 realpathSync 处理 macOS /var→/private/var 符号链接
- bedrockClient.test.ts: 适配 Bedrock SDK v0.80 Bearer 认证(原 AWS4-HMAC-SHA256)
- bridge.ts: forwardSessionUpdates 添加 null guard 防止空消息导致 TypeError

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 22:52:37 +08:00

161 lines
5.2 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, 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(realpathSync(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(realpathSync(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?.()
}
}