/**
* Phase 3 ツール動作確認スクリプト(複数DB対応版)
*
* 4つの分析ツールの動作を順次確認する
*
* 対象ツール(すべて読み取り専用):
* 1. fm_export_database_metadata - メタデータ集約
* 2. fm_infer_relationships - リレーション推測
* 3. fm_analyze_portal_data - ポータル分析
* 4. fm_global_search_data - 横断検索
*
* 使用方法:
* - npx tsx scripts/test-phase3-tools.ts
* → 環境変数から検出された最初のエイリアスを使用
* - npx tsx scripts/test-phase3-tools.ts --alias=PRODUCTION
* → 指定されたエイリアスを使用
*
* 環境変数パターン(複数DB対応):
* - FM_SERVER_{ALIAS} : FileMakerサーバーURL
* - FM_DATABASE_{ALIAS} : データベース名
* - FM_ACCOUNT_{ALIAS} : アカウント名
* - FM_PASSWORD_{ALIAS} : パスワード
*
* ⚠️ 注意: ミューテーション操作は一切行いません
*/
// dotenvを最初に読み込み、他のimportより前にprocess.envを設定
import { config } from 'dotenv';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const envPath = resolve(__dirname, '..', '.env');
// dotenv設定を同期的に読み込み
const dotenvResult = config({ path: envPath });
if (dotenvResult.error) {
console.error('Failed to load .env file:', dotenvResult.error);
process.exit(1);
}
// 色付き出力用
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
magenta: '\x1b[35m',
};
function log(message: string, color = colors.reset) {
console.log(`${color}${message}${colors.reset}`);
}
function logResult(name: string, success: boolean, details?: string) {
const icon = success ? '✅' : '❌';
const color = success ? colors.green : colors.red;
log(`${icon} ${name}: ${success ? 'SUCCESS' : 'FAILED'}`, color);
if (details) {
log(` ${details}`, colors.yellow);
}
}
// ============================================================================
// コマンドライン引数とエイリアス検出
// ============================================================================
/**
* コマンドライン引数から --alias= オプションを取得
*
* @returns 指定されたエイリアス(未指定の場合はundefined)
*/
function getAliasFromArgs(): string | undefined {
const aliasArg = process.argv.find((arg) => arg.startsWith('--alias='));
if (aliasArg) {
return aliasArg.split('=')[1]?.toUpperCase();
}
return undefined;
}
/**
* 環境変数からFM_SERVER_*パターンを検出してエイリアス一覧を取得
*
* @returns 検出されたエイリアスの配列(ソート済み)
*/
function detectAvailableAliases(): string[] {
const aliases: Set<string> = new Set();
for (const key of Object.keys(process.env)) {
const match = key.match(/^FM_SERVER_(.+)$/);
if (match?.[1]) {
aliases.add(match[1].toUpperCase());
}
}
return Array.from(aliases).sort();
}
// ============================================================================
// メイン処理
// ============================================================================
async function testPhase3Tools() {
log('\n========================================', colors.cyan);
log(' Phase 3 分析ツール動作確認(複数DB対応版)', colors.cyan);
log(' ⚠️ 読み取り専用テスト(ミューテーションなし)', colors.yellow);
log('========================================\n', colors.cyan);
// ------------------------------------------
// エイリアスの決定
// ------------------------------------------
const availableAliases = detectAvailableAliases();
log('検出されたエイリアス: ' + (availableAliases.length > 0 ? availableAliases.join(', ') : 'なし'), colors.cyan);
const specifiedAlias = getAliasFromArgs();
let testAlias: string;
if (specifiedAlias) {
if (!availableAliases.includes(specifiedAlias)) {
log(`エラー: 指定されたエイリアス "${specifiedAlias}" は環境変数に存在しません`, colors.red);
log(`利用可能なエイリアス: ${availableAliases.join(', ')}`, colors.yellow);
process.exit(1);
}
testAlias = specifiedAlias;
log(`指定されたエイリアスを使用: ${testAlias}`, colors.green);
} else if (availableAliases.length > 0) {
testAlias = availableAliases[0];
log(`最初のエイリアスを使用: ${testAlias}`, colors.yellow);
} else {
log('エラー: FM_SERVER_{ALIAS} 形式の環境変数が見つかりません', colors.red);
log('設定例: FM_SERVER_PRODUCTION=https://your-server.com', colors.yellow);
process.exit(1);
}
// 環境変数の確認
const serverEnvKey = `FM_SERVER_${testAlias}`;
log(`環境変数: ${serverEnvKey}=${process.env[serverEnvKey] || 'NOT SET'}`, colors.cyan);
log('', colors.reset);
// ------------------------------------------
// モジュールのインポートとレジストリ初期化
// ------------------------------------------
log('モジュールを読み込み中...', colors.cyan);
// 設定とレジストリ
const configModule = await import('../src/config.ts');
const sessionModule = await import('../src/api/session.ts');
const { loadMultiDatabaseConfig } = configModule;
const { initializeRegistry, resetRegistry, getRegistry } = sessionModule;
// レジストリをリセットして初期化
resetRegistry();
const dbRegistry = loadMultiDatabaseConfig();
initializeRegistry(dbRegistry);
// ツールハンドラ
const authModule = await import('../src/tools/auth.ts');
const metadataModule = await import('../src/tools/metadata.ts');
// Phase 3 分析モジュール
const metadataAggregator = await import('../src/analyzers/metadata-aggregator.ts');
const relationshipInferrer = await import('../src/analyzers/relationship-inferrer.ts');
const portalAnalyzer = await import('../src/analyzers/portal-analyzer.ts');
const globalSearcher = await import('../src/analyzers/global-searcher.ts');
const { handleLogin, handleLogout } = authModule;
const { handleGetLayouts, handleGetLayoutMetadata } = metadataModule;
const { exportDatabaseMetadata } = metadataAggregator;
const { inferRelationships } = relationshipInferrer;
const { analyzePortalData } = portalAnalyzer;
const { globalSearchData } = globalSearcher;
const results: { name: string; success: boolean; details?: string }[] = [];
let testLayout = '';
let testLayoutWithPortal = '';
// ------------------------------------------
// 0. ログイン(前提条件)
// ------------------------------------------
log('0. fm_login(前提条件)...', colors.cyan);
try {
const loginResult = await handleLogin({ alias: testAlias });
if (loginResult.success) {
log(` ログイン成功 (エイリアス: ${testAlias})`, colors.green);
} else {
log(' ログイン失敗: ' + (loginResult as any).error?.message, colors.red);
process.exit(1);
}
} catch (e) {
log(' ログインエラー: ' + String(e), colors.red);
process.exit(1);
}
// ------------------------------------------
// テスト用レイアウトの取得
// ------------------------------------------
log('\nテスト用レイアウトを取得中...', colors.cyan);
try {
const layoutsResult = await handleGetLayouts({ alias: testAlias });
if (layoutsResult.success && 'layouts' in layoutsResult) {
const layouts = layoutsResult.layouts.filter((l) => !l.isFolder);
testLayout = layouts[0]?.name || '';
log(` 使用レイアウト: ${testLayout}`, colors.yellow);
// ポータルがあるレイアウトを探す
for (const layout of layouts.slice(0, 10)) {
const meta = await handleGetLayoutMetadata({ layout: layout.name, alias: testAlias });
if (
meta.success &&
'portalMetaData' in meta &&
meta.portalMetaData &&
Object.keys(meta.portalMetaData).length > 0
) {
testLayoutWithPortal = layout.name;
log(` ポータルありレイアウト: ${testLayoutWithPortal}`, colors.yellow);
break;
}
}
}
} catch (e) {
log(' レイアウト取得エラー: ' + String(e), colors.red);
}
if (!testLayout) {
log('\nテスト用レイアウトが見つかりません。テストを中止します。', colors.red);
await handleLogout({ alias: testAlias });
process.exit(1);
}
// SessionManagerを取得(analyzer関数に渡すため)
const sessionManager = getRegistry().getManager(testAlias);
// ------------------------------------------
// 1. fm_export_database_metadata
// ------------------------------------------
log('\n1. fm_export_database_metadata テスト...', colors.cyan);
log(' (読み取り専用: レイアウト・スクリプト情報を集約)', colors.magenta);
try {
const metadataResult = await exportDatabaseMetadata(
{
format: 'json',
options: {
includeLayouts: true,
includeScripts: true,
includeValueLists: true,
maxLayouts: 5, // テスト用に制限
},
},
sessionManager
);
if (metadataResult.success && 'data' in metadataResult) {
const data = metadataResult.data;
results.push({
name: 'fm_export_database_metadata',
success: true,
details: `レイアウト: ${data.layouts?.length || 0}件, スクリプト: ${data.scripts?.length || 0}件`,
});
// 制限事項が含まれているか確認
if ('limitations' in metadataResult && metadataResult.limitations) {
log(` 制限事項: ${metadataResult.limitations.length}件記載あり`, colors.yellow);
}
} else {
results.push({
name: 'fm_export_database_metadata',
success: false,
details: (metadataResult as any).error?.message,
});
}
} catch (e) {
results.push({
name: 'fm_export_database_metadata',
success: false,
details: String(e),
});
}
// ------------------------------------------
// 2. fm_infer_relationships
// ------------------------------------------
log('\n2. fm_infer_relationships テスト...', colors.cyan);
log(' (読み取り専用: フィールド名パターンからリレーション推測)', colors.magenta);
try {
const inferResult = await inferRelationships(
{
layout: testLayout,
depth: 1,
},
sessionManager
);
if (inferResult.success && 'inferredRelationships' in inferResult) {
results.push({
name: 'fm_infer_relationships',
success: true,
details: `推測リレーション: ${inferResult.inferredRelationships?.length || 0}件, 外部キー: ${inferResult.inferredForeignKeys?.length || 0}件`,
});
// disclaimer が含まれているか確認
if ('disclaimer' in inferResult && inferResult.disclaimer) {
log(` 免責事項: 「${inferResult.disclaimer.substring(0, 50)}...」`, colors.yellow);
}
} else {
results.push({
name: 'fm_infer_relationships',
success: false,
details: (inferResult as any).error?.message,
});
}
} catch (e) {
results.push({
name: 'fm_infer_relationships',
success: false,
details: String(e),
});
}
// ------------------------------------------
// 3. fm_analyze_portal_data
// ------------------------------------------
log('\n3. fm_analyze_portal_data テスト...', colors.cyan);
log(' (読み取り専用: ポータル構造とサンプルデータ取得)', colors.magenta);
const portalTestLayout = testLayoutWithPortal || testLayout;
try {
const portalResult = await analyzePortalData(
{
layout: portalTestLayout,
includeSampleData: true,
sampleLimit: 3,
},
sessionManager
);
if (portalResult.success && 'portals' in portalResult) {
const portalCount = portalResult.portals?.length || 0;
results.push({
name: 'fm_analyze_portal_data',
success: true,
details: `ポータル数: ${portalCount}件${portalCount > 0 ? ', 関連テーブル: ' + portalResult.summary?.relatedTables?.join(', ') : ''}`,
});
} else {
results.push({
name: 'fm_analyze_portal_data',
success: false,
details: (portalResult as any).error?.message,
});
}
} catch (e) {
results.push({
name: 'fm_analyze_portal_data',
success: false,
details: String(e),
});
}
// ------------------------------------------
// 4. fm_global_search_data
// ------------------------------------------
log('\n4. fm_global_search_data テスト...', colors.cyan);
log(' (読み取り専用: 複数レイアウト横断検索)', colors.magenta);
try {
// テスト用の検索文字列(一般的な文字で検索)
const searchResult = await globalSearchData(
{
searchText: 'test',
layouts: [testLayout],
options: {
maxFieldsPerLayout: 5,
maxRecordsPerLayout: 3,
searchMode: 'contains',
},
},
sessionManager
);
if (searchResult.success && 'results' in searchResult) {
const totalRecords = searchResult.summary?.totalRecordsFound || 0;
results.push({
name: 'fm_global_search_data',
success: true,
details: `検索結果: ${totalRecords}件, 検索レイアウト: ${searchResult.summary?.searchedLayouts?.length || 0}件`,
});
// 制限事項と免責事項の確認
if ('limitations' in searchResult && searchResult.limitations) {
log(` 制限事項: ${searchResult.limitations.length}件記載あり`, colors.yellow);
}
if ('disclaimer' in searchResult && searchResult.disclaimer) {
log(` 免責事項あり`, colors.yellow);
}
} else {
results.push({
name: 'fm_global_search_data',
success: false,
details: (searchResult as any).error?.message,
});
}
} catch (e) {
results.push({
name: 'fm_global_search_data',
success: false,
details: String(e),
});
}
// ------------------------------------------
// ログアウト
// ------------------------------------------
log('\n5. fm_logout(クリーンアップ)...', colors.cyan);
try {
await handleLogout({ alias: testAlias });
log(' ログアウト成功', colors.green);
} catch (e) {
log(' ログアウトエラー: ' + String(e), colors.yellow);
}
// ------------------------------------------
// サマリー出力
// ------------------------------------------
log('\n========================================', colors.cyan);
log(' Phase 3 テスト結果サマリー', colors.cyan);
log(` 使用エイリアス: ${testAlias}`, colors.cyan);
log('========================================\n', colors.cyan);
for (const r of results) {
logResult(r.name, r.success, r.details);
}
const passed = results.filter((r) => r.success).length;
const total = results.length;
const allPassed = passed === total;
log('\n----------------------------------------', colors.cyan);
log(`結果: ${passed}/${total} 成功`, allPassed ? colors.green : colors.yellow);
log('----------------------------------------', colors.cyan);
if (allPassed) {
log('\n✅ すべてのPhase 3ツールが正常に動作しています', colors.green);
log(' (すべて読み取り専用操作でした)', colors.magenta);
} else {
log('\n⚠️ 一部のテストが失敗しました', colors.yellow);
}
log('\n', colors.reset);
process.exit(allPassed ? 0 : 1);
}
// 実行
testPhase3Tools().catch((e) => {
console.error('テスト実行エラー:', e);
process.exit(1);
});