From 2735e700802cd88b908430339a4dcef2bb8b6e60 Mon Sep 17 00:00:00 2001 From: lqvp <183242690+lqvp@users.noreply.github.com> Date: Sat, 20 Dec 2025 19:48:41 +0900 Subject: [PATCH 1/4] feat: add InfoFeatureFlags type and resolveInfoFeatureFlags function with tests --- package.json | 2 +- src/modules/info/feature-flags.ts | 40 ++++++++++++++++++++++++++ src/modules/info/index.ts | 21 ++++++++------ test/info-feature-flags.test.mjs | 47 +++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 src/modules/info/feature-flags.ts create mode 100644 test/info-feature-flags.test.mjs diff --git a/package.json b/package.json index 49f6d30e..0f97c577 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dev:tsc": "chokidar \"src/**/*.ts\" -c \"pnpm build\"", "dev:nodemon": "nodemon --watch built --ext js --delay 1500ms built/index.js", "build": "tspc", - "test": "jest", + "test": "pnpm -s build && node --test test/*.test.mjs", "lint": "biome lint src --write", "format": "biome format src --write", "preinstall": "npx only-allow pnpm" diff --git a/src/modules/info/feature-flags.ts b/src/modules/info/feature-flags.ts new file mode 100644 index 00000000..9ad053c8 --- /dev/null +++ b/src/modules/info/feature-flags.ts @@ -0,0 +1,40 @@ +export type InfoFeatureFlags = { + keywordSearch: boolean; + reversi: boolean; + autoWord: boolean; + chart: boolean; + timeSignal: boolean; + serverMonitoring: boolean; + emojiCheck: boolean; + maze: boolean; + poll: boolean; +}; + +function isEnabledByDefaultUnlessFalse(value: boolean | undefined): boolean { + return value !== false; +} + +export function resolveInfoFeatureFlags(cfg: { + keywordEnabled?: boolean; + reversiEnabled?: boolean; + notingEnabled?: boolean; + chartEnabled?: boolean; + timeSignalEnabled?: boolean; + serverMonitoring?: boolean; + checkEmojisEnabled?: boolean; + mazeEnable?: boolean; + pollEnable?: boolean; +}): InfoFeatureFlags { + return { + keywordSearch: cfg.keywordEnabled === true, + reversi: cfg.reversiEnabled === true, + autoWord: isEnabledByDefaultUnlessFalse(cfg.notingEnabled), + chart: isEnabledByDefaultUnlessFalse(cfg.chartEnabled), + timeSignal: isEnabledByDefaultUnlessFalse(cfg.timeSignalEnabled), + serverMonitoring: cfg.serverMonitoring === true, + emojiCheck: cfg.checkEmojisEnabled === true, + maze: cfg.mazeEnable === true, + poll: cfg.pollEnable === true, + }; +} + diff --git a/src/modules/info/index.ts b/src/modules/info/index.ts index 6c788b87..39565a72 100644 --- a/src/modules/info/index.ts +++ b/src/modules/info/index.ts @@ -7,6 +7,7 @@ import type DatabaseManager from '@/database/DatabaseManager.js'; import * as fs from 'fs'; import path from 'path'; import os from 'os'; +import { resolveInfoFeatureFlags } from './feature-flags.js'; // 型定義 declare namespace NodeJS { @@ -288,41 +289,43 @@ function formatBooleanSetting(value: boolean | undefined): string { } function formatBasicFeatures(): string { + const flags = resolveInfoFeatureFlags(config); const lines: string[] = [ CONFIG_LABELS.sections.basicFeatures, `- ${CONFIG_LABELS.basic.keywordEnabled}: ${formatBooleanSetting( - config.keywordEnabled + flags.keywordSearch )}`, `- ${CONFIG_LABELS.basic.reversiEnabled}: ${formatBooleanSetting( - config.reversiEnabled + flags.reversi )}`, `- ${CONFIG_LABELS.basic.notingEnabled}: ${formatBooleanSetting( - config.notingEnabled + flags.autoWord )}`, `- ${CONFIG_LABELS.basic.chartEnabled}: ${formatBooleanSetting( - config.chartEnabled + flags.chart )}`, `- ${CONFIG_LABELS.basic.timeSignalEnabled}: ${formatBooleanSetting( - config.timeSignalEnabled + flags.timeSignal )}`, `- ${CONFIG_LABELS.basic.serverMonitoring}: ${formatBooleanSetting( - config.serverMonitoring + flags.serverMonitoring )}`, `- ${CONFIG_LABELS.basic.checkEmojisEnabled}: ${formatBooleanSetting( - config.checkEmojisEnabled + flags.emojiCheck )}`, ]; return lines.join('\n') + '\n'; } function formatGameFeatures(): string { + const flags = resolveInfoFeatureFlags(config); const lines: string[] = [ CONFIG_LABELS.sections.gameFeatures, `- ${CONFIG_LABELS.game.mazeEnable}: ${formatBooleanSetting( - config.mazeEnable + flags.maze )}`, `- ${CONFIG_LABELS.game.pollEnable}: ${formatBooleanSetting( - config.pollEnable + flags.poll )}`, ]; return lines.join('\n') + '\n'; diff --git a/test/info-feature-flags.test.mjs b/test/info-feature-flags.test.mjs new file mode 100644 index 00000000..5f92d4bb --- /dev/null +++ b/test/info-feature-flags.test.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { resolveInfoFeatureFlags } from '../built/modules/info/feature-flags.js'; + +test('info: default-enabled features show enabled when unset', () => { + const flags = resolveInfoFeatureFlags({ + // unset/legacy configs commonly omit these keys + chartEnabled: undefined, + notingEnabled: undefined, + timeSignalEnabled: undefined, + }); + + assert.equal(flags.chart, true); + assert.equal(flags.autoWord, true); + assert.equal(flags.timeSignal, true); +}); + +test('info: default-enabled features can be disabled explicitly', () => { + const flags = resolveInfoFeatureFlags({ + chartEnabled: false, + notingEnabled: false, + timeSignalEnabled: false, + }); + + assert.equal(flags.chart, false); + assert.equal(flags.autoWord, false); + assert.equal(flags.timeSignal, false); +}); + +test('info: opt-in features are disabled when unset', () => { + const flags = resolveInfoFeatureFlags({ + keywordEnabled: undefined, + reversiEnabled: undefined, + serverMonitoring: undefined, + checkEmojisEnabled: undefined, + mazeEnable: undefined, + pollEnable: undefined, + }); + + assert.equal(flags.keywordSearch, false); + assert.equal(flags.reversi, false); + assert.equal(flags.serverMonitoring, false); + assert.equal(flags.emojiCheck, false); + assert.equal(flags.maze, false); + assert.equal(flags.poll, false); +}); From eb8cfeaf18102ba2a8ce37e39782cab57bfab1e2 Mon Sep 17 00:00:00 2001 From: lqvp <183242690+lqvp@users.noreply.github.com> Date: Sat, 20 Dec 2025 19:58:50 +0900 Subject: [PATCH 2/4] refactor --- src/modules/info/feature-flags.ts | 15 +++++----- src/modules/info/index.ts | 13 ++++----- test/info-feature-flags.test.mjs | 48 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/modules/info/feature-flags.ts b/src/modules/info/feature-flags.ts index 9ad053c8..77ce9d1f 100644 --- a/src/modules/info/feature-flags.ts +++ b/src/modules/info/feature-flags.ts @@ -10,11 +10,7 @@ export type InfoFeatureFlags = { poll: boolean; }; -function isEnabledByDefaultUnlessFalse(value: boolean | undefined): boolean { - return value !== false; -} - -export function resolveInfoFeatureFlags(cfg: { +export type FeatureFlagsConfig = { keywordEnabled?: boolean; reversiEnabled?: boolean; notingEnabled?: boolean; @@ -24,7 +20,13 @@ export function resolveInfoFeatureFlags(cfg: { checkEmojisEnabled?: boolean; mazeEnable?: boolean; pollEnable?: boolean; -}): InfoFeatureFlags { +}; + +function isEnabledByDefaultUnlessFalse(value: boolean | undefined): boolean { + return value !== false; +} + +export function resolveInfoFeatureFlags(cfg: FeatureFlagsConfig): InfoFeatureFlags { return { keywordSearch: cfg.keywordEnabled === true, reversi: cfg.reversiEnabled === true, @@ -37,4 +39,3 @@ export function resolveInfoFeatureFlags(cfg: { poll: cfg.pollEnable === true, }; } - diff --git a/src/modules/info/index.ts b/src/modules/info/index.ts index 39565a72..9b9d434e 100644 --- a/src/modules/info/index.ts +++ b/src/modules/info/index.ts @@ -7,7 +7,7 @@ import type DatabaseManager from '@/database/DatabaseManager.js'; import * as fs from 'fs'; import path from 'path'; import os from 'os'; -import { resolveInfoFeatureFlags } from './feature-flags.js'; +import { resolveInfoFeatureFlags, type InfoFeatureFlags } from './feature-flags.js'; // 型定義 declare namespace NodeJS { @@ -288,8 +288,7 @@ function formatBooleanSetting(value: boolean | undefined): string { return value ? DEFAULTS.enabled : DEFAULTS.disabled; } -function formatBasicFeatures(): string { - const flags = resolveInfoFeatureFlags(config); +function formatBasicFeatures(flags: InfoFeatureFlags): string { const lines: string[] = [ CONFIG_LABELS.sections.basicFeatures, `- ${CONFIG_LABELS.basic.keywordEnabled}: ${formatBooleanSetting( @@ -317,8 +316,7 @@ function formatBasicFeatures(): string { return lines.join('\n') + '\n'; } -function formatGameFeatures(): string { - const flags = resolveInfoFeatureFlags(config); +function formatGameFeatures(flags: InfoFeatureFlags): string { const lines: string[] = [ CONFIG_LABELS.sections.gameFeatures, `- ${CONFIG_LABELS.game.mazeEnable}: ${formatBooleanSetting( @@ -510,10 +508,11 @@ function formatOtherSettings(): string { } function formatSafeConfigInfo(): string { + const flags = resolveInfoFeatureFlags(config); let configInfo = `\n⚙️ **設定情報**\n`; - configInfo += formatBasicFeatures(); - configInfo += formatGameFeatures(); + configInfo += formatBasicFeatures(flags); + configInfo += formatGameFeatures(flags); configInfo += formatPostSettings(); configInfo += formatAIFeatures(); configInfo += formatEarthquakeSettings(); diff --git a/test/info-feature-flags.test.mjs b/test/info-feature-flags.test.mjs index 5f92d4bb..9219e9a6 100644 --- a/test/info-feature-flags.test.mjs +++ b/test/info-feature-flags.test.mjs @@ -45,3 +45,51 @@ test('info: opt-in features are disabled when unset', () => { assert.equal(flags.maze, false); assert.equal(flags.poll, false); }); + +test('info: opt-in features are enabled when set to true', () => { + const flags = resolveInfoFeatureFlags({ + keywordEnabled: true, + reversiEnabled: true, + serverMonitoring: true, + checkEmojisEnabled: true, + mazeEnable: true, + pollEnable: true, + }); + + assert.equal(flags.keywordSearch, true); + assert.equal(flags.reversi, true); + assert.equal(flags.serverMonitoring, true); + assert.equal(flags.emojiCheck, true); + assert.equal(flags.maze, true); + assert.equal(flags.poll, true); +}); + +test('info: opt-in features are disabled when set to false', () => { + const flags = resolveInfoFeatureFlags({ + keywordEnabled: false, + reversiEnabled: false, + serverMonitoring: false, + checkEmojisEnabled: false, + mazeEnable: false, + pollEnable: false, + }); + + assert.equal(flags.keywordSearch, false); + assert.equal(flags.reversi, false); + assert.equal(flags.serverMonitoring, false); + assert.equal(flags.emojiCheck, false); + assert.equal(flags.maze, false); + assert.equal(flags.poll, false); +}); + +test('info: default-enabled features stay enabled when set to true', () => { + const flags = resolveInfoFeatureFlags({ + chartEnabled: true, + notingEnabled: true, + timeSignalEnabled: true, + }); + + assert.equal(flags.chart, true); + assert.equal(flags.autoWord, true); + assert.equal(flags.timeSignal, true); +}); From 156baf5d63e5be8f44a312d2dc972de1d6ac671b Mon Sep 17 00:00:00 2001 From: lqvp <183242690+lqvp@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:21:39 +0900 Subject: [PATCH 3/4] fix --- src/config-updater.ts | 80 ++++++++++------ test/config-updater.test.mjs | 173 +++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 26 deletions(-) create mode 100644 test/config-updater.test.mjs diff --git a/src/config-updater.ts b/src/config-updater.ts index cdb5a037..50765f1e 100644 --- a/src/config-updater.ts +++ b/src/config-updater.ts @@ -7,8 +7,27 @@ type Config = { [key: string]: any; // その他の設定項目 }; -function loadConfigTemplate(): Config { - const templatePath = resolve('./example.config.toml'); +export type ConfigCheckLogger = Pick; + +export type StartupConfigCheckOptions = { + cwd?: string; + configPath?: string; + templatePath?: string; + logger?: ConfigCheckLogger; + exit?: (code: number) => never; +}; + +function resolvePathFromOptions( + opts: StartupConfigCheckOptions | undefined, + kind: 'config' | 'template' +): string { + const cwd = opts?.cwd ?? process.cwd(); + if (kind === 'config') return opts?.configPath ?? resolve(cwd, 'config.toml'); + return opts?.templatePath ?? resolve(cwd, 'example.config.toml'); +} + +function loadConfigTemplate(options?: StartupConfigCheckOptions): Config { + const templatePath = resolvePathFromOptions(options, 'template'); if (!existsSync(templatePath)) { throw new Error( @@ -99,42 +118,46 @@ function formatValuePreview(value: any): string { /** * 設定ファイルの差分チェックと通知 */ -export function checkMissingConfigKeys(userConfig: Config): Config { - console.log('🔍 設定ファイルチェックを開始します'); +export function checkMissingConfigKeys( + userConfig: Config, + options?: StartupConfigCheckOptions +): Config { + const logger = options?.logger ?? console; + logger.log('🔍 設定ファイルチェックを開始します'); try { // テンプレートを読み込み - const template = loadConfigTemplate(); + const template = loadConfigTemplate(options); // 不足しているキーを検出 const missingKeys = findMissingKeys(userConfig, template); if (missingKeys.length === 0) { - console.log('✅ 設定ファイルは完全です'); + logger.log('✅ 設定ファイルは完全です'); return userConfig; } if (missingKeys.length > 0) { - console.log( + logger.log( `\n📋 以下の設定項目が不足しています (${missingKeys.length}個):` ); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); missingKeys.forEach((key) => { const defaultValue = getNestedValue(template, key); const valuePreview = formatValuePreview(defaultValue); - console.log(` 📝 ${key} = ${valuePreview}`); + logger.log(` 📝 ${key} = ${valuePreview}`); }); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('💡 これらの設定を config.toml に追加することをお勧めします'); - console.log('📖 詳細は example.config.toml を参照してください'); + logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + logger.log('💡 これらの設定を config.toml に追加することをお勧めします'); + logger.log('📖 詳細は example.config.toml を参照してください'); } return userConfig; } catch (error) { - console.error('❌ 設定差分チェック中にエラーが発生しました:', error); - console.log('⚠️ 設定チェックをスキップして起動を続行します'); + logger.error('❌ 設定差分チェック中にエラーが発生しました:', error); + logger.log('⚠️ 設定チェックをスキップして起動を続行します'); return userConfig; } } @@ -142,18 +165,23 @@ export function checkMissingConfigKeys(userConfig: Config): Config { /** * 起動時の設定チェック(メイン関数) */ -export function performStartupConfigCheck(): Config { - console.log('🚀 藍 (Ai) 起動中...'); - console.log('📋 設定ファイルチェックを開始します'); +export function performStartupConfigCheck( + options?: StartupConfigCheckOptions +): Config { + const logger = options?.logger ?? console; + const exit = options?.exit ?? ((code: number) => process.exit(code) as never); + + logger.log('🚀 藍 (Ai) 起動中...'); + logger.log('📋 設定ファイルチェックを開始します'); - const configPath = resolve('./config.toml'); + const configPath = resolvePathFromOptions(options, 'config'); if (!existsSync(configPath)) { - console.error('❌ config.toml が見つかりません'); - console.log( + logger.error('❌ config.toml が見つかりません'); + logger.log( '💡 example.config.toml をコピーして config.toml を作成してください' ); - process.exit(1); + return exit(1); } try { @@ -162,14 +190,14 @@ export function performStartupConfigCheck(): Config { const userConfig = TOML.parse(configData) as Config; // 設定差分チェックと通知 - const checkedConfig = checkMissingConfigKeys(userConfig); + const checkedConfig = checkMissingConfigKeys(userConfig, options); - console.log('✅ 設定ファイルチェック完了'); - console.log('🎉 Bot起動準備完了!'); + logger.log('✅ 設定ファイルチェック完了'); + logger.log('🎉 Bot起動準備完了!'); return checkedConfig; } catch (error) { - console.error('❌ 設定ファイルの読み込みに失敗しました:', error); - process.exit(1); + logger.error('❌ 設定ファイルの読み込みに失敗しました:', error); + return exit(1); } } diff --git a/test/config-updater.test.mjs b/test/config-updater.test.mjs new file mode 100644 index 00000000..2966c97a --- /dev/null +++ b/test/config-updater.test.mjs @@ -0,0 +1,173 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { + checkMissingConfigKeys, + performStartupConfigCheck, +} from '../built/config-updater.js'; + +function createCaptureLogger() { + const logs = []; + const errors = []; + return { + logs, + errors, + logger: { + log: (...args) => logs.push(args.map(String).join(' ')), + error: (...args) => errors.push(args.map(String).join(' ')), + }, + }; +} + +function exitThatThrows() { + return (code) => { + const error = new Error(`process.exit: ${code}`); + error.code = code; + throw error; + }; +} + +test('checkMissingConfigKeys: reports missing keys (top-level + nested)', async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + const templatePath = path.join(dir, 'example.config.toml'); + + await writeFile( + templatePath, + ` +host = "https://example.test" +i = "token" + +[http] +userAgent = "UA-http" + +[parent] +a = 1 +b = 2 +`, + 'utf8' + ); + + const { logger, logs, errors } = createCaptureLogger(); + + const userConfig = { + host: 'https://example.test', + i: 'token', + parent: { a: 1 }, + }; + + const returned = checkMissingConfigKeys(userConfig, { + templatePath, + logger, + }); + + assert.equal(returned, userConfig); + assert.equal(errors.length, 0); + + const all = logs.join('\n'); + assert.match(all, /以下の設定項目が不足しています/); + assert.match(all, /📝 http =/); + assert.match(all, /📝 parent\.b = 2/); +}); + +test('checkMissingConfigKeys: prints complete when no missing keys', async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + const templatePath = path.join(dir, 'example.config.toml'); + + await writeFile( + templatePath, + ` +host = "https://example.test" +i = "token" + +[parent] +a = 1 +`, + 'utf8' + ); + + const { logger, logs } = createCaptureLogger(); + + const userConfig = { + host: 'https://example.test', + i: 'token', + parent: { a: 1 }, + }; + + checkMissingConfigKeys(userConfig, { templatePath, logger }); + + assert.ok(logs.some((l) => l.includes('✅ 設定ファイルは完全です'))); +}); + +test('checkMissingConfigKeys: skips diff check when template is missing', () => { + const { logger, logs, errors } = createCaptureLogger(); + + const userConfig = { host: 'https://example.test', i: 'token' }; + const returned = checkMissingConfigKeys(userConfig, { + templatePath: '/__does_not_exist__/example.config.toml', + logger, + }); + + assert.equal(returned, userConfig); + assert.ok(errors.some((e) => e.includes('❌ 設定差分チェック中にエラー'))); + assert.ok(logs.some((l) => l.includes('設定チェックをスキップ'))); +}); + +test('performStartupConfigCheck: exits when config.toml is missing', () => { + const { logger } = createCaptureLogger(); + + assert.throws( + () => + performStartupConfigCheck({ + configPath: '/__does_not_exist__/config.toml', + templatePath: '/__does_not_exist__/example.config.toml', + logger, + exit: exitThatThrows(), + }), + (err) => err && err.code === 1 + ); +}); + +test('performStartupConfigCheck: exits when config.toml is invalid TOML', async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + const configPath = path.join(dir, 'config.toml'); + + await writeFile(configPath, 'host = "https://x"\ninvalid = [\n', 'utf8'); + + const { logger } = createCaptureLogger(); + + assert.throws( + () => + performStartupConfigCheck({ + configPath, + templatePath: '/__does_not_exist__/example.config.toml', + logger, + exit: exitThatThrows(), + }), + (err) => err && err.code === 1 + ); +}); + +test('performStartupConfigCheck: returns parsed config on success', async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + const configPath = path.join(dir, 'config.toml'); + const templatePath = path.join(dir, 'example.config.toml'); + + await writeFile(templatePath, 'host = "https://example.test"\ni = "token"\n', 'utf8'); + await writeFile(configPath, 'host = "https://example.test"\ni = "token"\n', 'utf8'); + + const { logger } = createCaptureLogger(); + + const cfg = performStartupConfigCheck({ + configPath, + templatePath, + logger, + exit: exitThatThrows(), + }); + + assert.equal(cfg.host, 'https://example.test'); + assert.equal(cfg.i, 'token'); +}); + From 3ff16cff3672ca6d428f9abf34390e3b3bdec569 Mon Sep 17 00:00:00 2001 From: lqvp <183242690+lqvp@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:39:51 +0900 Subject: [PATCH 4/4] chore --- src/config-updater.ts | 30 ++++++++++++++---------------- test/config-updater.test.mjs | 15 +++++++++------ tsconfig.json | 1 + 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/config-updater.ts b/src/config-updater.ts index 50765f1e..5921e308 100644 --- a/src/config-updater.ts +++ b/src/config-updater.ts @@ -31,7 +31,7 @@ function loadConfigTemplate(options?: StartupConfigCheckOptions): Config { if (!existsSync(templatePath)) { throw new Error( - 'example.config.toml が見つかりません。テンプレートファイルが必要です。' + `${templatePath} が見つかりません。テンプレートファイルが必要です。` ); } @@ -39,7 +39,7 @@ function loadConfigTemplate(options?: StartupConfigCheckOptions): Config { const templateData = readFileSync(templatePath, 'utf8'); return TOML.parse(templateData) as Config; } catch (error) { - throw new Error(`example.config.toml の読み込みに失敗しました: ${error}`); + throw new Error(`${templatePath} の読み込みに失敗しました: ${error}`); } } @@ -137,22 +137,20 @@ export function checkMissingConfigKeys( return userConfig; } - if (missingKeys.length > 0) { - logger.log( - `\n📋 以下の設定項目が不足しています (${missingKeys.length}個):` - ); - logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + logger.log( + `\n📋 以下の設定項目が不足しています (${missingKeys.length}個):` + ); + logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - missingKeys.forEach((key) => { - const defaultValue = getNestedValue(template, key); - const valuePreview = formatValuePreview(defaultValue); - logger.log(` 📝 ${key} = ${valuePreview}`); - }); + missingKeys.forEach((key) => { + const defaultValue = getNestedValue(template, key); + const valuePreview = formatValuePreview(defaultValue); + logger.log(` 📝 ${key} = ${valuePreview}`); + }); - logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - logger.log('💡 これらの設定を config.toml に追加することをお勧めします'); - logger.log('📖 詳細は example.config.toml を参照してください'); - } + logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + logger.log('💡 これらの設定を config.toml に追加することをお勧めします'); + logger.log('📖 詳細は example.config.toml を参照してください'); return userConfig; } catch (error) { diff --git a/test/config-updater.test.mjs b/test/config-updater.test.mjs index 2966c97a..b01cbff3 100644 --- a/test/config-updater.test.mjs +++ b/test/config-updater.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { mkdtemp, writeFile } from 'node:fs/promises'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; @@ -30,8 +30,9 @@ function exitThatThrows() { }; } -test('checkMissingConfigKeys: reports missing keys (top-level + nested)', async () => { +test('checkMissingConfigKeys: reports missing keys (top-level + nested)', async (t) => { const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + t.after(() => rm(dir, { recursive: true, force: true })); const templatePath = path.join(dir, 'example.config.toml'); await writeFile( @@ -72,8 +73,9 @@ b = 2 assert.match(all, /📝 parent\.b = 2/); }); -test('checkMissingConfigKeys: prints complete when no missing keys', async () => { +test('checkMissingConfigKeys: prints complete when no missing keys', async (t) => { const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + t.after(() => rm(dir, { recursive: true, force: true })); const templatePath = path.join(dir, 'example.config.toml'); await writeFile( @@ -130,8 +132,9 @@ test('performStartupConfigCheck: exits when config.toml is missing', () => { ); }); -test('performStartupConfigCheck: exits when config.toml is invalid TOML', async () => { +test('performStartupConfigCheck: exits when config.toml is invalid TOML', async (t) => { const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + t.after(() => rm(dir, { recursive: true, force: true })); const configPath = path.join(dir, 'config.toml'); await writeFile(configPath, 'host = "https://x"\ninvalid = [\n', 'utf8'); @@ -150,8 +153,9 @@ test('performStartupConfigCheck: exits when config.toml is invalid TOML', async ); }); -test('performStartupConfigCheck: returns parsed config on success', async () => { +test('performStartupConfigCheck: returns parsed config on success', async (t) => { const dir = await mkdtemp(path.join(os.tmpdir(), 'ai-config-test-')); + t.after(() => rm(dir, { recursive: true, force: true })); const configPath = path.join(dir, 'config.toml'); const templatePath = path.join(dir, 'example.config.toml'); @@ -170,4 +174,3 @@ test('performStartupConfigCheck: returns parsed config on success', async () => assert.equal(cfg.host, 'https://example.test'); assert.equal(cfg.i, 'token'); }); - diff --git a/tsconfig.json b/tsconfig.json index 3828d887..78e05e39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "noImplicitAny": false, "esModuleInterop": true, "typeRoots": ["./node_modules/@types"], + "types": ["node"], "lib": ["esnext", "dom"], "paths": { "@/*": ["./src/*"]