Skip to content

Commit 7a8b08d

Browse files
groksrcclaude
andauthored
fix: Windows test failures and add Windows CI support (#273)
Signed-off-by: Drew Cain <groksrc@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9aa4024 commit 7a8b08d

14 files changed

Lines changed: 147 additions & 29 deletions

.github/workflows/test.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ on:
1414

1515
jobs:
1616
test:
17-
runs-on: ubuntu-latest
1817
strategy:
1918
fail-fast: false
2019
matrix:
20+
os: [ubuntu-latest, windows-latest]
2121
python-version: [ "3.12" ]
22+
runs-on: ${{ matrix.os }}
2223

2324
steps:
2425
- uses: actions/checkout@v4
@@ -35,10 +36,18 @@ jobs:
3536
run: |
3637
pip install uv
3738
38-
- name: Install just
39+
- name: Install just (Linux/macOS)
40+
if: runner.os != 'Windows'
3941
run: |
4042
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
4143
44+
- name: Install just (Windows)
45+
if: runner.os == 'Windows'
46+
run: |
47+
# Install just using Chocolatey (pre-installed on GitHub Actions Windows runners)
48+
choco install just --yes
49+
shell: pwsh
50+
4251
- name: Create virtual env
4352
run: |
4453
uv venv
@@ -54,4 +63,4 @@ jobs:
5463
- name: Run tests
5564
run: |
5665
uv pip install pytest pytest-cov
57-
just test
66+
just test

create_individual_exhibits.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,15 @@ def create_overall_summary_exhibit(data, output_dir):
150150
for category, count in sorted(summary['categories'].items()):
151151
summary_content += f"- **{category.replace('_', ' ').title()}:** {count} files\n"
152152

153-
summary_content += f"""
153+
summary_content += """
154154
155155
## Contributor Summary
156156
"""
157157

158158
for contrib in summary['top_contributors']:
159159
summary_content += f"- **{contrib['name']}** ({contrib['email']}): {contrib['file_count']} files, {contrib['commit_count']} commits\n"
160160

161-
summary_content += f"""
161+
summary_content += """
162162
163163
## Legal Significance
164164
This inventory represents the complete codebase of Basic Memory as licensed from Basic Machines LLC to Basic Memory LLC under the copyright license agreement dated [DATE].

legal_file_inventory.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616
"""
1717

1818
import os
19-
import sys
2019
import json
2120
import csv
2221
import subprocess
2322
from pathlib import Path
2423
from datetime import datetime
25-
from typing import Dict, List, Set, Optional, Tuple
24+
from typing import Dict, List
2625
import argparse
2726
import hashlib
2827

@@ -419,15 +418,15 @@ def main():
419418

420419
# Print summary
421420
stats = generator.get_summary_statistics()
422-
print(f"\n=== Legal File Inventory Complete ===")
421+
print("\n=== Legal File Inventory Complete ===")
423422
print(f"Repository: {stats.get('repository_path', 'Unknown')}")
424423
print(f"Total files inventoried: {stats.get('total_files', 0)}")
425424
print(f"Total contributors identified: {stats.get('total_contributors', 0)}")
426425
print(f"Output written to: {args.output}")
427426

428427
# Show top contributors
429428
if 'contributor_file_counts' in stats:
430-
print(f"\nTop 5 contributors by files modified:")
429+
print("\nTop 5 contributors by files modified:")
431430
sorted_contributors = sorted(
432431
stats['contributor_file_counts'].items(),
433432
key=lambda x: x[1],

scripts/generate_legal_inventory.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@
1818
import csv
1919
import hashlib
2020
import json
21-
import os
2221
import subprocess
23-
import sys
2422
from collections import defaultdict
2523
from datetime import datetime
2624
from pathlib import Path
27-
from typing import Dict, List, Optional, Set, Tuple
25+
from typing import Dict, List
2826

2927

3028
class LegalInventoryGenerator:
@@ -477,7 +475,7 @@ def main():
477475
if 'markdown' in formats:
478476
generator.export_markdown(output_dir / f'basic_memory_inventory_{timestamp}.md')
479477

480-
print(f"\nLegal inventory generation complete!")
478+
print("\nLegal inventory generation complete!")
481479
print(f"Output saved to: {output_dir}")
482480

483481

src/basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from typing import Sequence, Union
1010

1111
from alembic import op
12-
import sqlalchemy as sa
1312

1413

1514
# revision identifiers, used by Alembic.

src/basic_memory/services/directory_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,15 @@ async def list_directory(
106106
List of DirectoryNode objects matching the criteria
107107
"""
108108
# Normalize directory path
109+
# Strip ./ prefix if present (handles relative path notation)
110+
if dir_name.startswith("./"):
111+
dir_name = dir_name[2:] # Remove "./" prefix
112+
113+
# Ensure path starts with "/"
109114
if not dir_name.startswith("/"):
110115
dir_name = f"/{dir_name}"
116+
117+
# Remove trailing slashes except for root
111118
if dir_name != "/" and dir_name.endswith("/"):
112119
dir_name = dir_name.rstrip("/")
113120

test-int/mcp/test_list_directory_integration.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,79 @@ async def test_list_directory_complex_glob_patterns(mcp_server, app):
465465
assert "Project Beta Plan.md" in list_text
466466
assert "Meeting Minutes" not in list_text
467467
assert "matching 'Project*'" in list_text
468+
469+
470+
@pytest.mark.asyncio
471+
async def test_list_directory_dot_slash_prefix_paths(mcp_server, app):
472+
"""Test directory listing with ./ prefix paths (reproduces bug report issue)."""
473+
474+
async with Client(mcp_server) as client:
475+
# Create test files in a subdirectory
476+
await client.call_tool(
477+
"write_note",
478+
{
479+
"title": "Artifact One",
480+
"folder": "artifacts",
481+
"content": "# Artifact One\n\nFirst artifact document.",
482+
"tags": "artifact,test",
483+
},
484+
)
485+
486+
await client.call_tool(
487+
"write_note",
488+
{
489+
"title": "Artifact Two",
490+
"folder": "artifacts",
491+
"content": "# Artifact Two\n\nSecond artifact document.",
492+
"tags": "artifact,test",
493+
},
494+
)
495+
496+
# Test normal path without ./ prefix (should work)
497+
normal_result = await client.call_tool(
498+
"list_directory",
499+
{
500+
"dir_name": "artifacts",
501+
"depth": 1,
502+
},
503+
)
504+
505+
assert len(normal_result.content) == 1
506+
normal_text = normal_result.content[0].text
507+
assert "Artifact One.md" in normal_text
508+
assert "Artifact Two.md" in normal_text
509+
assert "2 files" in normal_text
510+
511+
# Test with ./ prefix (this was the failing case in the bug report)
512+
dot_slash_result = await client.call_tool(
513+
"list_directory",
514+
{
515+
"dir_name": "./artifacts",
516+
"depth": 1,
517+
},
518+
)
519+
520+
assert len(dot_slash_result.content) == 1
521+
dot_slash_text = dot_slash_result.content[0].text
522+
523+
# Should show the same files as normal path
524+
assert "Artifact One.md" in dot_slash_text
525+
assert "Artifact Two.md" in dot_slash_text
526+
assert "2 files" in dot_slash_text
527+
528+
# Test with trailing slash after ./ prefix
529+
dot_slash_trailing_result = await client.call_tool(
530+
"list_directory",
531+
{
532+
"dir_name": "./artifacts/",
533+
"depth": 1,
534+
},
535+
)
536+
537+
assert len(dot_slash_trailing_result.content) == 1
538+
dot_slash_trailing_text = dot_slash_trailing_result.content[0].text
539+
540+
# Should show the same files
541+
assert "Artifact One.md" in dot_slash_trailing_text
542+
assert "Artifact Two.md" in dot_slash_trailing_text
543+
assert "2 files" in dot_slash_trailing_text

tests/cli/test_project_commands.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ def test_project_default_command(mock_reload, mock_run, cli_env):
8585
# Patching call_put directly since it's imported at the module level
8686

8787
# Patch the os.environ for checking
88-
with patch.dict(os.environ, {}, clear=True):
88+
# On Windows, preserve USERPROFILE to allow home directory detection
89+
env_vars = {}
90+
if os.name == 'nt' and 'USERPROFILE' in os.environ:
91+
env_vars['USERPROFILE'] = os.environ['USERPROFILE']
92+
93+
with patch.dict(os.environ, env_vars, clear=True):
8994
# Patch ConfigManager.set_default_project to prevent validation error
9095
with patch("basic_memory.config.ConfigManager.set_default_project"):
9196
runner = CliRunner()

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from textwrap import dedent
77
from typing import AsyncGenerator
88

9+
import os
910
import pytest
1011
import pytest_asyncio
1112
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
@@ -51,6 +52,9 @@ def project_root() -> Path:
5152
def config_home(tmp_path, monkeypatch) -> Path:
5253
# Patch HOME environment variable for the duration of the test
5354
monkeypatch.setenv("HOME", str(tmp_path))
55+
# On Windows, also set USERPROFILE
56+
if os.name == 'nt':
57+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
5458
# Set BASIC_MEMORY_HOME to the test directory
5559
monkeypatch.setenv("BASIC_MEMORY_HOME", str(tmp_path / "basic-memory"))
5660
return tmp_path

tests/services/test_directory_service.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,28 @@ async def test_list_directory_path_normalization(directory_service: DirectorySer
168168
assert result_names == base_names
169169

170170

171+
@pytest.mark.asyncio
172+
async def test_list_directory_dot_slash_prefix_normalization(
173+
directory_service: DirectoryService, test_graph
174+
):
175+
"""Test that ./ prefixed directory paths are normalized correctly."""
176+
# This test reproduces the bug report issue where ./dirname fails
177+
base_result = await directory_service.list_directory(dir_name="/test")
178+
179+
# Test paths with ./ prefix that should be equivalent to /test
180+
dot_paths_to_test = ["./test", "./test/"]
181+
182+
for path in dot_paths_to_test:
183+
result = await directory_service.list_directory(dir_name=path)
184+
assert len(result) == len(base_result), (
185+
f"Path '{path}' returned {len(result)} results, expected {len(base_result)}"
186+
)
187+
# Compare by name since the objects might be different instances
188+
result_names = {node.name for node in result}
189+
base_names = {node.name for node in base_result}
190+
assert result_names == base_names, f"Path '{path}' returned different files than expected"
191+
192+
171193
@pytest.mark.asyncio
172194
async def test_list_directory_glob_no_matches(directory_service: DirectoryService, test_graph):
173195
"""Test listing directory with glob that matches nothing."""

0 commit comments

Comments
 (0)