feat(templates): enable all assets for local trial

This commit is contained in:
2026-10-06 11:42:45 +08:00
parent afe39f92de
commit d405c1ba65
7 changed files with 116 additions and 19 deletions
@@ -793,7 +793,8 @@ def _load_templates() -> list[ContractTemplateItem]:
item = ContractTemplateItem(
id=scene["id"], name=scene["name"], version=scene["metadata"]["version"],
contract_type=scene["contract_type"], body_preview=_preview_text(scene["body"]),
description="新增场景草稿 · " + scene["metadata"]["applicable"] + ";不适用:" + "、".join(scene["metadata"]["excluded"]),
description=("新增场景开发试用 · " if scene["id"] in trial_ids else "新增场景草稿 · ")
+ scene["metadata"]["applicable"] + ";不适用:" + "、".join(scene["metadata"]["excluded"]),
required_variables=[ContractTemplateVariable(name=name, label=name) for name in scene["required"]],
optional_variables=[ContractTemplateVariable(name=name, label=name, required=False) for name in scene["optional"]],
input_schema=scene["schema"], validation_rules=scene["metadata"]["review_scope_checklist"],
@@ -801,9 +802,15 @@ def _load_templates() -> list[ContractTemplateItem]:
# presented as an equipment-rental or logistics-specific check.
clause_ids=["CLAUSE-COMMON-AUTHORIZATION-001", "CLAUSE-COMMON-FORMAT-001"],
review_rule_ids=["RULE-AUTHORIZATION-SUBJECT-001", "RULE-FORMAT-CLAUSE-001"],
lifecycle_status="draft", source="scene-pack-draft-v1",
# The allowlist grants local/test drafting only; it is not a
# publication event and does not satisfy the release gate.
lifecycle_status="internal_trial" if scene["id"] in trial_ids else "draft",
source="scene-pack-draft-v1",
)
templates.append(item.model_copy(update={"content_sha256": template_content_sha256(item, scene["body"])}))
unknown_trial_ids = set(trial_ids) - {item.id for item in templates}
if unknown_trial_ids:
raise ValueError("Internal-trial template catalog references unknown assets.")
return sorted(templates, key=lambda item: (item.contract_type, item.name))
@@ -1,7 +1,7 @@
{
"schema_version": "template-source-catalog.v1",
"recorded_on": "2026-08-28",
"scope": "Existing builtin assets for local/test evaluation only. This catalog is not a publication, legal approval or source verification. Unlisted assets default to draft.",
"recorded_on": "2026-10-06",
"scope": "All 60 builtin assets are explicitly allowlisted for local/test development trial only. This catalog is not a publication, legal approval or source verification. Unlisted assets default to draft.",
"internal_trial_template_ids": [
"TPL-AGENCY-DISTRIBUTION-001", "TPL-COMMISSION-SERVICE-001", "TPL-COOPERATION-FRAMEWORK-001",
"TPL-DATA-PROCESSING-001", "TPL-DEMAND-LETTER-001", "TPL-EQUITY-TRANSFER-001", "TPL-FINANCIAL-LEASE-001",
@@ -10,6 +10,36 @@
"TPL-LEASE-HOUSE-001", "TPL-LEGAL-OPINION-001", "TPL-NDA-COMMERCIAL-001", "TPL-PARTNERSHIP-001",
"TPL-POWER-OF-ATTORNEY-001", "TPL-PROPERTY-SERVICE-001", "TPL-PURCHASE-GOODS-001", "TPL-SALES-GOODS-001",
"TPL-SERVICE-GENERAL-001", "TPL-SHAREHOLDER-RESOLUTION-001", "TPL-SOFTWARE-SAAS-001", "TPL-TECH-DEVELOPMENT-001",
"TPL-CONTRACT-TERMINATION-NOTICE-001", "TPL-VALUATION-ADJUSTMENT-001", "TPL-WORKS-001"
"TPL-CONTRACT-TERMINATION-NOTICE-001", "TPL-VALUATION-ADJUSTMENT-001", "TPL-WORKS-001",
"TPL-TRANSPORT-ORDINARY-001",
"TPL-WAREHOUSING-ORDINARY-001",
"TPL-LEASE-EQUIPMENT-001",
"TPL-SERVICE-EQUIPMENT-MAINTENANCE-001",
"TPL-WORKS-MANUFACTURING-001",
"TPL-SERVICE-ADVERTISEMENT-001",
"TPL-COMMISSION-MARKETING-001",
"TPL-SERVICE-BRAND-DESIGN-001",
"TPL-SERVICE-SOFTWARE-OPS-001",
"TPL-LEASE-RETAIL-SHOP-001",
"TPL-LABOR-AMENDMENT-001",
"TPL-AMENDMENT-GENERAL-001",
"TPL-TRANSPORT-COLD-CHAIN-001",
"TPL-WAREHOUSING-ECOMMERCE-FULFILLMENT-001",
"TPL-COMMISSION-ECOMMERCE-OPERATIONS-001",
"TPL-COOPERATION-LIVESTREAM-MARKETING-001",
"TPL-FRANCHISE-RESTAURANT-CHAIN-001",
"TPL-SERVICE-PRODUCT-TESTING-001",
"TPL-TECHNICAL-CONSULTING-001",
"TPL-TECHNOLOGY-TRANSFER-001",
"TPL-DATA-SUPPLY-001",
"TPL-DATA-ANNOTATION-PROCESSING-001",
"TPL-SERVICE-CONFERENCE-EVENT-001",
"TPL-SERVICE-ENTERPRISE-TRAINING-001",
"TPL-LABOR-HR-OUTSOURCING-001",
"TPL-PROPERTY-COMMERCIAL-MANAGEMENT-001",
"TPL-CONSTRUCTION-OFFICE-RENOVATION-001",
"TPL-CONSTRUCTION-PROJECT-001",
"TPL-INTERCOMPANY-SHAREHOLDER-LOAN-001",
"TPL-RECEIVABLES-ASSIGNMENT-001"
]
}
@@ -52,11 +52,11 @@ def test_every_exclusion_routes_away_from_generic_scene(template):
compile_template_body(template, get_contract_template_body(template.id), values, allow_incomplete=True)
def test_scene_catalog_is_draft_only_and_cannot_be_published_by_stripping_metadata():
def test_scene_catalog_is_development_trial_only_and_cannot_be_published_by_stripping_metadata():
assert len(SCENES) == 30 and len(CASES) == 300
repo = InMemoryContractRepository(contracts=[])
for template in SCENES:
assert template.lifecycle_status == "draft" and not repo.template_drafting_allowed(template)
assert template.lifecycle_status == "internal_trial" and not repo.template_drafting_allowed(template)
assert template.input_schema.scene["origin"] == "platform_authored_draft"
assert template.input_schema.scene["legal_approval"] is False
assert template.input_schema.scene["source_references"]
@@ -74,6 +74,8 @@ def test_scene_catalog_is_draft_only_and_cannot_be_published_by_stripping_metada
with pytest.raises(ContractWorkflowError):
repo.publish_template(template.id, ContractTemplateLifecycleRequest(version="unreviewed"))
assert repo.list_template_lifecycle_events(template.id).total == 0
repo.allow_internal_trial_templates = True
assert all(repo.template_drafting_allowed(template) for template in SCENES)
target = SCENES[0]
schema = target.input_schema.model_copy(update={"scene": None})
repo.save_template_edit_draft(target.id, ContractTemplateEditRequest(input_schema=schema))
@@ -118,7 +120,7 @@ def test_corrected_examples_are_versioned_and_complete_on_observed_gaps():
else "tp05-2-20260828-1")
)
assert template.input_schema.scene["version"] == expected
assert template.lifecycle_status == "draft"
assert template.lifecycle_status == "internal_trial"
equipment_values = equipment.input_schema.sample_values
assert "各5000元" in equipment_values["付款与税费"]
assert "TEST-OWN-001" in equipment_values["设备清单"]
@@ -139,7 +141,7 @@ def test_second_batch_has_independent_boundaries_sources_fixtures_and_review_rou
}
for template in BATCH_B:
scene = template.input_schema.scene
assert template.lifecycle_status == "draft"
assert template.lifecycle_status == "internal_trial"
assert len(template.input_schema.fixtures) == 10
assert len(scene["excluded"]) >= 3
assert len(scene["review_scope_checklist"]) >= 5
@@ -945,7 +945,8 @@ def test_v1_template_assets_have_complete_clause_and_rule_bindings(tmp_path: Pat
assert templates_response.json()["total"] == 60
assert len(templates) == 60
assert sum(item["lifecycle_status"] == "draft" for item in templates) == 30
assert sum(item["lifecycle_status"] == "internal_trial" for item in templates) == 60
assert all(item["drafting_available"] is True for item in templates)
orphan_clause_ids: dict[str, list[str]] = {}
orphan_rule_ids: dict[str, list[str]] = {}
@@ -54,7 +54,7 @@ def test_marketing_contact_data_has_bounded_use_access_retention_and_incident_ha
pipl = next(item for item in references if "个人信息保护法" in item["title"])
assert pipl["current_effectiveness_verified"] is False
assert pipl["accessed_on"] == "2026-09-10"
assert template.lifecycle_status == "draft"
assert template.lifecycle_status == "internal_trial"
def test_maintenance_fixed_closeout_duties_survive_non_sample_commercial_terms():
@@ -43,7 +43,7 @@ def test_batch_b_key_obligation_is_required_and_rendered(template_id, field, che
rendered = compile_template_body(template, get_contract_template_body(template_id), values)
assert values[field] in rendered.body_content
assert all(marker in rendered.body_content for marker in markers)
assert template.lifecycle_status == "draft"
assert template.lifecycle_status == "internal_trial"
assert template.input_schema.scene["legal_approval"] is False
@@ -18,8 +18,8 @@ from yuqei_contract_api.tenant_context import set_tenant_context, clear_tenant_c
SCOPE = {"tenant": "release-test", "owner": "release-owner"}
def request_for(repository, *, incomplete=False):
template = repository.get_template("TPL-SERVICE-GENERAL-001")
def request_for(repository, *, incomplete=False, template_id="TPL-SERVICE-GENERAL-001"):
template = repository.get_template(template_id)
values = {} if incomplete else deepcopy(template_sample_values(template.input_schema,
[item.name for item in template.required_variables], [item.name for item in template.optional_variables], with_optional=False))
return DraftingCreateRequest(template_id=template.id, title="合成发布隔离验收", values=values,
@@ -44,7 +44,8 @@ def test_builtin_catalog_is_not_publication_and_unknown_source_defaults_to_draft
all_templates = assets.list_contract_templates().items
baseline = [item for item in all_templates if item.source != "scene-pack-draft-v1"]
scenes = [item for item in all_templates if item.source == "scene-pack-draft-v1"]
assert len(scenes) == 30 and all(item.lifecycle_status == "draft" for item in scenes)
assert len(scenes) == 30
assert all(item.lifecycle_status == "internal_trial" and item.published_at is None for item in scenes)
assert len(baseline) == 30
assert all(item.lifecycle_status == "internal_trial" and item.published_at is None for item in baseline)
assert assets.ContractTemplateItem(id="new", name="new", version="dev", contract_type="test", body_preview="").lifecycle_status == "draft"
@@ -62,24 +63,80 @@ def test_builtin_catalog_is_not_publication_and_unknown_source_defaults_to_draft
assets._load_templates.cache_clear()
@pytest.mark.parametrize("template_id", ["TPL-SERVICE-GENERAL-001", "TPL-TRANSPORT-ORDINARY-001"])
@pytest.mark.parametrize("environment,available", [("local", True), ("test", True), ("staging", False), ("production", False)])
def test_http_trial_capability_is_environment_bound(environment, available):
def test_http_trial_capability_is_environment_bound(environment, available, template_id):
repo = InMemoryContractRepository(contracts=[])
client = TestClient(create_app(ContractApiSettings(environment=environment), repo))
response = client.get("/api/v1/templates/TPL-SERVICE-GENERAL-001")
response = client.get(f"/api/v1/templates/{template_id}")
assert response.status_code == 200, response.text
item = response.json()
assert item["lifecycle_status"] == "internal_trial"
assert item["drafting_available"] is available
inputs = request_for(repo, incomplete=True).model_dump()
inputs = request_for(repo, incomplete=True, template_id=template_id).model_dump()
created = client.post("/api/v1/drafting-workspaces", json=inputs,
headers={"X-Tenant-Id": "release-test", "X-User-Id": "release-owner", "Idempotency-Key": "release-boundary"})
assert created.status_code == (201 if available else 409), created.text
# Old direct draft and input-validation endpoints enforce the same policy.
validation = client.post("/api/v1/templates/TPL-SERVICE-GENERAL-001/validate-input", json={"values": {}, "partial": True})
validation = client.post(f"/api/v1/templates/{template_id}/validate-input", json={"values": {}, "partial": True})
assert validation.status_code == (200 if available else 409), validation.text
def test_local_http_exposes_all_60_templates_as_draftable_without_publishing():
repo = InMemoryContractRepository(contracts=[])
client = TestClient(create_app(ContractApiSettings(environment="local"), repo))
response = client.get("/api/v1/templates/tree")
assert response.status_code == 200, response.text
items = [template for node in response.json()["types"] for template in node["templates"]]
assert len(items) == 60
assert all(item["lifecycle_status"] == "internal_trial" for item in items)
assert all(item["drafting_available"] is True for item in items)
assert all(item["published_at"] is None and item["lifecycle_event_count"] == 0 for item in items)
@pytest.mark.parametrize("environment", ["staging", "production"])
def test_non_local_http_keeps_all_60_unpublished_trials_fail_closed(environment):
repo = InMemoryContractRepository(contracts=[])
client = TestClient(create_app(ContractApiSettings(environment=environment), repo))
response = client.get("/api/v1/templates/tree")
assert response.status_code == 200, response.text
items = [template for node in response.json()["types"] for template in node["templates"]]
assert len(items) == 60
assert all(item["lifecycle_status"] == "internal_trial" for item in items)
assert all(item["drafting_available"] is False for item in items)
assert all(item["published_at"] is None and item["lifecycle_event_count"] == 0 for item in items)
def test_all_60_internal_trials_create_valid_local_drafting_workspaces():
repo = InMemoryContractRepository(contracts=[])
repo.allow_internal_trial_templates = True
store = DraftingWorkspaceStore(repo)
templates = repo.list_templates().items
assert len(templates) == 60
for template in templates:
values = template_sample_values(
template.input_schema,
[item.name for item in template.required_variables],
[item.name for item in template.optional_variables],
with_optional=False,
)
record = store.create(
DraftingCreateRequest(
template_id=template.id,
title=f"{template.name}开发试用起草",
values=values,
expected_template_content_sha256=template.content_sha256,
),
tenant="release-test",
owner="release-owner",
key=f"all-templates-{template.id}",
)
assert record.template.id == template.id
assert record.input_validation.valid, (template.id, record.input_validation.issues)
assert record.base_document.strip(), template.id
assert record.preview_error is None, (template.id, record.preview_error)
def test_no_implicit_published_history_or_rollback_escape(store):
template_id = "TPL-SERVICE-GENERAL-001"
assert store.repository.get_template(template_id).lifecycle_status == "internal_trial"