mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
- 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>
161 lines
5.2 KiB
TypeScript
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?.()
|
|
}
|
|
}
|