Skip to content

Commit 9a5b7be

Browse files
test(daemon): cover namespaced secret store resolve and remove paths
Add unit/integration coverage for providerCredentialSource, memory-vs-keychain resolveApiKey, RemoveProvider owned-secret deletion (and env/shared skips), ListSecrets per-backend rows, and HTTP memory delete/key-status isolation. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6c1e6b8 commit 9a5b7be

6 files changed

Lines changed: 349 additions & 12 deletions

File tree

packages/daemon/src/core/config.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import {
3131
getEnabledModelNames,
3232
isValidVirtualName,
3333
resolveEngineModelId,
34+
providerSecretName,
35+
isProviderOwnedSecretName,
36+
providerCredentialSource,
3437
type ConfigFile,
3538
} from './config.js';
3639

@@ -65,6 +68,73 @@ function createWorkspace(name: string, data: Record<string, unknown>): string {
6568

6669
// ── isValidVirtualName ────────────────────────────────────────────────────────
6770

71+
describe('providerSecretName / isProviderOwnedSecretName / providerCredentialSource', () => {
72+
it('prefers secret_name over legacy api_key_keychain_name', () => {
73+
expect(providerSecretName({ engine: 'openai', secret_name: 'SHARED' })).toBe('SHARED');
74+
expect(providerSecretName({
75+
engine: 'openai',
76+
secret_name: 'SHARED',
77+
api_key_keychain_name: 'LEGACY',
78+
})).toBe('SHARED');
79+
expect(providerSecretName({
80+
engine: 'openai',
81+
api_key_keychain_name: 'LEGACY',
82+
})).toBe('LEGACY');
83+
expect(providerSecretName({ engine: 'openai' })).toBeUndefined();
84+
});
85+
86+
it('recognizes only invented provider-owned secret names', () => {
87+
expect(isProviderOwnedSecretName('work-openai', 'WORK-OPENAI_API_KEY')).toBe(true);
88+
expect(isProviderOwnedSecretName('work-openai', 'abbenay.work-openai')).toBe(true);
89+
expect(isProviderOwnedSecretName('work-openai', 'SHARED_OPENAI')).toBe(false);
90+
expect(isProviderOwnedSecretName('work-openai', 'OPENAI_API_KEY')).toBe(false);
91+
});
92+
93+
it('resolves store backends with keychain default and memory override', () => {
94+
expect(providerCredentialSource({
95+
engine: 'openai',
96+
secret_name: 'K',
97+
})).toEqual({ kind: 'store', name: 'K', backend: 'keychain' });
98+
99+
expect(providerCredentialSource({
100+
engine: 'openai',
101+
secret_name: 'K',
102+
secret_store: 'memory',
103+
})).toEqual({ kind: 'store', name: 'K', backend: 'memory' });
104+
105+
expect(providerCredentialSource({
106+
engine: 'openai',
107+
secret_name: 'K',
108+
secret_store: 'keychain',
109+
})).toEqual({ kind: 'store', name: 'K', backend: 'keychain' });
110+
});
111+
112+
it('resolves env via secret_store=env or legacy api_key_env_var_name', () => {
113+
expect(providerCredentialSource({
114+
engine: 'openai',
115+
secret_name: 'MY_ENV',
116+
secret_store: 'env',
117+
})).toEqual({ kind: 'env', name: 'MY_ENV' });
118+
119+
expect(providerCredentialSource({
120+
engine: 'openai',
121+
secret_store: 'env',
122+
api_key_env_var_name: 'LEGACY_ENV',
123+
})).toEqual({ kind: 'env', name: 'LEGACY_ENV' });
124+
125+
expect(providerCredentialSource({
126+
engine: 'openai',
127+
api_key_env_var_name: 'ONLY_ENV',
128+
})).toEqual({ kind: 'env', name: 'ONLY_ENV' });
129+
130+
expect(providerCredentialSource({ engine: 'openai' })).toBeNull();
131+
expect(providerCredentialSource({
132+
engine: 'openai',
133+
secret_store: 'env',
134+
})).toBeNull();
135+
});
136+
});
137+
68138
describe('isValidVirtualName', () => {
69139
it('should accept simple lowercase names', () => {
70140
expect(isValidVirtualName('openrouter')).toBe(true);

packages/daemon/src/core/state.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ConfigFile } from './config.js';
77
import type { DiscoveredModel } from './engines.js';
88
import { MemorySecretStore } from './secrets.js';
99
import { ToolRegistry } from './tool-registry.js';
10+
import type { NamespacedSecretStore } from './secrets.js';
1011

1112
// ── Partial engines mock (preserve real getEngine for addProvider policy tests) ──
1213

@@ -260,6 +261,68 @@ describe('CoreState.resolveApiKey', () => {
260261
expect(await core.resolveApiKey('openrouter')).toBe('sk-from-store');
261262
});
262263

264+
it('reads only the configured memory backend from a namespaced store', async () => {
265+
const memory = new Map<string, string>([['SHARED', 'from-memory']]);
266+
const keychain = new Map<string, string>([['SHARED', 'from-keychain']]);
267+
const store: NamespacedSecretStore = {
268+
get: async (key) => keychain.get(key) ?? null,
269+
set: async (key, value) => { keychain.set(key, value); },
270+
delete: async (key) => keychain.delete(key),
271+
has: async (key) => keychain.has(key),
272+
getFrom: async (backend, key) => (backend === 'memory' ? memory : keychain).get(key) ?? null,
273+
setIn: async (backend, key, value) => { (backend === 'memory' ? memory : keychain).set(key, value); },
274+
deleteFrom: async (backend, key) => (backend === 'memory' ? memory : keychain).delete(key),
275+
hasIn: async (backend, key) => (backend === 'memory' ? memory : keychain).has(key),
276+
};
277+
const core = createCore({
278+
secretStore: store,
279+
config: {
280+
providers: {
281+
mem: {
282+
engine: 'openrouter',
283+
secret_name: 'SHARED',
284+
secret_store: 'memory',
285+
models: {},
286+
},
287+
kc: {
288+
engine: 'openrouter',
289+
secret_name: 'SHARED',
290+
secret_store: 'keychain',
291+
models: {},
292+
},
293+
},
294+
},
295+
});
296+
297+
expect(await core.resolveApiKey('mem')).toBe('from-memory');
298+
expect(await core.resolveApiKey('kc')).toBe('from-keychain');
299+
});
300+
301+
it('resolves secret_store=env without consulting the secret store', async () => {
302+
const store = new MemorySecretStore();
303+
await store.set('SHOULD_NOT_USE', 'from-store');
304+
const core = createCore({
305+
secretStore: store,
306+
config: {
307+
providers: {
308+
envp: {
309+
engine: 'openrouter',
310+
secret_name: 'ABBENAY_TEST_RESOLVE_ENV',
311+
secret_store: 'env',
312+
models: {},
313+
},
314+
},
315+
},
316+
});
317+
318+
process.env.ABBENAY_TEST_RESOLVE_ENV = 'from-env';
319+
try {
320+
expect(await core.resolveApiKey('envp')).toBe('from-env');
321+
} finally {
322+
delete process.env.ABBENAY_TEST_RESOLVE_ENV;
323+
}
324+
});
325+
263326
it('falls back to configured env var then engine default env var', async () => {
264327
const core = createCore({
265328
config: {

packages/daemon/src/daemon/server/abbenay-service.test.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ vi.mock('../../core/session-summarizer.js', () => ({
8888
generateSessionSummary: (...a: unknown[]) => mockGenerateSessionSummary(...a),
8989
}));
9090

91-
vi.mock('../../core/secrets.js', () => ({
92-
auditSecretChange: (...a: unknown[]) => mockAuditSecretChange(...a),
93-
}));
91+
vi.mock('../../core/secrets.js', async (importOriginal) => {
92+
const actual = await importOriginal<typeof import('../../core/secrets.js')>();
93+
return {
94+
...actual,
95+
auditSecretChange: (...a: unknown[]) => mockAuditSecretChange(...a),
96+
};
97+
});
9498

9599
vi.mock('../../core/provider-endpoint.js', () => ({
96100
auditProviderEndpointChange: (...a: unknown[]) => mockAuditProviderEndpointChange(...a),
@@ -995,6 +999,75 @@ describe('createAbbenayService handlers', () => {
995999
expect(state.notifyModelsChanged).toHaveBeenCalledWith('provider_removed');
9961000
});
9971001

1002+
it('RemoveProvider deletes owned memory secret from the memory backend only', async () => {
1003+
const { SecretStoreRegistry } = await import('../secrets/registry.js');
1004+
const { MemorySecretStore } = await import('../../core/secrets.js');
1005+
const memory = new MemorySecretStore();
1006+
const keychain = new MemorySecretStore();
1007+
await memory.set('MYMOCK_API_KEY', 'mem-value');
1008+
await keychain.set('MYMOCK_API_KEY', 'kc-value');
1009+
const registry = new SecretStoreRegistry(memory, keychain);
1010+
1011+
mockLoadConfig.mockReturnValue({
1012+
providers: {
1013+
mymock: {
1014+
engine: 'mock',
1015+
secret_name: 'MYMOCK_API_KEY',
1016+
secret_store: 'memory',
1017+
},
1018+
},
1019+
});
1020+
const state = createMockState({ secretStore: registry });
1021+
const service = createServiceHandlers(state);
1022+
1023+
const { error } = await invokeUnary(service.RemoveProvider, { provider_id: 'mymock' });
1024+
expect(error).toBeNull();
1025+
expect(await memory.has('MYMOCK_API_KEY')).toBe(false);
1026+
expect(await keychain.has('MYMOCK_API_KEY')).toBe(true);
1027+
expect(mockAuditSecretChange).toHaveBeenCalledWith({
1028+
key: 'MYMOCK_API_KEY',
1029+
op: 'delete',
1030+
source: 'grpc-configure',
1031+
});
1032+
});
1033+
1034+
it('RemoveProvider skips env-backed and shared secret names', async () => {
1035+
const { SecretStoreRegistry } = await import('../secrets/registry.js');
1036+
const { MemorySecretStore } = await import('../../core/secrets.js');
1037+
const memory = new MemorySecretStore();
1038+
const keychain = new MemorySecretStore();
1039+
await keychain.set('SHARED_OPENAI', 'shared');
1040+
await keychain.set('ENV_LOOKALIKE', 'should-remain');
1041+
const registry = new SecretStoreRegistry(memory, keychain);
1042+
const state = createMockState({ secretStore: registry });
1043+
const service = createServiceHandlers(state);
1044+
1045+
mockLoadConfig.mockReturnValue({
1046+
providers: {
1047+
'env-openai': {
1048+
engine: 'mock',
1049+
secret_name: 'ENV_LOOKALIKE',
1050+
secret_store: 'env',
1051+
},
1052+
},
1053+
});
1054+
expect((await invokeUnary(service.RemoveProvider, { provider_id: 'env-openai' })).error).toBeNull();
1055+
expect(await keychain.has('ENV_LOOKALIKE')).toBe(true);
1056+
1057+
mockLoadConfig.mockReturnValue({
1058+
providers: {
1059+
'shared-openai': {
1060+
engine: 'mock',
1061+
secret_name: 'SHARED_OPENAI',
1062+
secret_store: 'keychain',
1063+
},
1064+
},
1065+
});
1066+
expect((await invokeUnary(service.RemoveProvider, { provider_id: 'shared-openai' })).error).toBeNull();
1067+
expect(await keychain.has('SHARED_OPENAI')).toBe(true);
1068+
expect(mockAuditSecretChange).not.toHaveBeenCalled();
1069+
});
1070+
9981071
it('GetKeyStatus checks keychain and env sources', async () => {
9991072
const state = createMockState({
10001073
secretStore: { has: vi.fn().mockResolvedValue(true) },
@@ -1018,6 +1091,22 @@ describe('createAbbenayService handlers', () => {
10181091
expect(missing.error?.code).toBe(grpc.status.INVALID_ARGUMENT);
10191092
});
10201093

1094+
it('GetKeyStatus checks memory independently of keychain', async () => {
1095+
const { SecretStoreRegistry } = await import('../secrets/registry.js');
1096+
const { MemorySecretStore } = await import('../../core/secrets.js');
1097+
const memory = new MemorySecretStore();
1098+
const keychain = new MemorySecretStore();
1099+
await memory.set('MEM_ONLY', 'v');
1100+
const registry = new SecretStoreRegistry(memory, keychain);
1101+
const state = createMockState({ secretStore: registry });
1102+
const service = createServiceHandlers(state);
1103+
1104+
const mem = await invokeUnary(service.GetKeyStatus, { source: 'memory', name: 'MEM_ONLY' });
1105+
expect(mem.response?.exists).toBe(true);
1106+
const kc = await invokeUnary(service.GetKeyStatus, { source: 'keychain', name: 'MEM_ONLY' });
1107+
expect(kc.response?.exists).toBe(false);
1108+
});
1109+
10211110
it('ListMcpServerConfigs merges config and runtime statuses', async () => {
10221111
mockLoadConfig.mockReturnValue({
10231112
mcp_servers: {

packages/daemon/tests/integration/consumer-auth.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ describe('Consumer auth RPC gating', () => {
341341
}
342342
});
343343

344-
it('ConfigureProvider rejects secret_store=env is not required when unset', async () => {
344+
it('ConfigureProvider accepts secret_store=env even when the env var is unset', async () => {
345345
delete process.env.ABBENAY_TEST_PROVIDER_ENV_MISSING;
346346
const res = await callUnary(client, 'ConfigureProvider', {
347347
provider_id: 'env-missing',

packages/daemon/tests/integration/grpc-real-service.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,69 @@ describe('Real gRPC service: Secrets', () => {
337337
}),
338338
).rejects.toMatchObject({ code: grpc.status.INVALID_ARGUMENT });
339339
});
340+
341+
it('should delete SECRET_STORE_MEMORY without touching keychain', async () => {
342+
await callUnary(client, 'SetSecret', {
343+
key: 'BOTH_DEL',
344+
value: 'persistent',
345+
store: 'SECRET_STORE_KEYCHAIN',
346+
});
347+
await callUnary(client, 'SetSecret', {
348+
key: 'BOTH_DEL',
349+
value: 'ephemeral',
350+
store: 'SECRET_STORE_MEMORY',
351+
});
352+
353+
await callUnary(client, 'DeleteSecret', {
354+
key: 'BOTH_DEL',
355+
store: 'SECRET_STORE_MEMORY',
356+
});
357+
358+
const mem = await callUnary(client, 'GetSecret', {
359+
key: 'BOTH_DEL',
360+
store: 'SECRET_STORE_MEMORY',
361+
});
362+
expect(mem.value).toBe('');
363+
const kc = await callUnary(client, 'GetSecret', {
364+
key: 'BOTH_DEL',
365+
store: 'SECRET_STORE_KEYCHAIN',
366+
});
367+
expect(kc.value).toBe('persistent');
368+
expect(mockSecretStoreData.get('BOTH_DEL')).toBe('persistent');
369+
});
370+
371+
it('ListSecrets emits one row per backend that holds an engine key', async () => {
372+
mockGetEngines.mockReturnValueOnce([
373+
{
374+
id: 'openai',
375+
requiresKey: true,
376+
defaultEnvVar: 'OPENAI_API_KEY',
377+
supportsTools: true,
378+
createModel: () => { throw new Error('mock'); },
379+
},
380+
]);
381+
382+
await callUnary(client, 'SetSecret', {
383+
key: 'OPENAI_API_KEY',
384+
value: 'kc',
385+
store: 'SECRET_STORE_KEYCHAIN',
386+
});
387+
await callUnary(client, 'SetSecret', {
388+
key: 'OPENAI_API_KEY',
389+
value: 'mem',
390+
store: 'SECRET_STORE_MEMORY',
391+
});
392+
393+
const res = await callUnary(client, 'ListSecrets', {});
394+
const openaiRows = (res.secrets as Array<{ key: string; store?: string | number }>).filter(
395+
(s) => s.key === 'OPENAI_API_KEY' && s.store,
396+
);
397+
expect(openaiRows.length).toBe(2);
398+
const stores = openaiRows.map((r) => String(r.store));
399+
expect(stores).toEqual(
400+
expect.arrayContaining(['SECRET_STORE_MEMORY', 'SECRET_STORE_KEYCHAIN']),
401+
);
402+
});
340403
});
341404

342405
describe('Real gRPC service: Chat streaming', () => {

0 commit comments

Comments
 (0)