Quellcode durchsuchen

fix(输出): 让十表空白库脱离旧历史表运行

为旧详情投影增加可控的历史 Decision Trace 开关,新任务在未创建 script_build_decision_trace 时仍可读取正文、元素和关系。

为旧日志接口增加历史 Log 开关;空白输出库没有 script_build_log 时直接使用构建 summary 或错误摘要,不再触发缺表异常。

新增严格十表场景测试,只创建五张复用业务表和五张新版表,验证 detail 与 log 兼容读取不依赖任何老数据。

补充 Raw Artifact 内容寻址写入和读取校验,确认本地文件产物会按摘要持久化并在回读时重新验证。
SamLee vor 1 Tag
Ursprung
Commit
2201d13110

+ 15 - 10
script_build_host/src/script_build_host/application/legacy_api.py

@@ -23,10 +23,13 @@ class LegacyScriptBuildApiService:
         read_sessions: async_sessionmaker[AsyncSession],
         write_sessions: async_sessionmaker[AsyncSession],
         authorizer: BuildAuthorizer,
+        *,
+        include_legacy_logs: bool = True,
     ) -> None:
         self._read = read_sessions
         self._write = write_sessions
         self._authorizer = authorizer
+        self._include_legacy_logs = include_legacy_logs
 
     async def list_authorized(
         self,
@@ -131,16 +134,18 @@ class LegacyScriptBuildApiService:
 
     async def log(self, script_build_id: int, principal: Principal) -> dict[str, Any]:
         row = await self._row(script_build_id, principal)
-        async with self._read() as session:
-            log_rows = list(
-                (
-                    await session.execute(
-                        select(script_build_log.c.log_content)
-                        .where(script_build_log.c.script_build_id == script_build_id)
-                        .order_by(script_build_log.c.id)
-                    )
-                ).scalars()
-            )
+        log_rows: list[Any] = []
+        if self._include_legacy_logs:
+            async with self._read() as session:
+                log_rows = list(
+                    (
+                        await session.execute(
+                            select(script_build_log.c.log_content)
+                            .where(script_build_log.c.script_build_id == script_build_id)
+                            .order_by(script_build_log.c.id)
+                        )
+                    ).scalars()
+                )
         content = "\n".join(str(item) for item in log_rows if item is not None)
         if not content:
             content = str(row["summary"] or row["error_message"] or "")

+ 13 - 9
script_build_host/src/script_build_host/application/legacy_projection.py

@@ -45,12 +45,14 @@ class LegacyDetailProjectionService:
         topic_reader: Any | None = None,
         bindings: MissionBindingRepository | None = None,
         snapshots: InputSnapshotRepository | None = None,
+        include_legacy_decision_traces: bool = True,
     ) -> None:
         self._sessions = sessions
         self._authorizer = authorizer
         self._topic_reader = topic_reader
         self._bindings = bindings
         self._snapshots = snapshots
+        self._include_legacy_decision_traces = include_legacy_decision_traces
 
     async def project_authorized(
         self, script_build_id: int, *, principal: Principal
@@ -149,17 +151,19 @@ class LegacyDetailProjectionService:
             .mappings()
             .all()
         )
-        decision_traces: list[Any] = list(
-            (
-                await session.execute(
-                    select(script_build_decision_trace)
-                    .where(script_build_decision_trace.c.script_build_id == script_build_id)
-                    .order_by(script_build_decision_trace.c.id)
+        decision_traces: list[Any] = []
+        if self._include_legacy_decision_traces:
+            decision_traces = list(
+                (
+                    await session.execute(
+                        select(script_build_decision_trace)
+                        .where(script_build_decision_trace.c.script_build_id == script_build_id)
+                        .order_by(script_build_decision_trace.c.id)
+                    )
                 )
+                .mappings()
+                .all()
             )
-            .mappings()
-            .all()
-        )
         paragraph_ids = {int(item["id"]) for item in paragraphs}
         element_ids = {int(item["id"]) for item in elements}
         owned_links = [

+ 88 - 0
script_build_host/tests/test_fresh_output_storage.py

@@ -0,0 +1,88 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from sqlalchemy import insert
+from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
+
+from script_build_host.application.legacy_api import LegacyScriptBuildApiService
+from script_build_host.application.legacy_projection import LegacyDetailProjectionService
+from script_build_host.domain.records import Principal
+from script_build_host.infrastructure.legacy_tables import (
+    legacy_metadata,
+    script_build_element,
+    script_build_paragraph,
+    script_build_paragraph_element,
+    script_build_record,
+    script_build_task_plan_step,
+)
+from script_build_host.infrastructure.raw_artifacts import FileRawArtifactStore
+from script_build_host.infrastructure.tables import metadata
+
+
+class _Authorizer:
+    async def require_access(self, _principal: Principal, _script_build_id: int) -> None:
+        return None
+
+
+async def test_ten_table_fresh_database_supports_detail_and_log_without_history_tables() -> None:
+    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
+    legacy_tables = (
+        script_build_record,
+        script_build_paragraph,
+        script_build_element,
+        script_build_paragraph_element,
+        script_build_task_plan_step,
+    )
+    async with engine.begin() as connection:
+        await connection.run_sync(metadata.create_all)
+        await connection.run_sync(
+            lambda sync_connection: legacy_metadata.create_all(
+                sync_connection,
+                tables=legacy_tables,
+            )
+        )
+
+    sessions = async_sessionmaker(engine, expire_on_commit=False)
+    async with sessions() as session, session.begin():
+        await session.execute(
+            insert(script_build_record).values(
+                id=1,
+                execution_id=11,
+                topic_build_id=12,
+                topic_id=13,
+                status="success",
+                reson_trace_id="root-1",
+                is_deleted=False,
+                is_favorited=False,
+                summary="fresh output",
+            )
+        )
+
+    authorizer = _Authorizer()
+    projection = LegacyDetailProjectionService(
+        sessions,
+        authorizer,  # type: ignore[arg-type]
+        include_legacy_decision_traces=False,
+    )
+    detail = await projection.project_authorized(1, principal=Principal("owner"))
+    assert detail["build"]["id"] == 1
+    assert detail["decision_traces"] == []
+
+    legacy_api = LegacyScriptBuildApiService(
+        sessions,
+        sessions,
+        authorizer,  # type: ignore[arg-type]
+        include_legacy_logs=False,
+    )
+    log = await legacy_api.log(1, Principal("owner"))
+    assert log["log_content"] == "fresh output"
+
+    await engine.dispose()
+
+
+async def test_raw_artifact_store_persists_and_revalidates_content(tmp_path: Path) -> None:
+    store = FileRawArtifactStore(tmp_path)
+    reference = await store.freeze_bytes(b"fresh raw output", media_type="text/plain")
+
+    assert await store.read_bytes(reference) == b"fresh raw output"