Skip to content

Commit 3565a07

Browse files
committed
feat(rules): allow agent language to follow project config
- Override agent's communication.language with codingbuddy.config.js language setting at runtime. This enables consistent language responses across all agents based on project configuration. close #220
1 parent 6eec17e commit 3565a07

3 files changed

Lines changed: 166 additions & 2 deletions

File tree

apps/mcp-server/src/rules/rules.service.spec.ts

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ const createMockCustomService = (): CustomService =>
2828
}) as unknown as CustomService;
2929

3030
// Create a mock ConfigService
31-
const createMockConfigService = (): ConfigService =>
31+
const createMockConfigService = (language?: string): ConfigService =>
3232
({
3333
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
3434
getSettings: vi.fn().mockResolvedValue({}),
35+
getLanguage: vi.fn().mockResolvedValue(language),
3536
}) as unknown as ConfigService;
3637

3738
describe('RulesService', () => {
@@ -307,6 +308,141 @@ describe('RulesService', () => {
307308
'Failed to read rule file',
308309
);
309310
});
311+
312+
describe('language override from config', () => {
313+
it('should override communication.language with config language', async () => {
314+
const mockAgent = {
315+
name: 'Frontend Developer',
316+
description: 'Frontend development specialist',
317+
role: {
318+
title: 'Senior Frontend Developer',
319+
expertise: ['React', 'TypeScript'],
320+
},
321+
communication: {
322+
language: 'en',
323+
style: 'Technical and precise',
324+
},
325+
};
326+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockAgent));
327+
328+
// Create service with config language 'ko'
329+
const serviceWithLang = new RulesService(
330+
createMockCustomService(),
331+
createMockConfigService('ko'),
332+
);
333+
334+
const result = await serviceWithLang.getAgent('frontend-developer');
335+
336+
expect(result.communication?.language).toBe('ko');
337+
// Other communication properties should be preserved
338+
expect(result.communication?.style).toBe('Technical and precise');
339+
});
340+
341+
it('should preserve agent language when config has no language', async () => {
342+
const mockAgent = {
343+
name: 'Frontend Developer',
344+
description: 'Frontend development specialist',
345+
role: {
346+
title: 'Senior Frontend Developer',
347+
expertise: ['React', 'TypeScript'],
348+
},
349+
communication: {
350+
language: 'en',
351+
},
352+
};
353+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockAgent));
354+
355+
// Create service with no config language
356+
const serviceWithoutLang = new RulesService(
357+
createMockCustomService(),
358+
createMockConfigService(undefined),
359+
);
360+
361+
const result = await serviceWithoutLang.getAgent('frontend-developer');
362+
363+
expect(result.communication?.language).toBe('en');
364+
});
365+
366+
it('should create communication object with config language when agent has none', async () => {
367+
const mockAgent = {
368+
name: 'Frontend Developer',
369+
description: 'Frontend development specialist',
370+
role: {
371+
title: 'Senior Frontend Developer',
372+
expertise: ['React', 'TypeScript'],
373+
},
374+
// No communication field
375+
};
376+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockAgent));
377+
378+
// Create service with config language 'ja'
379+
const serviceWithLang = new RulesService(
380+
createMockCustomService(),
381+
createMockConfigService('ja'),
382+
);
383+
384+
const result = await serviceWithLang.getAgent('frontend-developer');
385+
386+
expect(result.communication?.language).toBe('ja');
387+
});
388+
389+
it('should not modify agent when config language is undefined and agent has no communication', async () => {
390+
const mockAgent = {
391+
name: 'Frontend Developer',
392+
description: 'Frontend development specialist',
393+
role: {
394+
title: 'Senior Frontend Developer',
395+
expertise: ['React', 'TypeScript'],
396+
},
397+
// No communication field
398+
};
399+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockAgent));
400+
401+
// Create service without config language
402+
const serviceWithoutLang = new RulesService(
403+
createMockCustomService(),
404+
createMockConfigService(undefined),
405+
);
406+
407+
const result = await serviceWithoutLang.getAgent('frontend-developer');
408+
409+
expect(result.communication).toBeUndefined();
410+
});
411+
412+
it('should return agent with original language when getLanguage() fails', async () => {
413+
const mockAgent = {
414+
name: 'Frontend Developer',
415+
description: 'Frontend development specialist',
416+
role: {
417+
title: 'Senior Frontend Developer',
418+
expertise: ['React', 'TypeScript'],
419+
},
420+
communication: {
421+
language: 'en',
422+
style: 'Technical and precise',
423+
},
424+
};
425+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockAgent));
426+
427+
// Create service with failing getLanguage
428+
const failingConfigService = {
429+
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
430+
getSettings: vi.fn().mockResolvedValue({}),
431+
getLanguage: vi.fn().mockRejectedValue(new Error('Config error')),
432+
} as unknown as ConfigService;
433+
434+
const serviceWithError = new RulesService(
435+
createMockCustomService(),
436+
failingConfigService,
437+
);
438+
439+
const result = await serviceWithError.getAgent('frontend-developer');
440+
441+
// Should still return agent with original language
442+
expect(result.communication?.language).toBe('en');
443+
expect(result.communication?.style).toBe('Technical and precise');
444+
});
445+
});
310446
});
311447

312448
describe('searchRules', () => {

apps/mcp-server/src/rules/rules.service.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,27 @@ export class RulesService {
128128
// Validate against schema and check for prototype pollution
129129
const validated = parseAgentProfile(parsed);
130130
// Add source field for default agents
131-
return { ...(validated as unknown as AgentProfile), source: 'default' };
131+
const agent: AgentProfile = {
132+
...(validated as unknown as AgentProfile),
133+
source: 'default',
134+
};
135+
136+
// Override communication.language with config language if available
137+
try {
138+
const configLanguage = await this.configService.getLanguage();
139+
if (configLanguage) {
140+
agent.communication = {
141+
...agent.communication,
142+
language: configLanguage,
143+
};
144+
}
145+
} catch (error) {
146+
this.logger.warn(
147+
`Failed to get config language for agent '${name}', using agent default: ${error instanceof Error ? error.message : 'Unknown error'}`,
148+
);
149+
}
150+
151+
return agent;
132152
} catch (error) {
133153
if (error instanceof AgentSchemaError) {
134154
this.logger.warn(`Invalid agent profile: ${name}`, error.message);

apps/mcp-server/src/rules/rules.types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
export type RuleSource = 'custom' | 'default';
22

3+
export interface AgentCommunication {
4+
language?: string;
5+
style?: string;
6+
approach?: string[];
7+
[key: string]: unknown; // Allow additional fields for extensibility
8+
}
9+
310
export interface AgentProfile {
411
name: string;
512
description: string;
@@ -9,6 +16,7 @@ export interface AgentProfile {
916
tech_stack_reference?: string;
1017
responsibilities?: string[];
1118
};
19+
communication?: AgentCommunication;
1220
source?: RuleSource;
1321
[key: string]: unknown; // Allow additional fields (passthrough)
1422
}

0 commit comments

Comments
 (0)