This commit is contained in:
2026-10-18 04:39:37 +08:00
parent ff366a4daf
commit e686763c35
21 changed files with 951 additions and 339 deletions

View File

@@ -734,4 +734,64 @@ describe('coerceLoadedSettings(schema 兜底)', () => {
expect(coerceLoadedSettings(undefined)).toEqual({ ...DEFAULT_CONFIG });
expect(coerceLoadedSettings('garbage')).toEqual({ ...DEFAULT_CONFIG });
});
});
// aiApiKey 包络降级路径(loadConfig 解密失败 → pendingAiKeyEnvelope)。
// 单测环境没有 Electron safeStorage → secretStore.decrypt 对 enc:v1: 包络
// 恒返回 ''(「不可用」契约),天然进入降级分支,无需额外 mock。
describe('aiApiKey 包络降级(decrypt 失败)', () => {
const ENVELOPE = 'enc:v1:Zm9vYmFy';
it('解密失败:内存 aiApiKey 置空(明文契约)+ hasPendingAiKeyEnvelope 为 true', () => {
writeFileSync(join(mockUserDataDir, 'config.json'), JSON.stringify({ aiApiKey: ENVELOPE, theme: 'dark' }), 'utf-8');
configStore.init();
expect(configStore.getConfig().aiApiKey).toBe('');
expect(configStore.hasPendingAiKeyEnvelope()).toBe(true);
});
it('saveConfig 未触碰 aiApiKey:包络原样写回磁盘,不被空串覆盖', async () => {
writeFileSync(join(mockUserDataDir, 'config.json'), JSON.stringify({ aiApiKey: ENVELOPE, theme: 'dark' }), 'utf-8');
configStore.init();
const res = await configStore.saveConfig({ theme: 'light' });
expect(res.ok).toBe(true);
const raw = JSON.parse(readFileSync(join(mockUserDataDir, 'config.json'), 'utf-8'));
expect(raw.aiApiKey).toBe(ENVELOPE);
expect(raw.theme).toBe('light');
// 内存侧仍守「aiApiKey 是明文或 ''」契约:不把包络塞回去
expect(configStore.getConfig().aiApiKey).toBe('');
});
it('saveConfig 显式重填 Key:写新值、清掉旧包络', async () => {
writeFileSync(join(mockUserDataDir, 'config.json'), JSON.stringify({ aiApiKey: ENVELOPE }), 'utf-8');
configStore.init();
const res = await configStore.saveConfig({ aiApiKey: 'sk-new' });
expect(res.ok).toBe(true);
expect(configStore.hasPendingAiKeyEnvelope()).toBe(false);
// 测试环境 safeStorage 不可用 → encrypt 降级为明文透传
const raw = JSON.parse(readFileSync(join(mockUserDataDir, 'config.json'), 'utf-8'));
expect(raw.aiApiKey).toBe('sk-new');
expect(configStore.getConfig().aiApiKey).toBe('sk-new');
});
it('saveConfig 显式清空 Key:写空串,包络不复活', async () => {
writeFileSync(join(mockUserDataDir, 'config.json'), JSON.stringify({ aiApiKey: ENVELOPE }), 'utf-8');
configStore.init();
const res = await configStore.saveConfig({ aiApiKey: '' });
expect(res.ok).toBe(true);
expect(configStore.hasPendingAiKeyEnvelope()).toBe(false);
const raw = JSON.parse(readFileSync(join(mockUserDataDir, 'config.json'), 'utf-8'));
expect(raw.aiApiKey).toBe('');
});
it('明文 passthrough(无前缀)正常解密:不留包络', () => {
writeFileSync(join(mockUserDataDir, 'config.json'), JSON.stringify({ aiApiKey: 'sk-plain' }), 'utf-8');
configStore.init();
expect(configStore.getConfig().aiApiKey).toBe('sk-plain');
expect(configStore.hasPendingAiKeyEnvelope()).toBe(false);
});
it('首次启动(无 config.json)不残留包络状态', () => {
configStore.init();
expect(configStore.hasPendingAiKeyEnvelope()).toBe(false);
});
});