feat(templates): enable all assets for local trial
This commit is contained in:
+9
-2
@@ -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))
|
||||
|
||||
|
||||
|
||||
+33
-3
@@ -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]] = {}
|
||||
|
||||
+1
-1
@@ -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():
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
+64
-7
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user