mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
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>
This commit is contained in:
192
src/commands/local-vault/__tests__/launchLocalVault.test.ts
Normal file
192
src/commands/local-vault/__tests__/launchLocalVault.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user