Files
Claude-Code-Workflow/ccw/tests/storage-paths.test.js
catlog22 3da0ef2adb Add comprehensive tests for query parsing and Reciprocal Rank Fusion
- Implemented tests for the QueryParser class, covering various identifier splitting methods (CamelCase, snake_case, kebab-case), OR expansion, and FTS5 operator preservation.
- Added parameterized tests to validate expected token outputs for different query formats.
- Created edge case tests to ensure robustness against unusual input scenarios.
- Developed tests for the Reciprocal Rank Fusion (RRF) algorithm, including score computation, weight handling, and result ranking across multiple sources.
- Included tests for normalization of BM25 scores and tagging search results with source metadata.
2025-12-16 10:20:19 +08:00

315 lines
11 KiB
JavaScript

/**
* Storage Paths Hierarchical Structure Tests
* Tests for hierarchical storage path generation and migration
*/
import { describe, it, before, after, afterEach } from 'node:test';
import assert from 'node:assert';
import { join, resolve } from 'path';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import { homedir } from 'os';
// Mock CCW_HOME for testing
const TEST_CCW_HOME = join(homedir(), '.ccw-test');
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
// Import after setting env var
import {
detectHierarchy,
getProjectPaths,
clearHierarchyCache,
getProjectId
} from '../dist/config/storage-paths.js';
describe('Storage Paths - Hierarchical Structure', async () => {
const cleanTestEnv = () => {
if (existsSync(TEST_CCW_HOME)) {
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
}
mkdirSync(TEST_CCW_HOME, { recursive: true });
clearHierarchyCache();
};
before(async () => {
cleanTestEnv();
});
after(async () => {
cleanTestEnv();
});
describe('Project ID Generation', async () => {
afterEach(async () => {
cleanTestEnv();
});
it('should generate consistent project IDs', async () => {
const path1 = 'D:\\Claude_dms3';
const path2 = 'D:\\Claude_dms3';
const id1 = getProjectId(path1);
const id2 = getProjectId(path2);
assert.strictEqual(id1, id2);
assert.ok(id1.includes('d--claude_dms3'));
});
it('should handle different path formats', async () => {
// Test Windows path
const winId = getProjectId('D:\\Claude_dms3');
assert.ok(winId);
// Test Unix-like path
const unixId = getProjectId('/home/user/project');
assert.ok(unixId);
// Different paths should have different IDs
assert.notStrictEqual(winId, unixId);
});
});
describe('Hierarchy Detection', async () => {
afterEach(async () => {
cleanTestEnv();
});
it('should detect no parent for root project', async () => {
const hierarchy = detectHierarchy('D:\\Claude_dms3');
assert.strictEqual(hierarchy.parentId, null);
assert.strictEqual(hierarchy.relativePath, '');
assert.ok(hierarchy.currentId);
});
it('should detect parent when parent storage exists', async () => {
// Create parent storage
const parentPath = 'D:\\Claude_dms3';
const parentId = getProjectId(parentPath);
const parentStorageDir = join(TEST_CCW_HOME, 'projects', parentId);
mkdirSync(parentStorageDir, { recursive: true });
// Detect hierarchy for child
const childPath = 'D:\\Claude_dms3\\ccw';
const hierarchy = detectHierarchy(childPath);
assert.strictEqual(hierarchy.parentId, parentId);
assert.strictEqual(hierarchy.relativePath, 'ccw');
});
it('should detect nested hierarchy', async () => {
// Create parent storage
const rootPath = 'D:\\Claude_dms3';
const rootId = getProjectId(rootPath);
const rootStorageDir = join(TEST_CCW_HOME, 'projects', rootId);
mkdirSync(rootStorageDir, { recursive: true });
// Detect hierarchy for nested child
const nestedPath = 'D:\\Claude_dms3\\ccw\\src';
const hierarchy = detectHierarchy(nestedPath);
assert.strictEqual(hierarchy.parentId, rootId);
assert.strictEqual(hierarchy.relativePath, 'ccw/src');
});
it('should cache detection results', async () => {
const path = 'D:\\Claude_dms3\\ccw';
const result1 = detectHierarchy(path);
const result2 = detectHierarchy(path);
// Should return exact same object (cached)
assert.strictEqual(result1, result2);
});
it('should clear cache when requested', async () => {
const path = 'D:\\Claude_dms3\\ccw';
const result1 = detectHierarchy(path);
clearHierarchyCache();
const result2 = detectHierarchy(path);
// Should return different object instances after cache clear
assert.notStrictEqual(result1, result2);
// But same values
assert.strictEqual(result1.currentId, result2.currentId);
});
});
describe('Hierarchical Path Generation', async () => {
afterEach(async () => {
cleanTestEnv();
});
it('should generate flat path for root project', async () => {
const projectPath = 'D:\\Claude_dms3';
const paths = getProjectPaths(projectPath);
assert.ok(paths.root.includes('projects'));
assert.ok(paths.root.includes('d--claude_dms3'));
// Check that path ends with project ID, not a subdirectory
assert.ok(paths.root.endsWith('d--claude_dms3') || paths.root.endsWith('d--claude_dms3\\') || paths.root.endsWith('d--claude_dms3/'));
});
it('should generate hierarchical path when parent exists', async () => {
// Create parent storage
const parentPath = 'D:\\Claude_dms3';
const parentId = getProjectId(parentPath);
const parentStorageDir = join(TEST_CCW_HOME, 'projects', parentId);
mkdirSync(parentStorageDir, { recursive: true });
// Generate paths for child
const childPath = 'D:\\Claude_dms3\\ccw';
const paths = getProjectPaths(childPath);
assert.ok(paths.root.includes(parentId));
assert.ok(paths.root.includes('ccw'));
assert.ok(paths.root.endsWith('ccw'));
});
it('should generate nested hierarchical paths', async () => {
// Create parent storage
const parentPath = 'D:\\Claude_dms3';
const parentId = getProjectId(parentPath);
const parentStorageDir = join(TEST_CCW_HOME, 'projects', parentId);
mkdirSync(parentStorageDir, { recursive: true });
// Generate paths for nested child
const nestedPath = 'D:\\Claude_dms3\\ccw\\src';
const paths = getProjectPaths(nestedPath);
assert.ok(paths.root.includes(parentId));
assert.ok(paths.root.includes('ccw'));
assert.ok(paths.root.includes('src'));
assert.ok(paths.root.endsWith('src'));
});
it('should include all required subdirectories', async () => {
const projectPath = 'D:\\Claude_dms3';
const paths = getProjectPaths(projectPath);
assert.ok(paths.cliHistory.includes('cli-history'));
assert.ok(paths.memory.includes('memory'));
assert.ok(paths.cache.includes('cache'));
assert.ok(paths.config.includes('config'));
assert.ok(paths.historyDb.includes('history.db'));
assert.ok(paths.memoryDb.includes('memory.db'));
});
});
describe('Migration from Flat to Hierarchical', async () => {
it('should migrate flat structure to hierarchical', async () => {
// Setup: Create parent storage
const parentPath = 'D:\\Claude_dms3';
const parentId = getProjectId(parentPath);
const parentStorageDir = join(TEST_CCW_HOME, 'projects', parentId);
mkdirSync(parentStorageDir, { recursive: true });
// Create old flat structure for child
const childPath = 'D:\\Claude_dms3\\ccw';
const childId = getProjectId(childPath);
const flatStorageDir = join(TEST_CCW_HOME, 'projects', childId);
const flatCliHistoryDir = join(flatStorageDir, 'cli-history');
mkdirSync(flatCliHistoryDir, { recursive: true });
// Create a test file to verify migration
const testFile = join(flatCliHistoryDir, 'test.txt');
writeFileSync(testFile, 'test data');
// Trigger migration by calling getProjectPaths
const paths = getProjectPaths(childPath);
console.log('[DEBUG] Test file path:', testFile);
console.log('[DEBUG] Flat storage dir:', flatStorageDir);
console.log('[DEBUG] Flat storage exists before migration:', existsSync(flatStorageDir));
console.log('[DEBUG] Returned paths.root:', paths.root);
console.log('[DEBUG] Returned paths.cliHistory:', paths.cliHistory);
console.log('[DEBUG] Expected migrated file:', join(paths.cliHistory, 'test.txt'));
console.log('[DEBUG] Migrated file exists:', existsSync(join(paths.cliHistory, 'test.txt')));
console.log('[DEBUG] Flat storage exists after migration:', existsSync(flatStorageDir));
// Verify hierarchical path structure
assert.ok(paths.root.includes('ccw'));
assert.ok(paths.root.endsWith('ccw'));
// Verify data was migrated
const migratedFile = join(paths.cliHistory, 'test.txt');
assert.ok(existsSync(migratedFile));
// Verify old flat structure was deleted
assert.ok(!existsSync(flatStorageDir));
});
it('should handle migration failures gracefully', async () => {
// Create scenario that might fail migration
const parentPath = 'D:\\Claude_dms3';
const parentId = getProjectId(parentPath);
const parentStorageDir = join(TEST_CCW_HOME, 'projects', parentId);
mkdirSync(parentStorageDir, { recursive: true });
const childPath = 'D:\\Claude_dms3\\ccw';
// Should not throw error even if migration fails
assert.doesNotThrow(() => {
const paths = getProjectPaths(childPath);
assert.ok(paths);
});
});
});
describe('Path Normalization', async () => {
it('should normalize Windows path separators', async () => {
const hierarchy = detectHierarchy('D:\\Claude_dms3\\ccw\\src');
// Relative path should use forward slashes
if (hierarchy.relativePath) {
assert.ok(!hierarchy.relativePath.includes('\\'));
assert.ok(hierarchy.relativePath.includes('/'));
}
});
it('should handle trailing slashes', async () => {
const path1 = 'D:\\Claude_dms3\\ccw';
const path2 = 'D:\\Claude_dms3\\ccw\\';
const id1 = getProjectId(path1);
const id2 = getProjectId(path2);
// Should produce same ID regardless of trailing slash
assert.strictEqual(id1, id2);
});
});
describe('Edge Cases', async () => {
it('should handle very deep nesting', async () => {
// Create deep parent storage
const parentPath = 'D:\\Claude_dms3';
const parentId = getProjectId(parentPath);
const parentStorageDir = join(TEST_CCW_HOME, 'projects', parentId);
mkdirSync(parentStorageDir, { recursive: true });
// Generate paths for deeply nested child
const deepPath = 'D:\\Claude_dms3\\a\\b\\c\\d\\e';
const paths = getProjectPaths(deepPath);
assert.ok(paths.root.includes(parentId));
assert.ok(paths.root.includes('a'));
assert.ok(paths.root.includes('e'));
});
it('should handle special characters in path names', async () => {
const specialPath = 'D:\\Claude_dms3\\my-project_v2';
const id = getProjectId(specialPath);
assert.ok(id);
assert.ok(id.includes('my-project_v2'));
});
it('should handle relative paths by resolving them', async () => {
const relativePath = './ccw';
const paths = getProjectPaths(relativePath);
// Should resolve to absolute path
assert.ok(paths.root);
});
});
});