Просмотр исходного кода

Fix contract artifact timestamp mapping

SamLee 1 неделя назад
Родитель
Сommit
ac517324ed

+ 1 - 0
decode_content/models.py

@@ -141,6 +141,7 @@ class ContractSnapshot(DecodeModel):
     source_path: str | None = None
     source_path: str | None = None
     snapshot: JsonDict = Field(default_factory=dict)
     snapshot: JsonDict = Field(default_factory=dict)
     created_at: datetime | None = None
     created_at: datetime | None = None
+    updated_at: datetime | None = None
 
 
 
 
 Knowledge = KnowledgeParticle
 Knowledge = KnowledgeParticle

+ 2 - 1
decode_content/repositories/postgres.py

@@ -371,7 +371,8 @@ class PostgresDecodeRepository:
                 source_path = COALESCE(EXCLUDED.source_path, contract_artifacts.source_path),
                 source_path = COALESCE(EXCLUDED.source_path, contract_artifacts.source_path),
                 snapshot = EXCLUDED.snapshot,
                 snapshot = EXCLUDED.snapshot,
                 updated_at = now()
                 updated_at = now()
-            RETURNING *
+            RETURNING id, contract_name, contract_type, version_label,
+                content_hash, source_path, snapshot, created_at, updated_at
             """,
             """,
             (
             (
                 contract_name,
                 contract_name,

+ 24 - 0
tests/test_decode_contracts.py

@@ -1,5 +1,8 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
+from datetime import datetime, timezone
+from uuid import uuid4
+
 from decode_content.contracts import (
 from decode_content.contracts import (
     BUSINESS_STAGES,
     BUSINESS_STAGES,
     CREATION_STAGES,
     CREATION_STAGES,
@@ -11,6 +14,7 @@ from decode_content.contracts import (
     iter_contract_artifacts,
     iter_contract_artifacts,
     load_decode_contracts,
     load_decode_contracts,
 )
 )
+from decode_content.models import ContractSnapshot
 
 
 
 
 def test_load_decode_contracts_covers_skill_prompts_and_cache():
 def test_load_decode_contracts_covers_skill_prompts_and_cache():
@@ -45,3 +49,23 @@ def test_contract_artifacts_can_feed_repository():
     assert any(s.contract_type == "prompt_phase" for s in snapshots)
     assert any(s.contract_type == "prompt_phase" for s in snapshots)
     assert any(s.contract_type == "schema" for s in snapshots)
     assert any(s.contract_type == "schema" for s in snapshots)
     assert any(s.contract_type == "prompt" for s in snapshots)
     assert any(s.contract_type == "prompt" for s in snapshots)
+
+
+def test_contract_snapshot_accepts_contract_artifact_timestamps():
+    now = datetime.now(timezone.utc)
+
+    snapshot = ContractSnapshot.model_validate(
+        {
+            "id": uuid4(),
+            "contract_name": "创作知识提取-skill",
+            "contract_type": "skill",
+            "version_label": "test",
+            "content_hash": "a" * 64,
+            "source_path": "创作知识提取-skill",
+            "snapshot": {},
+            "created_at": now,
+            "updated_at": now,
+        }
+    )
+
+    assert snapshot.updated_at == now

+ 60 - 4
tests/test_pipeline_tracing.py

@@ -1,18 +1,26 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from pathlib import Path
 from contextlib import nullcontext
 from contextlib import nullcontext
+from datetime import datetime, timezone
+from pathlib import Path
 from uuid import uuid4
 from uuid import uuid4
 
 
 from acquisition.domain import CandidateItem, MediaAsset
 from acquisition.domain import CandidateItem, MediaAsset
 from core.models import Post
 from core.models import Post
 from decode_content.models import GateResult, ReadResult
 from decode_content.models import GateResult, ReadResult
+from decode_content.repositories.postgres import PostgresDecodeRepository
 from decode_content.service import DecodeWorkflowOutput
 from decode_content.service import DecodeWorkflowOutput
 from pipeline.decode_runner import run_decode_stage
 from pipeline.decode_runner import run_decode_stage
 from pipeline.tracing import TraceConfig, TraceContext, sanitize_payload
 from pipeline.tracing import TraceConfig, TraceContext, sanitize_payload
 from scripts import backfill_pipeline_traces
 from scripts import backfill_pipeline_traces
 
 
 
 
+def _method_source(text: str, method_name: str) -> str:
+    start = text.index(f"    def {method_name}")
+    next_method = text.find("\n    def ", start + 1)
+    return text[start:] if next_method == -1 else text[start:next_method]
+
+
 def test_formal_schema_declares_append_only_trace_tables():
 def test_formal_schema_declares_append_only_trace_tables():
     text = Path("db/migrations/001_creation_knowledge_schema.sql").read_text(encoding="utf-8")
     text = Path("db/migrations/001_creation_knowledge_schema.sql").read_text(encoding="utf-8")
 
 
@@ -40,13 +48,61 @@ def test_formal_schema_uses_deduped_contract_artifacts():
 
 
 def test_postgres_decode_repository_writes_deduped_contract_artifacts():
 def test_postgres_decode_repository_writes_deduped_contract_artifacts():
     text = Path("decode_content/repositories/postgres.py").read_text(encoding="utf-8")
     text = Path("decode_content/repositories/postgres.py").read_text(encoding="utf-8")
+    method = _method_source(text, "save_contract_snapshot")
 
 
-    assert "INSERT INTO contract_artifacts" in text
-    assert "INSERT INTO run_contract_artifacts" in text
-    assert "ON CONFLICT (contract_name, contract_type, source_path, content_hash)" in text
+    assert "INSERT INTO contract_artifacts" in method
+    assert "INSERT INTO run_contract_artifacts" in method
+    assert "ON CONFLICT (contract_name, contract_type, source_path, content_hash)" in method
+    assert "RETURNING id, contract_name, contract_type, version_label" in method
+    assert "content_hash, source_path, snapshot, created_at, updated_at" in method
     assert "INSERT INTO contract_snapshots" not in text
     assert "INSERT INTO contract_snapshots" not in text
 
 
 
 
+class RecordingDecodeRepo(PostgresDecodeRepository):
+    def __init__(self):
+        super().__init__(conn=None)
+        self.one_calls = []
+        self.one_results = []
+
+    def _one(self, sql, params):
+        self.one_calls.append((sql, params))
+        return self.one_results.pop(0)
+
+
+def test_save_contract_snapshot_accepts_updated_at_from_contract_artifacts():
+    now = datetime.now(timezone.utc)
+    artifact_id = uuid4()
+    repo = RecordingDecodeRepo()
+    repo.one_results.append(
+        {
+            "id": artifact_id,
+            "contract_name": "创作知识提取-skill",
+            "contract_type": "skill",
+            "version_label": None,
+            "content_hash": "a" * 64,
+            "source_path": "创作知识提取-skill",
+            "snapshot": {},
+            "created_at": now,
+            "updated_at": now,
+        }
+    )
+
+    snapshot = repo.save_contract_snapshot(
+        contract_name="创作知识提取-skill",
+        contract_type="skill",
+        content_hash="a" * 64,
+        source_path="创作知识提取-skill",
+        snapshot={},
+    )
+
+    sql, _params = repo.one_calls[0]
+    assert "INSERT INTO contract_artifacts" in sql
+    assert "RETURNING *" not in sql
+    assert "updated_at" in sql
+    assert snapshot.id == artifact_id
+    assert snapshot.updated_at == now
+
+
 def test_sanitize_payload_omits_base64_data_urls_but_keeps_metadata():
 def test_sanitize_payload_omits_base64_data_urls_but_keeps_metadata():
     data_url = "data:video/mp4;base64," + ("a" * 120)
     data_url = "data:video/mp4;base64," + ("a" * 120)