설계 개편
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from techviz.batch_audit import audit_batch, discover_specs
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class BatchAuditTests(unittest.TestCase):
|
||||
def test_runtime_profiles_do_not_collapse(self) -> None:
|
||||
paths = discover_specs(ROOT / "examples/runtime-profiles")
|
||||
audits, findings = audit_batch(paths)
|
||||
self.assertEqual(len(audits), 10)
|
||||
self.assertFalse(any(item.code == "batch-template-collapse" for item in findings), findings)
|
||||
|
||||
def test_repeated_template_is_rejected(self) -> None:
|
||||
source = json.loads(
|
||||
(ROOT / "examples/runtime-profiles/10-comparison/spec.json").read_text(encoding="utf-8")
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
for index in range(6):
|
||||
data = deepcopy(source)
|
||||
data["id"] = f"copy-{index}"
|
||||
folder = root / f"item-{index}"
|
||||
folder.mkdir()
|
||||
(folder / "spec.json").write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
audits, findings = audit_batch(discover_specs(root))
|
||||
self.assertEqual(len(audits), 6)
|
||||
self.assertTrue(any(item.code == "batch-template-collapse" and item.severity == "error" for item in findings), findings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+4
-1
@@ -30,7 +30,10 @@ class CliTests(unittest.TestCase):
|
||||
0,
|
||||
)
|
||||
self.assertEqual(main(["prompt", str(context), "-o", str(prompt)]), 0)
|
||||
self.assertIn("untrusted evidence data", prompt.read_text(encoding="utf-8"))
|
||||
prompt_text = prompt.read_text(encoding="utf-8")
|
||||
self.assertIn("untrusted evidence data", prompt_text)
|
||||
self.assertIn("Automatically selected reference cases", prompt_text)
|
||||
self.assertEqual(main(["references", str(context), "--limit", "2"]), 0)
|
||||
self.assertEqual(
|
||||
main([
|
||||
"lint",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from techviz.layout import build_layout
|
||||
from techviz.renderers.svg import render_svg
|
||||
from techviz.spec import VizSpec
|
||||
from techviz.validate import has_errors, validate_spec
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class CompositionTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.spec_data = json.loads((ROOT / "examples/work/payment/spec.json").read_text(encoding="utf-8"))
|
||||
self.context = json.loads((ROOT / "examples/work/payment/context.json").read_text(encoding="utf-8"))
|
||||
|
||||
def test_disconnected_cards_are_rejected(self) -> None:
|
||||
data = deepcopy(self.spec_data)
|
||||
data["nodes"] = data["nodes"][:3]
|
||||
data["edges"] = []
|
||||
data["composition"] = {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": True,
|
||||
"reference_ids": ["payment-event-flow"],
|
||||
"rationale": "Synthetic disconnected case.",
|
||||
}
|
||||
issues = validate_spec(VizSpec.from_dict(data), self.context)
|
||||
self.assertTrue(any(issue.code == "missing-central-relation" for issue in issues), issues)
|
||||
|
||||
def test_comparison_requires_comparable_details(self) -> None:
|
||||
data = deepcopy(self.spec_data)
|
||||
data["type"] = "concept"
|
||||
data["nodes"] = data["nodes"][:2]
|
||||
data["edges"] = []
|
||||
data["composition"] = {
|
||||
"profile": "comparison",
|
||||
"diagram_only": True,
|
||||
"reference_ids": ["contract-comparison"],
|
||||
"rationale": "Compare two explicit contracts.",
|
||||
}
|
||||
for node in data["nodes"]:
|
||||
node["role"] = "contract"
|
||||
node["details"] = ["responsibility", "lifecycle"]
|
||||
issues = validate_spec(VizSpec.from_dict(data))
|
||||
self.assertFalse(has_errors(issues), issues)
|
||||
|
||||
del data["nodes"][0]["details"]
|
||||
issues = validate_spec(VizSpec.from_dict(data))
|
||||
self.assertTrue(any(issue.code == "comparison-missing-details" for issue in issues), issues)
|
||||
|
||||
def test_svg_is_diagram_only(self) -> None:
|
||||
spec = VizSpec.from_dict(self.spec_data)
|
||||
svg = render_svg(spec, build_layout(spec))
|
||||
self.assertNotIn('class="diagram-title"', svg)
|
||||
self.assertNotIn('class="diagram-question"', svg)
|
||||
self.assertNotIn('class="footer"', svg)
|
||||
self.assertNotIn("Generated from grounded VizSpec", svg)
|
||||
self.assertNotIn("<filter", svg)
|
||||
self.assertNotIn("<linearGradient", svg)
|
||||
self.assertIn("<title id=", svg)
|
||||
self.assertIn("<desc id=", svg)
|
||||
|
||||
def test_reference_profile_mismatch_is_rejected(self) -> None:
|
||||
data = deepcopy(self.spec_data)
|
||||
data["composition"]["profile"] = "ports-adapters"
|
||||
data["composition"]["reference_ids"] = ["payment-event-flow"]
|
||||
issues = validate_spec(VizSpec.from_dict(data), self.context)
|
||||
self.assertTrue(any(issue.code == "reference-profile-mismatch" for issue in issues), issues)
|
||||
|
||||
def test_context_rejects_unselected_profile(self) -> None:
|
||||
data = deepcopy(self.spec_data)
|
||||
data["composition"]["profile"] = "ports-adapters"
|
||||
data["composition"]["reference_ids"] = ["order-ports-adapters"]
|
||||
issues = validate_spec(VizSpec.from_dict(data), self.context)
|
||||
self.assertTrue(any(issue.code == "profile-not-supported-by-context" for issue in issues), issues)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+16
-2
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
|
||||
from techviz.document import build_context
|
||||
from techviz.prompt import build_agent_prompt
|
||||
from techviz.reference_catalog import select_reference_cases
|
||||
from techviz.spec import validate_raw_spec
|
||||
|
||||
|
||||
@@ -19,12 +20,15 @@ class PromptTests(unittest.TestCase):
|
||||
marker_id="payment-request",
|
||||
)
|
||||
prompt = build_agent_prompt(context)
|
||||
section_start = prompt.index("## VizSpec 1.0 shape")
|
||||
section_start = prompt.index("## VizSpec 1.1 shape")
|
||||
json_start = prompt.index("{\n", section_start)
|
||||
json_end = prompt.index("\n\nFor a sequence diagram", json_start)
|
||||
json_end = prompt.index("\n\n## Final self-check", json_start)
|
||||
scaffold = json.loads(prompt[json_start:json_end])
|
||||
|
||||
validate_raw_spec(scaffold)
|
||||
self.assertEqual(scaffold["version"], "1.1")
|
||||
self.assertTrue(scaffold["composition"]["diagram_only"])
|
||||
self.assertTrue(scaffold["composition"]["reference_ids"])
|
||||
self.assertEqual(
|
||||
scaffold["source_context"],
|
||||
{
|
||||
@@ -35,6 +39,16 @@ class PromptTests(unittest.TestCase):
|
||||
)
|
||||
self.assertFalse(scaffold["groups"])
|
||||
self.assertFalse(scaffold["legend"])
|
||||
self.assertIn("disconnected rounded cards", prompt)
|
||||
self.assertIn("diagram-only", prompt)
|
||||
|
||||
def test_reference_selection_is_context_sensitive(self) -> None:
|
||||
context = {
|
||||
"numbered_context": "1 | Port와 Adapter를 통해 hexagonal core에 의존한다.",
|
||||
"current_section": {"text": "Inbound adapter와 outbound port를 분리한다."},
|
||||
}
|
||||
selected = select_reference_cases(context, limit=2)
|
||||
self.assertEqual(selected[0].profile, "ports-adapters")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from techviz.layout import build_layout
|
||||
from techviz.renderers.svg import render_svg
|
||||
from techviz.spec import load_spec
|
||||
from techviz.validate import has_errors, validate_spec
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class RuntimeProfileTests(unittest.TestCase):
|
||||
def test_every_profile_fixture_is_executable(self) -> None:
|
||||
paths = sorted((ROOT / "examples/runtime-profiles").glob("*/spec.json"))
|
||||
self.assertEqual(len(paths), 10)
|
||||
seen_profiles: set[str] = set()
|
||||
for path in paths:
|
||||
with self.subTest(path=path):
|
||||
spec = load_spec(path)
|
||||
issues = validate_spec(spec)
|
||||
self.assertFalse(has_errors(issues), issues)
|
||||
svg = render_svg(spec, build_layout(spec))
|
||||
ET.fromstring(svg)
|
||||
self.assertNotIn("Generated from grounded VizSpec", svg)
|
||||
self.assertNotIn('class="diagram-title"', svg)
|
||||
seen_profiles.add(spec.profile)
|
||||
self.assertEqual(len(seen_profiles), 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user