feat: Live Context System with Distributed CLAUDE.md Generation (#556)

* docs: add folder index generator plan

RFC for auto-generating folder-level CLAUDE.md files with observation
timelines. Includes IDE symlink support and root CLAUDE.md integration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: implement folder index generator (Phase 1)

Add automatic CLAUDE.md generation for folders containing observed files.
This enables IDE context providers to access relevant memory observations.

Core modules:
- FolderDiscovery: Extract folders from observation file paths
- FolderTimelineCompiler: Compile chronological timeline per folder
- ClaudeMdGenerator: Write CLAUDE.md with tag-based content replacement
- FolderIndexOrchestrator: Coordinate regeneration on observation save

Integration:
- Event-driven regeneration after observation save in ResponseProcessor
- HTTP endpoints for folder discovery, timeline, and manual generation
- Settings for enabling/configuring folder index behavior

The <claude-mem-context> tag wrapping ensures:
- Manual CLAUDE.md content is preserved
- Auto-generated content won't be recursively observed
- Clean separation between user and system content

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add updateFolderClaudeMd function to CursorHooksInstaller

Adds function to update CLAUDE.md files for folders touched by observations.
Uses existing /api/search/by-file endpoint, preserves content outside
<claude-mem-context> tags, and writes atomically via temp file + rename.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: hook updateFolderClaudeMd into ResponseProcessor

Calls updateFolderClaudeMd after observation save to update folder-level
CLAUDE.md files. Uses fire-and-forget pattern with error logging.
Extracts file paths from saved observations and workspace path from registry.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add timeline formatting for folder CLAUDE.md files

Implements formatTimelineForClaudeMd function that transforms API response
into compact markdown table format. Converts emojis to text labels,
handles ditto marks for timestamps, and groups under "Recent" header.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove old folder-index implementation

Deletes redundant folder-index services that were replaced by the simpler
updateFolderClaudeMd approach in CursorHooksInstaller.ts.

Removed:
- src/services/folder-index/ directory (5 files)
- FolderIndexRoutes.ts
- folder-index settings from SettingsDefaultsManager
- folder-index route registration from worker-service

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add worktree-aware project filtering for unified timelines

Detect git worktrees and show both parent repo and worktree observations
in the session start timeline. When running in a worktree, the context
now includes observations from both projects, interleaved chronologically.

- Add detectWorktree() utility to identify worktree directories
- Add getProjectContext() to return parent + worktree projects
- Update context hook to pass multi-project queries
- Add queryObservationsMulti() and querySummariesMulti() for IN clauses
- Maintain backward compatibility with single-project queries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix: restructure logging to prove session correctness and reduce noise

Add critical logging at each stage of the session lifecycle to prove the session ID chain (contentSessionId → sessionDbId → memorySessionId) stays aligned. New logs include CREATED, ENQUEUED, CLAIMED, MEMORY_ID_CAPTURED, STORING, and STORED. Move intermediate migration and backfill progress logs to DEBUG level to reduce noise, keeping only essential initialization and completion logs at INFO level.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* refactor: extract folder CLAUDE.md utils to shared location

Moves folder CLAUDE.md utilities from CursorHooksInstaller to a new
shared utils file. Removes Cursor registry dependency - file paths
from observations are already absolute, no workspace lookup needed.

New file: src/utils/claude-md-utils.ts
- replaceTaggedContent() - preserves user content outside tags
- writeClaudeMdToFolder() - atomic writes with tag preservation
- formatTimelineForClaudeMd() - API response to compact markdown
- updateFolderClaudeMdFiles() - orchestrates folder updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: trigger folder CLAUDE.md updates when observations are saved

The folder CLAUDE.md update was previously only triggered in
syncAndBroadcastSummary, but summaries run with observationCount=0
(observations are saved separately). Moved the update logic to
syncAndBroadcastObservations where file paths are available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* all the claudes

* test: add unit tests for claude-md-utils pure functions

Add 11 tests covering replaceTaggedContent and formatTimelineForClaudeMd:
- replaceTaggedContent: empty content, tag replacement, appending, partial tags
- formatTimelineForClaudeMd: empty input, parsing, ditto marks, session IDs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add integration tests for file operation functions

Add 9 tests for writeClaudeMdToFolder and updateFolderClaudeMdFiles:
- writeClaudeMdToFolder: folder creation, content preservation, nested dirs, atomic writes
- updateFolderClaudeMdFiles: empty skip, fetch/write, deduplication, error handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add unit tests for timeline-formatting utilities

Add 14 tests for extractFirstFile and groupByDate functions:
- extractFirstFile: relative paths, fallback to files_read, null handling, invalid JSON
- groupByDate: empty arrays, date grouping, chronological sorting, item preservation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: rebuild plugin scripts with merged features

* docs: add project-specific CLAUDE.md with architecture and development notes

* fix: exclude project root from auto-generated CLAUDE.md updates

Skip folders containing .git directory when auto-updating subfolder
CLAUDE.md files. This ensures:

1. Root CLAUDE.md remains user-managed and untouched by the system
2. SessionStart context injection stays pristine throughout the session
3. Subfolder CLAUDE.md files continue to receive live context updates
4. Cleaner separation between user-authored root docs and auto-generated folder indexes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: prevent crash from resuming stale SDK sessions on worker restart

When the worker restarts, it was incorrectly passing the `resume` parameter
to INIT prompts (lastPromptNumber=1) when a memorySessionId existed from a
previous SDK session. This caused "Claude Code process exited with code 1"
crashes because the SDK tried to resume into a session that no longer exists.

Root cause: The resume condition only checked `hasRealMemorySessionId` but
did not verify that this was a CONTINUATION prompt (lastPromptNumber > 1).

Fix: Add `session.lastPromptNumber > 1` check to the resume condition:
- Before: `...(hasRealMemorySessionId && { resume: session.memorySessionId })`
- After: `...(hasRealMemorySessionId && session.lastPromptNumber > 1 && { resume: ... })`

Also added:
- Enhanced debug logging that warns when skipping resume for INIT prompts
- Unit tests in tests/sdk-agent-resume.test.ts (9 test cases)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: properly handle Chroma MCP connection errors

Previously, ensureCollection() caught ALL errors from chroma_get_collection_info
and assumed they meant "collection doesn't exist", triggering unnecessary
collection creation attempts. Connection errors like "Not connected" or
"MCP error -32000: Connection closed" would cascade into failed creation attempts.

Similarly, queryChroma() would silently return empty results when the MCP call
failed, masking the underlying connection problem.

Changes:
- ensureCollection(): Detect connection errors and re-throw immediately instead
  of attempting collection creation
- queryChroma(): Wrap MCP call in try-catch and throw connection errors instead
  of returning empty results
- Both methods reset connection state (connected=false, client=null) on
  connection errors so subsequent operations can attempt to reconnect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* pushed

* fix: scope regenerate-claude-md.ts to current working directory

Critical bug fix: The script was querying ALL observations from the database
across ALL projects ever recorded (1396+ folders), then attempting to write
CLAUDE.md files everywhere including other projects, non-existent paths, and
ignored directories.

Changes:
- Use git ls-files to discover folders (respects .gitignore automatically)
- Filter database query to current project only (by folder name)
- Use relative paths for database queries (matches storage format)
- Add --clean flag to remove auto-generated CLAUDE.md files
- Add fallback directory walker for non-git repos

Now correctly scopes to 26 folders with observations instead of 1396+.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs and adjustments

* fix: cleanup mode strips tags instead of deleting files blindly

The cleanup mode was incorrectly deleting entire files that contained
<claude-mem-context> tags. The correct behavior (per original design):

1. Strip the <claude-mem-context>...</claude-mem-context> section
2. If empty after stripping → delete the file
3. If has remaining content → save the stripped version

Now properly preserves user content in CLAUDE.md files while removing
only the auto-generated sections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* deleted some files

* chore: regenerate folder CLAUDE.md files with fixed script

Regenerated 23 folder CLAUDE.md files using the corrected script that:
- Scopes to current working directory only
- Uses git ls-files to respect .gitignore
- Filters by project name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update CLAUDE.md files for January 5, 2026

- Regenerated and staged 23 CLAUDE.md files with a mix of new and modified content.
- Fixed cleanup mode to properly strip tags instead of deleting files blindly.
- Cleaned up empty CLAUDE.md files from various directories, including ~/.claude and ~/Scripts.
- Conducted dry-run cleanup that identified a significant reduction in auto-generated CLAUDE.md files.
- Removed the isAutoGeneratedClaudeMd function due to incorrect file deletion behavior.

* feat: use settings for observation limit in batch regeneration script

Replace hard-coded limit of 10 with configurable CLAUDE_MEM_CONTEXT_OBSERVATIONS
setting (default: 50). This allows users to control how many observations appear
in folder CLAUDE.md files.

Changes:
- Import SettingsDefaultsManager and load settings at script startup
- Use OBSERVATION_LIMIT constant derived from settings at both call sites
- Remove stale default parameter from findObservationsByFolder function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: use settings for observation limit in event-driven updates

Replace hard-coded limit of 10 in updateFolderClaudeMdFiles with
configurable CLAUDE_MEM_CONTEXT_OBSERVATIONS setting (default: 50).

Changes:
- Import SettingsDefaultsManager and os module
- Load settings at function start (once, not in loop)
- Use limit from settings in API call

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Implement configurable observation limits and enhance search functionality

- Added configurable observation limits to batch regeneration scripts.
- Enhanced SearchManager to handle folder queries and normalize parameters.
- Introduced methods to check for direct child files in observations and sessions.
- Updated SearchOptions interface to include isFolder flag for filtering.
- Improved code quality with comprehensive reviews and anti-pattern checks.
- Cleaned up auto-generated CLAUDE.md files across various directories.
- Documented recent changes and improvements in CLAUDE.md files.

* build asset

* Project Context from Claude-Mem auto-added (can be auto removed at any time)

* CLAUDE.md updates

* fix: resolve CLAUDE.md files to correct directory in worktree setups

When using git worktrees, CLAUDE.md files were being written relative to
the worker's process.cwd() instead of the actual project directory. This
fix threads the project's cwd from message processing through to the file
writing utilities, ensuring CLAUDE.md files are created in the correct
project directory regardless of where the worker was started.

Changes:
- Add projectRoot parameter to updateFolderClaudeMdFiles for path resolution
- Thread projectRoot through ResponseProcessor call chain
- Track lastCwd from messages in SDKAgent, GeminiAgent, OpenRouterAgent
- Add tests for relative/absolute path handling with projectRoot

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* more project context updates

* context updates

* fix: preserve actual dates in folder CLAUDE.md generation

Previously, formatTimelineForClaudeMd used today's date for all
observations because the API only returned time (e.g., "4:30 PM")
without date information. This caused all historical observations
to appear as if they happened today.

Changes:
- SearchManager.findByFile now groups results by date with headers
  (e.g., "### Jan 4, 2026") matching formatSearchResults behavior
- formatTimelineForClaudeMd now parses these date headers and uses
  the correct date when constructing epochs for date grouping

The timeline dates are critical for claude-mem context - LLMs need
accurate temporal context to understand when work happened.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* build: update worker assets with date parsing fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude-mem context: Fixed critical date parsing bug in PR #556

* fix: address PR #556 review items

- Use getWorkerHost() instead of hard-coded 127.0.0.1 in claude-md-utils
- Add error message and stack details to FOLDER_INDEX logging
- Add 5 new tests for worktree/projectRoot path resolution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Refactor CLAUDE documentation across multiple components and tests

- Updated CLAUDE.md files in src/ui/viewer, src/ui/viewer/constants, src/ui/viewer/hooks, tests/server, tests/worker/agents, and plans to reflect recent changes and improvements.
- Removed outdated entries and consolidated recent activities for clarity.
- Enhanced documentation for hooks, settings, and pagination implementations.
- Streamlined test suite documentation for server and worker agents, indicating recent test audits and cleanup efforts.
- Adjusted plans to remove obsolete entries and focus on current implementation strategies.

* docs: comprehensive v9.0 documentation audit and updates

- Add usage/folder-context to docs.json navigation (was documented but hidden!)
- Update introduction.mdx with v9.0 release notes (Live Context, Worktree Support, Windows Fixes)
- Add CLAUDE_MEM_WORKER_HOST setting to configuration.mdx
- Add Folder Context Files section with link to detailed docs
- Document worktree support in folder-context.mdx
- Update terminology from "mem-search skill" to "MCP tools" throughout active docs
- Update Search Pipeline in architecture/overview.mdx
- Update usage/getting-started.mdx with MCP tools terminology
- Update usage/claude-desktop.mdx title and terminology
- Update hooks-architecture.mdx reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add recent activity log for worker CLI with detailed entries

* chore: update CLAUDE.md context files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add brainstorming report for CLAUDE.md distribution architecture

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-01-05 22:41:42 -05:00
committed by GitHub
parent 21a1e272d9
commit e1ab73decc
152 changed files with 10367 additions and 2103 deletions
+37 -12
View File
@@ -3,13 +3,38 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
### Nov 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #6358 | 3:14 PM | 🔵 | SDK Agent Spatial Awareness Implementation | ~309 |
### Nov 21, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #13289 | 2:20 PM | 🟣 | Comprehensive Test Suite for Transcript Transformation | ~320 |
### Nov 23, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #14617 | 6:15 PM | 🟣 | Test Suite Successfully Passing - All 8 Tests Green | ~498 |
| #14615 | 6:14 PM | 🟣 | YAGNI-Focused Test Suite for Transcript Transformation | ~457 |
### Dec 5, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #20732 | 9:07 PM | 🔵 | Smart Install Version Marker Tests for Upgrade Detection | ~452 |
| #20399 | 7:17 PM | 🔵 | Smart install tests validate version tracking with backward compatibility | ~311 |
| #20392 | 7:15 PM | 🔵 | Memory tag stripping tests validate dual-tag system for JSON context filtering | ~404 |
| #20391 | " | 🔵 | User prompt tag stripping tests validate privacy controls for memory exclusion | ~182 |
### Jan 3, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36858 | 1:50 AM | 🟣 | Phase 1 Implementation Completed via Subagent | ~499 |
| #36854 | 1:49 AM | 🟣 | gemini-3-flash Model Tests Added to GeminiAgent Test Suite | ~470 |
| #36851 | " | 🔵 | GeminiAgent Test Structure Analyzed | ~565 |
| #36663 | 11:06 PM | ✅ | Third Validation Test Updated: Resume Safety Check Now Uses NULL Comparison | ~417 |
| #36662 | " | ✅ | Second Validation Test Updated: Post-Capture Check Now Uses NULL Comparison | ~418 |
| #36661 | 11:05 PM | ✅ | First Validation Test Updated: Placeholder Detection Now Checks for NULL | ~482 |
@@ -24,12 +49,12 @@
| #36648 | " | 🔵 | Session ID Refactor Test Suite Documents Database Migration 17 and Dual ID System | ~651 |
| #36647 | 11:01 PM | 🔵 | SessionStore Test Suite Validates Prompt Counting and Timestamp Override Features | ~506 |
| #36646 | " | 🔵 | Session ID Architecture Revealed Through Test File Analysis | ~611 |
| #20732 | 9:07 PM | 🔵 | Smart Install Version Marker Tests for Upgrade Detection | ~452 |
| #20399 | 7:17 PM | 🔵 | Smart install tests validate version tracking with backward compatibility | ~311 |
| #20392 | 7:15 PM | 🔵 | Memory tag stripping tests validate dual-tag system for JSON context filtering | ~404 |
| #20391 | " | 🔵 | User prompt tag stripping tests validate privacy controls for memory exclusion | ~182 |
| #14617 | 6:15 PM | 🟣 | Test Suite Successfully Passing - All 8 Tests Green | ~498 |
| #14615 | 6:14 PM | 🟣 | YAGNI-Focused Test Suite for Transcript Transformation | ~457 |
| #13289 | 2:20 PM | 🟣 | Comprehensive Test Suite for Transcript Transformation | ~320 |
| #6358 | 3:14 PM | 🔵 | SDK Agent Spatial Awareness Implementation | ~309 |
### Jan 4, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36858 | 1:50 AM | 🟣 | Phase 1 Implementation Completed via Subagent | ~499 |
| #36854 | 1:49 AM | 🟣 | gemini-3-flash Model Tests Added to GeminiAgent Test Suite | ~470 |
| #36851 | " | 🔵 | GeminiAgent Test Structure Analyzed | ~565 |
</claude-mem-context>
+31
View File
@@ -0,0 +1,31 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**markdown-formatter.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36565 | 9:51 PM | 🟣 | Phase 4 Context Generation Tests Committed | ~575 |
| #36564 | 9:50 PM | 🟣 | Phase 4 Context Generation Tests Committed | ~406 |
| #36562 | 9:49 PM | 🟣 | Phase 4 Context Generation Tests Completed | ~524 |
| #36561 | " | 🟣 | Phase 4 Context Generation Test Suite Completion | ~606 |
| #36557 | 9:47 PM | 🟣 | MarkdownFormatter Test Suite Created | ~520 |
### Jan 5, 2026
**markdown-formatter.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37642 | 5:41 PM | 🟣 | Second Task Agent Independently Created and Verified FormatTool Tests | ~544 |
| #37641 | " | 🔴 | Task Agent Confirmed Terminology Test Fixes Complete | ~465 |
| #37639 | 5:40 PM | 🔴 | Markdown Formatter Tests Now Pass After Terminology Updates | ~461 |
| #37637 | " | 🔴 | Fixed Second Failing Test for MCP Tools Terminology Update | ~424 |
| #37634 | 5:39 PM | 🔴 | Fixed First Failing Test for MCP Tools Terminology Update | ~378 |
| #37631 | 5:37 PM | 🔵 | Failing Tests Identified at Lines 176 and 495 | ~508 |
| #37630 | 5:36 PM | 🔴 | Test Suite Execution Reveals Two Failing Tests for Terminology Update | ~607 |
| #37629 | " | 🔵 | Comprehensive Testing Patterns Documentation Generated | ~629 |
| #37621 | 5:34 PM | 🔵 | Existing Bun Test Pattern for Formatter Functions | ~590 |
</claude-mem-context>
+37
View File
@@ -0,0 +1,37 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 4, 2026
**folder-timeline-compiler.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37022 | 4:59 PM | ✅ | Deleted Redundant Folder Index Test Directory | ~235 |
**folder-discovery.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37005 | 4:48 PM | ✅ | Verified folder-discovery and claude-md-generator tests pass successfully | ~238 |
| #37003 | 4:46 PM | 🔄 | Simplified FolderDiscovery tests to use production imports instead of duplicated logic | ~380 |
| #36999 | 4:36 PM | 🔄 | Simplified FolderDiscovery tests to use local algorithm implementations without production imports | ~401 |
**claude-md-generator.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37004 | 4:48 PM | 🔄 | Reverted ClaudeMdGenerator tests to use actual production code instead of local copies | ~406 |
| #36998 | 4:33 PM | 🟣 | Added unit tests for formatTimelineToMarkdown and replaceTaggedContent helper functions | ~435 |
| #36997 | 4:32 PM | 🔄 | Refactored ClaudeMdGenerator tests to use local implementation instead of importing production code | ~386 |
**folder-index-routes.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36996 | 4:31 PM | 🔄 | Refactored FolderIndexRoutes tests to focus on unit testing logic instead of HTTP integration | ~405 |
| #36995 | 4:30 PM | 🟣 | Created FolderIndexRoutes test suite with 13 HTTP endpoint tests | ~479 |
**folder-index-orchestrator.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36994 | 4:29 PM | 🟣 | Created FolderIndexOrchestrator test suite with 13 test cases validating orchestration logic | ~461 |
</claude-mem-context>
+53
View File
@@ -0,0 +1,53 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**graceful-shutdown.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36574 | 9:55 PM | 🟣 | Phase 5 Infrastructure Tests Committed | ~357 |
| #36569 | 9:54 PM | 🟣 | GracefulShutdown Test Suite Created | ~457 |
**process-manager.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36571 | 9:54 PM | 🟣 | Phase 5 Infrastructure Tests Completed | ~504 |
| #36570 | " | 🟣 | ProcessManager Infrastructure Test Suite Created | ~535 |
**health-monitor.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36568 | 9:53 PM | 🟣 | HealthMonitor Test Suite Created for Phase 5 | ~437 |
### Jan 4, 2026
**wmic-parsing.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36924 | 2:25 AM | ✅ | Merged fix/pr-538-followups branch into main with comprehensive updates | ~481 |
| #36908 | 2:01 AM | ✅ | Regression Tests Committed Successfully | ~418 |
| #36907 | " | ✅ | Regression Test Files Staged for Commit | ~317 |
**process-manager.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36842 | 1:43 AM | 🔵 | Complete Test Framework and Pattern Documentation | ~670 |
| #36837 | 1:42 AM | 🔵 | ProcessManager Test Patterns with File System Operations | ~466 |
| #36779 | 12:44 AM | 🔵 | ProcessManager Windows PowerShell Functions Complete Analysis | ~550 |
### Jan 5, 2026
**wmic-parsing.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37735 | 6:16 PM | ✅ | Test Suite Audit Report Generated: 41 Tests Scored and Analyzed | ~634 |
**process-manager.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37629 | 5:36 PM | 🔵 | Comprehensive Testing Patterns Documentation Generated | ~629 |
| #37622 | 5:34 PM | 🔵 | Bun Test Pattern for File I/O and State Management | ~571 |
</claude-mem-context>
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
### Jan 4, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
+161
View File
@@ -0,0 +1,161 @@
import { describe, it, expect } from 'bun:test';
/**
* Tests for SDKAgent resume parameter logic
*
* The resume parameter should ONLY be passed when:
* 1. memorySessionId exists (was captured from a previous SDK response)
* 2. lastPromptNumber > 1 (this is a continuation within the same SDK session)
*
* On worker restart or crash recovery, memorySessionId may exist from a previous
* SDK session but we must NOT resume because the SDK context was lost.
*/
describe('SDKAgent Resume Parameter Logic', () => {
/**
* Helper function that mirrors the logic in SDKAgent.startSession()
* This is the exact condition used at SDKAgent.ts line 99
*/
function shouldPassResumeParameter(session: {
memorySessionId: string | null;
lastPromptNumber: number;
}): boolean {
const hasRealMemorySessionId = !!session.memorySessionId;
return hasRealMemorySessionId && session.lastPromptNumber > 1;
}
describe('INIT prompt scenarios (lastPromptNumber === 1)', () => {
it('should NOT pass resume parameter when lastPromptNumber === 1 even if memorySessionId exists', () => {
// Scenario: Worker restart with stale memorySessionId from previous session
const session = {
memorySessionId: 'stale-session-id-from-previous-run',
lastPromptNumber: 1, // INIT prompt
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = shouldPassResumeParameter(session);
expect(hasRealMemorySessionId).toBe(true); // memorySessionId exists
expect(shouldResume).toBe(false); // but should NOT resume because it's INIT
});
it('should NOT pass resume parameter when memorySessionId is null and lastPromptNumber === 1', () => {
// Scenario: Fresh session, first prompt ever
const session = {
memorySessionId: null,
lastPromptNumber: 1,
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = shouldPassResumeParameter(session);
expect(hasRealMemorySessionId).toBe(false);
expect(shouldResume).toBe(false);
});
});
describe('CONTINUATION prompt scenarios (lastPromptNumber > 1)', () => {
it('should pass resume parameter when lastPromptNumber > 1 AND memorySessionId exists', () => {
// Scenario: Normal continuation within same SDK session
const session = {
memorySessionId: 'valid-session-id',
lastPromptNumber: 2, // CONTINUATION prompt
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = shouldPassResumeParameter(session);
expect(hasRealMemorySessionId).toBe(true);
expect(shouldResume).toBe(true);
});
it('should pass resume parameter for higher prompt numbers', () => {
// Scenario: Later in a multi-turn conversation
const session = {
memorySessionId: 'valid-session-id',
lastPromptNumber: 5, // 5th prompt in session
};
const shouldResume = shouldPassResumeParameter(session);
expect(shouldResume).toBe(true);
});
it('should NOT pass resume parameter when memorySessionId is null even for lastPromptNumber > 1', () => {
// Scenario: Bug case - somehow got to prompt 2 without capturing memorySessionId
// This shouldn't happen in practice but we should handle it safely
const session = {
memorySessionId: null,
lastPromptNumber: 2,
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = shouldPassResumeParameter(session);
expect(hasRealMemorySessionId).toBe(false);
expect(shouldResume).toBe(false);
});
});
describe('Edge cases', () => {
it('should handle empty string memorySessionId as falsy', () => {
// Empty string should be treated as "no session ID"
const session = {
memorySessionId: '' as unknown as null,
lastPromptNumber: 2,
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = shouldPassResumeParameter(session);
expect(hasRealMemorySessionId).toBe(false);
expect(shouldResume).toBe(false);
});
it('should handle undefined memorySessionId as falsy', () => {
const session = {
memorySessionId: undefined as unknown as null,
lastPromptNumber: 2,
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = shouldPassResumeParameter(session);
expect(hasRealMemorySessionId).toBe(false);
expect(shouldResume).toBe(false);
});
});
describe('Bug reproduction: stale session resume crash', () => {
it('should NOT resume when worker restarts with stale memorySessionId', () => {
// This is the exact bug scenario from the logs:
// [17:30:21.773] Starting SDK query {
// hasRealMemorySessionId=true,
// resume_parameter=5439891b-...,
// lastPromptNumber=1 ← NEW SDK session!
// }
// [17:30:24.450] Generator failed {error=Claude Code process exited with code 1}
const session = {
memorySessionId: '5439891b-7d4b-4ee3-8662-c000f66bc199', // Stale from previous session
lastPromptNumber: 1, // But this is a NEW session after restart
};
const shouldResume = shouldPassResumeParameter(session);
// The fix: should NOT try to resume, should start fresh
expect(shouldResume).toBe(false);
});
it('should resume correctly for normal continuation (not after restart)', () => {
// Normal case: same SDK session, continuing conversation
const session = {
memorySessionId: '5439891b-7d4b-4ee3-8662-c000f66bc199',
lastPromptNumber: 2, // Second prompt in SAME session
};
const shouldResume = shouldPassResumeParameter(session);
// Should resume - same session, valid memorySessionId
expect(shouldResume).toBe(true);
});
});
});
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+197
View File
@@ -0,0 +1,197 @@
import { describe, it, expect, mock, afterEach } from 'bun:test';
// Mock logger BEFORE imports (required pattern)
mock.module('../../src/utils/logger.js', () => ({
logger: {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
},
}));
// Import after mocks
import { extractFirstFile, groupByDate } from '../../src/shared/timeline-formatting.js';
afterEach(() => {
mock.restore();
});
describe('extractFirstFile', () => {
const cwd = '/Users/test/project';
it('should return first modified file as relative path', () => {
const filesModified = JSON.stringify(['/Users/test/project/src/app.ts', '/Users/test/project/src/utils.ts']);
const result = extractFirstFile(filesModified, cwd);
expect(result).toBe('src/app.ts');
});
it('should fall back to files_read when modified is empty', () => {
const filesModified = JSON.stringify([]);
const filesRead = JSON.stringify(['/Users/test/project/README.md']);
const result = extractFirstFile(filesModified, cwd, filesRead);
expect(result).toBe('README.md');
});
it('should return General when both are empty arrays', () => {
const filesModified = JSON.stringify([]);
const filesRead = JSON.stringify([]);
const result = extractFirstFile(filesModified, cwd, filesRead);
expect(result).toBe('General');
});
it('should return General when both are null', () => {
const result = extractFirstFile(null, cwd, null);
expect(result).toBe('General');
});
it('should handle invalid JSON in modified and fall back to read', () => {
const filesModified = 'invalid json {]';
const filesRead = JSON.stringify(['/Users/test/project/config.json']);
const result = extractFirstFile(filesModified, cwd, filesRead);
expect(result).toBe('config.json');
});
it('should return relative path (not absolute) for files inside cwd', () => {
const filesModified = JSON.stringify(['/Users/test/project/deeply/nested/file.ts']);
const result = extractFirstFile(filesModified, cwd);
expect(result).toBe('deeply/nested/file.ts');
expect(result).not.toContain('/Users/test/project');
});
it('should handle files that are already relative paths', () => {
const filesModified = JSON.stringify(['src/component.tsx']);
const result = extractFirstFile(filesModified, cwd);
expect(result).toBe('src/component.tsx');
});
});
describe('groupByDate', () => {
interface TestItem {
id: number;
date: string;
}
it('should return empty map for empty array', () => {
const items: TestItem[] = [];
const result = groupByDate(items, (item) => item.date);
expect(result.size).toBe(0);
});
it('should group items by formatted date', () => {
const items: TestItem[] = [
{ id: 1, date: '2025-01-04T10:00:00Z' },
{ id: 2, date: '2025-01-04T14:00:00Z' },
];
const result = groupByDate(items, (item) => item.date);
expect(result.size).toBe(1);
const dayItems = Array.from(result.values())[0];
expect(dayItems).toHaveLength(2);
expect(dayItems[0].id).toBe(1);
expect(dayItems[1].id).toBe(2);
});
it('should sort dates chronologically', () => {
const items: TestItem[] = [
{ id: 1, date: '2025-01-06T10:00:00Z' },
{ id: 2, date: '2025-01-04T10:00:00Z' },
{ id: 3, date: '2025-01-05T10:00:00Z' },
];
const result = groupByDate(items, (item) => item.date);
const dates = Array.from(result.keys());
expect(dates).toHaveLength(3);
// Dates should be in chronological order (oldest first)
expect(dates[0]).toContain('Jan 4');
expect(dates[1]).toContain('Jan 5');
expect(dates[2]).toContain('Jan 6');
});
it('should group multiple items on same date together', () => {
const items: TestItem[] = [
{ id: 1, date: '2025-01-04T08:00:00Z' },
{ id: 2, date: '2025-01-04T12:00:00Z' },
{ id: 3, date: '2025-01-04T18:00:00Z' },
];
const result = groupByDate(items, (item) => item.date);
expect(result.size).toBe(1);
const dayItems = Array.from(result.values())[0];
expect(dayItems).toHaveLength(3);
expect(dayItems.map(i => i.id)).toEqual([1, 2, 3]);
});
it('should handle items from different days correctly', () => {
const items: TestItem[] = [
{ id: 1, date: '2025-01-04T10:00:00Z' },
{ id: 2, date: '2025-01-05T10:00:00Z' },
{ id: 3, date: '2025-01-04T15:00:00Z' },
{ id: 4, date: '2025-01-05T20:00:00Z' },
];
const result = groupByDate(items, (item) => item.date);
expect(result.size).toBe(2);
const dates = Array.from(result.keys());
expect(dates[0]).toContain('Jan 4');
expect(dates[1]).toContain('Jan 5');
const jan4Items = result.get(dates[0])!;
const jan5Items = result.get(dates[1])!;
expect(jan4Items).toHaveLength(2);
expect(jan5Items).toHaveLength(2);
expect(jan4Items.map(i => i.id)).toEqual([1, 3]);
expect(jan5Items.map(i => i.id)).toEqual([2, 4]);
});
it('should handle numeric timestamps as date input', () => {
// Use clearly different dates (24+ hours apart to avoid timezone issues)
const items = [
{ id: 1, date: '2025-01-04T00:00:00Z' },
{ id: 2, date: '2025-01-06T00:00:00Z' }, // 2 days later
];
const result = groupByDate(items, (item) => item.date);
expect(result.size).toBe(2);
const dates = Array.from(result.keys());
expect(dates).toHaveLength(2);
expect(dates[0]).toContain('Jan 4');
expect(dates[1]).toContain('Jan 6');
});
it('should preserve item order within each date group', () => {
const items: TestItem[] = [
{ id: 3, date: '2025-01-04T08:00:00Z' },
{ id: 1, date: '2025-01-04T09:00:00Z' },
{ id: 2, date: '2025-01-04T10:00:00Z' },
];
const result = groupByDate(items, (item) => item.date);
const dayItems = Array.from(result.values())[0];
// Items should maintain their insertion order
expect(dayItems.map(i => i.id)).toEqual([3, 1, 2]);
});
});
+59
View File
@@ -0,0 +1,59 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**transactions.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36494 | 9:16 PM | 🔴 | Removed Non-Existent 'project' Column from Third pending_messages INSERT | ~540 |
| #36493 | 9:15 PM | 🔴 | Removed Non-Existent 'project' Column from Second pending_messages INSERT | ~423 |
| #36492 | " | 🔴 | Removed Non-Existent 'project' Column from First pending_messages INSERT | ~449 |
| #36490 | 9:14 PM | 🔴 | Fixed Foreign Key Constraints and Schema Updates in Transactions Test Suite | ~746 |
| #36486 | 9:12 PM | 🟣 | Transactions Module Test Suite Implemented | ~833 |
**prompts.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36489 | 9:14 PM | 🔴 | Fixed Foreign Key Constraint Issues in Prompts Test Suite | ~733 |
| #36485 | 9:12 PM | 🟣 | Prompts Module Test Suite Implemented | ~680 |
**summaries.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36488 | 9:13 PM | 🔴 | Fixed Foreign Key Constraint Issues in Summaries Test Suite | ~691 |
| #36484 | 9:11 PM | 🟣 | Summaries Module Test Suite Implemented | ~708 |
**observations.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36487 | 9:13 PM | 🔴 | Fixed Foreign Key Constraint Issues in Observations Test Suite | ~677 |
| #36483 | 9:11 PM | 🟣 | Observations Module Test Suite Implemented | ~716 |
**sessions.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36482 | 9:10 PM | 🟣 | Sessions Module Test Suite Implemented | ~627 |
### Jan 4, 2026
**observations.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36842 | 1:43 AM | 🔵 | Complete Test Framework and Pattern Documentation | ~670 |
| #36835 | 1:41 AM | 🔵 | SQLite Test Pattern with In-Memory Database | ~466 |
### Jan 5, 2026
**observations.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37758 | 6:25 PM | ⚖️ | Integration Test Design for Four Critical Testing Gaps | ~729 |
**summaries.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37735 | 6:16 PM | ✅ | Test Suite Audit Report Generated: 41 Tests Scored and Analyzed | ~634 |
</claude-mem-context>
+487
View File
@@ -0,0 +1,487 @@
import { describe, it, expect, mock, afterEach, beforeEach } from 'bun:test';
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
// Mock logger BEFORE imports (required pattern)
mock.module('../../src/utils/logger.js', () => ({
logger: {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
},
}));
// Import after mocks
import {
replaceTaggedContent,
formatTimelineForClaudeMd,
writeClaudeMdToFolder,
updateFolderClaudeMdFiles
} from '../../src/utils/claude-md-utils.js';
let tempDir: string;
const originalFetch = global.fetch;
beforeEach(() => {
tempDir = join(tmpdir(), `test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(tempDir, { recursive: true });
});
afterEach(() => {
mock.restore();
global.fetch = originalFetch;
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe('replaceTaggedContent', () => {
it('should wrap new content in tags when existing content is empty', () => {
const result = replaceTaggedContent('', 'New content here');
expect(result).toBe('<claude-mem-context>\nNew content here\n</claude-mem-context>');
});
it('should replace only tagged section when existing content has tags', () => {
const existingContent = 'User content before\n<claude-mem-context>\nOld generated content\n</claude-mem-context>\nUser content after';
const newContent = 'New generated content';
const result = replaceTaggedContent(existingContent, newContent);
expect(result).toBe('User content before\n<claude-mem-context>\nNew generated content\n</claude-mem-context>\nUser content after');
});
it('should append tagged content with separator when no tags exist in existing content', () => {
const existingContent = 'User written documentation';
const newContent = 'Generated timeline';
const result = replaceTaggedContent(existingContent, newContent);
expect(result).toBe('User written documentation\n\n<claude-mem-context>\nGenerated timeline\n</claude-mem-context>');
});
it('should append when only opening tag exists (no matching end tag)', () => {
const existingContent = 'Some content\n<claude-mem-context>\nIncomplete tag section';
const newContent = 'New content';
const result = replaceTaggedContent(existingContent, newContent);
expect(result).toBe('Some content\n<claude-mem-context>\nIncomplete tag section\n\n<claude-mem-context>\nNew content\n</claude-mem-context>');
});
it('should append when only closing tag exists (no matching start tag)', () => {
const existingContent = 'Some content\n</claude-mem-context>\nMore content';
const newContent = 'New content';
const result = replaceTaggedContent(existingContent, newContent);
expect(result).toBe('Some content\n</claude-mem-context>\nMore content\n\n<claude-mem-context>\nNew content\n</claude-mem-context>');
});
it('should preserve newlines in new content', () => {
const existingContent = '<claude-mem-context>\nOld content\n</claude-mem-context>';
const newContent = 'Line 1\nLine 2\nLine 3';
const result = replaceTaggedContent(existingContent, newContent);
expect(result).toBe('<claude-mem-context>\nLine 1\nLine 2\nLine 3\n</claude-mem-context>');
});
});
describe('formatTimelineForClaudeMd', () => {
it('should return "No recent activity" for empty input', () => {
const result = formatTimelineForClaudeMd('');
expect(result).toContain('# Recent Activity');
expect(result).toContain('*No recent activity*');
});
it('should return "No recent activity" when no table rows exist', () => {
const input = 'Just some plain text without table rows';
const result = formatTimelineForClaudeMd(input);
expect(result).toContain('*No recent activity*');
});
it('should parse single observation row correctly', () => {
const input = '| #123 | 4:30 PM | 🔵 | User logged in | ~100 |';
const result = formatTimelineForClaudeMd(input);
expect(result).toContain('#123');
expect(result).toContain('4:30 PM');
expect(result).toContain('🔵');
expect(result).toContain('User logged in');
expect(result).toContain('~100');
});
it('should parse ditto mark for repeated time correctly', () => {
const input = `| #123 | 4:30 PM | 🔵 | First action | ~100 |
| #124 | ″ | 🔵 | Second action | ~150 |`;
const result = formatTimelineForClaudeMd(input);
expect(result).toContain('#123');
expect(result).toContain('#124');
// First occurrence should show time
expect(result).toContain('4:30 PM');
// Second occurrence should show ditto mark
expect(result).toContain('"');
});
it('should parse session ID format (#S123) correctly', () => {
const input = '| #S123 | 4:30 PM | 🟣 | Session started | ~200 |';
const result = formatTimelineForClaudeMd(input);
expect(result).toContain('#S123');
expect(result).toContain('4:30 PM');
expect(result).toContain('🟣');
expect(result).toContain('Session started');
});
});
describe('writeClaudeMdToFolder', () => {
it('should create CLAUDE.md in new folder', () => {
const folderPath = join(tempDir, 'new-folder');
const content = '# Recent Activity\n\nTest content';
writeClaudeMdToFolder(folderPath, content);
const claudeMdPath = join(folderPath, 'CLAUDE.md');
expect(existsSync(claudeMdPath)).toBe(true);
const fileContent = readFileSync(claudeMdPath, 'utf-8');
expect(fileContent).toContain('<claude-mem-context>');
expect(fileContent).toContain('Test content');
expect(fileContent).toContain('</claude-mem-context>');
});
it('should preserve user content outside tags', () => {
const folderPath = join(tempDir, 'preserve-test');
mkdirSync(folderPath, { recursive: true });
const claudeMdPath = join(folderPath, 'CLAUDE.md');
const userContent = 'User-written docs\n<claude-mem-context>\nOld content\n</claude-mem-context>\nMore user docs';
writeFileSync(claudeMdPath, userContent);
const newContent = 'New generated content';
writeClaudeMdToFolder(folderPath, newContent);
const fileContent = readFileSync(claudeMdPath, 'utf-8');
expect(fileContent).toContain('User-written docs');
expect(fileContent).toContain('New generated content');
expect(fileContent).toContain('More user docs');
expect(fileContent).not.toContain('Old content');
});
it('should create nested directories', () => {
const folderPath = join(tempDir, 'deep', 'nested', 'folder');
const content = 'Nested content';
writeClaudeMdToFolder(folderPath, content);
const claudeMdPath = join(folderPath, 'CLAUDE.md');
expect(existsSync(claudeMdPath)).toBe(true);
expect(existsSync(join(tempDir, 'deep'))).toBe(true);
expect(existsSync(join(tempDir, 'deep', 'nested'))).toBe(true);
});
it('should not leave .tmp file after write (atomic write)', () => {
const folderPath = join(tempDir, 'atomic-test');
const content = 'Atomic write test';
writeClaudeMdToFolder(folderPath, content);
const claudeMdPath = join(folderPath, 'CLAUDE.md');
const tempFilePath = `${claudeMdPath}.tmp`;
expect(existsSync(claudeMdPath)).toBe(true);
expect(existsSync(tempFilePath)).toBe(false);
});
});
describe('updateFolderClaudeMdFiles', () => {
it('should skip when filePaths is empty', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles([], 'test-project', 37777);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should fetch timeline and write CLAUDE.md', async () => {
const folderPath = join(tempDir, 'api-test');
const filePath = join(folderPath, 'test.ts');
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test observation | ~100 |'
}]
};
global.fetch = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
await updateFolderClaudeMdFiles([filePath], 'test-project', 37777);
const claudeMdPath = join(folderPath, 'CLAUDE.md');
expect(existsSync(claudeMdPath)).toBe(true);
const content = readFileSync(claudeMdPath, 'utf-8');
expect(content).toContain('Recent Activity');
expect(content).toContain('#123');
expect(content).toContain('Test observation');
});
it('should deduplicate folders from multiple files', async () => {
const folderPath = join(tempDir, 'dedup-test');
const file1 = join(folderPath, 'file1.ts');
const file2 = join(folderPath, 'file2.ts');
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test | ~100 |'
}]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles([file1, file2], 'test-project', 37777);
// Should only fetch once for the shared folder
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('should handle API errors gracefully (404 response)', async () => {
const folderPath = join(tempDir, 'error-test');
const filePath = join(folderPath, 'test.ts');
global.fetch = mock(() => Promise.resolve({
ok: false,
status: 404
} as Response));
// Should not throw
await expect(updateFolderClaudeMdFiles([filePath], 'test-project', 37777)).resolves.toBeUndefined();
// CLAUDE.md should not be created
const claudeMdPath = join(folderPath, 'CLAUDE.md');
expect(existsSync(claudeMdPath)).toBe(false);
});
it('should handle network errors gracefully (fetch throws)', async () => {
const folderPath = join(tempDir, 'network-error-test');
const filePath = join(folderPath, 'test.ts');
global.fetch = mock(() => Promise.reject(new Error('Network error')));
// Should not throw
await expect(updateFolderClaudeMdFiles([filePath], 'test-project', 37777)).resolves.toBeUndefined();
// CLAUDE.md should not be created
const claudeMdPath = join(folderPath, 'CLAUDE.md');
expect(existsSync(claudeMdPath)).toBe(false);
});
it('should resolve relative paths using projectRoot', async () => {
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test observation | ~100 |'
}]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['src/utils/file.ts'], // relative path
'test-project',
37777,
'/home/user/my-project' // projectRoot
);
// Should call API with absolute path /home/user/my-project/src/utils
expect(fetchMock).toHaveBeenCalledTimes(1);
const callUrl = (fetchMock.mock.calls[0] as unknown[])[0] as string;
expect(callUrl).toContain(encodeURIComponent('/home/user/my-project/src/utils'));
});
it('should not modify absolute paths even with projectRoot', async () => {
const folderPath = join(tempDir, 'absolute-path-test');
const filePath = join(folderPath, 'file.ts');
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test observation | ~100 |'
}]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
[filePath], // absolute path
'test-project',
37777,
'/home/user/my-project' // projectRoot should be ignored
);
// Should call API with the original absolute path's folder
expect(fetchMock).toHaveBeenCalledTimes(1);
const callUrl = (fetchMock.mock.calls[0] as unknown[])[0] as string;
expect(callUrl).toContain(encodeURIComponent(folderPath));
});
it('should work without projectRoot for backward compatibility', async () => {
const folderPath = join(tempDir, 'backward-compat-test');
const filePath = join(folderPath, 'file.ts');
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test observation | ~100 |'
}]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
[filePath], // absolute path
'test-project',
37777
// No projectRoot - backward compatibility
);
// Should still make API call with the folder path
expect(fetchMock).toHaveBeenCalledTimes(1);
const callUrl = (fetchMock.mock.calls[0] as unknown[])[0] as string;
expect(callUrl).toContain(encodeURIComponent(folderPath));
});
it('should handle projectRoot with trailing slash correctly', async () => {
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test observation | ~100 |'
}]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
// projectRoot WITH trailing slash
await updateFolderClaudeMdFiles(
['src/utils/file.ts'],
'test-project',
37777,
'/home/user/my-project/' // trailing slash
);
// Should call API with normalized path (no double slashes)
expect(fetchMock).toHaveBeenCalledTimes(1);
const callUrl = (fetchMock.mock.calls[0] as unknown[])[0] as string;
// path.join normalizes the path, so /home/user/my-project/ + src/utils becomes /home/user/my-project/src/utils
expect(callUrl).toContain(encodeURIComponent('/home/user/my-project/src/utils'));
// Should NOT contain double slashes (except in http://)
expect(callUrl.replace('http://', '')).not.toContain('//');
});
it('should write CLAUDE.md to resolved projectRoot path', async () => {
const subfolderPath = join(tempDir, 'project-root-write-test', 'src', 'utils');
const apiResponse = {
content: [{
text: '| #456 | 5:00 PM | 🔵 | Written to correct path | ~200 |'
}]
};
global.fetch = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
// Use tempDir as projectRoot with relative path src/utils/file.ts
await updateFolderClaudeMdFiles(
['src/utils/file.ts'],
'test-project',
37777,
join(tempDir, 'project-root-write-test')
);
// Verify CLAUDE.md was written at the resolved absolute path
const claudeMdPath = join(subfolderPath, 'CLAUDE.md');
expect(existsSync(claudeMdPath)).toBe(true);
const content = readFileSync(claudeMdPath, 'utf-8');
expect(content).toContain('Written to correct path');
expect(content).toContain('#456');
});
it('should deduplicate relative paths from same folder with projectRoot', async () => {
const apiResponse = {
content: [{
text: '| #123 | 4:30 PM | 🔵 | Test | ~100 |'
}]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
// Multiple files in same folder (relative paths)
await updateFolderClaudeMdFiles(
['src/utils/file1.ts', 'src/utils/file2.ts', 'src/utils/file3.ts'],
'test-project',
37777,
'/home/user/project'
);
// Should only fetch once for the shared folder
expect(fetchMock).toHaveBeenCalledTimes(1);
const callUrl = (fetchMock.mock.calls[0] as unknown[])[0] as string;
expect(callUrl).toContain(encodeURIComponent('/home/user/project/src/utils'));
});
it('should handle empty string paths gracefully with projectRoot', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['', 'src/file.ts', ''], // includes empty strings
'test-project',
37777,
'/home/user/project'
);
// Should skip empty strings and only process valid path
expect(fetchMock).toHaveBeenCalledTimes(1);
const callUrl = (fetchMock.mock.calls[0] as unknown[])[0] as string;
expect(callUrl).toContain(encodeURIComponent('/home/user/project/src'));
});
});
+38
View File
@@ -0,0 +1,38 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**result-formatter.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36548 | 9:41 PM | 🟣 | Phase 3 Search Strategy Tests Committed | ~554 |
| #36547 | " | 🟣 | Phase 3 Search Test Suite Committed | ~310 |
| #36543 | 9:40 PM | 🟣 | Phase 3 Search Strategy Tests Completed | ~458 |
| #36540 | 9:39 PM | 🟣 | ResultFormatter Test Suite Created | ~424 |
**search-orchestrator.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36545 | 9:41 PM | 🟣 | Phase 3 Search Strategy Test Suite Completion | ~564 |
| #36544 | 9:40 PM | 🟣 | Phase 3 Search Strategy Test Suite Complete | ~449 |
| #36542 | 9:39 PM | 🔴 | Fixed Empty String Filter Test Expectations | ~260 |
| #36541 | " | 🟣 | SearchOrchestrator Test Suite Implementation | ~588 |
### Jan 4, 2026
**search-orchestrator.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36838 | 1:42 AM | 🔵 | Module-Level Mocking Pattern for Singleton Dependencies | ~420 |
### Jan 5, 2026
**result-formatter.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37629 | 5:36 PM | 🔵 | Comprehensive Testing Patterns Documentation Generated | ~629 |
| #37623 | 5:34 PM | 🔵 | Bun Test Pattern for Class Methods with Mock Data | ~603 |
</claude-mem-context>
+34
View File
@@ -0,0 +1,34 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**chroma-search-strategy.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36548 | 9:41 PM | 🟣 | Phase 3 Search Strategy Tests Committed | ~554 |
| #36547 | " | 🟣 | Phase 3 Search Test Suite Committed | ~310 |
| #36543 | 9:40 PM | 🟣 | Phase 3 Search Strategy Tests Completed | ~458 |
| #36536 | 9:38 PM | 🟣 | ChromaSearchStrategy Test Suite Created | ~430 |
**sqlite-search-strategy.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36545 | 9:41 PM | 🟣 | Phase 3 Search Strategy Test Suite Completion | ~564 |
| #36544 | 9:40 PM | 🟣 | Phase 3 Search Strategy Test Suite Complete | ~449 |
| #36538 | 9:38 PM | 🟣 | SQLiteSearchStrategy Test Suite Created | ~527 |
**hybrid-search-strategy.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36537 | 9:38 PM | 🟣 | HybridSearchStrategy Test Suite Created | ~435 |
### Jan 5, 2026
**chroma-search-strategy.test.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37735 | 6:16 PM | ✅ | Test Suite Audit Report Generated: 41 Tests Scored and Analyzed | ~634 |
</claude-mem-context>