Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions workspaces/boost/.changeset/connector-config-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@red-hat-developer-hub/backstage-plugin-boost-backend': minor
---

Add optional `defaultValue` field to `ConfigFieldMeta` and apply it as a third
precedence layer in `RuntimeConfigResolver`: DB override, YAML baseline, field
default, undefined. Connector defaults: `schedule.intervalMs` 300000,
`batchSize` 100, `timeout.connectionMs` 30000 (Jira only). Bumps
`BOOST_CONFIG_SCHEMA_VERSION` from 4 to 5.
5 changes: 0 additions & 5 deletions workspaces/boost/.changeset/connector-config-schemas.md

This file was deleted.

10 changes: 9 additions & 1 deletion workspaces/boost/plugins/boost-backend/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export interface BackendApprovalStoreOptions {
}

// @public
export const BOOST_CONFIG_SCHEMA_VERSION = 4;
export const BOOST_CONFIG_SCHEMA_VERSION = 5;

// @public
export const boostAiProviderServiceFactory: ServiceFactory<
Expand Down Expand Up @@ -235,6 +235,7 @@ export const boostConfigFields: {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 300000;
};
readonly 'boost.connectors.jira.schedule.cron': {
readonly schema: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
Expand All @@ -245,11 +246,13 @@ export const boostConfigFields: {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 100;
};
readonly 'boost.connectors.jira.timeout.connectionMs': {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 30000;
};
readonly 'boost.connectors.github.enabled': {
readonly schema: z.ZodOptional<z.ZodBoolean>;
Expand All @@ -265,11 +268,13 @@ export const boostConfigFields: {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 300000;
};
readonly 'boost.connectors.github.batchSize': {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 100;
};
readonly 'boost.connectors.gitlab.enabled': {
readonly schema: z.ZodOptional<z.ZodBoolean>;
Expand All @@ -285,11 +290,13 @@ export const boostConfigFields: {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 300000;
};
readonly 'boost.connectors.gitlab.batchSize': {
readonly schema: z.ZodOptional<z.ZodNumber>;
readonly configScope: ConfigScope;
readonly description: string;
readonly defaultValue: 100;
};
};

Expand Down Expand Up @@ -318,6 +325,7 @@ export interface ClassifyOptions {
// @public
export interface ConfigFieldMeta<T extends z.ZodTypeAny = z.ZodTypeAny> {
configScope: ConfigScope;
defaultValue?: z.output<T>;
description: string;
schema: T;
sensitive?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,4 +587,278 @@ describe('RuntimeConfigResolver', () => {
expect(allConfig.get('boost.connectors.gitlab.enabled')).toBe(true);
});
});

describe('field defaults', () => {
it('returns field default when DB and YAML are both unset', async () => {
const config = createMockConfig({});
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(new Map()),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

expect(
await resolver.resolve('boost.connectors.jira.schedule.intervalMs'),
).toBe(300000);
expect(
await resolver.resolve('boost.connectors.github.schedule.intervalMs'),
).toBe(300000);
expect(
await resolver.resolve('boost.connectors.gitlab.schedule.intervalMs'),
).toBe(300000);
});

it('returns batchSize default when DB and YAML are both unset', async () => {
const config = createMockConfig({});
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(new Map()),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

expect(await resolver.resolve('boost.connectors.jira.batchSize')).toBe(
100,
);
expect(await resolver.resolve('boost.connectors.github.batchSize')).toBe(
100,
);
expect(await resolver.resolve('boost.connectors.gitlab.batchSize')).toBe(
100,
);
});

it('returns timeout.connectionMs default for Jira when unset', async () => {
const config = createMockConfig({});
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(new Map()),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

expect(
await resolver.resolve('boost.connectors.jira.timeout.connectionMs'),
).toBe(30000);
});

it('YAML value takes precedence over field default', async () => {
const config = createMockConfig({
boost: {
connectors: {
jira: {
schedule: { intervalMs: 600000 },
batchSize: 50,
timeout: { connectionMs: 15000 },
},
},
},
});

const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(new Map()),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

expect(
await resolver.resolve('boost.connectors.jira.schedule.intervalMs'),
).toBe(600000);
expect(await resolver.resolve('boost.connectors.jira.batchSize')).toBe(
50,
);
expect(
await resolver.resolve('boost.connectors.jira.timeout.connectionMs'),
).toBe(15000);
});

it('DB override takes precedence over YAML and field default', async () => {
const config = createMockConfig({
boost: {
connectors: {
jira: {
schedule: { intervalMs: 600000 },
batchSize: 50,
},
},
},
});

const dbOverrides = new Map<string, unknown>([
['boost.connectors.jira.schedule.intervalMs', 120000],
['boost.connectors.jira.batchSize', 25],
]);
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(dbOverrides),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

expect(
await resolver.resolve('boost.connectors.jira.schedule.intervalMs'),
).toBe(120000);
expect(await resolver.resolve('boost.connectors.jira.batchSize')).toBe(
25,
);
});

it('falls back to default after removeOverride when no YAML', async () => {
const config = createMockConfig({});

let dbOverrides = new Map<string, unknown>([
['boost.connectors.jira.batchSize', 25],
]);
const adminConfigService = {
getAllOverrides: jest.fn().mockImplementation(async () => dbOverrides),
removeOverride: jest.fn().mockImplementation(async () => {
dbOverrides = new Map();
}),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

// DB override is present
expect(await resolver.resolve('boost.connectors.jira.batchSize')).toBe(
25,
);

// Remove override and invalidate
await resolver.remove('boost.connectors.jira.batchSize');

// Should fall back to field default
expect(await resolver.resolve('boost.connectors.jira.batchSize')).toBe(
100,
);
});

it('returns undefined for fields without defaults', async () => {
const config = createMockConfig({});
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(new Map()),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

// enabled, endpoint, schedule.cron have no defaults
expect(
await resolver.resolve('boost.connectors.jira.enabled'),
).toBeUndefined();
expect(
await resolver.resolve('boost.connectors.jira.endpoint'),
).toBeUndefined();
expect(
await resolver.resolve('boost.connectors.jira.schedule.cron'),
).toBeUndefined();
});

it('resolveAll includes field defaults for unset keys', async () => {
const config = createMockConfig({});
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(new Map()),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

const allConfig = await resolver.resolveAll();

// Defaults should be present
expect(allConfig.get('boost.connectors.jira.schedule.intervalMs')).toBe(
300000,
);
expect(allConfig.get('boost.connectors.jira.batchSize')).toBe(100);
expect(allConfig.get('boost.connectors.jira.timeout.connectionMs')).toBe(
30000,
);
expect(allConfig.get('boost.connectors.github.schedule.intervalMs')).toBe(
300000,
);
expect(allConfig.get('boost.connectors.github.batchSize')).toBe(100);
expect(allConfig.get('boost.connectors.gitlab.schedule.intervalMs')).toBe(
300000,
);
expect(allConfig.get('boost.connectors.gitlab.batchSize')).toBe(100);

// Fields without defaults should NOT be present
expect(allConfig.has('boost.connectors.jira.enabled')).toBe(false);
expect(allConfig.has('boost.connectors.jira.endpoint')).toBe(false);
expect(allConfig.has('boost.connectors.jira.schedule.cron')).toBe(false);
});

it('resolveAll does not override YAML or DB values with defaults', async () => {
const config = createMockConfig({
boost: {
connectors: {
jira: {
schedule: { intervalMs: 600000 },
},
},
},
});

const dbOverrides = new Map<string, unknown>([
['boost.connectors.jira.batchSize', 250],
]);
const adminConfigService = {
getAllOverrides: jest.fn().mockResolvedValue(dbOverrides),
} as unknown as AdminConfigService;

const resolver = new RuntimeConfigResolver({
cache,
config,
adminConfigService,
logger,
});

const allConfig = await resolver.resolveAll();

// YAML value beats default
expect(allConfig.get('boost.connectors.jira.schedule.intervalMs')).toBe(
600000,
);
// DB override beats default
expect(allConfig.get('boost.connectors.jira.batchSize')).toBe(250);
// No YAML or DB — default applied
expect(allConfig.get('boost.connectors.jira.timeout.connectionMs')).toBe(
30000,
);
});
});
});
Loading
Loading