2 Commits

Author SHA1 Message Date
4bcc178f43 fix: move test artifacts to tmp directory and update gitignore
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
- Create tmp/test_artifacts/ directory for test storage
- Add tmp/ to .gitignore to exclude test artifacts from version control
- Update test files to use project tmp directory instead of system temp
- Add test-specific path constants for consistent configuration
- Prevent asset_registry.json from being overwritten by tests

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 21:39:40 +02:00
501b64089f feat: implement filename convention for md-render --edit saved files - Issue #155
**Problem Solved:**
The md-render --edit mode had no functional save capability - clicking "Save" only
showed a temporary message without actually persisting changes.

**Solution Implemented:**
- **Filename Convention**: `original.md` → `original-edited-YYYY-MM-DD-HH-MM-SS.md`
- **Download-based Save**: Creates downloadable file with timestamped name
- **Content Reconstruction**: Converts edited HTML back to markdown format
- **Enhanced UI**: Clear button labels and filename preview in interface
- **Error Handling**: Graceful failure with user feedback

**Key Features:**
- Prevents accidental overwrites with timestamp suffix
- Preserves markdown structure (headings, paragraphs, lists, code blocks)
- User-friendly interface with clear save convention explanation
- Browser-compatible download functionality (no server required)

**Filename Examples:**
- `document.md` → `document-edited-2025-10-15-20-30-45.md`
- `README.md` → `README-edited-2025-10-15-20-30-45.md`

This resolves the missing save functionality while establishing a clear,
safe filename convention that prevents data loss and maintains file history.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 20:10:13 +02:00
5 changed files with 190 additions and 64 deletions

3
.gitignore vendored
View File

@@ -93,3 +93,6 @@ debug_*.py
# TDDAI-specific ignores # TDDAI-specific ignores
ISSUES.index ISSUES.index
# Test artifacts and temporary files
tmp/

View File

@@ -10,6 +10,10 @@ DEFAULT_ASSETS_DIR = "assets"
DEFAULT_REGISTRY_FILENAME = "asset_registry.json" DEFAULT_REGISTRY_FILENAME = "asset_registry.json"
DEFAULT_MANIFEST_FILENAME = "manifest.json" DEFAULT_MANIFEST_FILENAME = "manifest.json"
# Test-specific paths (for development/testing)
DEFAULT_TEST_ASSETS_DIR = "tmp/test_artifacts/assets"
DEFAULT_TEST_REGISTRY_FILENAME = "tmp/test_artifacts/asset_registry.json"
# Package file extension # Package file extension
PACKAGE_EXTENSION = ".mdpkg" PACKAGE_EXTENSION = ".mdpkg"

View File

