// roo-codeテストパターンの適用
describe('GROWI Editor Assistant Validation', () => {
describe('Phase 1: 基本的なバリデーション', () => {
it('should require startLine field')
it('should require non-empty search field')
it('should accept valid replace field (empty allowed)')
})
describe('Phase 2: 検索処理', () => {
it('should find exact matches at specified line')
it('should find fuzzy matches within threshold')
it('should reject matches below threshold')
})
describe('Phase 3: 置換処理', () => {
it('should replace content at correct position')
it('should preserve indentation')
it('should handle multiple replacements in order')
})
})
describe('Search-Replace Core Functionality', () => {
test('完全一致での置換', async () => {
const originalContent = `function test() {
console.log("hello");
}`;
const diff = {
search: 'console.log("hello");',
replace: 'console.log("world");',
startLine: 2
};
const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
expect(result).toBe(true);
expect(yText.toString()).toContain('console.log("world");');
});
test('Fuzzy Matching(80%類似度)', async () => {
const originalContent = `function test() {
console.log( "hello" );
}`;
const diff = {
search: 'console.log("hello");', // スペース違い
replace: 'console.log("world");',
startLine: 2
};
const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
expect(result).toBe(true); // 80%以上の類似度で成功
});
test('類似度不足での失敗', async () => {
const diff = {
search: 'completely_different_content',
replace: 'new_content',
startLine: 2
};
const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
expect(result).toBe(false); // 類似度不足で失敗
});
});
describe('startLine Validation', () => {
test('サーバー側バリデーション', () => {
const invalidDiff = { search: 'test', replace: 'new' }; // startLineなし
expect(() => validateDiffStructure(invalidDiff)).toThrow('startLine is required');
});
test('クライアント側バリデーション', () => {
const diffs = [{ search: 'test', replace: 'new' }]; // startLineなし
expect(() => validateDiffs(diffs)).toThrow('startLine is required for client processing');
});
});
describe('Error Handling and Reporting', () => {
test('類似度不足エラーの詳細情報', async () => {
const result = await fuzzyMatcher.findBestMatch(content, 'nonexistent', { preferredStartLine: 1 });
expect(result.success).toBe(false);
expect(result.error).toEqual({
type: 'SIMILARITY_TOO_LOW',
message: expect.stringContaining('類似度が不十分'),
details: {
searchContent: 'nonexistent',
bestMatch: expect.any(String),
similarity: expect.any(Number),
suggestions: expect.arrayContaining([
'read_fileツールで最新のファイル内容を確認',
'空白やインデントの違いを確認'
])
}
});
});
test('修正提案の生成', () => {
const error = createSimilarityError('search_text', 'best_match', 0.6);
expect(error.details.suggestions).toContain('類似度の閾値を下げることを検討');
});
});
describe('Indentation and Formatting', () => {
test('タブインデントの保持', async () => {
const originalContent = `function test() {
\tconsole.log("hello");
}`;
const diff = {
search: '\tconsole.log("hello");',
replace: '\tconsole.log("world");',
startLine: 2
};
const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
expect(yText.toString()).toContain('\tconsole.log("world");');
});
test('スペースインデントの保持', async () => {
const originalContent = `function test() {
console.log("hello");
}`;
const diff = {
search: ' console.log("hello");',
replace: ' console.log("world");',
startLine: 2
};
const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
expect(yText.toString()).toContain(' console.log("world");');
});
test('混在インデントの処理', async () => {
// タブとスペースが混在する場合の正規化テスト
});
});
describe('Multiple Diff Processing', () => {
test('複数diffの順序処理', async () => {
const diffs = [
{ search: 'line1', replace: 'newLine1', startLine: 1 },
{ search: 'line3', replace: 'newLine3', startLine: 3 },
{ search: 'line2', replace: 'newLine2', startLine: 2 }
];
// startLineでソートされて処理されることを確認
const result = await applyMultipleDiffs(yText, diffs);
expect(result.appliedCount).toBe(3);
expect(result.success).toBe(true);
});
test('部分失敗時の処理継続', async () => {
const diffs = [
{ search: 'existing', replace: 'new1', startLine: 1 },
{ search: 'nonexistent', replace: 'new2', startLine: 2 },
{ search: 'existing2', replace: 'new3', startLine: 3 }
];
const result = await applyMultipleDiffs(yText, diffs);
expect(result.appliedCount).toBe(2); // 1つ失敗、2つ成功
expect(result.failedParts).toHaveLength(1);
});
});
describe('Performance Tests', () => {
test('大きなファイルでの処理時間', async () => {
const largeContent = 'line\n'.repeat(10000); // 10,000行
const startTime = performance.now();
const result = await performSearchReplace(
createYTextFromString(largeContent),
'line',
'newLine',
5000 // 中央付近の行
);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(1000); // 1秒以内
expect(result).toBe(true);
});
test('Middle-out検索の効率性', async () => {
// 指定行から近い位置にある内容が素早く見つかることを確認
const content = generateTestContent(1000);
const matcher = new ClientFuzzyMatcher();
const result = await matcher.findBestMatch(content, 'target_line', {
preferredStartLine: 500,
bufferLines: 20
});
expect(result.success).toBe(true);
expect(result.matchedLine).toBeCloseTo(500, 20); // 指定行から20行以内
});
});
// テスト用のコンテンツ生成
export function createTestContent(type: 'javascript' | 'typescript' | 'mixed') {
switch (type) {
case 'javascript':
return `function test() {
console.log("hello");
return true;
}`;
case 'typescript':
return `interface User {
name: string;
age: number;
}`;
case 'mixed':
return `function test() {
\tconsole.log("tab indent");
console.log("space indent");
}`;
}
}
// Yjs YTextのモック実装
export class MockYText {
private content: string = '';
toString(): string { return this.content; }
insert(index: number, text: string): void { /* 実装 */ }
delete(index: number, length: number): void { /* 実装 */ }
}
テスト戦略作成日: 2025-06-18
参考資料: roo-code test suite (1,186行)
対象Phase: Phase 2A・2B実装の検証