Fix AI model registry staleness on self-hosted instances (#19427)
## Summary Fixes #19422. Self-hosted users hitting "No AI models are available" after configuring API keys via the admin panel were victims of a stale `AiModelRegistryService` cache. Only the four `addAiProvider` / `removeAiProvider` / `addModelToProvider` / `removeModelFromProvider` mutations called `refreshRegistry()` — setting an API key through `set/update/deleteDatabaseConfigVariable` left the registry pointing at the pre-mutation provider state. Rather than patch each mutation site (and re-introduce the same class of bug on the next one), the registry now invalidates lazily based on the LLM config-group hash, mirroring the pattern `WebSearchDriverFactory` already uses via `DriverFactoryBase`. Any mutation to an LLM-tagged config variable is picked up automatically on the next read — callers never have to remember to refresh. - Extracted `getConfigGroupHash` into a shared util reused by both `DriverFactoryBase` and `AiModelRegistryService` (and switched the hash to `JSON.stringify` so object-typed config vars like `AI_PROVIDERS` actually contribute meaningfully). - `AiModelRegistryService` gates all internal `Map` access behind private getters that call `ensureFresh()`, so future read paths can't accidentally observe stale state. Build path uses underscored backing fields directly to avoid recursing. - Dropped the now-redundant `refreshRegistry()` public method and its four call sites in `admin-panel.resolver.ts`. ## Test plan - [x] `nx typecheck twenty-server` passes - [x] Existing admin-panel + ai-models specs pass (79 tests) - [ ] Manual: on a self-hosted instance, set `OPENAI_API_KEY` (or another provider key) via the admin panel `setDatabaseConfigVariable` mutation and confirm AI features work without a server restart - [ ] Manual: existing flows (`addAiProvider`, `addModelToProvider`, etc.) still pick up new providers on the next read 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -2,15 +2,20 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EmailDriverFactory } from 'src/engine/core-modules/email/email-driver.factory';
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
|
||||
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
describe('EmailDriverFactory', () => {
|
||||
let factory: EmailDriverFactory;
|
||||
let twentyConfigService: TwentyConfigService;
|
||||
let configGroupHashService: ConfigGroupHashService;
|
||||
|
||||
const mockTwentyConfigService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
const mockConfigGroupHashService = {
|
||||
computeHash: jest.fn().mockReturnValue(''),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -20,11 +25,18 @@ describe('EmailDriverFactory', () => {
|
||||
provide: TwentyConfigService,
|
||||
useValue: mockTwentyConfigService,
|
||||
},
|
||||
{
|
||||
provide: ConfigGroupHashService,
|
||||
useValue: mockConfigGroupHashService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
factory = module.get<EmailDriverFactory>(EmailDriverFactory);
|
||||
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
||||
configGroupHashService = module.get<ConfigGroupHashService>(
|
||||
ConfigGroupHashService,
|
||||
);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -44,7 +56,7 @@ describe('EmailDriverFactory', () => {
|
||||
it('should return smtp config key for smtp driver', () => {
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(EmailDriver.SMTP);
|
||||
jest
|
||||
.spyOn(factory as any, 'getConfigGroupHash')
|
||||
.spyOn(configGroupHashService, 'computeHash')
|
||||
.mockReturnValue('smtp-hash-123');
|
||||
|
||||
const result = factory['buildConfigKey']();
|
||||
@@ -179,7 +191,7 @@ describe('EmailDriverFactory', () => {
|
||||
}
|
||||
});
|
||||
jest
|
||||
.spyOn(factory as any, 'getConfigGroupHash')
|
||||
.spyOn(configGroupHashService, 'computeHash')
|
||||
.mockReturnValue('smtp-hash-123');
|
||||
|
||||
const driver2 = factory.getCurrentDriver();
|
||||
@@ -214,7 +226,7 @@ describe('EmailDriverFactory', () => {
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(factory as any, 'getConfigGroupHash')
|
||||
.spyOn(configGroupHashService, 'computeHash')
|
||||
.mockReturnValue('smtp-hash-123');
|
||||
|
||||
jest.spyOn(factory as any, 'createDriver').mockImplementation(() => {
|
||||
|
||||
@@ -7,12 +7,16 @@ import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
|
||||
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface> {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
configGroupHashService: ConfigGroupHashService,
|
||||
) {
|
||||
super(twentyConfigService, configGroupHashService);
|
||||
}
|
||||
|
||||
protected buildConfigKey(): string {
|
||||
@@ -23,7 +27,7 @@ export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface>
|
||||
}
|
||||
|
||||
if (driver === EmailDriver.SMTP) {
|
||||
const emailConfigHash = this.getConfigGroupHash(
|
||||
const emailConfigHash = this.configGroupHashService.computeHash(
|
||||
ConfigVariablesGroup.EMAIL_SETTINGS,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user