feat: reorganize tests by capability with separate test targets
Some checks failed
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 / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Some checks failed
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 / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Separate capability-specific tests from core system tests to establish clear test organization and separation of concerns. ## Test Reorganization: - **markitect-content tests**: Moved 6 tests to capabilities/markitect-content/tests/ - **markitect-finance tests**: Moved 7 tests to markitect/finance/tests/ - **markitect-query tests**: Moved 1 test to markitect/query_paradigms/tests/ - **markitect-graphql tests**: Moved 2 tests to markitect/graphql/tests/ - **markitect-plugins tests**: Moved 2 tests to markitect/plugins/tests/ ## Makefile Updates: - **make test**: Excludes capability tests, runs only core system tests - **make test-capabilities**: Runs all capability tests - **make test-capability-***: Individual capability test targets - Updated all test targets (test-red, test-green, test-ultra-fast, test-perf) - Added capability test targets to help documentation ## Benefits: - Clear separation between core system tests and capability-specific tests - Faster core test execution (capability tests not run by default) - Individual capability testing for focused development - Supports future capability extraction workflow - Maintains capability test independence Test verification: - Core tests: 1291 tests (capability tests excluded) - Finance capability: 143 tests working independently - Content capability: 79 tests working independently 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Test suite for Issue #46: Schema generation capability outline
|
||||
|
||||
This test module validates outline mode schema generation improvements including:
|
||||
- Heading text capture in outline mode schemas
|
||||
- Integration with draft generation using captured heading text
|
||||
- Proper title formatting and depth limiting
|
||||
- Content instruction integration
|
||||
- End-to-end workflow from example document to generated drafts
|
||||
|
||||
Created for Issue #46: https://gitea.coulomb.social/coulomb/markitect_project/issues/46
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
from markitect.cli import cli
|
||||
|
||||
|
||||
class TestIssue46SchemaGenerationOutline:
|
||||
"""Test suite for schema generation outline mode improvements."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
self.runner = CliRunner()
|
||||
|
||||
# Create a test markdown file with specific headings
|
||||
self.test_md_content = """# Project Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
This is the project overview section.
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Database Requirements
|
||||
|
||||
The database should support:
|
||||
- User management
|
||||
- Data persistence
|
||||
- Backup functionality
|
||||
|
||||
### API Requirements
|
||||
|
||||
The API should provide:
|
||||
- RESTful endpoints
|
||||
- Authentication
|
||||
- Rate limiting
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
This section covers the implementation approach.
|
||||
"""
|
||||
|
||||
def test_outline_mode_captures_actual_heading_text(self):
|
||||
"""Test that outline mode captures actual heading text in enum constraints."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema in outline mode with heading text capture
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--depth', '3',
|
||||
str(md_file)
|
||||
])
|
||||
|
||||
# Assert - Command should succeed
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
|
||||
# Parse the generated schema
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should have correct title format
|
||||
assert schema['title'] == f"Schema from {md_file.name}"
|
||||
|
||||
# Should capture actual heading text in enum constraints
|
||||
level_1_content = schema['properties']['headings']['properties']['level_1']['items']['properties']['content']
|
||||
assert 'enum' in level_1_content
|
||||
assert "Project Requirements" in level_1_content['enum']
|
||||
|
||||
level_2_content = schema['properties']['headings']['properties']['level_2']['items']['properties']['content']
|
||||
assert 'enum' in level_2_content
|
||||
assert "Overview" in level_2_content['enum']
|
||||
assert "Technical Specifications" in level_2_content['enum']
|
||||
assert "Implementation Plan" in level_2_content['enum']
|
||||
|
||||
level_3_content = schema['properties']['headings']['properties']['level_3']['items']['properties']['content']
|
||||
assert 'enum' in level_3_content
|
||||
assert "Database Requirements" in level_3_content['enum']
|
||||
assert "API Requirements" in level_3_content['enum']
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
|
||||
def test_draft_generation_uses_captured_heading_text(self):
|
||||
"""Test that draft generation uses actual heading text from outline schema."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as draft_f:
|
||||
draft_file = Path(draft_f.name)
|
||||
|
||||
try:
|
||||
# Arrange - Generate outline schema with heading text capture
|
||||
schema_result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--depth', '3',
|
||||
'--outfile', str(schema_file),
|
||||
str(md_file)
|
||||
])
|
||||
assert schema_result.exit_code == 0
|
||||
|
||||
# Act - Generate draft from the outline schema
|
||||
draft_result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(draft_file)
|
||||
])
|
||||
|
||||
# Assert - Draft generation should succeed
|
||||
assert draft_result.exit_code == 0, f"Draft generation failed: {draft_result.output}"
|
||||
|
||||
# Read the generated draft
|
||||
draft_content = draft_file.read_text()
|
||||
|
||||
# Should use actual heading text, not generic placeholders
|
||||
assert "# Project Requirements" in draft_content
|
||||
assert "## Overview" in draft_content
|
||||
assert "## Technical Specifications" in draft_content
|
||||
assert "## Implementation Plan" in draft_content
|
||||
assert "### Database Requirements" in draft_content
|
||||
assert "### API Requirements" in draft_content
|
||||
|
||||
# Should NOT have generic headings
|
||||
assert "## Introduction" not in draft_content
|
||||
assert "## Main Content" not in draft_content
|
||||
assert "## Section 1" not in draft_content
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
if schema_file.exists():
|
||||
schema_file.unlink()
|
||||
if draft_file.exists():
|
||||
draft_file.unlink()
|
||||
|
||||
def test_outline_schema_integration_with_content_instructions(self):
|
||||
"""Test that outline schemas integrate properly with content instructions."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema with both outline mode and content instructions
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--include-content-instructions',
|
||||
'--depth', '2',
|
||||
str(md_file)
|
||||
])
|
||||
|
||||
# Assert - Command should succeed
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
|
||||
# Parse the generated schema
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should have both heading text capture and content instructions
|
||||
assert schema.get('x-markitect-heading-text-capture') == True
|
||||
assert schema.get('x-markitect-content-instructions-enabled') == True
|
||||
|
||||
# Check that headings have both enum constraints and content instructions
|
||||
level_1_items = schema['properties']['headings']['properties']['level_1']['items']['properties']
|
||||
assert 'enum' in level_1_items['content']
|
||||
assert 'x-markitect-content-instructions' in level_1_items
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
|
||||
def test_depth_limiting_works_correctly(self):
|
||||
"""Test that depth parameter correctly limits heading levels in outline mode."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema with depth limit of 2
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--depth', '2',
|
||||
str(md_file)
|
||||
])
|
||||
|
||||
# Assert - Command should succeed
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
|
||||
# Parse the generated schema
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should have level 1 and 2 headings
|
||||
headings = schema['properties']['headings']['properties']
|
||||
assert 'level_1' in headings
|
||||
assert 'level_2' in headings
|
||||
|
||||
# Should NOT have level 3 headings due to depth limit
|
||||
assert 'level_3' not in headings
|
||||
|
||||
# Verify outline depth is recorded
|
||||
assert schema.get('x-markitect-outline-depth') == 2
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
|
||||
def test_outline_mode_title_format_correction(self):
|
||||
"""Test that outline mode generates correct title format."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema in outline mode
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
str(md_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should use "Schema from" not "Schema for"
|
||||
expected_title = f"Schema from {md_file.name}"
|
||||
assert schema['title'] == expected_title
|
||||
|
||||
# Should have outline mode marker
|
||||
assert schema.get('x-markitect-outline-mode') == True
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
|
||||
def test_end_to_end_outline_workflow(self):
|
||||
"""Test complete workflow: example -> outline schema -> draft -> validation."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
example_file = Path(f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as draft_f:
|
||||
draft_file = Path(draft_f.name)
|
||||
|
||||
try:
|
||||
# Step 1: Generate outline schema from example
|
||||
schema_result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--include-content-instructions',
|
||||
'--depth', '3',
|
||||
'--outfile', str(schema_file),
|
||||
str(example_file)
|
||||
])
|
||||
assert schema_result.exit_code == 0
|
||||
|
||||
# Step 2: Generate draft from schema
|
||||
draft_result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(draft_file)
|
||||
])
|
||||
assert draft_result.exit_code == 0
|
||||
|
||||
# Step 3: Verify draft content quality
|
||||
# Note: Skip validation since outline mode schemas capture full structural
|
||||
# requirements but stubs generate minimal content. This is expected behavior.
|
||||
draft_content = draft_file.read_text()
|
||||
|
||||
# Should preserve the document structure from example
|
||||
assert "# Project Requirements" in draft_content
|
||||
assert "## Overview" in draft_content
|
||||
assert "## Technical Specifications" in draft_content
|
||||
assert "### Database Requirements" in draft_content
|
||||
assert "### API Requirements" in draft_content
|
||||
assert "## Implementation Plan" in draft_content
|
||||
|
||||
# Should have schema reference
|
||||
assert f"Generated from schema: {schema_file}" in draft_content
|
||||
|
||||
finally:
|
||||
example_file.unlink()
|
||||
if schema_file.exists():
|
||||
schema_file.unlink()
|
||||
if draft_file.exists():
|
||||
draft_file.unlink()
|
||||
|
||||
def test_outline_mode_backwards_compatibility(self):
|
||||
"""Test that outline mode maintains backwards compatibility."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Test both old and new parameter styles work
|
||||
old_style_result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--max-depth', '2',
|
||||
str(md_file)
|
||||
])
|
||||
|
||||
new_style_result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--depth', '2',
|
||||
str(md_file)
|
||||
])
|
||||
|
||||
# Both should work
|
||||
assert old_style_result.exit_code == 0
|
||||
assert new_style_result.exit_code == 0
|
||||
|
||||
# Should produce equivalent schemas
|
||||
old_schema = json.loads(old_style_result.output)
|
||||
new_schema = json.loads(new_style_result.output)
|
||||
|
||||
assert old_schema['title'] == new_schema['title']
|
||||
assert old_schema.get('x-markitect-outline-mode') == new_schema.get('x-markitect-outline-mode')
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
|
||||
def test_outline_schema_supports_data_driven_generation(self):
|
||||
"""Test that outline schemas work with data-driven draft generation."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(self.test_md_content)
|
||||
md_file = Path(f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
data_file = Path(data_f.name)
|
||||
# Create test data
|
||||
data_f.write(json.dumps([
|
||||
{"project": "Alpha", "version": "1.0"},
|
||||
{"project": "Beta", "version": "2.0"}
|
||||
]))
|
||||
data_f.flush()
|
||||
|
||||
try:
|
||||
# Generate outline schema
|
||||
schema_result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--depth', '2',
|
||||
'--outfile', str(schema_file),
|
||||
str(md_file)
|
||||
])
|
||||
assert schema_result.exit_code == 0
|
||||
|
||||
# Test data-driven generation (if implemented)
|
||||
# This tests integration with Issue #56
|
||||
draft_result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', '/tmp/outline_drafts'
|
||||
])
|
||||
|
||||
# Should work or gracefully indicate feature not implemented
|
||||
assert draft_result.exit_code == 0 or "not implemented" in draft_result.output.lower()
|
||||
|
||||
finally:
|
||||
md_file.unlink()
|
||||
if schema_file.exists():
|
||||
schema_file.unlink()
|
||||
if data_file.exists():
|
||||
data_file.unlink()
|
||||
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Tests for Issue #50: Define metaschema for JSON schema structure
|
||||
|
||||
This test module defines comprehensive tests for the MarkiTect metaschema that extends
|
||||
standard JSON Schema with MarkiTect-specific features like heading text capture,
|
||||
content field instructions, and outline structure representation.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from markitect.metaschema import MetaschemaValidator, MARKITECT_METASCHEMA_PATH
|
||||
|
||||
|
||||
class TestIssue50MetaschemaDefinition:
|
||||
"""Test suite for MarkiTect metaschema definition and validation."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.metaschema_validator = MetaschemaValidator()
|
||||
|
||||
def test_metaschema_file_exists_and_is_valid_json(self):
|
||||
"""Test that the metaschema JSON file exists and contains valid JSON."""
|
||||
# Arrange & Act
|
||||
metaschema_path = Path(MARKITECT_METASCHEMA_PATH)
|
||||
|
||||
# Assert
|
||||
assert metaschema_path.exists(), f"Metaschema file should exist at {MARKITECT_METASCHEMA_PATH}"
|
||||
|
||||
with open(metaschema_path) as f:
|
||||
metaschema = json.load(f)
|
||||
|
||||
assert isinstance(metaschema, dict), "Metaschema should be a valid JSON object"
|
||||
assert "$schema" in metaschema, "Metaschema should have $schema property"
|
||||
assert "type" in metaschema, "Metaschema should have type property"
|
||||
|
||||
def test_metaschema_extends_json_schema_draft_07(self):
|
||||
"""Test that metaschema properly extends JSON Schema Draft 07."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert
|
||||
assert metaschema["$schema"] == "http://json-schema.org/draft-07/schema#"
|
||||
assert metaschema["type"] == "object"
|
||||
assert "allOf" in metaschema, "Should extend base JSON Schema using allOf"
|
||||
|
||||
# Should reference standard JSON Schema
|
||||
found_json_schema_ref = False
|
||||
for schema_ref in metaschema["allOf"]:
|
||||
if "$ref" in schema_ref and "json-schema.org" in schema_ref["$ref"]:
|
||||
found_json_schema_ref = True
|
||||
break
|
||||
assert found_json_schema_ref, "Should reference standard JSON Schema Draft 07"
|
||||
|
||||
def test_metaschema_supports_heading_text_capture(self):
|
||||
"""Test that metaschema supports heading text capture extensions."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert - Check for MarkiTect-specific heading text properties
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
assert "x-markitect-heading-text" in markitect_properties, \
|
||||
"Should support x-markitect-heading-text property"
|
||||
|
||||
heading_text_schema = markitect_properties["x-markitect-heading-text"]
|
||||
assert heading_text_schema["type"] == "string"
|
||||
assert "description" in heading_text_schema
|
||||
assert "preserve actual heading text" in heading_text_schema["description"].lower()
|
||||
|
||||
def test_metaschema_supports_content_field_instructions(self):
|
||||
"""Test that metaschema supports content field instruction capabilities."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert - Check for content instruction properties
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
assert "x-markitect-content-instructions" in markitect_properties, \
|
||||
"Should support x-markitect-content-instructions property"
|
||||
|
||||
instructions_schema = markitect_properties["x-markitect-content-instructions"]
|
||||
assert instructions_schema["type"] == "string"
|
||||
assert "description" in instructions_schema
|
||||
assert "content author" in instructions_schema["description"].lower()
|
||||
|
||||
def test_metaschema_supports_outline_structure_representation(self):
|
||||
"""Test that metaschema supports outline structure representation."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert - Check for outline structure properties
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
assert "x-markitect-outline-mode" in markitect_properties, \
|
||||
"Should support x-markitect-outline-mode property"
|
||||
|
||||
outline_schema = markitect_properties["x-markitect-outline-mode"]
|
||||
assert outline_schema["type"] == "boolean"
|
||||
assert "description" in outline_schema
|
||||
|
||||
assert "x-markitect-outline-depth" in markitect_properties, \
|
||||
"Should support x-markitect-outline-depth property"
|
||||
|
||||
depth_schema = markitect_properties["x-markitect-outline-depth"]
|
||||
assert depth_schema["type"] == "integer"
|
||||
assert depth_schema["minimum"] == 1
|
||||
|
||||
def test_metaschema_validates_standard_json_schema(self):
|
||||
"""Test that metaschema accepts standard JSON Schema documents (backward compatibility)."""
|
||||
# Arrange
|
||||
standard_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Standard Schema",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer", "minimum": 0}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
is_valid = self.metaschema_validator.validate_schema(standard_schema)
|
||||
assert is_valid, "Standard JSON Schema should be valid against metaschema"
|
||||
|
||||
def test_metaschema_validates_markitect_extended_schema(self):
|
||||
"""Test that metaschema accepts MarkiTect extended schemas."""
|
||||
# Arrange
|
||||
markitect_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "MarkiTect Extended Schema",
|
||||
"x-markitect-outline-mode": True,
|
||||
"x-markitect-outline-depth": 3,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-heading-text": "Introduction",
|
||||
"x-markitect-content-instructions": "Provide overview of the document"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
is_valid = self.metaschema_validator.validate_schema(markitect_schema)
|
||||
assert is_valid, "MarkiTect extended schema should be valid against metaschema"
|
||||
|
||||
def test_metaschema_rejects_invalid_markitect_extensions(self):
|
||||
"""Test that metaschema rejects invalid MarkiTect extension values."""
|
||||
# Arrange
|
||||
invalid_schemas = [
|
||||
# Invalid outline depth (negative)
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-markitect-outline-depth": -1
|
||||
},
|
||||
# Invalid outline mode (not boolean)
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-markitect-outline-mode": "true"
|
||||
},
|
||||
# Invalid heading text (not string)
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {
|
||||
"x-markitect-heading-text": 123
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# Act & Assert
|
||||
for invalid_schema in invalid_schemas:
|
||||
is_valid = self.metaschema_validator.validate_schema(invalid_schema)
|
||||
assert not is_valid, f"Invalid schema should be rejected: {invalid_schema}"
|
||||
|
||||
def test_metaschema_validation_provides_detailed_errors(self):
|
||||
"""Test that metaschema validation provides detailed error messages."""
|
||||
# Arrange
|
||||
invalid_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-markitect-outline-depth": "not-a-number"
|
||||
}
|
||||
|
||||
# Act
|
||||
validation_result = self.metaschema_validator.validate_schema_with_errors(invalid_schema)
|
||||
|
||||
# Assert
|
||||
assert not validation_result.is_valid
|
||||
assert len(validation_result.errors) > 0
|
||||
|
||||
error_messages = [error.message for error in validation_result.errors]
|
||||
assert any("x-markitect-outline-depth" in msg for msg in error_messages), \
|
||||
"Error should mention the problematic MarkiTect extension"
|
||||
|
||||
def test_metaschema_supports_content_instruction_types(self):
|
||||
"""Test that metaschema supports different types of content instructions."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
# Assert - Check for instruction type support
|
||||
assert "x-markitect-instruction-type" in markitect_properties, \
|
||||
"Should support x-markitect-instruction-type property"
|
||||
|
||||
instruction_type_schema = markitect_properties["x-markitect-instruction-type"]
|
||||
assert instruction_type_schema["type"] == "string"
|
||||
assert "enum" in instruction_type_schema
|
||||
|
||||
expected_types = ["description", "example", "constraint", "template"]
|
||||
for instruction_type in expected_types:
|
||||
assert instruction_type in instruction_type_schema["enum"], \
|
||||
f"Should support {instruction_type} instruction type"
|
||||
|
||||
def test_metaschema_supports_schema_metadata(self):
|
||||
"""Test that metaschema supports MarkiTect-specific schema metadata."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
# Assert - Check for metadata properties
|
||||
assert "x-markitect-generated-from" in markitect_properties, \
|
||||
"Should support x-markitect-generated-from property"
|
||||
|
||||
assert "x-markitect-generation-mode" in markitect_properties, \
|
||||
"Should support x-markitect-generation-mode property"
|
||||
|
||||
generation_mode = markitect_properties["x-markitect-generation-mode"]
|
||||
assert "enum" in generation_mode
|
||||
assert "outline" in generation_mode["enum"]
|
||||
assert "full" in generation_mode["enum"]
|
||||
|
||||
def test_existing_schemas_validate_against_metaschema(self):
|
||||
"""Test that all existing MarkiTect schemas validate against the new metaschema."""
|
||||
# Arrange - Get sample existing schema structure
|
||||
existing_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema for example.md",
|
||||
"description": "JSON schema describing the structure of example.md",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"position": {"type": "integer"}
|
||||
},
|
||||
"required": ["content", "level"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total_elements": {"type": "integer"},
|
||||
"structure_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
is_valid = self.metaschema_validator.validate_schema(existing_schema)
|
||||
assert is_valid, "Existing MarkiTect schemas should remain valid (backward compatibility)"
|
||||
|
||||
def _get_markitect_extensions(self, metaschema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Helper to extract MarkiTect extension properties from metaschema."""
|
||||
# Look for MarkiTect extensions in the allOf extension
|
||||
for extension in metaschema.get("allOf", []):
|
||||
if "properties" in extension:
|
||||
markitect_props = {}
|
||||
for prop_name, prop_schema in extension["properties"].items():
|
||||
if prop_name.startswith("x-markitect-"):
|
||||
markitect_props[prop_name] = prop_schema
|
||||
if markitect_props:
|
||||
return markitect_props
|
||||
|
||||
# Fallback: look in patternProperties for x-markitect- patterns
|
||||
pattern_props = metaschema.get("patternProperties", {})
|
||||
for pattern, schema in pattern_props.items():
|
||||
if "x-markitect" in pattern:
|
||||
return {pattern: schema}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class TestMetaschemaValidator:
|
||||
"""Test the MetaschemaValidator utility class."""
|
||||
|
||||
def test_metaschema_validator_can_be_created(self):
|
||||
"""Test that MetaschemaValidator can be instantiated."""
|
||||
# Act & Assert
|
||||
validator = MetaschemaValidator()
|
||||
assert validator is not None
|
||||
|
||||
def test_metaschema_validator_loads_metaschema(self):
|
||||
"""Test that MetaschemaValidator properly loads the metaschema."""
|
||||
# Arrange & Act
|
||||
validator = MetaschemaValidator()
|
||||
metaschema = validator.get_metaschema()
|
||||
|
||||
# Assert
|
||||
assert isinstance(metaschema, dict)
|
||||
assert "$schema" in metaschema
|
||||
assert metaschema["$schema"] == "http://json-schema.org/draft-07/schema#"
|
||||
|
||||
def test_metaschema_validator_caches_metaschema(self):
|
||||
"""Test that MetaschemaValidator caches the loaded metaschema."""
|
||||
# Arrange & Act
|
||||
validator = MetaschemaValidator()
|
||||
metaschema1 = validator.get_metaschema()
|
||||
metaschema2 = validator.get_metaschema()
|
||||
|
||||
# Assert
|
||||
assert metaschema1 is metaschema2, "Should cache metaschema instance"
|
||||
@@ -0,0 +1,515 @@
|
||||
"""
|
||||
Tests for Issue #54: Add content field instruction capabilities
|
||||
|
||||
This test module implements comprehensive tests for content field instructions
|
||||
that provide guidance for content authors during document generation.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.cli import cli
|
||||
from markitect.schema_generator import SchemaGenerator
|
||||
from markitect.stub_generator import StubGenerator
|
||||
from markitect.exceptions import InvalidInstructionTypeError
|
||||
|
||||
|
||||
class TestIssue54ContentInstructions:
|
||||
"""Test suite for content field instruction functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.schema_generator = SchemaGenerator()
|
||||
self.stub_generator = StubGenerator()
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_cli_accepts_include_content_instructions_option(self):
|
||||
"""Test that CLI accepts --include-content-instructions option."""
|
||||
# Arrange
|
||||
markdown_content = """# Architecture Document
|
||||
|
||||
## Introduction
|
||||
This section provides an overview of the system.
|
||||
|
||||
## Design Principles
|
||||
Core principles guiding the design.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--include-content-instructions',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0, f"CLI should accept --include-content-instructions option, got: {result.output}"
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_schema_generation_with_content_instructions_includes_instruction_fields(self):
|
||||
"""Test that schema generation with content instructions includes instruction fields."""
|
||||
# Arrange
|
||||
markdown_content = """# Software Architecture Document
|
||||
|
||||
## Introduction
|
||||
This section provides an overview of the system architecture.
|
||||
|
||||
### Purpose
|
||||
Explain the purpose and goals of this document.
|
||||
|
||||
## System Design
|
||||
Describe the overall system design and architecture.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Schema should contain content instruction fields
|
||||
assert "properties" in schema
|
||||
assert "headings" in schema["properties"]
|
||||
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
|
||||
# Level 1 heading should have content instructions
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
assert items_props["x-markitect-content-instructions"]["type"] == "string"
|
||||
|
||||
# Level 2 headings should have content instructions
|
||||
level_2 = headings["level_2"]
|
||||
items_props = level_2["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_schema_includes_content_instruction_metaschema_extension(self):
|
||||
"""Test that schemas with content instructions include metaschema extension."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section A
|
||||
Content for section A.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Should have metaschema extension
|
||||
assert "x-markitect-content-instructions-enabled" in schema
|
||||
assert schema["x-markitect-content-instructions-enabled"] is True
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_support_different_instruction_types(self):
|
||||
"""Test that content instructions can specify different instruction types."""
|
||||
# Arrange
|
||||
markdown_content = """# Requirements Document
|
||||
|
||||
## Functional Requirements
|
||||
List all functional requirements.
|
||||
|
||||
## Non-Functional Requirements
|
||||
Describe performance, security, and usability requirements.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True,
|
||||
instruction_type="description"
|
||||
)
|
||||
|
||||
# Assert - Should have instruction type specified
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_2 = headings["level_2"]
|
||||
items_props = level_2["items"]["properties"]
|
||||
|
||||
assert "x-markitect-instruction-type" in items_props
|
||||
assert items_props["x-markitect-instruction-type"]["enum"] == ["description"]
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_integration_with_outline_mode(self):
|
||||
"""Test that content instructions work with outline mode."""
|
||||
# Arrange
|
||||
markdown_content = """# Project Plan
|
||||
|
||||
## Phase 1: Planning
|
||||
Planning activities and deliverables.
|
||||
|
||||
### Requirements Gathering
|
||||
Gather and document all requirements.
|
||||
|
||||
### Architecture Design
|
||||
Design the system architecture.
|
||||
|
||||
## Phase 2: Implementation
|
||||
Implementation activities.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--include-content-instructions',
|
||||
'--depth', '2',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should have both outline mode and content instructions extensions
|
||||
assert schema.get("x-markitect-outline-mode") is True
|
||||
assert schema.get("x-markitect-content-instructions-enabled") is True
|
||||
|
||||
# Should only include headings up to depth 2
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
assert "level_1" in headings
|
||||
assert "level_2" in headings
|
||||
assert "level_3" not in headings
|
||||
|
||||
# Should have content instructions in the headings
|
||||
level_2 = headings["level_2"]
|
||||
items_props = level_2["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_for_paragraphs_and_lists(self):
|
||||
"""Test that content instructions can be added for paragraphs and lists."""
|
||||
# Arrange
|
||||
markdown_content = """# User Guide
|
||||
|
||||
## Overview
|
||||
This guide explains how to use the system.
|
||||
|
||||
Some introductory text here.
|
||||
|
||||
- Feature A
|
||||
- Feature B
|
||||
- Feature C
|
||||
|
||||
## Getting Started
|
||||
Follow these steps to get started.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Paragraphs should have content instructions
|
||||
if "paragraphs" in schema["properties"]:
|
||||
paragraphs_schema = schema["properties"]["paragraphs"]
|
||||
items_props = paragraphs_schema["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
# Lists should have content instructions
|
||||
if "lists" in schema["properties"]:
|
||||
lists_schema = schema["properties"]["lists"]
|
||||
items_props = lists_schema["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_stub_generation_includes_content_instruction_placeholders(self):
|
||||
"""Test that stub generation includes content instruction placeholders."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Test Schema",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide the main title of the document"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Describe each major section of the document"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
stub_content = self.stub_generator.generate_stub_from_schema(schema)
|
||||
|
||||
# Assert - Stub should include instruction placeholders
|
||||
assert "Provide the main title of the document" in stub_content
|
||||
assert "Describe each major section of the document" in stub_content
|
||||
|
||||
def test_cli_supports_instruction_type_parameter(self):
|
||||
"""Test that CLI supports --instruction-type parameter."""
|
||||
# Arrange
|
||||
markdown_content = """# API Documentation
|
||||
|
||||
## Authentication
|
||||
How to authenticate with the API.
|
||||
|
||||
## Endpoints
|
||||
Available API endpoints.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--include-content-instructions',
|
||||
'--instruction-type', 'example',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Check that instruction type is set correctly
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
assert items_props["x-markitect-instruction-type"]["enum"] == ["example"]
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_backward_compatibility_without_content_instructions(self):
|
||||
"""Test that existing behavior is maintained when content instructions are not enabled."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section One
|
||||
Content here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema without content instructions (default behavior)
|
||||
schema = self.schema_generator.generate_schema_from_file(temp_file)
|
||||
|
||||
# Assert - Should NOT have content instruction fields
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
|
||||
# Should not have content instruction fields
|
||||
assert "x-markitect-content-instructions" not in items_props
|
||||
assert "x-markitect-instruction-type" not in items_props
|
||||
|
||||
# Should NOT have content instructions extension
|
||||
assert "x-markitect-content-instructions-enabled" not in schema
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_with_heading_text_capture_integration(self):
|
||||
"""Test that content instructions work with heading text capture."""
|
||||
# Arrange
|
||||
markdown_content = """# Architecture Overview
|
||||
|
||||
## System Components
|
||||
Core system components and their responsibilities.
|
||||
|
||||
## Data Flow
|
||||
How data flows through the system.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
capture_heading_text=True,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Should have both heading text capture and content instructions
|
||||
assert schema.get("x-markitect-heading-text-capture") is True
|
||||
assert schema.get("x-markitect-content-instructions-enabled") is True
|
||||
|
||||
# Headings should have both enum constraints and content instructions
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
|
||||
# Should have enum constraint from heading text capture
|
||||
assert "enum" in items_props["content"]
|
||||
assert items_props["content"]["enum"] == ["Architecture Overview"]
|
||||
|
||||
# Should also have content instructions
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_cli_help_includes_content_instructions_options(self):
|
||||
"""Test that CLI help includes documentation for content instruction options."""
|
||||
# Act
|
||||
result = self.runner.invoke(cli, ['schema-generate', '--help'])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
help_text = result.output
|
||||
assert "--include-content-instructions" in help_text
|
||||
assert "--instruction-type" in help_text
|
||||
assert "content instructions" in help_text or "content guidance" in help_text
|
||||
|
||||
def test_instruction_type_validation(self):
|
||||
"""Test that --instruction-type parameter validates input correctly."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section
|
||||
Content here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Test invalid instruction type
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--include-content-instructions',
|
||||
'--instruction-type', 'invalid-type',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code != 0
|
||||
assert "Invalid instruction type" in result.output or "invalid-type" in result.output
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_generate_appropriate_default_text(self):
|
||||
"""Test that content instructions generate appropriate default guidance text."""
|
||||
# Arrange
|
||||
markdown_content = """# Development Guide
|
||||
|
||||
## Prerequisites
|
||||
System requirements and prerequisites.
|
||||
|
||||
### Software Requirements
|
||||
Required software and versions.
|
||||
|
||||
## Installation
|
||||
Step-by-step installation instructions.
|
||||
|
||||
## Configuration
|
||||
How to configure the system.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True,
|
||||
instruction_type="description"
|
||||
)
|
||||
|
||||
# Assert - Content instructions should contain appropriate guidance
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
|
||||
# Level 1 should have appropriate instructions
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
instructions = items_props["x-markitect-content-instructions"]["const"]
|
||||
assert len(instructions) > 0
|
||||
assert isinstance(instructions, str)
|
||||
|
||||
# Instructions should be contextually appropriate
|
||||
# (implementation will determine specific text)
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
Tests for Issue #55: Schema-based draft generation
|
||||
|
||||
This test module implements comprehensive tests for enhanced schema-based draft
|
||||
generation that utilizes content field instructions and schema metadata.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.cli import cli
|
||||
from markitect.stub_generator import StubGenerator
|
||||
|
||||
|
||||
class TestIssue55SchemaBasedDraftGeneration:
|
||||
"""Test suite for enhanced schema-based draft generation functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.stub_generator = StubGenerator()
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_generate_stub_uses_content_instructions_from_schema(self):
|
||||
"""Test that generate-stub uses content instructions instead of generic placeholders."""
|
||||
# Arrange
|
||||
schema_with_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "API Documentation",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Write the main API documentation title"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Describe each API endpoint section"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use specific content instructions, not generic placeholders
|
||||
assert "Write the main API documentation title" in output
|
||||
assert "Describe each API endpoint section" in output
|
||||
|
||||
# Should NOT contain generic placeholder text
|
||||
assert "TODO: Add content for" not in output
|
||||
assert "section_level_2 section" not in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_includes_schema_reference_metadata(self):
|
||||
"""Test that generated drafts include reference to their source schema."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Requirements Document",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide the document title"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should include schema reference metadata
|
||||
assert "<!-- Generated from schema:" in output or "Source schema:" in output
|
||||
assert str(schema_file.name) in output or schema_file.name in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_supports_different_instruction_types(self):
|
||||
"""Test that generate-stub handles different instruction types appropriately."""
|
||||
# Arrange
|
||||
schema_with_example_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Tutorial Guide",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Example: Getting Started with Our API"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["example"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_example_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use the example-type instruction
|
||||
assert "Example: Getting Started with Our API" in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_handles_schemas_without_content_instructions(self):
|
||||
"""Test that generate-stub gracefully handles schemas without content instructions."""
|
||||
# Arrange - Schema without content instructions (backward compatibility)
|
||||
schema_without_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Basic Document",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_without_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert - Should still work with generic placeholders
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should fall back to generic placeholder behavior
|
||||
assert "Basic Document" in output # Should use schema title
|
||||
assert len(output) > 0 # Should generate some content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_supports_output_file_with_schema_reference(self):
|
||||
"""Test that generate-stub writes to output file with schema reference."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Project Plan",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide the project name and overview"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
output_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
content = output_file.read_text()
|
||||
assert "Provide the project name and overview" in content
|
||||
assert "Project Plan" in content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
if output_file.exists():
|
||||
output_file.unlink()
|
||||
|
||||
def test_generate_stub_validates_generated_draft_against_schema(self):
|
||||
"""Test that generated drafts can be validated against their source schema."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Meeting Notes",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Meeting title and date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
draft_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate draft
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(draft_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Act - Validate generated draft against schema
|
||||
validate_result = self.runner.invoke(cli, [
|
||||
'validate',
|
||||
str(draft_file),
|
||||
'--schema', str(schema_file)
|
||||
])
|
||||
|
||||
# Assert - Generated draft should be valid against its source schema
|
||||
assert validate_result.exit_code == 0
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
if draft_file.exists():
|
||||
draft_file.unlink()
|
||||
|
||||
def test_stub_generator_class_supports_content_instructions_directly(self):
|
||||
"""Test that StubGenerator class can be used directly with content instruction schemas."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Architecture Document",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "System architecture overview title"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
stub_content = self.stub_generator.generate_stub_from_schema(schema)
|
||||
|
||||
# Assert
|
||||
assert "System architecture overview title" in stub_content
|
||||
assert "Architecture Document" in stub_content
|
||||
|
||||
def test_generate_stub_with_outline_mode_schema_integration(self):
|
||||
"""Test that generate-stub works with schemas created by outline mode + content instructions."""
|
||||
# Arrange - Schema that would be generated by outline mode with content instructions
|
||||
outline_schema_with_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema from user_guide.md",
|
||||
"x-markitect-outline-mode": True,
|
||||
"x-markitect-outline-depth": 2,
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide content for the 'level 1 heading' section"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide content for the 'level 2 heading' section"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(outline_schema_with_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use content instructions from outline mode schema
|
||||
assert "Provide content for the 'level 1 heading' section" in output
|
||||
assert "Provide content for the 'level 2 heading' section" in output
|
||||
|
||||
# Should respect outline mode structure (depth limited to 2)
|
||||
assert output.count('##') >= 3 # Should have multiple level 2 headings
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_with_heading_text_capture_schema_integration(self):
|
||||
"""Test that generate-stub works with schemas that have heading text capture."""
|
||||
# Arrange - Schema with both heading text capture and content instructions
|
||||
schema_with_heading_capture = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema from api_docs.md",
|
||||
"x-markitect-heading-text-capture": True,
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"enum": ["API Reference"] # From heading text capture
|
||||
},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Complete API reference documentation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_heading_capture, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use the specific heading text from enum constraint
|
||||
assert "# API Reference" in output
|
||||
|
||||
# Should use content instructions
|
||||
assert "Complete API reference documentation" in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_preserves_schema_metadata_in_output(self):
|
||||
"""Test that important schema metadata is preserved in the generated draft."""
|
||||
# Arrange
|
||||
schema_with_metadata = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Design Document",
|
||||
"description": "Template for system design documents",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"x-markitect-outline-mode": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Design document title and scope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_metadata, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should include schema metadata information
|
||||
assert any(marker in output.lower() for marker in [
|
||||
"generated from", "source schema", "template for", "schema:", "outline mode"
|
||||
])
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
@@ -0,0 +1,736 @@
|
||||
"""
|
||||
Tests for Issue #56: Data-driven multiple draft generation
|
||||
|
||||
This test module implements comprehensive tests for data-driven draft generation
|
||||
that creates multiple documents from a schema and data source with field mapping.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import csv
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.cli import cli
|
||||
|
||||
|
||||
class TestIssue56DataDrivenDraftGeneration:
|
||||
"""Test suite for data-driven multiple draft generation functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_cli_has_generate_drafts_command(self):
|
||||
"""Test that CLI has a generate-drafts command for data-driven generation."""
|
||||
# Act
|
||||
result = self.runner.invoke(cli, ['--help'])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
assert 'generate-drafts' in result.output, "CLI should have generate-drafts command"
|
||||
|
||||
def test_generate_drafts_command_help(self):
|
||||
"""Test that generate-drafts command has proper help documentation."""
|
||||
# Act
|
||||
result = self.runner.invoke(cli, ['generate-drafts', '--help'])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
help_text = result.output.lower()
|
||||
assert 'data source' in help_text
|
||||
assert 'schema' in help_text
|
||||
assert 'multiple' in help_text or 'batch' in help_text
|
||||
|
||||
def test_generate_drafts_supports_json_data_source(self):
|
||||
"""Test that generate-drafts supports JSON data sources."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Employee Profile",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Employee name: {name}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Role: {role}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "role"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = [
|
||||
{"name": "Alice Johnson", "role": "Software Engineer", "department": "Engineering"},
|
||||
{"name": "Bob Smith", "role": "Product Manager", "department": "Product"}
|
||||
]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0, f"Command should succeed, got: {result.output}"
|
||||
|
||||
# Check that multiple files were generated
|
||||
output_path = Path(output_dir)
|
||||
generated_files = list(output_path.glob('*.md'))
|
||||
assert len(generated_files) >= 2, "Should generate multiple draft files"
|
||||
|
||||
# Check content of generated files
|
||||
for file in generated_files:
|
||||
content = file.read_text()
|
||||
# Should contain mapped data
|
||||
assert any(name in content for name in ["Alice Johnson", "Bob Smith"])
|
||||
assert any(role in content for role in ["Software Engineer", "Product Manager"])
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_supports_csv_data_source(self):
|
||||
"""Test that generate-drafts supports CSV data sources."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Product Description",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Product: {product_name}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "product_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as csv_f:
|
||||
writer = csv.writer(csv_f)
|
||||
writer.writerow(['product_name', 'price', 'category'])
|
||||
writer.writerow(['Laptop Pro', '1299.99', 'Electronics'])
|
||||
writer.writerow(['Office Chair', '249.99', 'Furniture'])
|
||||
csv_file = Path(csv_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(csv_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0, f"CSV processing should work, got: {result.output}"
|
||||
|
||||
# Check generated files
|
||||
output_path = Path(output_dir)
|
||||
generated_files = list(output_path.glob('*.md'))
|
||||
assert len(generated_files) >= 2, "Should generate files for each CSV row"
|
||||
|
||||
# Check content contains mapped CSV data
|
||||
all_content = ""
|
||||
for file in generated_files:
|
||||
all_content += file.read_text()
|
||||
|
||||
assert "Laptop Pro" in all_content
|
||||
assert "Office Chair" in all_content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
csv_file.unlink()
|
||||
|
||||
def test_generate_drafts_field_mapping_functionality(self):
|
||||
"""Test that field mapping works correctly between data and schema."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Blog Post",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "{title}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Author: {author_name}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "author_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = [
|
||||
{"title": "Getting Started with Python", "author_name": "Jane Doe", "tags": ["python", "beginner"]},
|
||||
{"title": "Advanced JavaScript Patterns", "author_name": "John Smith", "tags": ["javascript", "advanced"]}
|
||||
]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify field mapping worked correctly
|
||||
generated_files = list(Path(output_dir).glob('*.md'))
|
||||
assert len(generated_files) == 2
|
||||
|
||||
contents = [file.read_text() for file in generated_files]
|
||||
|
||||
# Check that titles and authors are properly mapped
|
||||
assert any("Getting Started with Python" in content for content in contents)
|
||||
assert any("Advanced JavaScript Patterns" in content for content in contents)
|
||||
assert any("Author: Jane Doe" in content for content in contents)
|
||||
assert any("Author: John Smith" in content for content in contents)
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_maintains_schema_references(self):
|
||||
"""Test that generated drafts maintain schema references for validation."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Meeting Notes",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Meeting: {meeting_title}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "meeting_title"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = [{"meeting_title": "Weekly Standup", "date": "2024-01-15"}]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Check schema reference is maintained
|
||||
generated_files = list(Path(output_dir).glob('*.md'))
|
||||
assert len(generated_files) >= 1
|
||||
|
||||
for file in generated_files:
|
||||
content = file.read_text()
|
||||
assert f"Generated from schema: {schema_file}" in content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_output_directory_specification(self):
|
||||
"""Test that CLI supports output directory specification for batch generation."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Test Document",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "{name}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = [{"name": "Test1"}, {"name": "Test2"}]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
output_dir = Path(temp_dir) / "custom_output"
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', str(output_dir)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
assert output_dir.exists(), "Output directory should be created"
|
||||
|
||||
generated_files = list(output_dir.glob('*.md'))
|
||||
assert len(generated_files) >= 2, "Should generate files in specified directory"
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_data_validation_compatibility(self):
|
||||
"""Test that data validation ensures compatibility with schema requirements."""
|
||||
# Arrange - Schema requires specific fields
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Validated Document",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Required field: {required_field}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "required_field"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-markitect-required-fields": ["required_field"]
|
||||
}
|
||||
|
||||
# Data missing required field
|
||||
invalid_data = [{"optional_field": "value"}]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(invalid_data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert - Should fail validation or provide warning
|
||||
# Could be exit code != 0 or warning in output
|
||||
assert result.exit_code != 0 or "warning" in result.output.lower() or "missing" in result.output.lower()
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_error_handling_data_schema_mismatch(self):
|
||||
"""Test error handling for data-schema mismatches."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Test Schema",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Data with different field names
|
||||
mismatched_data = [{"different_field": "value"}]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(mismatched_data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert - Should handle mismatch gracefully
|
||||
# Either succeed with warnings or fail with clear error
|
||||
if result.exit_code != 0:
|
||||
assert len(result.output) > 0 # Should have error message
|
||||
else:
|
||||
# If succeeded, should have warnings or default handling
|
||||
assert "warning" in result.output.lower() or len(list(Path(output_dir).glob('*.md'))) > 0
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_file_naming_convention(self):
|
||||
"""Test that generated files follow a consistent naming convention."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Item Description",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Item: {id}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = [
|
||||
{"id": "item-001", "name": "First Item"},
|
||||
{"id": "item-002", "name": "Second Item"}
|
||||
]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with TemporaryDirectory() as output_dir:
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', output_dir
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
|
||||
generated_files = list(Path(output_dir).glob('*.md'))
|
||||
assert len(generated_files) == 2
|
||||
|
||||
# Check naming convention
|
||||
filenames = [f.name for f in generated_files]
|
||||
for filename in filenames:
|
||||
assert filename.endswith('.md')
|
||||
# Should contain identifier or be sequentially named
|
||||
assert len(filename) > 3 # At least "x.md"
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
|
||||
def test_generate_drafts_integration_with_existing_stub_generation(self):
|
||||
"""Test that generate-drafts integrates properly with existing stub generation from Issue #55."""
|
||||
# Arrange - Use schema that works with single draft generation
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Integration Test",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Title: {title}"
|
||||
},
|
||||
"x-markitect-field-mapping": {
|
||||
"type": "string",
|
||||
"const": "title"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = [{"title": "Test Document"}]
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as schema_f:
|
||||
json.dump(schema, schema_f, indent=2)
|
||||
schema_file = Path(schema_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as data_f:
|
||||
json.dump(data, data_f, indent=2)
|
||||
data_file = Path(data_f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as single_output_f:
|
||||
single_output_file = Path(single_output_f.name)
|
||||
|
||||
with TemporaryDirectory() as batch_output_dir:
|
||||
try:
|
||||
# Act - Test both single and batch generation
|
||||
single_result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(single_output_file)
|
||||
])
|
||||
|
||||
batch_result = self.runner.invoke(cli, [
|
||||
'generate-drafts',
|
||||
str(schema_file),
|
||||
str(data_file),
|
||||
'--output-dir', batch_output_dir
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert single_result.exit_code == 0
|
||||
assert batch_result.exit_code == 0
|
||||
|
||||
# Check single output
|
||||
single_content = single_output_file.read_text()
|
||||
assert "Integration Test" in single_content
|
||||
|
||||
# Check batch output
|
||||
batch_files = list(Path(batch_output_dir).glob('*.md'))
|
||||
assert len(batch_files) >= 1
|
||||
|
||||
batch_content = batch_files[0].read_text()
|
||||
assert "Test Document" in batch_content
|
||||
|
||||
# Both should have schema references
|
||||
assert "Generated from schema:" in single_content
|
||||
assert "Generated from schema:" in batch_content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
data_file.unlink()
|
||||
if single_output_file.exists():
|
||||
single_output_file.unlink()
|
||||
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Integration tests for complete MarkdownMatters CLI implementation.
|
||||
Tests all four command families working together.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect_content.commands import content_get, content_stats
|
||||
from markitect.matter_frontmatter.commands import frontmatter_get, frontmatter_keys
|
||||
from markitect.matter_contentmatter.commands import contentmatter_get, contentmatter_keys
|
||||
from markitect.matter_tailmatter.commands import tailmatter_get, tailmatter_check
|
||||
|
||||
|
||||
class TestMarkdownMattersIntegration:
|
||||
"""Test complete MarkdownMatters functionality integration."""
|
||||
|
||||
@pytest.fixture
|
||||
def complete_document(self):
|
||||
"""A complete MarkdownMatters document with all three zones."""
|
||||
return """---
|
||||
title: "Complete MarkdownMatters Document"
|
||||
author: "Integration Test"
|
||||
version: 1.0
|
||||
status: "testing"
|
||||
---
|
||||
|
||||
# Complete MarkdownMatters Document
|
||||
|
||||
This document demonstrates all three matter zones working together.
|
||||
|
||||
Author: Dr. Test Researcher
|
||||
Institution: MarkdownMatters University
|
||||
Email: test@markdownmatters.edu
|
||||
Project: Integration Testing
|
||||
Version: 2.0
|
||||
Status: Active
|
||||
|
||||
## Research Content
|
||||
|
||||
Research Method: Integration Testing
|
||||
Sample Size: Complete document
|
||||
Test Framework: MarkdownMatters CLI
|
||||
|
||||
The content includes various MultiMarkdown key-value pairs that provide contextual metadata.
|
||||
|
||||
## Results
|
||||
|
||||
Result Status: All systems operational
|
||||
Performance: Excellent
|
||||
Coverage: 100%
|
||||
|
||||
All matter zones are properly separated and accessible through their respective CLI commands.
|
||||
|
||||
---
|
||||
|
||||
```yaml tailmatter
|
||||
qa_checklist:
|
||||
- requirement: "All three matter zones tested"
|
||||
complete: true
|
||||
- requirement: "CLI commands validated"
|
||||
complete: true
|
||||
- requirement: "Integration verified"
|
||||
complete: false
|
||||
|
||||
editorial:
|
||||
status: "Integration Testing"
|
||||
reviewer: "integration.tester@markdownmatters.edu"
|
||||
version: 3.0
|
||||
|
||||
agent_config:
|
||||
role: "integration_validator"
|
||||
access_scope: "all_zones"
|
||||
validation_mode: "comprehensive"
|
||||
```"""
|
||||
|
||||
@pytest.fixture
|
||||
def runner(self):
|
||||
"""CLI test runner."""
|
||||
return CliRunner()
|
||||
|
||||
def test_all_command_families_work_on_same_document(self, runner, complete_document):
|
||||
"""Test that all four command families can process the same document."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(complete_document)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Test content commands
|
||||
result = runner.invoke(content_get, ['--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "Complete MarkdownMatters Document" in result.output
|
||||
assert "---" not in result.output # No frontmatter
|
||||
assert "qa_checklist" not in result.output # No tailmatter
|
||||
|
||||
result = runner.invoke(content_stats, ['--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "word_count" in result.output
|
||||
|
||||
# Test frontmatter commands
|
||||
result = runner.invoke(frontmatter_get, ['title', '--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "Complete MarkdownMatters Document" in result.output
|
||||
|
||||
result = runner.invoke(frontmatter_keys, ['--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "title" in result.output
|
||||
assert "author" in result.output
|
||||
|
||||
# Test contentmatter commands
|
||||
result = runner.invoke(contentmatter_get, ['Author', '--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "Dr. Test Researcher" in result.output
|
||||
|
||||
result = runner.invoke(contentmatter_keys, ['--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "Author" in result.output
|
||||
assert "Institution" in result.output
|
||||
|
||||
# Test tailmatter commands
|
||||
result = runner.invoke(tailmatter_get, ['editorial.status', '--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "Integration Testing" in result.output
|
||||
|
||||
result = runner.invoke(tailmatter_check, ['--file', temp_file])
|
||||
assert result.exit_code == 0
|
||||
assert "QA Checklist Status" in result.output
|
||||
assert "✅" in result.output
|
||||
assert "❌" in result.output
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_matter_zone_separation(self, runner, complete_document):
|
||||
"""Test that each command family only accesses its designated zone."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(complete_document)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Frontmatter should not include contentmatter or tailmatter
|
||||
result = runner.invoke(frontmatter_keys, ['--file', temp_file])
|
||||
assert "Author" not in result.output # This is contentmatter
|
||||
assert "qa_checklist" not in result.output # This is tailmatter
|
||||
|
||||
# Contentmatter should not include frontmatter or tailmatter
|
||||
result = runner.invoke(contentmatter_keys, ['--file', temp_file])
|
||||
assert "title" not in result.output # This is frontmatter
|
||||
assert "qa_checklist" not in result.output # This is tailmatter
|
||||
|
||||
# Content should not include any matter zones in the actual content
|
||||
result = runner.invoke(content_get, ['--file', temp_file])
|
||||
assert "title:" not in result.output # No frontmatter YAML
|
||||
assert "qa_checklist:" not in result.output # No tailmatter YAML
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_performance_with_large_document(self, runner):
|
||||
"""Test performance with a large document containing all matter zones."""
|
||||
# Create a large document
|
||||
large_content = []
|
||||
large_content.append("---")
|
||||
large_content.append("title: 'Large Document Performance Test'")
|
||||
for i in range(50):
|
||||
large_content.append(f"field_{i}: 'value_{i}'")
|
||||
large_content.append("---")
|
||||
large_content.append("")
|
||||
|
||||
large_content.append("# Large Document Performance Test")
|
||||
large_content.append("")
|
||||
|
||||
# Add many contentmatter pairs
|
||||
for i in range(100):
|
||||
large_content.append(f"Data Field {i}: Value for field {i}")
|
||||
large_content.append("")
|
||||
|
||||
# Add substantial content
|
||||
for i in range(50):
|
||||
large_content.append(f"## Section {i}")
|
||||
large_content.append("")
|
||||
large_content.append(f"Content for section {i} with detailed information and multiple paragraphs.")
|
||||
large_content.append("")
|
||||
large_content.append("More content here to make the document substantial in size.")
|
||||
large_content.append("")
|
||||
|
||||
large_content.append("---")
|
||||
large_content.append("")
|
||||
large_content.append("```yaml tailmatter")
|
||||
large_content.append("qa_checklist:")
|
||||
for i in range(20):
|
||||
complete = "true" if i % 3 == 0 else "false"
|
||||
large_content.append(f" - requirement: 'Test requirement {i}'")
|
||||
large_content.append(f" complete: {complete}")
|
||||
large_content.append("editorial:")
|
||||
large_content.append(" status: 'Performance Testing'")
|
||||
large_content.append("```")
|
||||
|
||||
large_document = "\n".join(large_content)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(large_document)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Test that all commands complete in reasonable time
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
result = runner.invoke(content_stats, ['--file', temp_file])
|
||||
content_time = time.time() - start_time
|
||||
assert result.exit_code == 0
|
||||
assert content_time < 2.0 # Should complete in under 2 seconds
|
||||
|
||||
start_time = time.time()
|
||||
result = runner.invoke(frontmatter_keys, ['--file', temp_file])
|
||||
frontmatter_time = time.time() - start_time
|
||||
assert result.exit_code == 0
|
||||
assert frontmatter_time < 1.0 # Should complete in under 1 second
|
||||
|
||||
start_time = time.time()
|
||||
result = runner.invoke(contentmatter_keys, ['--file', temp_file])
|
||||
contentmatter_time = time.time() - start_time
|
||||
assert result.exit_code == 0
|
||||
assert contentmatter_time < 2.0 # Should complete in under 2 seconds
|
||||
|
||||
start_time = time.time()
|
||||
result = runner.invoke(tailmatter_check, ['--file', temp_file])
|
||||
tailmatter_time = time.time() - start_time
|
||||
assert result.exit_code == 0
|
||||
assert tailmatter_time < 1.0 # Should complete in under 1 second
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_error_handling_consistency(self, runner):
|
||||
"""Test that all command families handle errors consistently."""
|
||||
non_existent_file = "/tmp/non_existent_file.md"
|
||||
|
||||
# All commands should handle missing files gracefully
|
||||
commands_and_args = [
|
||||
(content_get, ['--file', non_existent_file]),
|
||||
(content_stats, ['--file', non_existent_file]),
|
||||
(frontmatter_get, ['title', '--file', non_existent_file]),
|
||||
(frontmatter_keys, ['--file', non_existent_file]),
|
||||
(contentmatter_get, ['Author', '--file', non_existent_file]),
|
||||
(contentmatter_keys, ['--file', non_existent_file]),
|
||||
(tailmatter_get, ['editorial.status', '--file', non_existent_file]),
|
||||
(tailmatter_check, ['--file', non_existent_file]),
|
||||
]
|
||||
|
||||
for command, args in commands_and_args:
|
||||
result = runner.invoke(command, args)
|
||||
assert result.exit_code != 0 # Should fail for non-existent file
|
||||
|
||||
def test_help_commands_consistency(self, runner):
|
||||
"""Test that all commands provide consistent help."""
|
||||
commands = [
|
||||
content_get, content_stats,
|
||||
frontmatter_get, frontmatter_keys,
|
||||
contentmatter_get, contentmatter_keys,
|
||||
tailmatter_get, tailmatter_check
|
||||
]
|
||||
|
||||
for command in commands:
|
||||
result = runner.invoke(command, ['--help'])
|
||||
assert result.exit_code == 0
|
||||
assert "Usage:" in result.output
|
||||
assert "--help" in result.output
|
||||
|
||||
def test_output_format_consistency(self, runner, complete_document):
|
||||
"""Test that commands with format options work consistently."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(complete_document)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Test JSON format consistency
|
||||
result = runner.invoke(content_stats, ['--file', temp_file, '--format', 'json'])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.startswith('{')
|
||||
|
||||
result = runner.invoke(frontmatter_keys, ['--file', temp_file, '--format', 'json'])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.startswith('[')
|
||||
|
||||
result = runner.invoke(contentmatter_keys, ['--file', temp_file, '--format', 'json'])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.startswith('[')
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
Reference in New Issue
Block a user