Files
claude-code/src/commands/skill-learning/__tests__/skill-learning.test.ts
unraid 95fece4b51 feat: 整合功能恢复与技能学习闭环(含 ECC v2.1 parity + Opus 4.7 接入 + prompt 工程优化)
主要变更:
- Skill Learning 闭环系统 (9/9 AC)
- Opus 4.7 模型层接入 + adaptive thinking
- Prompt 工程优化 (64 审计测试)
- Agent Teams 简化门控 (默认启用)
- Windows Terminal 后端修复 (EncodedCommand/WT_SESSION)
- TF-IDF 技能搜索精准化 (字段加权/CJK 优化)
- Autonomy 系统 (/autonomy 命令)
- ACP 协议完整实现
- mock.module 泄漏修复 (CI 全绿)
- 152+ lint/type 修复
2026-04-22 16:07:42 +08:00

153 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { call } from '../skill-learning.js'
import {
recordSkillGap,
saveInstinct,
createInstinct,
resolveProjectContext,
} from '../../../services/skillLearning/index.js'
let root: string
const originalEnv = { ...process.env }
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'skill-learning-command-'))
process.env = { ...originalEnv }
process.env.CLAUDE_SKILL_LEARNING_HOME = root
process.env.CLAUDE_CONFIG_DIR = join(root, 'config')
process.env.SKILL_LEARNING_ENABLED = '1'
})
afterEach(() => {
process.env = { ...originalEnv }
rmSync(root, { recursive: true, force: true })
})
describe('skill-learning command', () => {
test('status reports observations and instincts', async () => {
const result = await call('status', {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('Skill Learning status')
expect(result.value).toContain('Observations: 0')
}
})
test('promote (no args) prints usage and candidate summary', async () => {
const result = await call('promote', {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('Promotion candidates')
expect(result.value).toContain('promote gap')
expect(result.value).toContain('promote instinct')
}
})
test('promote gap <key> promotes a pending gap to draft', async () => {
const project = resolveProjectContext(process.cwd())
const gap = await recordSkillGap({
prompt: 'refactor the api gateway',
cwd: process.cwd(),
project,
rootDir: root,
})
expect(gap.status).toBe('pending')
const result = await call(`promote gap ${gap.key}`, {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('Promoted gap')
expect(result.value).toContain('status=draft')
}
})
test('promote gap <unknown-key> reports not found', async () => {
const result = await call('promote gap does-not-exist', {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('No gap found')
}
})
test('promote instinct <id> copies a project instinct to global scope', async () => {
const project = resolveProjectContext(process.cwd())
const instinct = createInstinct({
trigger: 'when committing',
action: 'run tests first',
confidence: 0.85,
domain: 'testing',
source: 'session-observation',
scope: 'project',
projectId: project.projectId,
projectName: project.projectName,
evidence: ['observed twice'],
})
await saveInstinct(instinct, { project, rootDir: root })
const result = await call(`promote instinct ${instinct.id}`, {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('Promoted instinct')
expect(result.value).toContain('global scope')
}
})
test('projects lists known project scopes', async () => {
// Resolving once registers the current project in the registry.
resolveProjectContext(root)
const result = await call('projects', {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(
result.value.includes('Known project scopes') ||
result.value.includes('No known project scopes'),
).toBe(true)
}
})
test('default help mentions promote and projects, no write-fixture', async () => {
const result = await call('unknown-sub', {} as any)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('promote')
expect(result.value).toContain('projects')
expect(result.value).not.toContain('write-fixture')
}
})
test('ingest imports transcript observations and instincts', async () => {
const transcript = join(root, 'session.jsonl')
writeFileSync(
transcript,
JSON.stringify({
type: 'user',
sessionId: 's1',
cwd: root,
message: { role: 'user', content: '不要 mock用 testing-library' },
}) + '\n',
)
// Pass --min-session-length=0 so the 1-line test transcript is not skipped
// by the ECC-parity gate (default threshold: 10 observations).
const result = await call(
`ingest ${transcript} --min-session-length=0`,
{} as any,
)
expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('Ingested')
expect(result.value).toContain('saved 1 instincts')
}
})
})