The isDirectChild() function failed to match files when the API used absolute paths (/Users/x/project/app/api) but the database stored relative paths (app/api/router.py). This caused all folder CLAUDE.md files to incorrectly show "No recent activity". Changes: - Create shared path-utils module with proper path normalization - Implement suffix matching strategy for mixed path formats - Update SessionSearch.ts to use shared utilities - Update regenerate-claude-md.ts to use shared utilities (was using outdated broken logic) - Prevent spurious directory creation from malformed paths - Add comprehensive test coverage for path matching edge cases This is the proper fix for #794, replacing PR #809 which only masked the bug by skipping file creation when "no activity" was shown. Co-authored-by: bigphoot <bigphoot@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,8 +43,10 @@ interface ObservationRow {
|
|||||||
discovery_tokens: number | null;
|
discovery_tokens: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import shared formatting utilities
|
// Import shared utilities
|
||||||
import { formatTime, groupByDate } from '../src/shared/timeline-formatting.js';
|
import { formatTime, groupByDate } from '../src/shared/timeline-formatting.js';
|
||||||
|
import { isDirectChild } from '../src/shared/path-utils.js';
|
||||||
|
import { replaceTaggedContent } from '../src/utils/claude-md-utils.js';
|
||||||
|
|
||||||
// Type icon map (matches ModeManager)
|
// Type icon map (matches ModeManager)
|
||||||
const TYPE_ICONS: Record<string, string> = {
|
const TYPE_ICONS: Record<string, string> = {
|
||||||
@@ -135,19 +137,6 @@ function walkDirectoriesWithIgnore(dir: string, folders: Set<string>, depth: num
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a file is a direct child of a folder (not in a subfolder)
|
|
||||||
* @param filePath - File path like "src/services/foo.ts"
|
|
||||||
* @param folderPath - Folder path like "src/services"
|
|
||||||
* @returns true if file is directly in folder, false if in a subfolder
|
|
||||||
*/
|
|
||||||
function isDirectChild(filePath: string, folderPath: string): boolean {
|
|
||||||
if (!filePath.startsWith(folderPath + '/')) return false;
|
|
||||||
const remainder = filePath.slice(folderPath.length + 1);
|
|
||||||
// If remainder contains a slash, it's in a subfolder
|
|
||||||
return !remainder.includes('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an observation has any files that are direct children of the folder
|
* Check if an observation has any files that are direct children of the folder
|
||||||
*/
|
*/
|
||||||
@@ -288,37 +277,27 @@ function formatObservationsForClaudeMd(observations: ObservationRow[], folderPat
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Write CLAUDE.md file with tagged content preservation
|
* Write CLAUDE.md file with tagged content preservation
|
||||||
|
* Note: For the CLI regenerate tool, we DO create directories since the user
|
||||||
|
* explicitly requested regeneration. This differs from the runtime behavior
|
||||||
|
* which only writes to existing folders.
|
||||||
*/
|
*/
|
||||||
function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
|
function writeClaudeMdToFolderForRegenerate(folderPath: string, newContent: string): void {
|
||||||
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
||||||
const tempFile = `${claudeMdPath}.tmp`;
|
const tempFile = `${claudeMdPath}.tmp`;
|
||||||
|
|
||||||
|
// For regenerate CLI, we create the folder if needed
|
||||||
mkdirSync(folderPath, { recursive: true });
|
mkdirSync(folderPath, { recursive: true });
|
||||||
|
|
||||||
|
// Read existing content if file exists
|
||||||
let existingContent = '';
|
let existingContent = '';
|
||||||
if (existsSync(claudeMdPath)) {
|
if (existsSync(claudeMdPath)) {
|
||||||
existingContent = readFileSync(claudeMdPath, 'utf-8');
|
existingContent = readFileSync(claudeMdPath, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTag = '<claude-mem-context>';
|
// Use shared utility to preserve user content outside tags
|
||||||
const endTag = '</claude-mem-context>';
|
const finalContent = replaceTaggedContent(existingContent, newContent);
|
||||||
|
|
||||||
let finalContent: string;
|
|
||||||
if (!existingContent) {
|
|
||||||
finalContent = `${startTag}\n${newContent}\n${endTag}`;
|
|
||||||
} else {
|
|
||||||
const startIdx = existingContent.indexOf(startTag);
|
|
||||||
const endIdx = existingContent.indexOf(endTag);
|
|
||||||
|
|
||||||
if (startIdx !== -1 && endIdx !== -1) {
|
|
||||||
finalContent = existingContent.substring(0, startIdx) +
|
|
||||||
`${startTag}\n${newContent}\n${endTag}` +
|
|
||||||
existingContent.substring(endIdx + endTag.length);
|
|
||||||
} else {
|
|
||||||
finalContent = existingContent + `\n\n${startTag}\n${newContent}\n${endTag}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Atomic write: temp file + rename
|
||||||
writeFileSync(tempFile, finalContent);
|
writeFileSync(tempFile, finalContent);
|
||||||
renameSync(tempFile, claudeMdPath);
|
renameSync(tempFile, claudeMdPath);
|
||||||
}
|
}
|
||||||
@@ -450,7 +429,7 @@ function regenerateFolder(
|
|||||||
|
|
||||||
// Format using relative path for display, write to absolute path
|
// Format using relative path for display, write to absolute path
|
||||||
const formatted = formatObservationsForClaudeMd(observations, relativeFolder);
|
const formatted = formatObservationsForClaudeMd(observations, relativeFolder);
|
||||||
writeClaudeMdToFolder(absoluteFolder, formatted);
|
writeClaudeMdToFolderForRegenerate(absoluteFolder, formatted);
|
||||||
|
|
||||||
return { success: true, observationCount: observations.length };
|
return { success: true, observationCount: observations.length };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Database } from 'bun:sqlite';
|
|||||||
import { TableNameRow } from '../../types/database.js';
|
import { TableNameRow } from '../../types/database.js';
|
||||||
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
||||||
import { logger } from '../../utils/logger.js';
|
import { logger } from '../../utils/logger.js';
|
||||||
|
import { isDirectChild } from '../../shared/path-utils.js';
|
||||||
import {
|
import {
|
||||||
ObservationSearchResult,
|
ObservationSearchResult,
|
||||||
SessionSummarySearchResult,
|
SessionSummarySearchResult,
|
||||||
@@ -336,15 +337,6 @@ export class SessionSearch {
|
|||||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a file is a direct child of a folder (not in a subfolder)
|
|
||||||
*/
|
|
||||||
private isDirectChild(filePath: string, folderPath: string): boolean {
|
|
||||||
if (!filePath.startsWith(folderPath + '/')) return false;
|
|
||||||
const remainder = filePath.slice(folderPath.length + 1);
|
|
||||||
return !remainder.includes('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an observation has any files that are direct children of the folder
|
* Check if an observation has any files that are direct children of the folder
|
||||||
*/
|
*/
|
||||||
@@ -354,7 +346,7 @@ export class SessionSearch {
|
|||||||
try {
|
try {
|
||||||
const files = JSON.parse(filesJson);
|
const files = JSON.parse(filesJson);
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
return files.some(f => this.isDirectChild(f, folderPath));
|
return files.some(f => isDirectChild(f, folderPath));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
@@ -372,7 +364,7 @@ export class SessionSearch {
|
|||||||
try {
|
try {
|
||||||
const files = JSON.parse(filesJson);
|
const files = JSON.parse(filesJson);
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
return files.some(f => this.isDirectChild(f, folderPath));
|
return files.some(f => isDirectChild(f, folderPath));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Shared path utilities for CLAUDE.md file generation
|
||||||
|
*
|
||||||
|
* These utilities handle path normalization and matching, particularly
|
||||||
|
* for comparing absolute and relative paths in folder CLAUDE.md generation.
|
||||||
|
*
|
||||||
|
* @see Issue #794 - Path format mismatch causes folder CLAUDE.md files to show "No recent activity"
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize path separators to forward slashes, collapse consecutive slashes,
|
||||||
|
* and remove trailing slashes.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* normalizePath('app\\api\\router.py') // 'app/api/router.py'
|
||||||
|
* normalizePath('app//api///router.py') // 'app/api/router.py'
|
||||||
|
* normalizePath('app/api/') // 'app/api'
|
||||||
|
*/
|
||||||
|
export function normalizePath(p: string): string {
|
||||||
|
return p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is a direct child of a folder (not in a subfolder).
|
||||||
|
*
|
||||||
|
* Handles path format mismatches where folderPath may be absolute but
|
||||||
|
* filePath is stored as relative in the database.
|
||||||
|
*
|
||||||
|
* NOTE: This uses suffix matching which assumes both paths are relative to
|
||||||
|
* the same project root. It may produce false positives if used across
|
||||||
|
* different project roots, but this is mitigated by project-scoped queries.
|
||||||
|
*
|
||||||
|
* @param filePath - Path to the file (e.g., "app/api/router.py" or "/Users/x/project/app/api/router.py")
|
||||||
|
* @param folderPath - Path to the folder (e.g., "app/api" or "/Users/x/project/app/api")
|
||||||
|
* @returns true if file is directly in folder, false if in a subfolder or different folder
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Same format (both relative)
|
||||||
|
* isDirectChild('app/api/router.py', 'app/api') // true
|
||||||
|
* isDirectChild('app/api/v1/router.py', 'app/api') // false (in subfolder)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Mixed format (absolute folder, relative file) - fixes #794
|
||||||
|
* isDirectChild('app/api/router.py', '/Users/dev/project/app/api') // true
|
||||||
|
*/
|
||||||
|
export function isDirectChild(filePath: string, folderPath: string): boolean {
|
||||||
|
const normFile = normalizePath(filePath);
|
||||||
|
const normFolder = normalizePath(folderPath);
|
||||||
|
|
||||||
|
// Strategy 1: Direct prefix match (both paths in same format)
|
||||||
|
if (normFile.startsWith(normFolder + '/')) {
|
||||||
|
const remainder = normFile.slice(normFolder.length + 1);
|
||||||
|
return !remainder.includes('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Handle absolute folderPath with relative filePath
|
||||||
|
// e.g., folderPath="/Users/x/project/app/api" and filePath="app/api/router.py"
|
||||||
|
const folderSegments = normFolder.split('/');
|
||||||
|
const fileSegments = normFile.split('/');
|
||||||
|
|
||||||
|
if (fileSegments.length < 2) return false; // Need at least folder/file
|
||||||
|
|
||||||
|
const fileDir = fileSegments.slice(0, -1).join('/'); // Directory part of file
|
||||||
|
const fileName = fileSegments[fileSegments.length - 1]; // Actual filename
|
||||||
|
|
||||||
|
// Check if folder path ends with the file's directory path
|
||||||
|
if (normFolder.endsWith('/' + fileDir) || normFolder === fileDir) {
|
||||||
|
// File is a direct child (no additional subdirectories)
|
||||||
|
return !fileName.includes('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file's directory is contained at the end of folder path
|
||||||
|
// by progressively checking suffixes
|
||||||
|
for (let i = 0; i < folderSegments.length; i++) {
|
||||||
|
const folderSuffix = folderSegments.slice(i).join('/');
|
||||||
|
if (folderSuffix === fileDir) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* <claude-mem-context> tags.
|
* <claude-mem-context> tags.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
@@ -86,17 +86,22 @@ export function replaceTaggedContent(existingContent: string, newContent: string
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Write CLAUDE.md file to folder with atomic writes.
|
* Write CLAUDE.md file to folder with atomic writes.
|
||||||
* Creates directory structure if needed.
|
* Only writes to existing folders; skips non-existent paths to prevent
|
||||||
|
* creating spurious directory structures from malformed paths.
|
||||||
*
|
*
|
||||||
* @param folderPath - Absolute path to the folder
|
* @param folderPath - Absolute path to the folder (must already exist)
|
||||||
* @param newContent - Content to write inside tags
|
* @param newContent - Content to write inside tags
|
||||||
*/
|
*/
|
||||||
export function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
|
export function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
|
||||||
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
||||||
const tempFile = `${claudeMdPath}.tmp`;
|
const tempFile = `${claudeMdPath}.tmp`;
|
||||||
|
|
||||||
// Ensure directory exists
|
// Only write to folders that already exist - never create new directories
|
||||||
mkdirSync(folderPath, { recursive: true });
|
// This prevents creating spurious folder structures from malformed paths
|
||||||
|
if (!existsSync(folderPath)) {
|
||||||
|
logger.debug('FOLDER_INDEX', 'Skipping non-existent folder', { folderPath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Read existing content if file exists
|
// Read existing content if file exists
|
||||||
let existingContent = '';
|
let existingContent = '';
|
||||||
@@ -321,7 +326,7 @@ export async function updateFolderClaudeMdFiles(
|
|||||||
|
|
||||||
const formatted = formatTimelineForClaudeMd(result.content[0].text);
|
const formatted = formatTimelineForClaudeMd(result.content[0].text);
|
||||||
|
|
||||||
// Fix for #758: Don't create new CLAUDE.md files if there's no activity
|
// Fix for #794: Don't create new CLAUDE.md files if there's no activity
|
||||||
// But update existing ones to show "No recent activity" if they already exist
|
// But update existing ones to show "No recent activity" if they already exist
|
||||||
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
||||||
const hasNoActivity = formatted.includes('*No recent activity*');
|
const hasNoActivity = formatted.includes('*No recent activity*');
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { isDirectChild, normalizePath } from '../../../src/shared/path-utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for path matching logic, specifically the isDirectChild() algorithm
|
||||||
|
* Covers fix for issue #794: Path format mismatch causes folder CLAUDE.md files to show "No recent activity"
|
||||||
|
*
|
||||||
|
* These tests validate the shared path-utils module which is used by:
|
||||||
|
* - SessionSearch.ts (runtime folder CLAUDE.md generation)
|
||||||
|
* - regenerate-claude-md.ts (CLI regeneration tool)
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('isDirectChild path matching', () => {
|
||||||
|
describe('same path format', () => {
|
||||||
|
test('returns true for direct child with relative paths', () => {
|
||||||
|
expect(isDirectChild('app/api/router.py', 'app/api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true for direct child with absolute paths', () => {
|
||||||
|
expect(isDirectChild('/Users/dev/project/app/api/router.py', '/Users/dev/project/app/api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for files in subdirectory with relative paths', () => {
|
||||||
|
expect(isDirectChild('app/api/v1/router.py', 'app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for files in subdirectory with absolute paths', () => {
|
||||||
|
expect(isDirectChild('/Users/dev/project/app/api/v1/router.py', '/Users/dev/project/app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for unrelated paths', () => {
|
||||||
|
expect(isDirectChild('lib/utils/helper.py', 'app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mixed path formats (absolute folder, relative file) - fixes #794', () => {
|
||||||
|
test('returns true when absolute folder ends with relative file directory', () => {
|
||||||
|
// This is the exact bug case from #794
|
||||||
|
expect(isDirectChild('app/api/router.py', '/Users/dev/project/app/api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true for deeply nested folder match', () => {
|
||||||
|
expect(isDirectChild('src/components/Button.tsx', '/home/user/project/src/components')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for files in subdirectory of matched folder', () => {
|
||||||
|
expect(isDirectChild('app/api/v1/router.py', '/Users/dev/project/app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when file path does not match folder suffix', () => {
|
||||||
|
expect(isDirectChild('lib/api/router.py', '/Users/dev/project/app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('path normalization', () => {
|
||||||
|
test('handles Windows backslash paths', () => {
|
||||||
|
expect(isDirectChild('app\\api\\router.py', 'app\\api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles mixed slashes', () => {
|
||||||
|
expect(isDirectChild('app/api\\router.py', 'app\\api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles trailing slashes on folder path', () => {
|
||||||
|
expect(isDirectChild('app/api/router.py', 'app/api/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles double slashes (path normalization bug)', () => {
|
||||||
|
expect(isDirectChild('app//api/router.py', 'app/api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collapses multiple consecutive slashes', () => {
|
||||||
|
expect(isDirectChild('app///api///router.py', 'app//api//')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
test('returns false for single segment file path', () => {
|
||||||
|
expect(isDirectChild('router.py', '/Users/dev/project/app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for empty paths', () => {
|
||||||
|
expect(isDirectChild('', 'app/api')).toBe(false);
|
||||||
|
expect(isDirectChild('app/api/router.py', '')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles root-level folders', () => {
|
||||||
|
expect(isDirectChild('src/file.ts', '/project/src')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prevents false positive from partial segment match', () => {
|
||||||
|
// "api" folder should not match "api-v2" folder
|
||||||
|
expect(isDirectChild('app/api-v2/router.py', '/Users/dev/project/app/api')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles similar folder names correctly', () => {
|
||||||
|
// "components" should not match "components-old"
|
||||||
|
expect(isDirectChild('src/components-old/Button.tsx', '/project/src/components')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizePath', () => {
|
||||||
|
test('converts backslashes to forward slashes', () => {
|
||||||
|
expect(normalizePath('app\\api\\router.py')).toBe('app/api/router.py');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collapses consecutive slashes', () => {
|
||||||
|
expect(normalizePath('app//api///router.py')).toBe('app/api/router.py');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes trailing slashes', () => {
|
||||||
|
expect(normalizePath('app/api/')).toBe('app/api');
|
||||||
|
expect(normalizePath('app/api///')).toBe('app/api');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles Windows UNC paths', () => {
|
||||||
|
expect(normalizePath('\\\\server\\share\\file.txt')).toBe('/server/share/file.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves leading slash for absolute paths', () => {
|
||||||
|
expect(normalizePath('/Users/dev/project')).toBe('/Users/dev/project');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -147,8 +147,22 @@ describe('formatTimelineForClaudeMd', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('writeClaudeMdToFolder', () => {
|
describe('writeClaudeMdToFolder', () => {
|
||||||
it('should create CLAUDE.md in new folder', () => {
|
it('should skip non-existent folders (fix for spurious directory creation)', () => {
|
||||||
const folderPath = join(tempDir, 'new-folder');
|
const folderPath = join(tempDir, 'non-existent-folder');
|
||||||
|
const content = '# Recent Activity\n\nTest content';
|
||||||
|
|
||||||
|
// Should not throw, should silently skip
|
||||||
|
writeClaudeMdToFolder(folderPath, content);
|
||||||
|
|
||||||
|
// Folder and CLAUDE.md should NOT be created
|
||||||
|
expect(existsSync(folderPath)).toBe(false);
|
||||||
|
const claudeMdPath = join(folderPath, 'CLAUDE.md');
|
||||||
|
expect(existsSync(claudeMdPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create CLAUDE.md in existing folder', () => {
|
||||||
|
const folderPath = join(tempDir, 'existing-folder');
|
||||||
|
mkdirSync(folderPath, { recursive: true });
|
||||||
const content = '# Recent Activity\n\nTest content';
|
const content = '# Recent Activity\n\nTest content';
|
||||||
|
|
||||||
writeClaudeMdToFolder(folderPath, content);
|
writeClaudeMdToFolder(folderPath, content);
|
||||||
@@ -180,20 +194,22 @@ describe('writeClaudeMdToFolder', () => {
|
|||||||
expect(fileContent).not.toContain('Old content');
|
expect(fileContent).not.toContain('Old content');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create nested directories', () => {
|
it('should not create nested directories (fix for spurious directory creation)', () => {
|
||||||
const folderPath = join(tempDir, 'deep', 'nested', 'folder');
|
const folderPath = join(tempDir, 'deep', 'nested', 'folder');
|
||||||
const content = 'Nested content';
|
const content = 'Nested content';
|
||||||
|
|
||||||
|
// Should not throw, should silently skip
|
||||||
writeClaudeMdToFolder(folderPath, content);
|
writeClaudeMdToFolder(folderPath, content);
|
||||||
|
|
||||||
|
// Nested directories should NOT be created
|
||||||
const claudeMdPath = join(folderPath, 'CLAUDE.md');
|
const claudeMdPath = join(folderPath, 'CLAUDE.md');
|
||||||
expect(existsSync(claudeMdPath)).toBe(true);
|
expect(existsSync(claudeMdPath)).toBe(false);
|
||||||
expect(existsSync(join(tempDir, 'deep'))).toBe(true);
|
expect(existsSync(join(tempDir, 'deep'))).toBe(false);
|
||||||
expect(existsSync(join(tempDir, 'deep', 'nested'))).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not leave .tmp file after write (atomic write)', () => {
|
it('should not leave .tmp file after write (atomic write)', () => {
|
||||||
const folderPath = join(tempDir, 'atomic-test');
|
const folderPath = join(tempDir, 'atomic-test');
|
||||||
|
mkdirSync(folderPath, { recursive: true });
|
||||||
const content = 'Atomic write test';
|
const content = 'Atomic write test';
|
||||||
|
|
||||||
writeClaudeMdToFolder(folderPath, content);
|
writeClaudeMdToFolder(folderPath, content);
|
||||||
@@ -218,6 +234,7 @@ describe('updateFolderClaudeMdFiles', () => {
|
|||||||
|
|
||||||
it('should fetch timeline and write CLAUDE.md', async () => {
|
it('should fetch timeline and write CLAUDE.md', async () => {
|
||||||
const folderPath = join(tempDir, 'api-test');
|
const folderPath = join(tempDir, 'api-test');
|
||||||
|
mkdirSync(folderPath, { recursive: true }); // Folder must exist - we no longer create directories
|
||||||
const filePath = join(folderPath, 'test.ts');
|
const filePath = join(folderPath, 'test.ts');
|
||||||
|
|
||||||
const apiResponse = {
|
const apiResponse = {
|
||||||
@@ -412,6 +429,7 @@ describe('updateFolderClaudeMdFiles', () => {
|
|||||||
|
|
||||||
it('should write CLAUDE.md to resolved projectRoot path', async () => {
|
it('should write CLAUDE.md to resolved projectRoot path', async () => {
|
||||||
const subfolderPath = join(tempDir, 'project-root-write-test', 'src', 'utils');
|
const subfolderPath = join(tempDir, 'project-root-write-test', 'src', 'utils');
|
||||||
|
mkdirSync(subfolderPath, { recursive: true }); // Folder must exist - we no longer create directories
|
||||||
|
|
||||||
const apiResponse = {
|
const apiResponse = {
|
||||||
content: [{
|
content: [{
|
||||||
|
|||||||
Reference in New Issue
Block a user