feat(config): persist config.patch updates atomically

This commit is contained in:
William Valentin
2026-02-15 22:03:21 -08:00
parent c314e0f067
commit 0220ec10dd
13 changed files with 205 additions and 11 deletions
+45 -3
View File
@@ -718,9 +718,10 @@ describe('config handlers', () => {
};
const result = await handlers['config.patch'](req) as GatewayResponse;
const r = result.result as { applied: string[]; rejected: string[] };
const r = result.result as { applied: string[]; rejected: string[]; persisted: boolean };
expect(r.applied).toEqual(['hooks.confirm', 'hooks.log']);
expect(r.rejected).toEqual([]);
expect(r.persisted).toBe(false);
// Verify the config was actually mutated
expect(config.hooks.confirm).toEqual(['shell.exec', 'file.write']);
expect(config.hooks.log).toEqual(['file.read']);
@@ -741,9 +742,10 @@ describe('config handlers', () => {
};
const result = await handlers['config.patch'](req) as GatewayResponse;
const r = result.result as { applied: string[]; rejected: string[] };
const r = result.result as { applied: string[]; rejected: string[]; persisted: boolean };
expect(r.applied).toEqual(['hooks.confirm']);
expect(r.rejected).toEqual(['telegram.bot_token']);
expect(r.persisted).toBe(false);
});
it('config.patch rejects invalid value types', async () => {
@@ -760,9 +762,49 @@ describe('config handlers', () => {
};
const result = await handlers['config.patch'](req) as GatewayResponse;
const r = result.result as { applied: string[]; rejected: string[] };
const r = result.result as { applied: string[]; rejected: string[]; persisted: boolean };
expect(r.applied).toEqual([]);
expect(r.rejected).toEqual(['hooks.confirm']);
expect(r.persisted).toBe(false);
});
it('config.patch persists changes when persistence callback is provided', async () => {
const config = makeConfig();
const persist = vi.fn();
const handlers = createConfigHandlers({ config: config as any, persistConfig: persist as any });
const req: GatewayRequest = {
id: 6,
method: 'config.patch',
params: { patches: { 'hooks.confirm': ['shell.exec', 'file.write'] } },
};
const result = await handlers['config.patch'](req) as GatewayResponse;
const r = result.result as { applied: string[]; rejected: string[]; persisted: boolean };
expect(r.applied).toEqual(['hooks.confirm']);
expect(r.rejected).toEqual([]);
expect(r.persisted).toBe(true);
expect(persist).toHaveBeenCalledTimes(1);
expect(config.hooks.confirm).toEqual(['shell.exec', 'file.write']);
});
it('config.patch does not mutate runtime config when persistence fails', async () => {
const config = makeConfig();
const before = [...config.hooks.confirm];
const persist = vi.fn().mockRejectedValue(new Error('disk full'));
const handlers = createConfigHandlers({ config: config as any, persistConfig: persist as any });
const req: GatewayRequest = {
id: 7,
method: 'config.patch',
params: { patches: { 'hooks.confirm': ['file.write'] } },
};
const result = await handlers['config.patch'](req) as GatewayResponse;
const r = result.result as { applied: string[]; rejected: string[]; persisted: boolean; persistError?: string };
expect(r.applied).toEqual([]);
expect(r.rejected).toEqual([]);
expect(r.persisted).toBe(false);
expect(r.persistError).toContain('disk full');
expect(config.hooks.confirm).toEqual(before);
});
it('config.patch requires patches object', async () => {