@@ -447,9 +447,12 @@ class DocumentManager:
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'markitect-floating-header'; header.className = 'markitect-floating-header';
header.innerHTML = ` header.innerHTML = `
<button onclick="markitectEditor.save()">Save</button> <button onclick="markitectEditor.save()" title="Download edited file with timestamp">💾 Save & Download</button>
<button onclick="markitectEditor.togglePreview()">Toggle Preview</button> <button onclick="markitectEditor.togglePreview()" title="Toggle preview mode">👁️ Preview</button>
<span id="save-status">Ready</span> <span id="save-status" style="margin-left: 15px; font-size: 0.9em;">Ready</span>
<span style="margin-left: 15px; font-size: 0.8em; color: #666;">
Saves as: filename-edited-YYYY-MM-DD-HH-MM-SS.md
</span>
`; `;
document.body.insertBefore(header, document.body.firstChild); document.body.insertBefore(header, document.body.firstChild);
@@ -520,10 +523,88 @@ class DocumentManager:
} }
save() { save() {
document.getElementById('save-status').textContent = 'Saved!'; try {
setTimeout(() => { // Get the current markdown content from the editor
document.getElementById('save-status').textContent = 'Ready'; const markdownContent = this.getMarkdownContent();
}, 2000);
// Create filename with timestamp suffix for backup convention
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
const originalFilename = window.location.pathname.split('/').pop().replace('.html', '.md');
const backupFilename = `${originalFilename.replace('.md', '')}-edited-${timestamp}.md`;
// Create and download the file
const blob = new Blob([markdownContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = backupFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Update status with filename convention info
const statusEl = document.getElementById('save-status');
statusEl.textContent = `Downloaded: ${backupFilename}`;
statusEl.title = 'File saved with timestamp to avoid overwriting original';
setTimeout(() => {
statusEl.textContent = 'Ready';
statusEl.title = '';
}, 5000);
} catch (error) {
document.getElementById('save-status').textContent = 'Save failed!';
console.error('Save error:', error);
setTimeout(() => {
document.getElementById('save-status').textContent = 'Ready';
}, 3000);
}
}
getMarkdownContent() {
// Reconstruct markdown content from the current state of sections
const content = document.getElementById('markdown-content');
if (!content) {
return markdownContent; // fallback to original
}
// Simple approach: get the text content and convert back to markdown
// This is a basic implementation - could be enhanced for better preservation
const sections = content.querySelectorAll('.markitect-section-editable');
let reconstructed = '';
sections.forEach(section => {
const tagName = section.tagName.toLowerCase();
const text = section.textContent.trim();
if (tagName.startsWith('h')) {
const level = parseInt(tagName.charAt(1));
reconstructed += '#'.repeat(level) + ' ' + text + '\n\n';
} else if (tagName === 'p') {
reconstructed += text + '\n\n';
} else if (tagName === 'blockquote') {
reconstructed += '> ' + text + '\n\n';
} else if (tagName === 'pre') {
reconstructed += '```\n' + text + '\n```\n\n';
} else if (tagName === 'ul') {
const items = section.querySelectorAll('li');
items.forEach(item => {
reconstructed += '- ' + item.textContent.trim() + '\n';
});
reconstructed += '\n';
} else if (tagName === 'ol') {
const items = section.querySelectorAll('li');
items.forEach((item, index) => {
reconstructed += `${index + 1}. ` + item.textContent.trim() + '\n';
});
reconstructed += '\n';
} else {
reconstructed += text + '\n\n';
}
});
return reconstructed.trim();
} }
togglePreview() { togglePreview() {

View File

@@ -14,6 +14,9 @@ Requirements:
import tempfile import tempfile
import json import json
import time
import shutil
from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from unittest.mock import Mock, patch, MagicMock from unittest.mock import Mock, patch, MagicMock
import pytest import pytest
@@ -27,16 +30,38 @@ from markitect.assets.exceptions import AssetError, AssetManagerError
from markitect.config_manager import ConfigurationManager from markitect.config_manager import ConfigurationManager
@contextmanager
def test_workspace():
"""Create a test workspace in the project tmp directory."""
project_root = Path(__file__).parent.parent
temp_dir = project_root / "tmp" / "test_artifacts" / f"test_{int(time.time())}"
temp_dir.mkdir(parents=True, exist_ok=True)
try:
yield temp_dir
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def create_test_workspace():
"""Create a test workspace in the project tmp directory."""
project_root = Path(__file__).parent.parent
temp_dir = project_root / "tmp" / "test_artifacts" / f"test_{int(time.time())}"
temp_dir.mkdir(parents=True, exist_ok=True)
return temp_dir
class TestAssetManagerInitialization: class TestAssetManagerInitialization:
"""Test AssetManager initialization and configuration.""" """Test AssetManager initialization and configuration."""
def test_manager_initialization_with_config(self): def test_manager_initialization_with_config(self):
"""Test AssetManager can be initialized with configuration.""" """Test AssetManager can be initialized with configuration."""
with tempfile.TemporaryDirectory() as temp_dir: temp_dir = create_test_workspace()
try:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json"), "registry_path": str(temp_dir / "registry.json"),
"enable_deduplication": True, "enable_deduplication": True,
"default_conflict_resolution": "backup" "default_conflict_resolution": "backup"
} }
@@ -44,10 +69,13 @@ class TestAssetManagerInitialization:
manager = AssetManager(config) manager = AssetManager(config)
assert manager.storage_path == Path(temp_dir) / "assets" assert manager.storage_path == temp_dir / "assets"
assert manager.registry_path == Path(temp_dir) / "registry.json" assert manager.registry_path == temp_dir / "registry.json"
assert manager.enable_deduplication is True assert manager.enable_deduplication is True
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_manager_initialization_with_defaults(self): def test_manager_initialization_with_defaults(self):
"""Test AssetManager initialization with default configuration.""" """Test AssetManager initialization with default configuration."""
manager = AssetManager() manager = AssetManager()
@@ -59,11 +87,11 @@ class TestAssetManagerInitialization:
def test_manager_creates_required_components(self): def test_manager_creates_required_components(self):
"""Test that AssetManager creates required component instances.""" """Test that AssetManager creates required component instances."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -75,12 +103,12 @@ class TestAssetManagerInitialization:
def test_manager_integration_with_config_manager(self): def test_manager_integration_with_config_manager(self):
"""Test AssetManager integration with ConfigurationManager.""" """Test AssetManager integration with ConfigurationManager."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
# Create config file # Create config file
config_file = Path(temp_dir) / ".markitect.json" config_file = Path(temp_dir) / ".markitect.json"
config_data = { config_data = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "custom_assets"), "storage_path": str(temp_dir / "custom_assets"),
"enable_deduplication": False "enable_deduplication": False
} }
} }
@@ -99,11 +127,11 @@ class TestAssetManagerHighLevelOperations:
def test_add_asset_with_deduplication(self): def test_add_asset_with_deduplication(self):
"""Test adding asset with automatic deduplication.""" """Test adding asset with automatic deduplication."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -123,11 +151,11 @@ class TestAssetManagerHighLevelOperations:
def test_add_duplicate_asset_detected(self): def test_add_duplicate_asset_detected(self):
"""Test that duplicate assets are properly detected and handled.""" """Test that duplicate assets are properly detected and handled."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -152,11 +180,11 @@ class TestAssetManagerHighLevelOperations:
def test_list_assets_with_metadata(self): def test_list_assets_with_metadata(self):
"""Test listing all assets with their metadata.""" """Test listing all assets with their metadata."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -182,11 +210,11 @@ class TestAssetManagerHighLevelOperations:
def test_get_asset_info_by_hash(self): def test_get_asset_info_by_hash(self):
"""Test retrieving detailed asset information by content hash.""" """Test retrieving detailed asset information by content hash."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -209,11 +237,11 @@ class TestAssetManagerHighLevelOperations:
def test_remove_asset_by_hash(self): def test_remove_asset_by_hash(self):
"""Test removing asset by content hash.""" """Test removing asset by content hash."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -241,11 +269,11 @@ class TestAssetManagerPackaging:
def test_create_document_package(self): def test_create_document_package(self):
"""Test creating complete document package with assets.""" """Test creating complete document package with assets."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -284,11 +312,11 @@ This document has assets:
def test_extract_document_package_to_workspace(self): def test_extract_document_package_to_workspace(self):
"""Test extracting package to workspace with proper asset linking.""" """Test extracting package to workspace with proper asset linking."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -316,11 +344,11 @@ This document has assets:
def test_package_with_custom_options(self): def test_package_with_custom_options(self):
"""Test package creation with custom options and exclude patterns.""" """Test package creation with custom options and exclude patterns."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -360,11 +388,11 @@ class TestAssetManagerErrorHandling:
def test_add_nonexistent_asset_raises_error(self): def test_add_nonexistent_asset_raises_error(self):
"""Test that adding non-existent asset raises appropriate error.""" """Test that adding non-existent asset raises appropriate error."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -377,11 +405,11 @@ class TestAssetManagerErrorHandling:
def test_get_info_for_nonexistent_asset_raises_error(self): def test_get_info_for_nonexistent_asset_raises_error(self):
"""Test that getting info for non-existent asset raises error.""" """Test that getting info for non-existent asset raises error."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -392,11 +420,11 @@ class TestAssetManagerErrorHandling:
def test_manager_logs_operations(self): def test_manager_logs_operations(self):
"""Test that AssetManager logs important operations.""" """Test that AssetManager logs important operations."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -426,14 +454,14 @@ class TestAssetManagerErrorHandling:
def test_configuration_validation_errors(self): def test_configuration_validation_errors(self):
"""Test that invalid configuration raises appropriate errors.""" """Test that invalid configuration raises appropriate errors."""
# Invalid storage path (file instead of directory) # Invalid storage path (file instead of directory)
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
invalid_file = Path(temp_dir) / "not_a_directory.txt" invalid_file = Path(temp_dir) / "not_a_directory.txt"
invalid_file.write_text("This is a file") invalid_file.write_text("This is a file")
config = { config = {
"assets": { "assets": {
"storage_path": str(invalid_file), "storage_path": str(invalid_file),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -446,11 +474,11 @@ class TestAssetManagerWorkflows:
def test_complete_document_workflow(self): def test_complete_document_workflow(self):
"""Test complete workflow: add assets, create package, extract elsewhere.""" """Test complete workflow: add assets, create package, extract elsewhere."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -496,11 +524,11 @@ Assets:
def test_asset_sharing_between_packages(self): def test_asset_sharing_between_packages(self):
"""Test that assets can be shared between different packages.""" """Test that assets can be shared between different packages."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }
@@ -545,11 +573,11 @@ Assets:
def test_performance_requirements_met(self): def test_performance_requirements_met(self):
"""Test that operations complete within performance requirements (<100ms).""" """Test that operations complete within performance requirements (<100ms)."""
with tempfile.TemporaryDirectory() as temp_dir: with test_workspace() as temp_dir:
config = { config = {
"assets": { "assets": {
"storage_path": str(Path(temp_dir) / "assets"), "storage_path": str(temp_dir / "assets"),
"registry_path": str(Path(temp_dir) / "registry.json") "registry_path": str(temp_dir / "registry.json")
} }
} }

View File

@@ -45,7 +45,10 @@ class TestFinalAssetManagementIntegration:
@pytest.fixture @pytest.fixture
def integration_workspace(self): def integration_workspace(self):
"""Create a comprehensive test workspace with realistic data.""" """Create a comprehensive test workspace with realistic data."""
temp_dir = Path(tempfile.mkdtemp(prefix="asset_integration_")) # Use project tmp directory instead of system temp
project_root = Path(__file__).parent.parent
temp_dir = project_root / "tmp" / "test_artifacts" / f"integration_{int(time.time())}"
temp_dir.mkdir(parents=True, exist_ok=True)
# Create realistic project structure # Create realistic project structure
project_dir = temp_dir / "test_project" project_dir = temp_dir / "test_project"
@@ -92,7 +95,11 @@ Content for comprehensive testing of the asset management system.
def asset_manager(self, integration_workspace): def asset_manager(self, integration_workspace):
"""Initialize AssetManager for integration testing.""" """Initialize AssetManager for integration testing."""
storage_path = integration_workspace / "asset_storage" storage_path = integration_workspace / "asset_storage"
manager = AssetManager(storage_path=storage_path) registry_path = integration_workspace / "test_registry.json"
manager = AssetManager(
storage_path=storage_path,
registry_path=registry_path
)
return manager return manager
def test_complete_ecosystem_initialization(self, integration_workspace): def test_complete_ecosystem_initialization(self, integration_workspace):
@@ -505,7 +512,10 @@ class TestAssetManagementPerformanceBenchmarks:
@pytest.fixture @pytest.fixture
def benchmark_workspace(self): def benchmark_workspace(self):
"""Create large-scale test workspace for benchmarking.""" """Create large-scale test workspace for benchmarking."""
temp_dir = Path(tempfile.mkdtemp(prefix="asset_benchmark_")) # Use project tmp directory instead of system temp
project_root = Path(__file__).parent.parent
temp_dir = project_root / "tmp" / "test_artifacts" / f"benchmark_{int(time.time())}"
temp_dir.mkdir(parents=True, exist_ok=True)
# Create variety of file types and sizes # Create variety of file types and sizes
file_types = [ file_types = [