Files
claude-code/src/commands/env/__tests__/env.test.ts
claude-code-best fdddb6dbe8 feat: 添加工具类命令(teleport、recap、break-cache、env、tui 等)
- /teleport: 从 claude.ai 恢复会话
- /recap: 生成会话摘要
- /break-cache: 提示缓存管理(once/always/off/status)
- /env: 环境信息展示(含密钥脱敏)
- /tui: 无闪烁 TUI 模式管理
- /onboarding: 引导流程
- /perf-issue: 性能问题诊断
- /debug-tool-call: 工具调用调试
- /usage: 用量统计(合并 /cost 和 /stats 别名)

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
2026-05-09 23:04:31 +08:00

183 lines
5.8 KiB
TypeScript

/**
* Tests for src/commands/env/index.ts
* Covers: isSecretKey, maskValue, ENV_PREFIX_ALLOWLIST branches, formatRuntime, full call()
*
* Note: We do NOT mock src/bootstrap/state.js here to avoid the incomplete-mock
* cross-test pollution described in tests/mocks/README. The real state module
* is safe to import (getSessionId() returns a stable UUID per process).
*/
import { afterEach, beforeAll, describe, expect, test } from 'bun:test'
let envCmd: {
load?: () => Promise<{ call: () => Promise<{ type: string; value: string }> }>
isEnabled?: () => boolean
supportsNonInteractive?: boolean
name?: string
}
beforeAll(async () => {
const mod = await import('../index.js')
envCmd = mod.default as typeof envCmd
})
describe('env command metadata', () => {
test('isEnabled returns true', () => {
expect(envCmd.isEnabled?.()).toBe(true)
})
test('supportsNonInteractive is true', () => {
expect(envCmd.supportsNonInteractive).toBe(true)
})
test('name is "env"', () => {
expect(envCmd.name).toBe('env')
})
test('type is local', async () => {
const mod = await import('../index.js')
const cmd = mod.default as { type?: string }
expect(cmd.type).toBe('local')
})
})
describe('env command output', () => {
const savedEnvVars: Record<string, string | undefined> = {}
afterEach(() => {
// Restore env vars set during tests
for (const [k, v] of Object.entries(savedEnvVars)) {
if (v === undefined) {
delete process.env[k]
} else {
process.env[k] = v
}
}
Object.keys(savedEnvVars).forEach(k => delete savedEnvVars[k])
})
function setEnv(key: string, value: string): void {
savedEnvVars[key] = process.env[key]
process.env[key] = value
}
function deleteEnv(key: string): void {
savedEnvVars[key] = process.env[key]
delete process.env[key]
}
test('call() returns type=text', async () => {
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.type).toBe('text')
})
test('call() contains ## Runtime section', async () => {
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('## Runtime')
})
test('call() contains ## Environment Variables section', async () => {
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('## Environment Variables')
})
test('call() contains platform info', async () => {
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('platform:')
})
test('call() contains session field', async () => {
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('session:')
})
test('CLAUDE_ prefixed var appears in output', async () => {
setEnv('CLAUDE_TEST_MYVAR', 'hello_env')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('CLAUDE_TEST_MYVAR=hello_env')
})
test('FEATURE_ var appears in output', async () => {
setEnv('FEATURE_MYTEST', '1')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('FEATURE_MYTEST=1')
})
test('secret key (token) value is masked — short value shows ***', async () => {
setEnv('CLAUDE_TEST_TOKEN', 'short')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('CLAUDE_TEST_TOKEN=***')
})
test('secret key (token) value is masked — long value shows partial with length', async () => {
setEnv('CLAUDE_TEST_TOKEN', 'verylongtokenvalue1234')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).not.toContain('verylongtokenvalue1234')
expect(result.value).toContain('CLAUDE_TEST_TOKEN=very')
expect(result.value).toContain('chars)')
})
test('non-allowlisted var does NOT appear in output', async () => {
setEnv('RANDOM_UNRELATED_TEST_VAR', 'should-not-appear')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).not.toContain('RANDOM_UNRELATED_TEST_VAR')
})
test('password key is recognized as secret', async () => {
setEnv('ANTHROPIC_TEST_PASSWORD', 'mysecret12345')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).not.toContain('mysecret12345')
expect(result.value).toContain('ANTHROPIC_TEST_PASSWORD=')
})
test('no recognized env vars shows placeholder when all removed', async () => {
const allowlistPrefixes = [
'CLAUDE_',
'FEATURE_',
'ANTHROPIC_',
'BUN_',
'NODE_',
'GEMINI_',
'OPENAI_',
'GROK_',
'CCR_',
'KAIROS_',
'BUGHUNTER_',
]
for (const key of Object.keys(process.env)) {
if (allowlistPrefixes.some(p => key.startsWith(p))) {
deleteEnv(key)
}
}
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('(no recognized env vars set)')
})
// ── M1 regression: KAIROS_ prefix must include underscore ──
test('M1: KAIROS_ var (with underscore) appears in output', async () => {
setEnv('KAIROS_MY_VAR', 'kairos_value')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).toContain('KAIROS_MY_VAR=kairos_value')
})
test('M1: KAIROSE_ (wrong prefix, no match) does NOT appear in output', async () => {
// KAIROSE_ should NOT be shown — only exact KAIROS_ prefix is allowed
setEnv('KAIROSE_INTERNAL', 'should_not_appear')
const loaded = await envCmd.load!()
const result = await loaded.call()
expect(result.value).not.toContain('KAIROSE_INTERNAL')
})
})