merge: T006 schema tests + fixtures

This commit is contained in:
m2 (AI Agent) 2026-07-02 02:30:19 +02:00
commit 60b7cf760e
8 changed files with 333 additions and 0 deletions

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"
}

View file

@ -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
}
}

View file

@ -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]