220 lines
8.7 KiB
Python
220 lines
8.7 KiB
Python
import json
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
from lead_scoring.cli.contract_pack import (
|
|
API_MAJOR_VERSION,
|
|
CONTRACT_PACK_VERSION,
|
|
MARKER_CLOSE,
|
|
MARKER_OPEN,
|
|
canonical_json,
|
|
generate_contract_files,
|
|
run_contract_pack_cli,
|
|
validate_fixture_corpus,
|
|
)
|
|
from lead_scoring.cli.webhook_contract import generate_contract_files as generate_webhook_files
|
|
from lead_scoring.schemas.errors import ErrorTaxonomy
|
|
from lead_scoring.schemas.lineage import ScoringRunLineage
|
|
from lead_scoring.schemas.outcomes import OutcomeTaxonomy
|
|
from lead_scoring.schemas.review import LeadScoringReviewedEvent, ReviewResponse
|
|
from lead_scoring.schemas.scoring_config import ScoringConfig, ScoringConfigVersionSelection
|
|
from lead_scoring.schemas.scoring_run import (
|
|
CreateScoringRunRequest,
|
|
CreateScoringRunResponse,
|
|
ScoringRunResponse,
|
|
)
|
|
from lead_scoring.schemas.snapshot import CanonicalSnapshot
|
|
from lead_scoring.schemas.webhook import (
|
|
LeadScoringTerminalEvent,
|
|
WebhookIngestErrorResponse,
|
|
)
|
|
|
|
|
|
class LegacyTerminalEvent(BaseModel):
|
|
"""Consumer model tối giản mô phỏng reader cũ tương thích additive."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
event_id: UUID
|
|
event_type: Literal["lead_scoring.run.terminal"]
|
|
event_version: Literal["1.0.0"]
|
|
tenant_id: str
|
|
subject_ref: str
|
|
scoring_config_version: str
|
|
run_id: UUID
|
|
run_created_at: str
|
|
occurred_at: str
|
|
data: dict
|
|
|
|
|
|
def test_contract_pack_is_deterministic_and_versioned():
|
|
files = generate_contract_files()
|
|
|
|
assert files == generate_contract_files()
|
|
assert json.loads(files["contract.json"])["contract_pack_version"] == CONTRACT_PACK_VERSION
|
|
manifest = json.loads(files["contract.json"])
|
|
assert manifest["api_major_version"] == API_MAJOR_VERSION
|
|
assert manifest["release_status"] == "pre-release"
|
|
assert set(manifest["profiles"]) == {
|
|
"canonical_snapshot",
|
|
"scoring_config",
|
|
"admission",
|
|
"polling",
|
|
"review",
|
|
"lineage",
|
|
"webhook",
|
|
"errors",
|
|
}
|
|
assert all(profile["version"] == "1.0.0" for profile in manifest["profiles"].values())
|
|
assert "openapi.json" in files
|
|
assert "SHA256SUMS" in files
|
|
assert (
|
|
files["fixtures/tenant-unavailable.json"]
|
|
== generate_webhook_files()["fixtures/tenant-unavailable.json"]
|
|
)
|
|
|
|
|
|
def test_contract_schemas_and_fixture_corpus_use_pydantic_models():
|
|
files = generate_contract_files()
|
|
|
|
assert json.loads(files["schemas/canonical-snapshot.schema.json"]) == (
|
|
CanonicalSnapshot.model_json_schema()
|
|
)
|
|
assert (
|
|
json.loads(files["schemas/scoring-config.schema.json"]) == ScoringConfig.model_json_schema()
|
|
)
|
|
assert json.loads(files["schemas/scoring-config-selection.schema.json"]) == (
|
|
ScoringConfigVersionSelection.model_json_schema()
|
|
)
|
|
assert json.loads(files["schemas/admission-request.schema.json"]) == (
|
|
CreateScoringRunRequest.model_json_schema()
|
|
)
|
|
assert json.loads(files["schemas/admission-response.schema.json"]) == (
|
|
CreateScoringRunResponse.model_json_schema()
|
|
)
|
|
assert json.loads(files["schemas/polling-response.schema.json"]) == (
|
|
ScoringRunResponse.model_json_schema()
|
|
)
|
|
assert (
|
|
json.loads(files["schemas/review-response.schema.json"])
|
|
== ReviewResponse.model_json_schema()
|
|
)
|
|
assert json.loads(files["schemas/lineage.schema.json"]) == ScoringRunLineage.model_json_schema()
|
|
assert json.loads(files["schemas/terminal-event.schema.json"]) == (
|
|
LeadScoringTerminalEvent.model_json_schema()
|
|
)
|
|
assert json.loads(files["schemas/reviewed-event.schema.json"]) == (
|
|
LeadScoringReviewedEvent.model_json_schema()
|
|
)
|
|
ErrorTaxonomy.model_validate(json.loads(files["error-taxonomy.json"]))
|
|
OutcomeTaxonomy.model_validate(json.loads(files["outcome-taxonomy.json"]))
|
|
|
|
validate_fixture_corpus(files)
|
|
assert json.loads(files["fixtures/snapshot.json"])["version"] == "1.0.0"
|
|
assert json.loads(files["fixtures/admission-request.json"])["subject_ref"] == "ems-lead-1001"
|
|
ScoringConfigVersionSelection.model_validate(
|
|
json.loads(files["fixtures/scoring-config-selection.json"])
|
|
)
|
|
assert json.loads(files["fixtures/review-override-request.json"])["override_score"] == 90
|
|
WebhookIngestErrorResponse.model_validate(json.loads(files["fixtures/tenant-unavailable.json"]))
|
|
|
|
|
|
def test_marker_fixtures_cover_shared_merge_cases():
|
|
files = generate_contract_files()
|
|
names = {
|
|
"plain-description-append",
|
|
"empty-description-append",
|
|
"single-valid-block-replace",
|
|
"orphan-opening",
|
|
"orphan-closing",
|
|
"nested-block",
|
|
"overlapping-block",
|
|
"multiple-complete-blocks",
|
|
"unicode-multiline-preservation",
|
|
"deleted-block-followed-by-append",
|
|
"failed-run-no-render",
|
|
}
|
|
fixtures = {name: json.loads(files[f"fixtures/marker/{name}.json"]) for name in names}
|
|
|
|
assert MARKER_OPEN in fixtures["plain-description-append"]["expected_description"]
|
|
assert MARKER_CLOSE in fixtures["plain-description-append"]["expected_description"]
|
|
assert fixtures["empty-description-append"]["current_description"] == ""
|
|
assert fixtures["empty-description-append"]["expected_description"].startswith(MARKER_OPEN)
|
|
assert fixtures["unicode-multiline-preservation"]["writeback"] is True
|
|
assert fixtures["unicode-multiline-preservation"]["current_description"].startswith(
|
|
"Khách hàng: Nguyễn Ánh 😊"
|
|
)
|
|
|
|
malformed = [
|
|
fixtures[name]
|
|
for name in (
|
|
"orphan-opening",
|
|
"orphan-closing",
|
|
"nested-block",
|
|
"overlapping-block",
|
|
"multiple-complete-blocks",
|
|
)
|
|
]
|
|
assert all(item["expected_error"] == "marker_block_malformed" for item in malformed)
|
|
assert all(item["expected_description"] is None for item in malformed)
|
|
assert all(item["writeback"] is False for item in malformed)
|
|
assert fixtures["failed-run-no-render"]["marker_input"] is None
|
|
assert fixtures["failed-run-no-render"]["writeback"] is False
|
|
|
|
|
|
def test_fixture_index_declares_shared_provider_consumer_corpus():
|
|
files = generate_contract_files()
|
|
index = json.loads(files["fixture-index.json"])
|
|
|
|
assert index["provider_validation"] == "validate_fixture_corpus"
|
|
assert index["consumer_validation"]
|
|
assert all(item["role"] == "provider_consumer" for item in index["positive"])
|
|
assert {item["path"] for item in index["marker"]} == {
|
|
"fixtures/marker/deleted-block-followed-by-append.json",
|
|
"fixtures/marker/empty-description-append.json",
|
|
"fixtures/marker/failed-run-no-render.json",
|
|
"fixtures/marker/multiple-complete-blocks.json",
|
|
"fixtures/marker/nested-block.json",
|
|
"fixtures/marker/orphan-closing.json",
|
|
"fixtures/marker/orphan-opening.json",
|
|
"fixtures/marker/overlapping-block.json",
|
|
"fixtures/marker/plain-description-append.json",
|
|
"fixtures/marker/single-valid-block-replace.json",
|
|
"fixtures/marker/unicode-multiline-preservation.json",
|
|
}
|
|
assert all(item["role"] == "provider_consumer" for item in index["marker"])
|
|
assert {item["path"] for item in index["negative"]} == {
|
|
"fixtures/negative/snapshot-future-message.json",
|
|
"fixtures/negative/admission-subject-mismatch.json",
|
|
"fixtures/negative/review-override-out-of-range.json",
|
|
"fixtures/negative/event-missing-version.json",
|
|
}
|
|
|
|
|
|
def test_additive_fixture_remains_compatible_with_previous_consumer():
|
|
files = generate_contract_files()
|
|
current = json.loads(files["fixtures/webhook/completed.json"])
|
|
|
|
legacy = LegacyTerminalEvent.model_validate({**current, "new_optional_field": "ignored"})
|
|
assert legacy.event_type == "lead_scoring.run.terminal"
|
|
assert legacy.run_id == UUID(current["run_id"])
|
|
|
|
|
|
def test_contract_pack_cli_generates_checks_and_rejects_drift(tmp_path: Path):
|
|
output_dir = tmp_path / "contract-pack"
|
|
|
|
assert run_contract_pack_cli(["--output", str(output_dir)]) == 0
|
|
assert run_contract_pack_cli(["--output", str(output_dir), "--check"]) == 0
|
|
assert run_contract_pack_cli(["--output", str(output_dir), "--validate-fixtures"]) == 0
|
|
|
|
fixture = output_dir / "fixtures" / "snapshot.json"
|
|
fixture.write_text(canonical_json({"stale": True}), encoding="utf-8")
|
|
assert run_contract_pack_cli(["--output", str(output_dir), "--check"]) == 1
|
|
|
|
assert run_contract_pack_cli(["--output", str(output_dir)]) == 0
|
|
(output_dir / "unexpected.json").write_text("{}\n", encoding="utf-8")
|
|
assert run_contract_pack_cli(["--output", str(output_dir), "--check"]) == 1
|