Skip to content

Commit dc1655f

Browse files
author
eust-w
committed
fix: address CodeRabbit review feedback for multilingual PR
- Fix code block language tag in spec (markdownlint MD040) - Fix extend mode language selection to prompt user with stored language as default - Add tests for extend mode language change scenarios Addresses CodeRabbit feedback on PR Fission-AI#300
1 parent b3b2df3 commit dc1655f

File tree

2 files changed

+90
-24
lines changed

2 files changed

+90
-24
lines changed

src/core/init.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,11 @@ export class InitCommand {
400400

401401
this.renderBanner(extendMode);
402402

403+
// Read existing config before language selection to detect language changes
404+
const existingConfigBeforeLanguageChange = extendMode
405+
? await this.readConfigFile(openspecPath)
406+
: null;
407+
403408
// Get language configuration first (before AI tool selection)
404409
const selectedLanguage = await this.getSelectedLanguage(openspecPath, extendMode);
405410

@@ -447,7 +452,7 @@ export class InitCommand {
447452
)
448453
);
449454
await this.createDirectoryStructure(openspecPath);
450-
await this.ensureTemplateFiles(openspecPath, config);
455+
await this.ensureTemplateFiles(openspecPath, config, existingConfigBeforeLanguageChange);
451456
}
452457

453458
// Step 2: Configure AI tools
@@ -505,27 +510,23 @@ export class InitCommand {
505510
openspecPath: string,
506511
extendMode: boolean
507512
): Promise<string> {
508-
// Try to read existing config first
513+
// Non-interactive mode - check for --language flag first
514+
const nonInteractiveLanguage = this.resolveLanguageArg();
515+
if (nonInteractiveLanguage !== null) {
516+
return nonInteractiveLanguage;
517+
}
518+
519+
// Try to read existing config for default value
520+
let defaultLanguage = DEFAULT_LANGUAGE;
509521
if (extendMode) {
510522
const existingConfig = await this.readConfigFile(openspecPath);
511523
if (existingConfig?.language) {
512-
// In extend mode, use existing language unless explicitly changed
513-
const nonInteractiveLanguage = this.resolveLanguageArg();
514-
if (nonInteractiveLanguage !== null) {
515-
return nonInteractiveLanguage;
516-
}
517-
return existingConfig.language;
524+
defaultLanguage = existingConfig.language;
518525
}
519526
}
520527

521-
// Non-interactive mode
522-
const nonInteractiveLanguage = this.resolveLanguageArg();
523-
if (nonInteractiveLanguage !== null) {
524-
return nonInteractiveLanguage;
525-
}
526-
527-
// Interactive mode - prompt for language
528-
return this.promptForLanguage();
528+
// Interactive mode - prompt for language with default
529+
return this.promptForLanguage(defaultLanguage);
529530
}
530531

531532
private resolveLanguageArg(): string | null {
@@ -555,7 +556,7 @@ export class InitCommand {
555556
return language.code;
556557
}
557558

558-
private async promptForLanguage(): Promise<string> {
559+
private async promptForLanguage(defaultLanguage: string = DEFAULT_LANGUAGE): Promise<string> {
559560
const { select } = await import('@inquirer/prompts');
560561

561562
const language = await select({
@@ -564,7 +565,7 @@ export class InitCommand {
564565
name: `${lang.nativeName} (${lang.code})`,
565566
value: lang.code,
566567
})),
567-
default: DEFAULT_LANGUAGE,
568+
default: defaultLanguage,
568569
});
569570

570571
return language;
@@ -829,9 +830,18 @@ export class InitCommand {
829830

830831
private async ensureTemplateFiles(
831832
openspecPath: string,
832-
config: OpenSpecConfig
833+
config: OpenSpecConfig,
834+
previousConfig: OpenSpecConfig | null = null
833835
): Promise<void> {
834-
await this.writeTemplateFiles(openspecPath, config, true);
836+
// Check if language has changed - if so, regenerate templates
837+
// Use previousConfig if provided (read before language was saved), otherwise read from file
838+
const existingConfig = previousConfig || await this.readConfigFile(openspecPath);
839+
const languageChanged = existingConfig?.language &&
840+
existingConfig.language !== config.language;
841+
842+
// If language changed, force regeneration (skipExisting = false)
843+
// Otherwise, skip existing files to preserve user modifications
844+
await this.writeTemplateFiles(openspecPath, config, !languageChanged);
835845
}
836846

837847
private async writeTemplateFiles(
@@ -1074,3 +1084,4 @@ export class InitCommand {
10741084
}).start();
10751085
}
10761086
}
1087+

