diff --git a/schemas/tests/fixtures/listing_bad_status.json b/schemas/tests/fixtures/listing_bad_status.json new file mode 100644 index 0000000..fee8e4d --- /dev/null +++ b/schemas/tests/fixtures/listing_bad_status.json @@ -0,0 +1,19 @@ +{ + "schema_version": "m2.listing.v1", + "listing_id": "lst_mm-pdf-report", + "solution_id": "sol_mm-pdf-report", + "solution_version": "1.0.0", + "inventory_type": "solution", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "category": "reporting", + "price": { + "amount": 500, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "tenant_visibility": ["*"], + "status": "archived", + "install_ref": "lst_mm-pdf-report-v1.0.0" +} diff --git a/schemas/tests/fixtures/listing_missing_tenant_visibility.json b/schemas/tests/fixtures/listing_missing_tenant_visibility.json new file mode 100644 index 0000000..43c6330 --- /dev/null +++ b/schemas/tests/fixtures/listing_missing_tenant_visibility.json @@ -0,0 +1,18 @@ +{ + "schema_version": "m2.listing.v1", + "listing_id": "lst_mm-pdf-report", + "solution_id": "sol_mm-pdf-report", + "solution_version": "1.0.0", + "inventory_type": "solution", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "category": "reporting", + "price": { + "amount": 500, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "status": "published", + "install_ref": "lst_mm-pdf-report-v1.0.0" +} diff --git a/schemas/tests/fixtures/solution_bad_price.json b/schemas/tests/fixtures/solution_bad_price.json new file mode 100644 index 0000000..1d625f9 --- /dev/null +++ b/schemas/tests/fixtures/solution_bad_price.json @@ -0,0 +1,34 @@ +{ + "schema_version": "m2.solution.v1", + "solution_id": "sol_mm-pdf-report", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "intent": "Produce publish-ready branded PDF/slide deck output from Markdown content without manual design work", + "deployment": { + "recipe_ref": "recipes/mm-pdf-report/recipe.yaml", + "entrypoint": "skills/mm-pdf/render.py", + "verify_command": "python skills/mm-pdf/render.py --self-test" + }, + "applicability": { + "image_classes": ["desktop-agent"], + "roles": ["operator", "analyst"], + "tool_requirements": ["weasyprint"] + }, + "tenant_scope": "m2-core", + "evidence": [ + { + "source": "session:2026-05-11-mm-pdf-branded-report" + } + ], + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "price": { + "amount": 0, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "license": { + "terms": "Single-operator install license; covers all machines under the buying operator_id.", + "major_version_coverage": true + } +} diff --git a/schemas/tests/fixtures/solution_missing_evidence.json b/schemas/tests/fixtures/solution_missing_evidence.json new file mode 100644 index 0000000..4f3d66f --- /dev/null +++ b/schemas/tests/fixtures/solution_missing_evidence.json @@ -0,0 +1,30 @@ +{ + "schema_version": "m2.solution.v1", + "solution_id": "sol_mm-pdf-report", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "intent": "Produce publish-ready branded PDF/slide deck output from Markdown content without manual design work", + "deployment": { + "recipe_ref": "recipes/mm-pdf-report/recipe.yaml", + "entrypoint": "skills/mm-pdf/render.py", + "verify_command": "python skills/mm-pdf/render.py --self-test" + }, + "applicability": { + "image_classes": ["desktop-agent"], + "roles": ["operator", "analyst"], + "tool_requirements": ["weasyprint"] + }, + "tenant_scope": "m2-core", + "evidence": [], + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "price": { + "amount": 500, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "license": { + "terms": "Single-operator install license; covers all machines under the buying operator_id.", + "major_version_coverage": true + } +} diff --git a/schemas/tests/fixtures/solution_tenant_violation.json b/schemas/tests/fixtures/solution_tenant_violation.json new file mode 100644 index 0000000..415554d --- /dev/null +++ b/schemas/tests/fixtures/solution_tenant_violation.json @@ -0,0 +1,34 @@ +{ + "schema_version": "m2.solution.v1", + "solution_id": "sol_mm-pdf-report", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "intent": "Produce publish-ready branded PDF/slide deck output from Markdown content without manual design work", + "deployment": { + "recipe_ref": "recipes/mm-pdf-report/recipe.yaml", + "entrypoint": "skills/mm-pdf/render.py", + "verify_command": "python skills/mm-pdf/render.py --self-test" + }, + "applicability": { + "image_classes": ["desktop-agent"], + "roles": ["operator", "analyst"], + "tool_requirements": ["weasyprint"] + }, + "tenant_scope": "sdjs", + "evidence": [ + { + "source": "session:2026-05-11-mm-pdf-branded-report" + } + ], + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "price": { + "amount": 500, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "license": { + "terms": "Single-operator install license; covers all machines under the buying operator_id.", + "major_version_coverage": true + } +} diff --git a/schemas/tests/fixtures/valid_listing.json b/schemas/tests/fixtures/valid_listing.json new file mode 100644 index 0000000..1922157 --- /dev/null +++ b/schemas/tests/fixtures/valid_listing.json @@ -0,0 +1,27 @@ +{ + "schema_version": "m2.listing.v1", + "listing_id": "lst_mm-pdf-report", + "solution_id": "sol_mm-pdf-report", + "solution_version": "1.0.0", + "inventory_type": "solution", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "category": "reporting", + "keywords": ["pdf", "report", "branded", "slides"], + "icon": "https://git.machinemachine.ai/m2/market-registry/assets/mm-pdf-report/icon.png", + "price": { + "amount": 500, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "evidence_summary": "Generated branded PDF report for SDJS quarterly summary; operator confirmed styling matched brand deck.", + "tenant_visibility": ["*"], + "stats": { + "installs": 0, + "proposals_shown": 0, + "proposals_accepted": 0 + }, + "status": "published", + "install_ref": "lst_mm-pdf-report-v1.0.0" +} diff --git a/schemas/tests/fixtures/valid_solution.json b/schemas/tests/fixtures/valid_solution.json new file mode 100644 index 0000000..7442ad6 --- /dev/null +++ b/schemas/tests/fixtures/valid_solution.json @@ -0,0 +1,69 @@ +{ + "schema_version": "m2.solution.v1", + "solution_id": "sol_mm-pdf-report", + "name": "MM PDF Branded Report", + "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", + "description": "Renders styled dark-navy/cyan PDF reports and Marp slide decks from Markdown source, using WeasyPrint or Marp depending on target.", + "intent": "Produce publish-ready branded PDF/slide deck output from Markdown content without manual design work", + "behavior": { + "skill_ref": "skills/mm-pdf/SKILL.md" + }, + "tools": [ + { + "name": "weasyprint", + "type": "cli" + }, + { + "name": "marp", + "type": "container", + "container": "nasr-m2o" + } + ], + "runtime": { + "surfaces": ["desktop", "hermes"] + }, + "memory_schema": { + "partitions": ["mm-pdf:renders"] + }, + "permissions": [ + { + "scope": "filesystem:write", + "path": "/tmp/mm-pdf-output" + } + ], + "deployment": { + "recipe_ref": "recipes/mm-pdf-report/recipe.yaml", + "entrypoint": "skills/mm-pdf/render.py", + "verify_command": "python skills/mm-pdf/render.py --self-test" + }, + "applicability": { + "image_classes": ["desktop-agent"], + "roles": ["operator", "analyst"], + "tool_requirements": ["weasyprint"] + }, + "tenant_scope": "m2-core", + "evidence": [ + { + "source": "session:2026-05-11-mm-pdf-branded-report", + "excerpt": "Generated branded PDF report for SDJS quarterly summary; operator confirmed styling matched brand deck.", + "machine": "nasr-m2o", + "session": "sess_8f21c", + "tokens": 4200, + "wall_time": 38.4 + } + ], + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "price": { + "amount": 500, + "currency": "m2cr", + "model": "fixed" + }, + "seller": "sdjs-operator", + "license": { + "terms": "Single-operator install license; covers all machines under the buying operator_id.", + "major_version_coverage": true + }, + "revenue_split": { + "platform_pct": 10 + } +} diff --git a/schemas/tests/test_schemas.py b/schemas/tests/test_schemas.py new file mode 100644 index 0000000..aae8dc8 --- /dev/null +++ b/schemas/tests/test_schemas.py @@ -0,0 +1,102 @@ +"""Schema validation tests for schemas/solution.schema.json and schemas/listing.schema.json. + +Frozen v1 schemas (specs/001-market-first-wedge/data-model.md) — do not modify. +""" +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator + +SCHEMAS_DIR = Path(__file__).resolve().parent.parent +FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" + + +def load(path): + with open(path) as f: + return json.load(f) + + +SOLUTION_SCHEMA = load(SCHEMAS_DIR / "solution.schema.json") +LISTING_SCHEMA = load(SCHEMAS_DIR / "listing.schema.json") + +solution_validator = Draft202012Validator(SOLUTION_SCHEMA) +listing_validator = Draft202012Validator(LISTING_SCHEMA) + + +def fixture(name): + return load(FIXTURES_DIR / name) + + +def errors(validator, instance): + return list(validator.iter_errors(instance)) + + +# --- valid fixtures --------------------------------------------------------- + +def test_valid_solution_passes(): + errs = errors(solution_validator, fixture("valid_solution.json")) + assert errs == [], [e.message for e in errs] + + +def test_valid_listing_passes(): + errs = errors(listing_validator, fixture("valid_listing.json")) + assert errs == [], [e.message for e in errs] + + +# --- invalid solution fixtures ---------------------------------------------- + +def test_solution_missing_evidence_fails_on_evidence_minitems(): + errs = errors(solution_validator, fixture("solution_missing_evidence.json")) + assert errs, "expected validation errors" + assert any( + e.validator == "minItems" and list(e.path) == ["evidence"] + for e in errs + ), [(e.validator, list(e.path), e.message) for e in errs] + + +def test_solution_tenant_violation_fails_on_else_branch(): + errs = errors(solution_validator, fixture("solution_tenant_violation.json")) + assert errs, "expected validation errors" + # tenant_scope != "m2-core" routes through the top-level if/else; the + # else branch requires owner_initiated + scrub_status, both absent here. + # jsonschema surfaces these as top-level "required" errors (path == []). + missing_required_props = { + prop + for e in errs + if e.validator == "required" and list(e.path) == [] + for prop in ("owner_initiated", "scrub_status") + if f"'{prop}'" in e.message + } + assert missing_required_props == {"owner_initiated", "scrub_status"}, [ + (e.validator, list(e.path), e.message) for e in errs + ] + + +def test_solution_bad_price_fails_on_price_amount_minimum(): + errs = errors(solution_validator, fixture("solution_bad_price.json")) + assert errs, "expected validation errors" + assert any( + e.validator == "minimum" and list(e.path) == ["price", "amount"] + for e in errs + ), [(e.validator, list(e.path), e.message) for e in errs] + + +# --- invalid listing fixtures ------------------------------------------------ + +def test_listing_bad_status_fails_on_status_enum(): + errs = errors(listing_validator, fixture("listing_bad_status.json")) + assert errs, "expected validation errors" + assert any( + e.validator == "enum" and list(e.path) == ["status"] + for e in errs + ), [(e.validator, list(e.path), e.message) for e in errs] + + +def test_listing_missing_tenant_visibility_fails_on_required(): + errs = errors(listing_validator, fixture("listing_missing_tenant_visibility.json")) + assert errs, "expected validation errors" + assert any( + e.validator == "required" and "tenant_visibility" in e.message and list(e.path) == [] + for e in errs + ), [(e.validator, list(e.path), e.message) for e in errs]