@@ -538,3 +538,135 @@ def test_discover_via_config(tmp_path, monkeypatch):
538538 importlib .reload (ctx )
539539 result = ctx .discover_project_path ()
540540 assert result == project_dir
541+
542+
543+ # ── Semantic validation tests (view dry-plan + description checks) ─────────
544+
545+ import base64
546+ import json as _json
547+
548+ import orjson
549+ import pytest
550+
551+ from wren .context import validate_manifest
552+ from wren .model .data_source import DataSource
553+
554+
555+ def _b64 (manifest : dict ) -> str :
556+ return base64 .b64encode (orjson .dumps (manifest )).decode ()
557+
558+
559+ _SEM_MODEL_WITH_DESC = {
560+ "name" : "orders" ,
561+ "tableReference" : {"schema" : "main" , "table" : "orders" },
562+ "columns" : [
563+ {"name" : "o_orderkey" , "type" : "integer" },
564+ {"name" : "o_custkey" , "type" : "integer" },
565+ ],
566+ "primaryKey" : "o_orderkey" ,
567+ "properties" : {"description" : "Orders model" },
568+ }
569+
570+ _SEM_MODEL_WITHOUT_DESC = {
571+ "name" : "accounts" ,
572+ "tableReference" : {"schema" : "main" , "table" : "accounts" },
573+ "columns" : [
574+ {"name" : "acct_id" , "type" : "integer" },
575+ {"name" : "plan_cd" , "type" : "varchar" },
576+ ],
577+ "primaryKey" : "acct_id" ,
578+ }
579+
580+ _VALID_VIEW = {
581+ "name" : "valid_view" ,
582+ "statement" : 'SELECT o_orderkey FROM "orders"' ,
583+ "properties" : {"description" : "A valid view" },
584+ }
585+
586+ _VIEW_WITHOUT_DESC = {
587+ "name" : "daily_usage" ,
588+ "statement" : 'SELECT o_orderkey FROM "orders"' ,
589+ }
590+
591+ _BROKEN_VIEW = {
592+ "name" : "stale_report" ,
593+ "statement" : 'SELECT * FROM "deleted_model"' ,
594+ }
595+
596+ _EMPTY_STMT_VIEW = {
597+ "name" : "empty_view" ,
598+ "statement" : "" ,
599+ }
600+
601+ _SEM_BASE_MANIFEST = {
602+ "catalog" : "wren" ,
603+ "schema" : "public" ,
604+ "models" : [_SEM_MODEL_WITH_DESC ],
605+ }
606+
607+
608+ @pytest .mark .unit
609+ def test_validate_manifest_view_pass ():
610+ manifest = {** _SEM_BASE_MANIFEST , "views" : [_VALID_VIEW ]}
611+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb )
612+ assert result ["errors" ] == []
613+
614+
615+ @pytest .mark .unit
616+ def test_validate_manifest_view_dry_plan_error ():
617+ manifest = {** _SEM_BASE_MANIFEST , "views" : [_BROKEN_VIEW ]}
618+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb )
619+ assert len (result ["errors" ]) == 1
620+ assert "stale_report" in result ["errors" ][0 ]
621+
622+
623+ @pytest .mark .unit
624+ def test_validate_manifest_empty_statement ():
625+ manifest = {** _SEM_BASE_MANIFEST , "views" : [_EMPTY_STMT_VIEW ]}
626+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb )
627+ assert any ("empty statement" in e for e in result ["errors" ])
628+
629+
630+ @pytest .mark .unit
631+ def test_validate_manifest_model_no_description ():
632+ manifest = {"catalog" : "wren" , "schema" : "public" , "models" : [_SEM_MODEL_WITHOUT_DESC ]}
633+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb )
634+ assert result ["errors" ] == []
635+ assert any ("accounts" in w for w in result ["warnings" ])
636+
637+
638+ @pytest .mark .unit
639+ def test_validate_manifest_view_no_description ():
640+ manifest = {** _SEM_BASE_MANIFEST , "views" : [_VIEW_WITHOUT_DESC ]}
641+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb )
642+ assert result ["errors" ] == []
643+ assert any ("daily_usage" in w for w in result ["warnings" ])
644+
645+
646+ @pytest .mark .unit
647+ def test_validate_manifest_level_error_suppresses_warnings ():
648+ manifest = {"catalog" : "wren" , "schema" : "public" , "models" : [_SEM_MODEL_WITHOUT_DESC ]}
649+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb , level = "error" )
650+ assert result ["warnings" ] == []
651+
652+
653+ @pytest .mark .unit
654+ def test_validate_manifest_strict_column_warnings ():
655+ manifest = {"catalog" : "wren" , "schema" : "public" , "models" : [_SEM_MODEL_WITHOUT_DESC ]}
656+ result = validate_manifest (_b64 (manifest ), DataSource .duckdb , level = "strict" )
657+ text = " " .join (result ["warnings" ])
658+ assert "plan_cd" in text
659+ assert "acct_id" in text
660+
661+
662+ @pytest .mark .unit
663+ def test_validate_manifest_invalid_level ():
664+ result = validate_manifest (_b64 (_SEM_BASE_MANIFEST ), DataSource .duckdb , level = "nope" )
665+ assert any ("nope" in e for e in result ["errors" ])
666+
667+
668+ @pytest .mark .unit
669+ def test_validate_manifest_invalid_datasource ():
670+ manifest = {** _SEM_BASE_MANIFEST , "views" : [_VALID_VIEW ]}
671+ result = validate_manifest (_b64 (manifest ), "not-a-datasource" )
672+ assert len (result ["errors" ]) == 1
0 commit comments