Files
claude-code/src/commands/local-vault/__tests__/launchLocalVault.test.ts
claude-code-best 4f0aa8615a feat: 添加本地 Memory/Vault 管理命令
- /local-memory: 本地记忆管理(store/entry CRUD、搜索、归档)
- /local-vault: 本地密钥保险库管理(加解密、keychain 集成)
- permissionValidation: vault 权限校验增强

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

193 lines
6.3 KiB
TypeScript

import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { logMock } from '../../../../tests/mocks/log.js'
mock.module('src/utils/log.ts', logMock)
mock.module('bun:bundle', () => ({ feature: () => false }))
// No keychain mock here — the real store falls back to encrypted file when
// @napi-rs/keyring is not installed (which it is not in this environment).
// This exercises the full file-fallback path without cross-test module pollution.
let callLocalVault: typeof import('../launchLocalVault.js').callLocalVault
describe('callLocalVault', () => {
let tmpDir: string
const messages: string[] = []
const onDone = (msg?: string) => {
if (msg) messages.push(msg)
}
beforeEach(async () => {
tmpDir = mkdtempSync(join(tmpdir(), 'lv-launch-test-'))
process.env['CLAUDE_CONFIG_DIR'] = tmpDir
process.env['CLAUDE_LOCAL_VAULT_PASSPHRASE'] =
'test-passphrase-fixed-32chars-xxx'
messages.length = 0
const mod = await import('../launchLocalVault.js')
callLocalVault = mod.callLocalVault
})
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true })
delete process.env['CLAUDE_CONFIG_DIR']
delete process.env['CLAUDE_LOCAL_VAULT_PASSPHRASE']
})
test('no args renders action panel without completing', async () => {
const node = await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'',
)
expect(node).not.toBeNull()
expect(messages).toHaveLength(0)
})
test('list sub-command shows key count', async () => {
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'list',
)
expect(messages.some(m => m.includes('0') || m.includes('secret'))).toBe(
true,
)
})
test('set sub-command stores secret; onDone contains [REDACTED], not value', async () => {
const secretValue = 'SUPER_SENSITIVE_VALUE_XYZ_789'
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
`set MY_API_KEY ${secretValue}`,
)
// Security invariant: value must NOT appear in any message
for (const msg of messages) {
expect(msg).not.toContain(secretValue)
}
expect(messages.some(m => m.includes('[REDACTED]'))).toBe(true)
})
test('get sub-command shows masked value by default', async () => {
const secretValue = 'ABCDEFGHIJ1234567890'
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
`set KEY_MASK ${secretValue}`,
)
messages.length = 0
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'get KEY_MASK',
)
// Masked: should contain "..." but NOT the full value
const allMessages = messages.join('\n')
expect(allMessages).toContain('...')
// Security invariant: full secret should NOT appear in masked messages
expect(allMessages).not.toContain(secretValue)
})
test('get --reveal shows plaintext value', async () => {
const secretValue = 'REVEAL_TEST_VALUE_9988'
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
`set REVEAL_KEY ${secretValue}`,
)
messages.length = 0
const node = await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'get REVEAL_KEY --reveal',
)
expect(messages.some(m => m.includes('REVEAL_KEY'))).toBe(true)
const allMessages = messages.join('\n')
expect(allMessages).toContain(secretValue)
expect(allMessages).toContain('Warning')
expect(node).toBeNull()
})
test('get without --reveal does NOT expose full secret in onDone messages', async () => {
const secretValue = 'MUST_NOT_APPEAR_IN_MESSAGES_ZZZZ'
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
`set MASK_CHECK ${secretValue}`,
)
messages.length = 0
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'get MASK_CHECK',
)
for (const msg of messages) {
expect(msg).not.toContain(secretValue)
}
})
test('get for nonexistent key → not-found view', async () => {
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'get GHOST_KEY',
)
expect(
messages.some(m => m.includes('not found') || m.includes('GHOST_KEY')),
).toBe(true)
})
test('delete sub-command removes key', async () => {
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'set TO_DEL_KEY some-value',
)
messages.length = 0
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'delete TO_DEL_KEY',
)
expect(
messages.some(m => m.includes('Deleted') || m.includes('TO_DEL_KEY')),
).toBe(true)
})
test('invalid sub-command shows usage', async () => {
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'frobnicate MY_KEY',
)
expect(
messages.some(
m => m.toLowerCase().includes('usage') || m.includes('frobnicate'),
),
).toBe(true)
})
test('reveal flag safety invariant: masked path never exposes full value in messages', async () => {
const secret = 'INVARIANT_TEST_123456789ABC'
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
`set INV_KEY ${secret}`,
)
messages.length = 0
// Without --reveal
await callLocalVault(
onDone as Parameters<typeof callLocalVault>[0],
{} as Parameters<typeof callLocalVault>[1],
'get INV_KEY',
)
for (const msg of messages) {
expect(msg).not.toContain(secret)
}
})
})