generated from coulomb/repo-seed
Contract framework with markdown-native contracts utilizing fenced YAML blocks
This commit is contained in:
336
tests/test_contract_framework.py
Normal file
336
tests/test_contract_framework.py
Normal file
@@ -0,0 +1,336 @@
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect_tool.cli import main
|
||||
from markitect_tool.contract import (
|
||||
check_markdown_file,
|
||||
collect_metrics,
|
||||
load_contract_file,
|
||||
validate_contract,
|
||||
)
|
||||
from markitect_tool.core import parse_markdown
|
||||
|
||||
|
||||
EXAMPLE_CASES = [
|
||||
(
|
||||
"adr",
|
||||
Path("examples/contracts/adr.contract.md"),
|
||||
Path("examples/documents/adr-valid.md"),
|
||||
Path("examples/documents/adr-invalid.md"),
|
||||
{
|
||||
"contract.field.missing",
|
||||
"contract.metric.too_low",
|
||||
"contract.assertion.contains_any_missing",
|
||||
"contract.section.missing",
|
||||
"contract.section.recommended_missing",
|
||||
"contract.section.forbidden",
|
||||
},
|
||||
),
|
||||
(
|
||||
"prd-frs",
|
||||
Path("examples/contracts/prd-frs.contract.md"),
|
||||
Path("examples/documents/prd-frs-valid.md"),
|
||||
Path("examples/documents/prd-frs-invalid.md"),
|
||||
{
|
||||
"contract.field.missing",
|
||||
"contract.metric.too_low",
|
||||
"contract.assertion.contains_any_missing",
|
||||
"contract.section.missing",
|
||||
"contract.section.recommended_missing",
|
||||
"contract.section.discouraged",
|
||||
},
|
||||
),
|
||||
(
|
||||
"workplan",
|
||||
Path("examples/contracts/workplan.contract.md"),
|
||||
Path("examples/documents/workplan-valid.md"),
|
||||
Path("examples/documents/workplan-invalid.md"),
|
||||
{
|
||||
"contract.field.missing",
|
||||
"contract.field.enum",
|
||||
"contract.assertion.contains_missing",
|
||||
"contract.section.recommended_missing",
|
||||
},
|
||||
),
|
||||
(
|
||||
"business-letter",
|
||||
Path("examples/contracts/business-letter.contract.md"),
|
||||
Path("examples/documents/business-letter-valid.md"),
|
||||
Path("examples/documents/business-letter-invalid.md"),
|
||||
{
|
||||
"contract.field.missing",
|
||||
"contract.section.missing",
|
||||
"contract.metric.too_low",
|
||||
},
|
||||
),
|
||||
(
|
||||
"concept-note",
|
||||
Path("examples/contracts/concept-note.contract.md"),
|
||||
Path("examples/documents/concept-note-valid.md"),
|
||||
Path("examples/documents/concept-note-invalid.md"),
|
||||
{
|
||||
"contract.field.enum",
|
||||
"contract.metric.too_low",
|
||||
"contract.section.missing",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
CONTRACT_TEXT = """---
|
||||
title: ADR Contract
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# ADR Contract
|
||||
|
||||
```yaml contract
|
||||
id: adr-contract-v1
|
||||
document:
|
||||
type: adr
|
||||
title: Architecture Decision Record
|
||||
fields:
|
||||
status:
|
||||
type: string
|
||||
required: true
|
||||
enum: [proposed, accepted, superseded]
|
||||
metrics:
|
||||
document:
|
||||
words:
|
||||
min: 12
|
||||
max: 240
|
||||
severity: warning
|
||||
sections:
|
||||
- id: context
|
||||
title: Context
|
||||
presence: required
|
||||
level: 2
|
||||
order:
|
||||
before: decision
|
||||
metrics:
|
||||
words:
|
||||
min: 4
|
||||
max: 80
|
||||
severity: warning
|
||||
assertions:
|
||||
- id: context-names-problem
|
||||
contains_any: [problem, motivation]
|
||||
severity: warning
|
||||
guidance: Explain why the decision exists.
|
||||
- id: decision
|
||||
title: Decision
|
||||
presence: required
|
||||
level: 2
|
||||
assertions:
|
||||
- id: decision-commits
|
||||
matches: "\\\\b(choose|adopt|use|will)\\\\b"
|
||||
severity: error
|
||||
guidance: State the actual decision, not only background.
|
||||
- id: consequences
|
||||
title: Consequences
|
||||
presence: recommended
|
||||
level: 2
|
||||
- id: deprecated
|
||||
title: Deprecated Approach
|
||||
presence: forbidden
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
VALID_ADR = """---
|
||||
document_type: adr
|
||||
status: accepted
|
||||
---
|
||||
|
||||
# Use Markdown Contracts
|
||||
|
||||
## Context
|
||||
|
||||
The problem is that plain heading counts do not explain whether content is useful.
|
||||
|
||||
## Decision
|
||||
|
||||
We will use a markdown-native document contract with deterministic diagnostics.
|
||||
|
||||
## Consequences
|
||||
|
||||
The tool can check author intent before generation or review work continues.
|
||||
"""
|
||||
|
||||
|
||||
INVALID_ADR = """---
|
||||
document_type: adr
|
||||
---
|
||||
|
||||
# Weak ADR
|
||||
|
||||
## Context
|
||||
|
||||
This is short.
|
||||
|
||||
## Deprecated Approach
|
||||
|
||||
This section should not be here.
|
||||
"""
|
||||
|
||||
|
||||
def test_load_contract_file_extracts_markdown_yaml_contract(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
|
||||
contract = load_contract_file(contract_file)
|
||||
|
||||
assert contract.id == "adr-contract-v1"
|
||||
assert contract.document_type == "adr"
|
||||
assert contract.fields[0].id == "status"
|
||||
assert [section.id for section in contract.sections] == [
|
||||
"context",
|
||||
"decision",
|
||||
"consequences",
|
||||
"deprecated",
|
||||
]
|
||||
|
||||
|
||||
def test_validate_contract_accepts_complete_contract(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
|
||||
result = validate_contract(load_contract_file(contract_file))
|
||||
|
||||
assert result.valid is True
|
||||
assert result.diagnostics == []
|
||||
|
||||
|
||||
def test_validate_contract_reports_bad_regex(tmp_path: Path):
|
||||
contract_file = tmp_path / "bad.contract.md"
|
||||
contract_file.write_text(
|
||||
CONTRACT_TEXT.replace("\\\\b(choose|adopt|use|will)\\\\b", "[bad"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = validate_contract(load_contract_file(contract_file))
|
||||
|
||||
assert result.valid is False
|
||||
assert result.diagnostics[0].code == "contract.regex.invalid"
|
||||
|
||||
|
||||
def test_check_markdown_file_accepts_valid_document(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
document_file = tmp_path / "adr.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
document_file.write_text(VALID_ADR, encoding="utf-8")
|
||||
|
||||
result = check_markdown_file(document_file, contract_file)
|
||||
|
||||
assert result.valid is True
|
||||
assert result.diagnostics == []
|
||||
assert result.metrics["document"]["sections"] == 4
|
||||
|
||||
|
||||
def test_check_markdown_file_reports_practical_failures(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
document_file = tmp_path / "adr.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
document_file.write_text(INVALID_ADR, encoding="utf-8")
|
||||
|
||||
result = check_markdown_file(document_file, contract_file)
|
||||
codes = {diagnostic.code for diagnostic in result.diagnostics}
|
||||
|
||||
assert result.valid is False
|
||||
assert "contract.field.missing" in codes
|
||||
assert "contract.section.missing" in codes
|
||||
assert "contract.section.forbidden" in codes
|
||||
assert "contract.metric.too_low" in codes
|
||||
|
||||
|
||||
def test_check_markdown_file_keeps_warning_only_results_valid(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
document_file = tmp_path / "adr.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
document_file.write_text(
|
||||
VALID_ADR.replace("The problem is", "The situation is"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = check_markdown_file(document_file, contract_file)
|
||||
|
||||
assert result.valid is True
|
||||
assert [diagnostic.code for diagnostic in result.diagnostics] == [
|
||||
"contract.assertion.contains_any_missing"
|
||||
]
|
||||
assert result.diagnostics[0].severity == "warning"
|
||||
|
||||
|
||||
def test_collect_metrics_counts_document_and_sections():
|
||||
document = parse_markdown(VALID_ADR)
|
||||
|
||||
metrics = collect_metrics(document)
|
||||
|
||||
assert metrics.words > 20
|
||||
assert metrics.sections == 4
|
||||
context_metrics = next(
|
||||
section for section in metrics.section_metrics if section.heading == "Context"
|
||||
)
|
||||
assert context_metrics.words >= 10
|
||||
|
||||
|
||||
def test_mkt_contract_validate(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
|
||||
result = CliRunner().invoke(main, ["contract", "validate", str(contract_file)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "valid" in result.output
|
||||
|
||||
|
||||
def test_mkt_contract_check_reports_invalid_document(tmp_path: Path):
|
||||
contract_file = tmp_path / "adr.contract.md"
|
||||
document_file = tmp_path / "adr.md"
|
||||
contract_file.write_text(CONTRACT_TEXT, encoding="utf-8")
|
||||
document_file.write_text(INVALID_ADR, encoding="utf-8")
|
||||
|
||||
result = CliRunner().invoke(
|
||||
main, ["contract", "check", str(document_file), "--contract", str(contract_file)]
|
||||
)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "contract.section.missing" in result.output
|
||||
assert "guidance" in result.output
|
||||
|
||||
|
||||
def test_mkt_metrics_outputs_text(tmp_path: Path):
|
||||
document_file = tmp_path / "adr.md"
|
||||
document_file.write_text(VALID_ADR, encoding="utf-8")
|
||||
|
||||
result = CliRunner().invoke(main, ["metrics", str(document_file)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "document" in result.output
|
||||
assert "words" in result.output
|
||||
assert "Context" in result.output
|
||||
|
||||
|
||||
def test_example_contracts_validate():
|
||||
for _name, contract_path, _valid_path, _invalid_path, _expected in EXAMPLE_CASES:
|
||||
result = validate_contract(load_contract_file(contract_path))
|
||||
|
||||
assert result.valid is True
|
||||
|
||||
|
||||
def test_example_valid_documents_have_no_error_diagnostics():
|
||||
for name, contract_path, valid_path, _invalid_path, _expected in EXAMPLE_CASES:
|
||||
result = check_markdown_file(valid_path, contract_path)
|
||||
|
||||
assert result.valid is True, name
|
||||
assert all(diagnostic.severity != "error" for diagnostic in result.diagnostics)
|
||||
|
||||
|
||||
def test_example_invalid_documents_report_expected_diagnostics():
|
||||
for name, contract_path, _valid_path, invalid_path, expected in EXAMPLE_CASES:
|
||||
result = check_markdown_file(invalid_path, contract_path)
|
||||
codes = {diagnostic.code for diagnostic in result.diagnostics}
|
||||
|
||||
assert result.valid is False, name
|
||||
assert expected <= codes
|
||||
Reference in New Issue
Block a user