/** * Unit tests for key persistence vs. ephemeral key handling. * * Mirrors the Python SDK's `key_dir` semantics: omitting it (the default) * generates a key pair in memory for this session only and never writes to * disk; supplying it reuses or creates a pair in that directory. */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { SecureCompletionClient } from '../../src/core/SecureCompletionClient'; /** Drive key initialisation without any network traffic. */ async function initKeys(client: SecureCompletionClient): Promise { await (client as unknown as { ensureKeys: () => Promise }).ensureKeys(); } function publicKeyOf(client: SecureCompletionClient): Promise { return (client as unknown as { keyManager: { getPublicKeyPEM: () => Promise }; }).keyManager.getPublicKeyPEM(); } /** * Watch for any attempt to write keys to disk. * * Asserting on the filesystem alone is unreliable here — a stale `client_keys/` * from before ephemeral keys became the default would make such a check pass or * fail for the wrong reason. This asserts the intent directly. */ function watchSaves(client: SecureCompletionClient): jest.SpyInstance { const keyManager = (client as unknown as { keyManager: object }).keyManager; return jest.spyOn(keyManager as { saveKeys: (...args: never[]) => Promise }, 'saveKeys'); } let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nomyo-keys-')); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe('ephemeral keys (default)', () => { test('writes nothing to disk when no keyDir is configured', async () => { const cwdBefore = fs.readdirSync(process.cwd()); const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, }); const saveSpy = watchSaves(client); await initKeys(client); expect(saveSpy).not.toHaveBeenCalled(); // And no new directory materialised in the working directory expect(fs.readdirSync(process.cwd())).toEqual(cwdBefore); client.dispose(); }); test('each client gets its own key pair', async () => { const a = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, }); const b = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, }); await initKeys(a); await initKeys(b); expect(await publicKeyOf(a)).not.toBe(await publicKeyOf(b)); a.dispose(); b.dispose(); }); test('keyDir: null is treated as ephemeral', async () => { const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, keyDir: null, }); await initKeys(client); expect(fs.readdirSync(tmpDir)).toEqual([]); client.dispose(); }); test('key rotation stays in memory', async () => { const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, }); await initKeys(client); const before = await publicKeyOf(client); const saveSpy = watchSaves(client); await (client as unknown as { rotateKeys: () => Promise }).rotateKeys(); // Rotated to a genuinely new pair, still with nothing written to disk expect(await publicKeyOf(client)).not.toBe(before); expect(saveSpy).not.toHaveBeenCalled(); client.dispose(); }); }); describe('persistent keys (explicit keyDir)', () => { test('generates and saves a key pair on first use', async () => { const keyDir = path.join(tmpDir, 'keys'); const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, keyDir, }); await initKeys(client); expect(fs.existsSync(path.join(keyDir, 'private_key.pem'))).toBe(true); expect(fs.existsSync(path.join(keyDir, 'public_key.pem'))).toBe(true); client.dispose(); }); test('private key is written owner-only (0600)', async () => { const keyDir = path.join(tmpDir, 'keys'); const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, keyDir, }); await initKeys(client); const mode = fs.statSync(path.join(keyDir, 'private_key.pem')).mode & 0o777; expect(mode).toBe(0o600); client.dispose(); }); test('reuses the existing key pair across clients', async () => { const keyDir = path.join(tmpDir, 'keys'); const config = { routerUrl: 'https://router.test', keySize: 2048 as const, keyRotationInterval: 0, keyDir, }; const first = new SecureCompletionClient(config); await initKeys(first); const firstKey = await publicKeyOf(first); first.dispose(); const second = new SecureCompletionClient(config); await initKeys(second); expect(await publicKeyOf(second)).toBe(firstKey); second.dispose(); }); test('rotation persists into the configured directory', async () => { const keyDir = path.join(tmpDir, 'keys'); const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keySize: 2048, keyRotationInterval: 0, keyDir, }); await initKeys(client); const before = fs.readFileSync(path.join(keyDir, 'public_key.pem'), 'utf-8'); await (client as unknown as { rotateKeys: () => Promise }).rotateKeys(); const after = fs.readFileSync(path.join(keyDir, 'public_key.pem'), 'utf-8'); expect(after).not.toBe(before); expect(after.trim()).toBe((await publicKeyOf(client)).trim()); client.dispose(); }); });