test/core/init.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import path from 'path';
44
import os from 'os';
55
import { InitCommand } from '../../src/core/init.js';
66

7+
// Mock @inquirer/prompts for language selection
8+
const mockSelect = vi.fn();
9+
vi.mock('@inquirer/prompts', async () => {
10+
const actual = await vi.importActual('@inquirer/prompts');
11+
return {
12+
...actual,
13+
select: mockSelect,
14+
};
15+
});
16+
717
const DONE = '__done__';
818

919
type SelectionQueue = string[][];
@@ -43,6 +53,7 @@ describe('InitCommand', () => {
4353
await fs.mkdir(testDir, { recursive: true });
4454
selectionQueue = [];
4555
mockPrompt.mockReset();
56+
mockSelect.mockReset();
4657
initCommand = new InitCommand({ prompt: mockPrompt });
4758

4859
// Route Codex global directory into the test sandbox
@@ -51,6 +62,11 @@ describe('InitCommand', () => {
5162

5263
// Mock console.log to suppress output during tests
5364
vi.spyOn(console, 'log').mockImplementation(() => {});
65+
66+
// Default language selection mock - return default language unless overridden
67+
mockSelect.mockImplementation(async (options: any) => {
68+
return options.default || 'en-US';
69+
});
5470
});
5571

5672
afterEach(async () => {
@@ -1296,22 +1312,61 @@ describe('InitCommand', () => {
12961312
expect(projectContent).toContain('Project Context');
12971313
});
12981314

1299-
it('should read existing language config in extend mode', async () => {
1315+
it('should read existing language config in extend mode and allow changing', async () => {
13001316
// First init with Chinese
13011317
const firstCommand = new InitCommand({ tools: 'none', language: 'zh-CN' });
13021318
await firstCommand.execute(testDir);
13031319

1304-
// Second init in extend mode should use existing language
1320+
// Second init in extend mode - should prompt with Chinese as default, but allow changing
1321+
// Mock language selection to return French (fr-FR)
1322+
mockSelect.mockResolvedValueOnce('fr-FR');
13051323
queueSelections(DONE);
13061324
await initCommand.execute(testDir);
13071325

1326+
// Verify language was changed to French
1327+
const configPath = path.join(testDir, 'openspec', 'config.json');
1328+
const configContent = await fs.readFile(configPath, 'utf-8');
1329+
const config = JSON.parse(configContent);
1330+
expect(config.language).toBe('fr-FR');
1331+
1332+
// Verify French templates were used
13081333
const projectPath = path.join(testDir, 'openspec', 'project.md');
13091334
const projectContent = await fs.readFile(projectPath, 'utf-8');
1310-
// Should still be Chinese (though file might not be regenerated in extend mode)
1335+
expect(projectContent).toContain('Contexte');
1336+
1337+
// Verify language prompt was called with Chinese as default
1338+
expect(mockSelect).toHaveBeenCalledWith(
1339+
expect.objectContaining({
1340+
message: 'Select your preferred language:',
1341+
default: 'zh-CN',
1342+
})
1343+
);
1344+
});
1345+
1346+
it('should use existing language as default in extend mode when user confirms', async () => {
1347+
// First init with Japanese
1348+
const firstCommand = new InitCommand({ tools: 'none', language: 'ja-JP' });
1349+
await firstCommand.execute(testDir);
1350+
1351+
// Second init in extend mode - user confirms default (Japanese)
1352+
// Mock language selection to return Japanese (default)
1353+
mockSelect.mockResolvedValueOnce('ja-JP');
1354+
queueSelections(DONE);
1355+
await initCommand.execute(testDir);
1356+
1357+
// Verify language remains Japanese
13111358
const configPath = path.join(testDir, 'openspec', 'config.json');
13121359
const configContent = await fs.readFile(configPath, 'utf-8');
13131360
const config = JSON.parse(configContent);
1314-
expect(config.language).toBe('zh-CN');
1361+
expect(config.language).toBe('ja-JP');
1362+
1363+
// Verify language prompt was called with Japanese as default
1364+
expect(mockSelect).toHaveBeenCalledWith(
1365+
expect.objectContaining({
1366+
message: 'Select your preferred language:',
1367+
default: 'ja-JP',
1368+
})
1369+
);
13151370
});
13161371

13171372
it('should use French templates when language is fr-FR', async () => {

0 commit comments

Comments
 (0)