generated from coulomb/repo-seed
337 lines
9.4 KiB
Python
337 lines
9.4 KiB
Python
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
|